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_for_tag_write` 失效文章/标签类缓存与公开页 SSR 缓存。
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        // 同名标签已存在时数据未变,无需冲刷缓存。
60        if created {
61            cache::invalidate_for_tag_write();
62        }
63
64        ok_json(TagResult {
65            success: true,
66            message: if created {
67                "标签已创建".into()
68            } else {
69                "标签已存在".into()
70            },
71            tag_id: Some(tag_id),
72            name,
73        })
74    }
75
76    /// 重命名指定标签。要求 write 作用域。
77    #[tool(description = "重命名指定标签。若目标名称已被其他标签占用则报错。")]
78    async fn rename_tag(
79        &self,
80        Parameters(p): Parameters<RenameTagParams>,
81        Extension(parts): Extension<http::request::Parts>,
82    ) -> Result<CallToolResult, McpError> {
83        let _principal = require_scope(&parts, "rename_tag", TokenScope::Write)?;
84
85        let name = p.name.trim().to_string();
86        if name.is_empty() {
87            return Err(McpError::invalid_request("name must not be empty", None));
88        }
89
90        let client = get_conn().await.map_err(|e| internal(e, "db connection"))?;
91
92        // 检查目标名称是否已被其他标签占用(排除自身)。
93        let conflict = client
94            .query_opt(
95                "SELECT 1 FROM tags WHERE name = $1 AND id != $2",
96                &[&name, &p.tag_id],
97            )
98            .await
99            .map_err(|e| internal(e, "check conflict"))?;
100        if conflict.is_some() {
101            return Err(McpError::invalid_request(
102                format!("标签名「{name}」已被占用"),
103                None,
104            ));
105        }
106
107        let result = client
108            .execute(
109                "UPDATE tags SET name = $1 WHERE id = $2",
110                &[&name, &p.tag_id],
111            )
112            .await
113            .map_err(|e| internal(e, "rename tag"))?;
114        if result == 0 {
115            return Err(McpError::invalid_request("标签不存在", None));
116        }
117
118        cache::invalidate_for_tag_write();
119
120        ok_json(TagResult {
121            success: true,
122            message: "标签已重命名".into(),
123            tag_id: Some(p.tag_id),
124            name,
125        })
126    }
127}
128
129// ---------------------------------------------------------------------------
130// 参数与输出结构
131// ---------------------------------------------------------------------------
132
133#[derive(Debug, Deserialize, schemars::JsonSchema)]
134pub struct CreateTagParams {
135    /// 标签名称。
136    pub name: String,
137}
138
139#[derive(Debug, Deserialize, schemars::JsonSchema)]
140pub struct RenameTagParams {
141    /// 要重命名的标签 id。
142    pub tag_id: i32,
143    /// 新标签名称。
144    pub name: String,
145}
146
147#[derive(Debug, serde::Serialize)]
148struct TagResult {
149    success: bool,
150    message: String,
151    tag_id: Option<i32>,
152    name: String,
153}