From 667e480c09108362ce8394bb1824ca16c04d54d1 Mon Sep 17 00:00:00 2001 From: jgrusewski Date: Wed, 4 Mar 2026 14:43:34 +0100 Subject: [PATCH] fix(fxt): replace MCP server stubs with honest not_implemented errors All 30 MCP tool handlers now return structured JSON error with isError=true instead of fake "stub: would ..." placeholder responses. Tool registrations preserved so list_tools still works. Co-Authored-By: Claude Opus 4.6 --- bin/fxt/src/mcp/server.rs | 228 ++++++++++++-------------------------- 1 file changed, 73 insertions(+), 155 deletions(-) diff --git a/bin/fxt/src/mcp/server.rs b/bin/fxt/src/mcp/server.rs index 9fd8e2309..829406859 100644 --- a/bin/fxt/src/mcp/server.rs +++ b/bin/fxt/src/mcp/server.rs @@ -141,8 +141,8 @@ impl McpServer { /// Handle `tools/call` -- execute a tool and return the result. /// - /// Currently returns stub responses; real implementations will use - /// the same gRPC calls as the CLI commands. + /// Tools return a "not implemented" error until the gRPC client + /// integration is wired up. async fn handle_tools_call(&self, id: Value, params: &Value) -> JsonRpcResponse { let Some(tool_name) = params.get("name").and_then(Value::as_str) else { return error_response(id, INVALID_PARAMS, "missing 'name' in tools/call params"); @@ -164,7 +164,6 @@ impl McpServer { ); } - // Execute the tool (stub for now). let result = self.execute_tool(tool_name, &arguments).await; match result { @@ -195,180 +194,89 @@ impl McpServer { /// Execute a tool by name with the given arguments. /// - /// Stub implementation: returns a placeholder message for each tool. - /// Real implementations will use gRPC calls to the API Gateway, - /// reusing the same client logic as the CLI commands. + /// All tools currently return a "not implemented" error because the + /// gRPC client integration is not yet wired up. Tool *registrations* + /// remain so that `tools/list` still advertises them; callers get an + /// honest error instead of fake data. async fn execute_tool( &self, tool_name: &str, - arguments: &Value, + _arguments: &Value, ) -> Result { - // Dispatch by tool name. For now, return informative stubs that - // indicate what will eventually happen. - let response = match tool_name { + // Every known tool maps to a specific gRPC endpoint that is not yet + // connected. Return a structured error so MCP clients can distinguish + // "not implemented" from a transient failure. + let grpc_method = match tool_name { // ── Service ───────────────────────────────────────────── - "fxt_service_list" => "stub: would list all services via gRPC ServiceDiscovery".into(), - "fxt_service_status" => { - let svc = arg_str(arguments, "service").unwrap_or_default(); - format!("stub: would get status of service '{svc}' via gRPC") - } - "fxt_service_health" => { - "stub: would health-check all services via gRPC Health/Check".into() - } + "fxt_service_list" => "ServiceDiscovery/ListServices", + "fxt_service_status" => "ServiceDiscovery/GetServiceStatus", + "fxt_service_health" => "Health/Check", // ── Training ──────────────────────────────────────────── - "fxt_train_start" => { - let model = arg_str(arguments, "model").unwrap_or_default(); - let symbol = arg_str(arguments, "symbol") - .unwrap_or_else(|| "ES.FUT".into()); - format!("stub: would start training model={model} symbol={symbol} via gRPC MLTraining/StartTraining") - } - "fxt_train_stop" => { - let job_id = arg_str(arguments, "job_id").unwrap_or_default(); - format!("stub: would stop training job={job_id} via gRPC MLTraining/StopTraining") - } - "fxt_train_status" => { - let job_id = arg_str(arguments, "job_id") - .unwrap_or_else(|| "latest".into()); - format!( - "stub: would get training status job={job_id} via gRPC MLTraining/GetTrainingStatus" - ) - } - "fxt_train_list" => { - "stub: would list training jobs via gRPC MLTraining/ListTrainingJobs".into() - } + "fxt_train_start" => "MLTraining/StartTraining", + "fxt_train_stop" => "MLTraining/StopTraining", + "fxt_train_status" => "MLTraining/GetTrainingStatus", + "fxt_train_list" => "MLTraining/ListTrainingJobs", // ── Trading ───────────────────────────────────────────── - "fxt_trade_positions" => { - "stub: would get positions via gRPC TradingService/GetPositions".into() - } - "fxt_trade_orders" => { - "stub: would list orders via gRPC TradingService/GetOrders".into() - } - "fxt_trade_account" => { - "stub: would get account state via gRPC TradingService/GetAccountState".into() - } - "fxt_trade_submit" => { - let symbol = arg_str(arguments, "symbol").unwrap_or_default(); - let side = arg_str(arguments, "side").unwrap_or_default(); - let qty = arguments - .get("quantity") - .and_then(Value::as_f64) - .unwrap_or(0.0); - let otype = arg_str(arguments, "order_type").unwrap_or_default(); - format!( - "stub: would submit order symbol={symbol} side={side} qty={qty} type={otype} via gRPC TradingService/SubmitOrder" - ) - } + "fxt_trade_positions" => "TradingService/GetPositions", + "fxt_trade_orders" => "TradingService/GetOrders", + "fxt_trade_account" => "TradingService/GetAccountState", + "fxt_trade_submit" => "TradingService/SubmitOrder", // ── Models ────────────────────────────────────────────── - "fxt_model_list" => "stub: would list models via gRPC MLService/ListModels".into(), - "fxt_model_status" => { - let model = arg_str(arguments, "model").unwrap_or_default(); - format!("stub: would get model status model={model} via gRPC MLService/GetModelStatus") - } - "fxt_model_predict" => { - let model = arg_str(arguments, "model").unwrap_or_default(); - let symbol = arg_str(arguments, "symbol").unwrap_or_default(); - format!( - "stub: would get prediction model={model} symbol={symbol} via gRPC MLService/Predict" - ) - } - "fxt_model_ensemble" => { - let symbol = arg_str(arguments, "symbol").unwrap_or_default(); - format!( - "stub: would get ensemble vote symbol={symbol} via gRPC MLService/GetEnsembleVote" - ) - } + "fxt_model_list" => "MLService/ListModels", + "fxt_model_status" => "MLService/GetModelStatus", + "fxt_model_predict" => "MLService/Predict", + "fxt_model_ensemble" => "MLService/GetEnsembleVote", // ── Broker ────────────────────────────────────────────── - "fxt_broker_status" => { - "stub: would get broker status via gRPC BrokerGateway/GetSessionStatus".into() - } - "fxt_broker_connect" => { - "stub: would connect broker via gRPC BrokerGateway/Connect".into() - } + "fxt_broker_status" => "BrokerGateway/GetSessionStatus", + "fxt_broker_connect" => "BrokerGateway/Connect", // ── Risk ──────────────────────────────────────────────── - "fxt_risk_status" => { - "stub: would get risk status via gRPC RiskService/GetRiskStatus".into() - } - "fxt_risk_limits" => { - "stub: would get risk limits via gRPC RiskService/GetLimits".into() - } - "fxt_risk_drawdown" => { - "stub: would get drawdown stats via gRPC RiskService/GetDrawdown".into() - } - "fxt_risk_emergency" => { - let action = arg_str(arguments, "action").unwrap_or_default(); - format!( - "stub: would {action} trading via gRPC RiskService/EmergencyAction" - ) - } + "fxt_risk_status" => "RiskService/GetRiskStatus", + "fxt_risk_limits" => "RiskService/GetLimits", + "fxt_risk_drawdown" => "RiskService/GetDrawdown", + "fxt_risk_emergency" => "RiskService/EmergencyAction", // ── Data ──────────────────────────────────────────────── - "fxt_data_status" => { - "stub: would get data status via gRPC DataAcquisition/GetStatus".into() - } - "fxt_data_feeds" => { - "stub: would list data feeds via gRPC DataAcquisition/ListFeeds".into() - } + "fxt_data_status" => "DataAcquisition/GetStatus", + "fxt_data_feeds" => "DataAcquisition/ListFeeds", // ── Cluster ───────────────────────────────────────────── - "fxt_cluster_status" => { - "stub: would get cluster status via gRPC Monitoring/GetClusterStatus".into() - } - "fxt_cluster_resources" => { - "stub: would get resource utilization via gRPC Monitoring/GetResources".into() - } + "fxt_cluster_status" => "Monitoring/GetClusterStatus", + "fxt_cluster_resources" => "Monitoring/GetResources", // ── Agent ─────────────────────────────────────────────── - "fxt_agent_start" => { - "stub: would start agent via gRPC TradingAgent/StartAgent".into() - } - "fxt_agent_stop" => { - "stub: would stop agent via gRPC TradingAgent/StopAgent".into() - } - "fxt_agent_status" => { - "stub: would get agent status via gRPC TradingAgent/GetAgentStatus".into() - } + "fxt_agent_start" => "TradingAgent/StartAgent", + "fxt_agent_stop" => "TradingAgent/StopAgent", + "fxt_agent_status" => "TradingAgent/GetAgentStatus", // ── Config ────────────────────────────────────────────── - "fxt_config_get" => { - let key = arg_str(arguments, "key").unwrap_or_default(); - format!("stub: would get config key={key} via gRPC ConfigService/GetConfig") - } - "fxt_config_set" => { - let key = arg_str(arguments, "key").unwrap_or_default(); - let val = arg_str(arguments, "value").unwrap_or_default(); - format!( - "stub: would set config key={key} value={val} via gRPC ConfigService/SetConfig" - ) - } + "fxt_config_get" => "ConfigService/GetConfig", + "fxt_config_set" => "ConfigService/SetConfig", // ── Tune ──────────────────────────────────────────────── - "fxt_tune_start" => { - let model = arg_str(arguments, "model").unwrap_or_default(); - format!( - "stub: would start tuning model={model} via gRPC MLTraining/StartHyperopt" - ) - } - "fxt_tune_status" => { - "stub: would get tuning progress via gRPC MLTraining/GetHyperoptStatus".into() - } + "fxt_tune_start" => "MLTraining/StartHyperopt", + "fxt_tune_status" => "MLTraining/GetHyperoptStatus", _ => return Err(anyhow::anyhow!("unknown tool: {tool_name}")), }; - Ok(response) + Err(anyhow::anyhow!( + "{}", + json!({ + "error": "not_implemented", + "tool": tool_name, + "message": format!( + "This tool is not yet connected to the API Gateway (gRPC {grpc_method})" + ) + }) + )) } } -/// Extract a string argument from the arguments object. -fn arg_str(args: &Value, key: &str) -> Option { - args.get(key).and_then(Value::as_str).map(String::from) -} - /// Write a JSON-RPC response as a single line to stdout. async fn write_response( writer: &mut tokio::io::Stdout, @@ -436,7 +344,7 @@ mod tests { } #[tokio::test] - async fn tools_call_known_tool() { + async fn tools_call_known_tool_returns_not_implemented() { let server = make_server(); let req = make_request( "tools/call", @@ -444,10 +352,13 @@ mod tests { json!({"name": "fxt_service_list", "arguments": {}}), ); let resp = server.handle_request(&req).await.unwrap(); + // Tool-level error: protocol success with isError flag. assert!(resp.error.is_none()); let result = resp.result.unwrap(); + assert_eq!(result["isError"], true); let text = result["content"][0]["text"].as_str().unwrap(); - assert!(text.contains("stub")); + assert!(text.contains("not_implemented")); + assert!(text.contains("fxt_service_list")); } #[tokio::test] @@ -482,7 +393,7 @@ mod tests { } #[tokio::test] - async fn tools_call_with_arguments() { + async fn tools_call_with_arguments_returns_not_implemented() { let server = make_server(); let req = make_request( "tools/call", @@ -494,13 +405,15 @@ mod tests { ); let resp = server.handle_request(&req).await.unwrap(); let result = resp.result.unwrap(); + assert_eq!(result["isError"], true); let text = result["content"][0]["text"].as_str().unwrap(); - assert!(text.contains("dqn")); - assert!(text.contains("NQ.FUT")); + assert!(text.contains("not_implemented")); + assert!(text.contains("fxt_train_start")); + assert!(text.contains("MLTraining/StartTraining")); } #[tokio::test] - async fn tools_call_trade_submit() { + async fn tools_call_trade_submit_returns_not_implemented() { let server = make_server(); let req = make_request( "tools/call", @@ -517,9 +430,11 @@ mod tests { ); let resp = server.handle_request(&req).await.unwrap(); let result = resp.result.unwrap(); + assert_eq!(result["isError"], true); let text = result["content"][0]["text"].as_str().unwrap(); - assert!(text.contains("ES.FUT")); - assert!(text.contains("buy")); + assert!(text.contains("not_implemented")); + assert!(text.contains("fxt_trade_submit")); + assert!(text.contains("TradingService/SubmitOrder")); } #[tokio::test] @@ -533,7 +448,7 @@ mod tests { } #[tokio::test] - async fn tool_execution_risk_emergency() { + async fn tool_execution_risk_emergency_returns_not_implemented() { let server = make_server(); let req = make_request( "tools/call", @@ -545,8 +460,11 @@ mod tests { ); let resp = server.handle_request(&req).await.unwrap(); let result = resp.result.unwrap(); + assert_eq!(result["isError"], true); let text = result["content"][0]["text"].as_str().unwrap(); - assert!(text.contains("halt")); + assert!(text.contains("not_implemented")); + assert!(text.contains("fxt_risk_emergency")); + assert!(text.contains("RiskService/EmergencyAction")); } #[tokio::test]