From d1aac070922f50424177b37c51a786d486a60e7d Mon Sep 17 00:00:00 2001 From: JSR Date: Sat, 21 Mar 2026 11:05:01 +0800 Subject: [PATCH] =?UTF-8?q?MCP=E8=B0=83=E7=94=A8=E9=93=BE=E8=B7=AF?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=90=8E=E7=AB=AF=E6=97=A5=E5=BF=97=E8=AE=B0?= =?UTF-8?q?=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src-tauri/src/commands/mcp.rs | 212 +++++++++++++++++++++++++++++----- 1 file changed, 184 insertions(+), 28 deletions(-) diff --git a/src-tauri/src/commands/mcp.rs b/src-tauri/src/commands/mcp.rs index f08d968..6398c32 100644 --- a/src-tauri/src/commands/mcp.rs +++ b/src-tauri/src/commands/mcp.rs @@ -140,6 +140,18 @@ fn check_admin_permission(state: &Arc>) -> Result<(), String> { Ok(()) } +fn mcp_log_info(app_state: &Arc>, message: &str, meta: Value) { + let state_guard = app_state.read(); + let logger = state_guard.logger.read(); + logger.info_with_meta(message, meta); +} + +fn mcp_log_error(app_state: &Arc>, message: &str, meta: Value) { + let state_guard = app_state.read(); + let logger = state_guard.logger.read(); + logger.error_with_meta(message, meta); +} + fn jsonrpc_ok(id: Value, result: Value) -> Value { json!({ "jsonrpc": "2.0", @@ -211,11 +223,25 @@ async fn handle_mcp( let req = match serde_json::from_value::(payload) { Ok(v) => v, Err(e) => { + mcp_log_error( + &app_state, + "mcp:request_parse_failed", + json!({ "error": e.to_string() }), + ); let body = jsonrpc_error(Value::Null, -32700, &format!("Parse error: {}", e)); return (StatusCode::OK, Json(body)).into_response(); } }; + mcp_log_info( + &app_state, + "mcp:request_received", + json!({ + "method": req.method.clone(), + "has_id": req.id.is_some() + }), + ); + let Some(id) = req.id.clone() else { return StatusCode::NO_CONTENT.into_response(); }; @@ -229,7 +255,14 @@ async fn handle_mcp( .and_then(|p| serde_json::from_value::(p).ok()); match params { - None => jsonrpc_error(id, -32602, "Invalid tools/call params"), + None => { + mcp_log_error( + &app_state, + "mcp:tools_call_invalid_params", + json!({ "reason": "invalid_tools_call_params" }), + ); + jsonrpc_error(id, -32602, "Invalid tools/call params") + } Some(params) => match params.name.as_str() { "add_score" => match serde_json::from_value::(params.arguments) { Ok(args) => match mcp_add_score(&app_state, args).await { @@ -249,20 +282,40 @@ async fn handle_mcp( "structuredContent": payload }), ), - Err(e) => jsonrpc_ok( - id, - json!({ - "content": [ - { - "type": "text", - "text": format!("加分失败: {}", e) - } - ], - "isError": true - }), - ), + Err(e) => { + mcp_log_error( + &app_state, + "mcp:tool_call_failed", + json!({ + "tool": "add_score", + "error": e + }), + ); + jsonrpc_ok( + id, + json!({ + "content": [ + { + "type": "text", + "text": format!("加分失败: {}", e) + } + ], + "isError": true + }), + ) + } }, - Err(e) => jsonrpc_error(id, -32602, &format!("Invalid arguments: {}", e)), + Err(e) => { + mcp_log_error( + &app_state, + "mcp:tool_call_invalid_arguments", + json!({ + "tool": "add_score", + "error": e.to_string() + }), + ); + jsonrpc_error(id, -32602, &format!("Invalid arguments: {}", e)) + } }, "list_students" => { match serde_json::from_value::(params.arguments) { @@ -280,25 +333,52 @@ async fn handle_mcp( "structuredContent": payload }), ), - Err(e) => jsonrpc_ok( - id, - json!({ - "content": [ - { - "type": "text", - "text": format!("获取学生列表失败: {}", e) - } - ], - "isError": true - }), - ), + Err(e) => { + mcp_log_error( + &app_state, + "mcp:tool_call_failed", + json!({ + "tool": "list_students", + "error": e + }), + ); + jsonrpc_ok( + id, + json!({ + "content": [ + { + "type": "text", + "text": format!("获取学生列表失败: {}", e) + } + ], + "isError": true + }), + ) + } }, Err(e) => { + mcp_log_error( + &app_state, + "mcp:tool_call_invalid_arguments", + json!({ + "tool": "list_students", + "error": e.to_string() + }), + ); jsonrpc_error(id, -32602, &format!("Invalid arguments: {}", e)) } } } - _ => jsonrpc_error(id, -32601, &format!("Unknown tool: {}", params.name)), + _ => { + mcp_log_error( + &app_state, + "mcp:tool_call_unknown_tool", + json!({ + "tool": params.name + }), + ); + jsonrpc_error(id, -32601, &format!("Unknown tool: {}", params.name)) + } }, } } @@ -313,6 +393,16 @@ async fn mcp_add_score( app_state: &Arc>, args: AddScoreArgs, ) -> Result { + mcp_log_info( + app_state, + "mcp:tool_call_started", + json!({ + "tool": "add_score", + "student_name": args.student_name.clone(), + "delta": args.delta + }), + ); + let student_name = args.student_name.trim(); if student_name.is_empty() { return Err("student_name 不能为空".to_string()); @@ -376,6 +466,18 @@ async fn mcp_add_score( realtime_dual_write_sync(app_state).await?; + mcp_log_info( + app_state, + "mcp:tool_call_succeeded", + json!({ + "tool": "add_score", + "student_name": student_name, + "delta": args.delta, + "event_id": inserted.id, + "event_uuid": event_uuid + }), + ); + Ok(AddScoreResult { event_id: inserted.id, event_uuid, @@ -392,6 +494,15 @@ async fn mcp_list_students( app_state: &Arc>, args: ListStudentsArgs, ) -> Result { + mcp_log_info( + app_state, + "mcp:tool_call_started", + json!({ + "tool": "list_students", + "limit": args.limit + }), + ); + let db_conn = { let state_guard = app_state.read(); let db_guard = state_guard.db.read(); @@ -419,6 +530,15 @@ async fn mcp_list_students( }) .collect::>(); + mcp_log_info( + app_state, + "mcp:tool_call_succeeded", + json!({ + "tool": "list_students", + "total": students.len() + }), + ); + Ok(ListStudentsResult { total: students.len(), students, @@ -431,9 +551,23 @@ pub async fn mcp_server_start( state: State<'_, Arc>>, ) -> Result, String> { check_admin_permission(&state)?; + mcp_log_info( + state.inner(), + "mcp:server_start_requested", + json!({ + "config": config.clone() + }), + ); let mut server_state = MCP_SERVER_STATE.lock().await; if server_state.is_running { + mcp_log_info( + state.inner(), + "mcp:server_already_running", + json!({ + "url": server_state.url.clone() + }), + ); return Ok(IpcResponse::failure_with_type( "MCP server is already running", )); @@ -482,6 +616,16 @@ pub async fn mcp_server_start( server_state.url = Some(url.clone()); server_state.shutdown_tx = Some(shutdown_tx); + mcp_log_info( + state.inner(), + "mcp:server_started", + json!({ + "url": url.clone(), + "host": server_state.config.host.clone(), + "port": server_state.config.port + }), + ); + Ok(IpcResponse::success(McpServerStartResult { url, config: server_state.config.clone(), @@ -493,10 +637,12 @@ pub async fn mcp_server_stop( state: State<'_, Arc>>, ) -> Result, String> { check_admin_permission(&state)?; + mcp_log_info(state.inner(), "mcp:server_stop_requested", json!({})); let mut server_state = MCP_SERVER_STATE.lock().await; if !server_state.is_running { + mcp_log_info(state.inner(), "mcp:server_already_stopped", json!({})); return Ok(IpcResponse::success_empty()); } @@ -507,14 +653,24 @@ pub async fn mcp_server_stop( server_state.is_running = false; server_state.url = None; + mcp_log_info(state.inner(), "mcp:server_stopped", json!({})); + Ok(IpcResponse::success_empty()) } #[tauri::command] pub async fn mcp_server_status( - _state: State<'_, Arc>>, + state: State<'_, Arc>>, ) -> Result, String> { let server_state = MCP_SERVER_STATE.lock().await; + mcp_log_info( + state.inner(), + "mcp:server_status_requested", + json!({ + "is_running": server_state.is_running, + "url": server_state.url.clone() + }), + ); Ok(IpcResponse::success(McpServerStatus { is_running: server_state.is_running,