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:
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user