Skip to main content

yggdrasil/mcp/tools/
common.rs

1//! MCP 工具共享辅助函数。
2//!
3//! `require_scope`/`require_admin`/`internal`/`ok_json` 此前在每个工具文件
4//! (posts/comments/media/tags/settings/runner)各有一份逐字拷贝,仅 `tracing::error!`
5//! 前缀字符串不同。集中到本模块,消除 ~10 份拷贝(R2)。
6
7#![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
15/// 从 request.extensions 读取 McpPrincipal 并校验作用域。
16///
17/// 作用域不足返回 `insufficient_scope`;principal 缺失返回 `invalid_request`。
18/// 成功返回 principal 克隆(调用方常需 `principal.user_id` 作 author_id)。
19pub(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
37/// admin 作用域守卫:要求 `token.scope >= admin`。
38pub(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
52/// 记录错误详情并返回脱敏的 internal_error(不向客户端泄露 SQL 细节)。
53///
54/// 服务端日志保留完整 `{e}`;客户端只收到静态 `ctx`。
55pub(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
60/// 把可序列化值编码为 MCP 工具成功结果(pretty JSON 文本块)。
61pub(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}