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 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 #[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 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#[derive(Debug, Deserialize, schemars::JsonSchema)]
134pub struct CreateTagParams {
135 pub name: String,
137}
138
139#[derive(Debug, Deserialize, schemars::JsonSchema)]
140pub struct RenameTagParams {
141 pub tag_id: i32,
143 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}