Skip to main content

yggdrasil/mcp/tools/
settings.rs

1//! 站点设置 MCP 工具:读取/更新回收站自动清理配置。
2//!
3//! 与 Web server-fn 共用 `api::settings` 的配置读写函数。鉴权入口各自保留:Web 走 cookie
4//! `get_current_admin_user()`,MCP 走 bearer token → `McpPrincipal`,要求 admin 作用域。
5//!
6//! 缓存失效:与 web fn 保持一致——`update_trash_settings` **不做任何缓存失效**。
7//! 理由:回收站配置只影响管理后台(SSR 缓存在 `admin/`,`invalidate_ssr_all_public`
8//! 明确保留不动)和后台清理任务,没有公开页缓存表面,故无需失效。
9//! (约束 #5 要求「按 web admin server fn 的方式失效」——该 fn 的方式就是不失效。)
10
11#![cfg(feature = "server")]
12
13use rmcp::handler::server::tool::Extension;
14use rmcp::handler::server::wrapper::Parameters;
15use rmcp::model::{CallToolResult, ContentBlock, TextContent};
16use rmcp::{schemars, tool, tool_router, ErrorData as McpError};
17
18use super::common::require_admin;
19use serde::Deserialize;
20
21use crate::api::error::{ApiError, AppError};
22use crate::api::settings::{load_trash_settings, save_trash_settings};
23use crate::db::pool::get_conn;
24
25/// `get_settings` 入参(无字段——预留扩展点,未来可按子域过滤)。
26///
27/// 显式补上 `properties: {}`:schemars 对零字段结构体只生成 `{"type":"object"}`,
28/// 而部分 OpenAI 兼容上游(如 MCA)要求对象 schema 必须带 `properties` 键,
29/// 否则整个请求会被判为非法工具 schema。
30#[derive(Debug, Deserialize, schemars::JsonSchema, Default)]
31#[schemars(extend("properties" = {}))]
32pub struct GetSettingsParams {}
33
34/// `update_settings` 入参:两项回收站配置。
35#[derive(Debug, Deserialize, schemars::JsonSchema)]
36pub struct UpdateSettingsParams {
37    /// 是否启用回收站自动清理。
38    pub auto_purge_enabled: bool,
39    /// 已删除文章保留天数(会被 clamp 到 [1, 365])。
40    pub retention_days: i32,
41}
42
43#[tool_router(router = settings_router, vis = "pub")]
44impl crate::mcp::server::YggMcpServer {
45    /// 读取站点回收站设置。要求 admin 作用域。
46    #[tool(description = "读取站点回收站配置(自动清理开关 + 保留天数)。需要 admin 作用域。")]
47    async fn get_settings(
48        &self,
49        Parameters(_p): Parameters<GetSettingsParams>,
50        Extension(parts): Extension<http::request::Parts>,
51    ) -> Result<CallToolResult, McpError> {
52        require_admin(&parts, "get_settings")?;
53
54        let settings = async {
55            let client = get_conn().await.map_err(AppError::db_conn)?;
56            load_trash_settings(&client).await
57        }
58        .await
59        .map_err(|e| McpError::internal_error(ApiError::from(e).to_string(), None))?;
60
61        let text = serde_json::to_string_pretty(&settings)
62            .map_err(|e| McpError::internal_error(format!("encode failed: {e}"), None))?;
63        Ok(CallToolResult::success(vec![ContentBlock::Text(
64            TextContent::new(text),
65        )]))
66    }
67
68    /// 更新站点回收站设置。要求 admin 作用域。retention_days 会 clamp 到 [1, 365]。
69    #[tool(
70        description = "更新站点回收站配置(自动清理开关 + 保留天数)。retention_days 会被钳制到 1..=365。需要 admin 作用域。"
71    )]
72    async fn update_settings(
73        &self,
74        Parameters(UpdateSettingsParams {
75            auto_purge_enabled,
76            retention_days,
77        }): Parameters<UpdateSettingsParams>,
78        Extension(parts): Extension<http::request::Parts>,
79    ) -> Result<CallToolResult, McpError> {
80        require_admin(&parts, "update_settings")?;
81
82        let updated = async {
83            let client = get_conn().await.map_err(AppError::db_conn)?;
84            save_trash_settings(&client, auto_purge_enabled, retention_days).await
85        }
86        .await
87        .map_err(|e| McpError::internal_error(ApiError::from(e).to_string(), None))?;
88
89        tracing::info!(
90            "MCP: trash settings updated: auto_purge={}, retention_days={}",
91            updated.auto_purge_enabled,
92            updated.retention_days
93        );
94
95        let text = serde_json::to_string_pretty(&updated)
96            .map_err(|e| McpError::internal_error(format!("encode failed: {e}"), None))?;
97        Ok(CallToolResult::success(vec![ContentBlock::Text(
98            TextContent::new(text),
99        )]))
100    }
101}