Skip to main content

yggdrasil/mcp/tools/
tags.rs

1//! MCP 写作用域工具:标签管理。
2//!
3//! `create_tag` 与 `rename_tag` 没有独立的 web 后台 server-fn(标签在文章
4//! 保存时由 `sync_tags` 隐式创建)。本模块直接操作 tags 表,并在写后失效
5//! 标签云缓存(`invalidate_all_tags`)。
6//!
7//! 本模块仅 `feature = "server"` 编译。
8
9#![cfg(feature = "server")]
10
11use rmcp::handler::server::tool::Extension;
12use rmcp::handler::server::wrapper::Parameters;
13use rmcp::model::CallToolResult;
14use rmcp::{schemars, tool, tool_router, ErrorData as McpError};
15use serde::Deserialize;
16
17use super::common::{internal, ok_json, require_scope};
18use crate::cache;
19use crate::db::pool::get_conn;
20use crate::models::mcp_token::TokenScope;
21
22#[tool_router(router = tags_router, vis = "pub")]
23impl crate::mcp::server::YggMcpServer {
24    /// 创建一个新标签。若同名标签已存在则返回已有标签 id。要求 write 作用域。
25    #[tool(description = "创建一个新标签。若同名标签已存在则返回其 id(幂等)。")]
26    async fn create_tag(
27        &self,
28        Parameters(p): Parameters<CreateTagParams>,
29        Extension(parts): Extension<http::request::Parts>,
30    ) -> Result<CallToolResult, McpError> {
31        let _principal = require_scope(&parts, "create_tag", TokenScope::Write)?;
32
33        let name = p.name.trim().to_string();
34        if name.is_empty() {
35            return Err(McpError::invalid_request("name must not be empty", None));
36        }
37
38        let client = get_conn().await.map_err(|e| internal(e, "db connection"))?;
39
40        // 与 sync_tags 一致的 upsert 语义。
41        let row = client
42            .query_opt(
43                "INSERT INTO tags (name) VALUES ($1) ON CONFLICT (name) DO NOTHING RETURNING id",
44                &[&name],
45            )
46            .await
47            .map_err(|e| internal(e, "insert tag"))?;
48        let (tag_id, created): (i32, bool) = match row {
49            Some(r) => (r.get(0), true),
50            None => {
51                let r = client
52                    .query_one("SELECT id FROM tags WHERE name = $1", &[&name])
53                    .await
54                    .map_err(|e| internal(e, "select existing tag"))?;
55                (r.get(0), false)
56            }
57        };
58
59        cache::invalidate_all_tags();
60
61        ok_json(TagResult {
62            success: true,
63            message: if created {
64                "标签已创建".into()
65            } else {
66                "标签已存在".into()
67            },
68            tag_id: Some(tag_id),
69            name,
70        })
71    }
72
73    /// 重命名指定标签。要求 write 作用域。
74    #[tool(description = "重命名指定标签。若目标名称已被其他标签占用则报错。")]
75    async fn rename_tag(
76        &self,
77        Parameters(p): Parameters<RenameTagParams>,
78        Extension(parts): Extension<http::request::Parts>,
79    ) -> Result<CallToolResult, McpError> {
80        let _principal = require_scope(&parts, "rename_tag", TokenScope::Write)?;
81
82        let name = p.name.trim().to_string();
83        if name.is_empty() {
84            return Err(McpError::invalid_request("name must not be empty", None));
85        }
86
87        let client = get_conn().await.map_err(|e| internal(e, "db connection"))?;
88
89        // 检查目标名称是否已被其他标签占用(排除自身)。
90        let conflict = client
91            .query_opt(
92                "SELECT 1 FROM tags WHERE name = $1 AND id != $2",
93                &[&name, &p.tag_id],
94            )
95            .await
96            .map_err(|e| internal(e, "check conflict"))?;
97        if conflict.is_some() {
98            return Err(McpError::invalid_request(
99                format!("标签名「{name}」已被占用"),
100                None,
101            ));
102        }
103
104        let result = client
105            .execute(
106                "UPDATE tags SET name = $1 WHERE id = $2",
107                &[&name, &p.tag_id],
108            )
109            .await
110            .map_err(|e| internal(e, "rename tag"))?;
111        if result == 0 {
112            return Err(McpError::invalid_request("标签不存在", None));
113        }
114
115        cache::invalidate_all_tags();
116
117        ok_json(TagResult {
118            success: true,
119            message: "标签已重命名".into(),
120            tag_id: Some(p.tag_id),
121            name,
122        })
123    }
124}
125
126// ---------------------------------------------------------------------------
127// 参数与输出结构
128// ---------------------------------------------------------------------------
129
130#[derive(Debug, Deserialize, schemars::JsonSchema)]
131pub struct CreateTagParams {
132    /// 标签名称。
133    pub name: String,
134}
135
136#[derive(Debug, Deserialize, schemars::JsonSchema)]
137pub struct RenameTagParams {
138    /// 要重命名的标签 id。
139    pub tag_id: i32,
140    /// 新标签名称。
141    pub name: String,
142}
143
144#[derive(Debug, serde::Serialize)]
145struct TagResult {
146    success: bool,
147    message: String,
148    tag_id: Option<i32>,
149    name: String,
150}