1use rmcp::{
2 model::{
3 CallToolRequestParams, CallToolResponse, CancelTaskParams, GetPromptRequestParams,
4 GetPromptResponse, GetPromptResult, GetTaskParams, GetTaskResult, Icon, Implementation,
5 ListPromptsResult, ListResourcesResult, ListToolsResult, PaginatedRequestParams,
6 PromptMessage, ProtocolVersion, ReadResourceRequestParams, ReadResourceResponse,
7 ReadResourceResult, Resource, ResourceContents, Role, ServerCapabilities, ServerInfo,
8 TaskStatus, Tool, UpdateTaskParams,
9 },
10 service::{NotificationContext, RequestContext, RoleServer},
11 ErrorData as McpError, ServerHandler,
12};
13
14use super::catalog::{server_icon_data_uri, winx_prompts, winx_tools};
15use super::principal::{
16 principal_from_context, scope_tool_request, session_affinity_from_context,
17 task_belongs_to_principal, RequestScope,
18};
19use super::WinxService;
20
21struct UsageEvent {
24 tool: String,
25 principal: String,
26 thread_id: String,
27 started: std::time::Instant,
28}
29
30impl UsageEvent {
31 fn start(request: &CallToolRequestParams, scope: &RequestScope) -> Self {
32 let thread_id = request
33 .arguments
34 .as_ref()
35 .and_then(|arguments| arguments.get("thread_id"))
36 .and_then(serde_json::Value::as_str)
37 .map(|thread_id| scope.unscope_text(thread_id))
38 .unwrap_or_default();
39 Self {
40 tool: request.name.to_string(),
41 principal: scope
42 .principal()
43 .map_or_else(|| "local".to_string(), |principal| principal.name().to_string()),
44 thread_id,
45 started: std::time::Instant::now(),
46 }
47 }
48
49 fn emit(&self, outcome: &str) {
50 tracing::info!(
51 target: "winx::usage",
52 event = "tool_call",
53 tool = %self.tool,
54 principal = %self.principal,
55 thread_id = %self.thread_id,
56 duration_ms = u64::try_from(self.started.elapsed().as_millis()).unwrap_or(u64::MAX),
57 outcome,
58 "tool call"
59 );
60 }
61}
62
63impl ServerHandler for WinxService {
64 fn get_info(&self) -> ServerInfo {
65 ServerInfo::new(
66 ServerCapabilities::builder()
67 .enable_tools()
68 .enable_resources()
69 .enable_prompts()
70 .enable_tasks()
71 .build(),
72 )
73 .with_server_info(
74 Implementation::new("winx-mcp-server", self.version.clone())
75 .with_title("Winx High-Performance MCP")
76 .with_icons(vec![Icon::new(server_icon_data_uri())
77 .with_mime_type("image/png")
78 .with_sizes(vec!["96x96".to_string()])]),
79 )
80 .with_protocol_version(ProtocolVersion::V_2026_07_28)
81 .with_instructions(
82 "Winx is a high-performance Rust implementation of MCP tools for shell and file management.",
83 )
84 }
85
86 async fn on_initialized(&self, context: NotificationContext<RoleServer>) {
87 self.initialize_from_client_roots(context).await;
88 }
89
90 async fn on_roots_list_changed(&self, context: NotificationContext<RoleServer>) {
91 self.initialize_from_client_roots(context).await;
94 }
95
96 fn get_tool(&self, name: &str) -> Option<Tool> {
97 winx_tools().into_iter().find(|tool| tool.name == name)
98 }
99
100 async fn get_task(
101 &self,
102 request: GetTaskParams,
103 context: RequestContext<RoleServer>,
104 ) -> Result<GetTaskResult, McpError> {
105 let principal = principal_from_context(&context);
106 if !task_belongs_to_principal(&request.task_id, principal.as_ref()) {
107 return Err(McpError::invalid_request(
108 format!("Unknown or expired task: {}", request.task_id),
109 None,
110 ));
111 }
112 let mut tasks = self.tasks.lock().await;
113 let entry = tasks.get(&request.task_id).ok_or_else(|| {
114 McpError::invalid_request(format!("Unknown or expired task: {}", request.task_id), None)
115 })?;
116 Ok(GetTaskResult::new(entry.detailed()))
117 }
118
119 async fn update_task(
120 &self,
121 request: UpdateTaskParams,
122 context: RequestContext<RoleServer>,
123 ) -> Result<(), McpError> {
124 let principal = principal_from_context(&context);
125 if !task_belongs_to_principal(&request.task_id, principal.as_ref()) {
126 return Err(McpError::invalid_request(
127 format!("Unknown or expired task: {}", request.task_id),
128 None,
129 ));
130 }
131 let mut tasks = self.tasks.lock().await;
132 tasks.get(&request.task_id).ok_or_else(|| {
133 McpError::invalid_request(format!("Unknown or expired task: {}", request.task_id), None)
134 })?;
135 Ok(())
136 }
137
138 async fn cancel_task(
139 &self,
140 request: CancelTaskParams,
141 context: RequestContext<RoleServer>,
142 ) -> Result<(), McpError> {
143 let principal = principal_from_context(&context);
144 if !task_belongs_to_principal(&request.task_id, principal.as_ref()) {
145 return Err(McpError::invalid_request(
146 format!("Unknown or expired task: {}", request.task_id),
147 None,
148 ));
149 }
150 let (abort_handle, thread_id) = {
151 let mut tasks = self.tasks.lock().await;
152 let entry = tasks.get_mut(&request.task_id).ok_or_else(|| {
153 McpError::invalid_request(
154 format!("Unknown or expired task: {}", request.task_id),
155 None,
156 )
157 })?;
158 if entry.task.status != TaskStatus::Working
159 && entry.task.status != TaskStatus::InputRequired
160 {
161 return Err(McpError::invalid_request(
162 format!("Task {} is already terminal", request.task_id),
163 None,
164 ));
165 }
166 let abort_handle = entry.abort_handle.take();
167 let thread_id = entry.thread_id.clone();
168 entry.finish(TaskStatus::Cancelled, Some("Cancelled by client".to_string()), None);
169 (abort_handle, thread_id)
170 };
171 if let Some(abort_handle) = abort_handle {
172 abort_handle.abort();
173 }
174 self.interrupt_task_thread(&thread_id).await;
175 Ok(())
176 }
177
178 async fn list_tools(
179 &self,
180 _request: Option<PaginatedRequestParams>,
181 _context: RequestContext<RoleServer>,
182 ) -> Result<ListToolsResult, McpError> {
183 Ok(ListToolsResult::with_all_items(winx_tools()))
184 }
185
186 async fn list_resources(
187 &self,
188 _request: Option<PaginatedRequestParams>,
189 _context: RequestContext<RoleServer>,
190 ) -> Result<ListResourcesResult, McpError> {
191 Ok(ListResourcesResult::with_all_items(vec![Resource::new("file://readme", "README")
192 .with_description("Project README documentation")
193 .with_mime_type("text/markdown")]))
194 }
195
196 async fn list_prompts(
197 &self,
198 _request: Option<PaginatedRequestParams>,
199 _context: RequestContext<RoleServer>,
200 ) -> Result<ListPromptsResult, McpError> {
201 Ok(ListPromptsResult::with_all_items(winx_prompts()))
202 }
203
204 async fn get_prompt(
205 &self,
206 request: GetPromptRequestParams,
207 context: RequestContext<RoleServer>,
208 ) -> Result<GetPromptResponse, McpError> {
209 if request.name != "KnowledgeTransfer" {
210 return Err(McpError::invalid_request(
211 format!("Unknown prompt: {}", request.name),
212 None,
213 ));
214 }
215
216 let principal = principal_from_context(&context);
217 let session_prefix = principal.as_ref().map(crate::config::HttpPrincipal::session_prefix);
218 let text = crate::utils::redact::redact(
219 &self.knowledge_transfer_prompt_text(session_prefix.as_deref()).await,
220 )
221 .into_owned();
222 Ok(GetPromptResult::new(vec![PromptMessage::new_text(Role::User, text)])
223 .with_description("Knowledge transfer handoff prompt")
224 .into())
225 }
226
227 async fn read_resource(
228 &self,
229 request: ReadResourceRequestParams,
230 _context: RequestContext<RoleServer>,
231 ) -> Result<ReadResourceResponse, McpError> {
232 let content = match request.uri.as_ref() {
233 "file://readme" => match tokio::fs::read_to_string("README.md").await {
234 Ok(content) => vec![ResourceContents::text(
235 crate::utils::redact::redact(&content).into_owned(),
236 request.uri.clone(),
237 )],
238 Err(_) => vec![ResourceContents::text(
239 "README.md not found".to_string(),
240 request.uri.clone(),
241 )],
242 },
243 _ => {
244 return Err(McpError::invalid_request(
245 format!("Unknown resource URI: {}", request.uri),
246 None,
247 ));
248 }
249 };
250 Ok(ReadResourceResult::new(content).into())
251 }
252
253 async fn call_tool(
254 &self,
255 request: CallToolRequestParams,
256 context: RequestContext<RoleServer>,
257 ) -> Result<CallToolResponse, McpError> {
258 let principal = principal_from_context(&context);
259 let affinity = session_affinity_from_context(&context);
260 let (request, scope) =
261 scope_tool_request(request, principal, affinity).map_err(|error| {
262 McpError::invalid_request(format!("Cannot scope remote request: {error}"), None)
263 })?;
264 let usage = UsageEvent::start(&request, &scope);
265 if context.client_capabilities().is_some_and(|capabilities| capabilities.supports_tasks())
266 && Self::bash_task_is_eligible(&request)
267 {
268 return match self.enqueue_bash_task(request, scope).await {
269 Ok(task) => {
270 usage.emit("task");
271 Ok(CallToolResponse::Task(task))
272 }
273 Err(error) => {
274 usage.emit("error");
275 Err(error)
276 }
277 };
278 }
279
280 match self.execute_tool_call(request).await {
281 Ok(mut result) => {
282 scope.unscope_result(&mut result);
283 usage.emit("ok");
284 Ok(result.into())
285 }
286 Err(mut error) => {
287 scope.unscope_error(&mut error);
288 usage.emit("error");
289 Err(error)
290 }
291 }
292 }
293}