yggdrasil/mcp/tools/
common.rs1#![cfg(feature = "server")]
8
9use rmcp::model::{CallToolResult, ContentBlock, TextContent};
10use rmcp::ErrorData as McpError;
11
12use crate::mcp::auth::McpPrincipal;
13use crate::models::mcp_token::TokenScope;
14
15pub(super) fn require_scope(
20 parts: &http::request::Parts,
21 tool: &str,
22 scope: TokenScope,
23) -> Result<McpPrincipal, McpError> {
24 let p = parts
25 .extensions
26 .get::<McpPrincipal>()
27 .ok_or_else(|| McpError::invalid_request("missing MCP principal", None))?;
28 if !p.scope.grants(scope) {
29 return Err(McpError::invalid_request(
30 format!("insufficient_scope: {tool} requires {}", scope.as_str()),
31 None,
32 ));
33 }
34 Ok(p.clone())
35}
36
37pub(super) fn require_admin(parts: &http::request::Parts, tool: &str) -> Result<(), McpError> {
39 let principal = parts
40 .extensions
41 .get::<McpPrincipal>()
42 .ok_or_else(|| McpError::invalid_request("missing MCP principal", None))?;
43 if !principal.scope.grants(TokenScope::Admin) {
44 return Err(McpError::invalid_request(
45 format!("insufficient_scope: {tool} requires admin"),
46 None,
47 ));
48 }
49 Ok(())
50}
51
52pub(super) fn internal<E: std::fmt::Display>(e: E, ctx: &'static str) -> McpError {
56 tracing::error!("mcp {ctx}: {e}");
57 McpError::internal_error(ctx, None)
58}
59
60pub(super) fn ok_json<T: serde::Serialize>(val: T) -> Result<CallToolResult, McpError> {
62 let text = serde_json::to_string_pretty(&val).map_err(|e| internal(e, "encode result"))?;
63 Ok(CallToolResult::success(vec![ContentBlock::Text(
64 TextContent::new(text),
65 )]))
66}