1use axum::response::sse::{Event, KeepAlive, Sse};
2use axum::{
3 extract::{Query, State},
4 http::{header, HeaderMap, StatusCode},
5 response::{IntoResponse, Response},
6 routing::{get, post},
7 Json, Router,
8};
9use serde::Deserialize;
10use serde_json::{json, Value};
11use std::sync::Arc;
12use std::time::Duration;
13use tokio::sync::RwLock;
14use tower_http::cors::CorsLayer;
15use tracing::{error, info};
16
17use crate::{
18 clipboard::{ClipboardContent, ClipboardManager},
19 file_manager::FileManager,
20 mcp::{auth::AuthManager, protocol::*},
21 ClaudeUtilsError, Result,
22};
23
24#[derive(Clone)]
25pub struct McpServerState {
26 pub clipboard: Arc<ClipboardManager>,
27 pub file_manager: Arc<FileManager>,
28 pub auth_manager: Arc<AuthManager>,
29 pub initialized: Arc<RwLock<bool>>,
30}
31
32#[derive(Debug, Deserialize)]
33pub struct AuthQuery {
34 token: Option<String>,
35}
36
37pub struct McpServer {
38 state: McpServerState,
39 port: u16,
40 host: String,
41}
42
43impl McpServer {
44 pub async fn new(
45 clipboard: Arc<ClipboardManager>,
46 file_manager: Arc<FileManager>,
47 auth_manager: AuthManager,
48 port: u16,
49 host: String,
50 ) -> Result<Self> {
51 let state = McpServerState {
52 clipboard,
53 file_manager,
54 auth_manager: Arc::new(auth_manager),
55 initialized: Arc::new(RwLock::new(false)),
56 };
57
58 Ok(Self { state, port, host })
59 }
60
61 pub async fn run(self) -> Result<()> {
62 let app = Router::new()
63 .route("/health", get(health_handler))
64 .route("/", post(jsonrpc_handler))
65 .route("/rpc", post(jsonrpc_handler))
66 .route("/sse", get(sse_handler))
67 .layer(CorsLayer::permissive())
68 .with_state(self.state);
69
70 let addr = format!("{}:{}", self.host, self.port);
71 let listener = tokio::net::TcpListener::bind(&addr)
72 .await
73 .map_err(|e| ClaudeUtilsError::Server(format!("Failed to bind to {addr}: {e}")))?;
74
75 info!("MCP server listening on http://{}", addr);
76
77 axum::serve(listener, app)
78 .await
79 .map_err(|e| ClaudeUtilsError::Server(e.to_string()))?;
80
81 Ok(())
82 }
83}
84
85async fn health_handler(State(state): State<McpServerState>) -> impl IntoResponse {
87 let token = state.auth_manager.get_token().await;
88
89 Json(json!({
90 "status": "healthy",
91 "version": env!("CARGO_PKG_VERSION"),
92 "platform": std::env::consts::OS,
93 "capabilities": ["text", "image", "watch"],
94 "auth_required": token.is_some(),
95 }))
96}
97
98async fn jsonrpc_handler(
100 State(state): State<McpServerState>,
101 headers: HeaderMap,
102 Json(request): Json<Value>,
103) -> Response {
104 let auth_header = headers
106 .get(header::AUTHORIZATION)
107 .and_then(|h| h.to_str().ok())
108 .and_then(|h| h.strip_prefix("Bearer "));
109
110 if !state.auth_manager.validate_token(auth_header).await {
111 return (
112 StatusCode::UNAUTHORIZED,
113 Json(create_error_response(
114 None,
115 -32000,
116 "Authentication required".to_string(),
117 )),
118 )
119 .into_response();
120 }
121
122 if request.is_array() {
124 let requests = request.as_array().unwrap();
125 let mut responses = Vec::new();
126
127 for req in requests {
128 if let Ok(rpc_req) = serde_json::from_value::<JsonRpcRequest>(req.clone()) {
129 responses.push(handle_single_request(state.clone(), rpc_req).await);
130 }
131 }
132
133 return Json(Value::Array(
134 responses
135 .into_iter()
136 .map(|r| serde_json::to_value(r).unwrap())
137 .collect(),
138 ))
139 .into_response();
140 }
141
142 match serde_json::from_value::<JsonRpcRequest>(request) {
144 Ok(rpc_req) => {
145 let response = handle_single_request(state, rpc_req).await;
146 Json(response).into_response()
147 }
148 Err(_) => Json(create_error_response(
149 None,
150 PARSE_ERROR,
151 "Invalid JSON-RPC request".to_string(),
152 ))
153 .into_response(),
154 }
155}
156
157async fn handle_single_request(state: McpServerState, request: JsonRpcRequest) -> JsonRpcResponse {
158 match request.method.as_str() {
159 INITIALIZE => handle_initialize(state, request).await,
160 INITIALIZED => handle_initialized(state, request).await,
161 TOOLS_LIST => handle_tools_list(state, request).await,
162 TOOLS_CALL => handle_tools_call(state, request).await,
163 _ => create_error_response(
164 request.id,
165 METHOD_NOT_FOUND,
166 format!("Method not found: {}", request.method),
167 ),
168 }
169}
170
171async fn handle_initialize(_state: McpServerState, request: JsonRpcRequest) -> JsonRpcResponse {
172 let response = InitializeResponse {
173 protocol_version: "1.0".to_string(),
174 capabilities: ServerCapabilities {
175 tools: Some(ToolsCapability {}),
176 resources: None,
177 prompts: None,
178 },
179 server_info: Some(ServerInfo {
180 name: "claude-utils-clipboard".to_string(),
181 version: Some(env!("CARGO_PKG_VERSION").to_string()),
182 }),
183 };
184
185 create_success_response(request.id, serde_json::to_value(response).unwrap())
186}
187
188async fn handle_initialized(state: McpServerState, request: JsonRpcRequest) -> JsonRpcResponse {
189 *state.initialized.write().await = true;
190 info!("MCP server initialized");
191 create_success_response(request.id, json!({}))
192}
193
194async fn handle_tools_list(_state: McpServerState, request: JsonRpcRequest) -> JsonRpcResponse {
195 let tools = vec![
196 Tool {
197 name: "clipboard.get".to_string(),
198 description: "Get current clipboard content (text or image)".to_string(),
199 input_schema: json!({
200 "type": "object",
201 "properties": {
202 "format": {
203 "type": "string",
204 "enum": ["auto", "text", "image"],
205 "description": "Preferred format (auto detects automatically)",
206 "default": "auto"
207 }
208 },
209 "required": []
210 }),
211 },
212 Tool {
213 name: "clipboard.set".to_string(),
214 description: "Set clipboard content (requires --write flag)".to_string(),
215 input_schema: json!({
216 "type": "object",
217 "properties": {
218 "type": {
219 "type": "string",
220 "enum": ["text/plain", "image/png"],
221 "description": "Content type"
222 },
223 "data": {
224 "type": "string",
225 "description": "Content data (text or base64 for images)"
226 }
227 },
228 "required": ["type", "data"]
229 }),
230 },
231 ];
232
233 let response = ToolListResponse { tools };
234 create_success_response(request.id, serde_json::to_value(response).unwrap())
235}
236
237async fn handle_tools_call(state: McpServerState, request: JsonRpcRequest) -> JsonRpcResponse {
238 let params = match request.params {
239 Some(p) => p,
240 None => {
241 return create_error_response(
242 request.id,
243 INVALID_PARAMS,
244 "Missing parameters".to_string(),
245 )
246 }
247 };
248
249 let tool_request: ToolCallRequest = match serde_json::from_value(params) {
250 Ok(r) => r,
251 Err(e) => {
252 return create_error_response(
253 request.id,
254 INVALID_PARAMS,
255 format!("Invalid parameters: {e}"),
256 )
257 }
258 };
259
260 match tool_request.name.as_str() {
261 "clipboard.get" => handle_clipboard_get(state, request.id, tool_request.arguments).await,
262 "clipboard.set" => handle_clipboard_set(state, request.id, tool_request.arguments).await,
263 _ => create_error_response(
264 request.id,
265 METHOD_NOT_FOUND,
266 format!("Unknown tool: {}", tool_request.name),
267 ),
268 }
269}
270
271async fn handle_clipboard_get(
272 state: McpServerState,
273 id: Option<Value>,
274 _args: Option<Value>,
275) -> JsonRpcResponse {
276 let clipboard_data = match state.clipboard.get_content() {
278 Ok(data) => data,
279 Err(e) => {
280 return create_error_response(id, INTERNAL_ERROR, format!("Clipboard error: {e}"))
281 }
282 };
283
284 let final_content = match &clipboard_data.content {
286 ClipboardContent::ImagePng {
287 data: None,
288 width,
289 height,
290 size,
291 ..
292 }
293 | ClipboardContent::ImageJpeg {
294 data: None,
295 width,
296 height,
297 size,
298 ..
299 } => {
300 match state.clipboard.get_raw_image() {
302 Ok(image_data) => {
303 match state.file_manager.stage_image(&image_data, "png").await {
304 Ok(staged) => {
305 match clipboard_data.content {
307 ClipboardContent::ImagePng { .. } => ClipboardContent::ImagePng {
308 data: None,
309 file: Some(staged.path.to_string_lossy().to_string()),
310 width: *width,
311 height: *height,
312 size: *size,
313 },
314 ClipboardContent::ImageJpeg { .. } => ClipboardContent::ImageJpeg {
315 data: None,
316 file: Some(staged.path.to_string_lossy().to_string()),
317 width: *width,
318 height: *height,
319 size: *size,
320 },
321 _ => clipboard_data.content.clone(),
322 }
323 }
324 Err(e) => {
325 error!("Failed to stage image: {}", e);
326 clipboard_data.content.clone()
327 }
328 }
329 }
330 Err(e) => {
331 error!("Failed to get raw image: {}", e);
332 clipboard_data.content.clone()
333 }
334 }
335 }
336 _ => clipboard_data.content.clone(),
337 };
338
339 let response_data = json!({
341 "content": final_content,
342 "metadata": clipboard_data.metadata,
343 });
344
345 let tool_response = ToolCallResponse {
346 content: vec![Content::Text {
347 text: serde_json::to_string_pretty(&response_data).unwrap(),
348 }],
349 };
350
351 create_success_response(id, serde_json::to_value(tool_response).unwrap())
352}
353
354async fn handle_clipboard_set(
355 state: McpServerState,
356 id: Option<Value>,
357 args: Option<Value>,
358) -> JsonRpcResponse {
359 #[derive(Deserialize)]
362 struct SetArgs {
363 r#type: String,
364 data: String,
365 }
366
367 let args: SetArgs = match args.and_then(|a| serde_json::from_value(a).ok()) {
368 Some(a) => a,
369 None => return create_error_response(id, INVALID_PARAMS, "Invalid arguments".to_string()),
370 };
371
372 let content = match args.r#type.as_str() {
373 "text/plain" => ClipboardContent::Text {
374 data: args.data,
375 truncated: None,
376 },
377 "image/png" => ClipboardContent::ImagePng {
378 data: Some(args.data),
379 file: None,
380 width: 0, height: 0,
382 size: 0,
383 },
384 _ => {
385 return create_error_response(
386 id,
387 INVALID_PARAMS,
388 format!("Unsupported type: {}", args.r#type),
389 )
390 }
391 };
392
393 match state.clipboard.set_content(&content) {
394 Ok(_) => {
395 let tool_response = ToolCallResponse {
396 content: vec![Content::Text {
397 text: "Clipboard updated successfully".to_string(),
398 }],
399 };
400 create_success_response(id, serde_json::to_value(tool_response).unwrap())
401 }
402 Err(e) => {
403 create_error_response(id, INTERNAL_ERROR, format!("Failed to set clipboard: {e}"))
404 }
405 }
406}
407
408async fn sse_handler(
410 State(state): State<McpServerState>,
411 Query(auth): Query<AuthQuery>,
412) -> std::result::Result<impl IntoResponse, StatusCode> {
413 if !state
415 .auth_manager
416 .validate_token(auth.token.as_deref())
417 .await
418 {
419 return Err(StatusCode::UNAUTHORIZED);
420 }
421
422 let stream = async_stream::stream! {
423 loop {
424 tokio::time::sleep(Duration::from_secs(30)).await;
425 yield Ok::<_, anyhow::Error>(Event::default()
426 .data("heartbeat")
427 .event("ping"));
428 }
429 };
430
431 Ok(Sse::new(stream).keep_alive(KeepAlive::default()))
432}