yggdrasil/mcp/tools/
tags.rs1#![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 #[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 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 #[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 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#[derive(Debug, Deserialize, schemars::JsonSchema)]
131pub struct CreateTagParams {
132 pub name: String,
134}
135
136#[derive(Debug, Deserialize, schemars::JsonSchema)]
137pub struct RenameTagParams {
138 pub tag_id: i32,
140 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}