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 <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-03-04 14:43:34 +01:00
parent 463b2cd130
commit 667e480c09

View File

@@ -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<String, anyhow::Error> {
// 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<String> {
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]