Skip to main content

yggdrasil/mcp/tools/
settings.rs

1//! 站点设置 MCP 工具:读取/更新回收站自动清理配置。
2//!
3//! 与 `src/api/settings.rs` 的 `get_trash_settings` / `update_trash_settings`
4//! server-fn 一致(同样的 SQL、同样的 clamp)。区别仅在鉴权入口:web 走 cookie
5//! `get_current_admin_user()`,MCP 走 bearer token → `McpPrincipal`,要求 admin 作用域。
6//!
7//! 缓存失效:与 web fn 保持一致——`update_trash_settings` **不做任何缓存失效**。
8//! 理由:回收站配置只影响管理后台(SSR 缓存在 `admin/`,`invalidate_ssr_all_public`
9//! 明确保留不动)和后台清理任务,没有公开页缓存表面,故无需失效。
10//! (约束 #5 要求「按 web admin server fn 的方式失效」——该 fn 的方式就是不失效。)
11
12#![cfg(feature = "server")]
13
14use rmcp::handler::server::tool::Extension;
15use rmcp::handler::server::wrapper::Parameters;
16use rmcp::model::{CallToolResult, ContentBlock, TextContent};
17use rmcp::{schemars, tool, tool_router, ErrorData as McpError};
18
19use super::common::require_admin;
20use serde::Deserialize;
21
22use crate::api::error::AppError;
23use crate::db::pool::get_conn;
24use crate::models::settings::{TrashSettings, DEFAULT_AUTO_PURGE_ENABLED, DEFAULT_RETENTION_DAYS};
25
26/// `get_settings` 入参(无字段——预留扩展点,未来可按子域过滤)。
27#[derive(Debug, Deserialize, schemars::JsonSchema, Default)]
28pub struct GetSettingsParams {}
29
30/// `update_settings` 入参:两项回收站配置。
31#[derive(Debug, Deserialize, schemars::JsonSchema)]
32pub struct UpdateSettingsParams {
33    /// 是否启用回收站自动清理。
34    pub auto_purge_enabled: bool,
35    /// 已删除文章保留天数(会被 clamp 到 [1, 365])。
36    pub retention_days: i32,
37}
38
39#[tool_router(router = settings_router, vis = "pub")]
40impl crate::mcp::server::YggMcpServer {
41    /// 读取站点回收站设置。要求 admin 作用域。
42    #[tool(description = "读取站点回收站配置(自动清理开关 + 保留天数)。需要 admin 作用域。")]
43    async fn get_settings(
44        &self,
45        Parameters(_p): Parameters<GetSettingsParams>,
46        Extension(parts): Extension<http::request::Parts>,
47    ) -> Result<CallToolResult, McpError> {
48        require_admin(&parts, "get_settings")?;
49
50        let settings = load_trash_settings()
51            .await
52            .map_err(|e| McpError::internal_error(format!("settings read failed: {e:?}"), None))?;
53
54        let text = serde_json::to_string_pretty(&settings)
55            .map_err(|e| McpError::internal_error(format!("encode failed: {e}"), None))?;
56        Ok(CallToolResult::success(vec![ContentBlock::Text(
57            TextContent::new(text),
58        )]))
59    }
60
61    /// 更新站点回收站设置。要求 admin 作用域。retention_days 会 clamp 到 [1, 365]。
62    #[tool(
63        description = "更新站点回收站配置(自动清理开关 + 保留天数)。retention_days 会被钳制到 1..=365。需要 admin 作用域。"
64    )]
65    async fn update_settings(
66        &self,
67        Parameters(UpdateSettingsParams {
68            auto_purge_enabled,
69            retention_days,
70        }): Parameters<UpdateSettingsParams>,
71        Extension(parts): Extension<http::request::Parts>,
72    ) -> Result<CallToolResult, McpError> {
73        require_admin(&parts, "update_settings")?;
74
75        let retention_days = TrashSettings::clamp_retention(retention_days);
76
77        let updated = save_trash_settings(auto_purge_enabled, retention_days)
78            .await
79            .map_err(|e| McpError::internal_error(format!("settings write failed: {e:?}"), None))?;
80
81        let text = serde_json::to_string_pretty(&updated)
82            .map_err(|e| McpError::internal_error(format!("encode failed: {e}"), None))?;
83        Ok(CallToolResult::success(vec![ContentBlock::Text(
84            TextContent::new(text),
85        )]))
86    }
87}
88
89/// 读取回收站配置(与 `get_trash_settings` 的 SQL 一致)。
90///
91/// settings 表缺失键时回退默认值,保证向后兼容。
92async fn load_trash_settings() -> Result<TrashSettings, AppError> {
93    let client = get_conn().await.map_err(AppError::db_conn)?;
94
95    let enabled: bool = client
96        .query_opt(
97            "SELECT value FROM settings WHERE key = 'trash_auto_purge_enabled'",
98            &[],
99        )
100        .await
101        .map_err(AppError::query)?
102        .and_then(|r| r.get::<_, String>("value").parse().ok())
103        .unwrap_or(DEFAULT_AUTO_PURGE_ENABLED);
104
105    let days: i32 = client
106        .query_opt(
107            "SELECT value FROM settings WHERE key = 'trash_retention_days'",
108            &[],
109        )
110        .await
111        .map_err(AppError::query)?
112        .and_then(|r| r.get::<_, String>("value").parse().ok())
113        .unwrap_or(DEFAULT_RETENTION_DAYS);
114
115    Ok(TrashSettings {
116        auto_purge_enabled: enabled,
117        retention_days: TrashSettings::clamp_retention(days),
118    })
119}
120
121/// 写入回收站配置(与 `update_trash_settings` 的 UPSERT 一致)。返回写入后的值。
122async fn save_trash_settings(
123    auto_purge_enabled: bool,
124    retention_days: i32,
125) -> Result<TrashSettings, AppError> {
126    let client = get_conn().await.map_err(AppError::db_conn)?;
127
128    client
129        .execute(
130            "INSERT INTO settings (key, value, updated_at) VALUES ('trash_auto_purge_enabled', $1, NOW())
131             ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = NOW()",
132            &[&auto_purge_enabled.to_string()],
133        )
134        .await
135        .map_err(AppError::query)?;
136
137    client
138        .execute(
139            "INSERT INTO settings (key, value, updated_at) VALUES ('trash_retention_days', $1, NOW())
140             ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = NOW()",
141            &[&retention_days.to_string()],
142        )
143        .await
144        .map_err(AppError::query)?;
145
146    tracing::info!(
147        "MCP: trash settings updated: auto_purge={}, retention_days={}",
148        auto_purge_enabled,
149        retention_days
150    );
151
152    Ok(TrashSettings {
153        auto_purge_enabled,
154        retention_days,
155    })
156}