huskies: merge 590_story_gateway_native_mcp_tools_return_json_rpc_responses_missing_request_id

This commit is contained in:
dave
2026-04-16 11:37:30 +00:00
parent 4ddf2a4367
commit e734e80da5
2 changed files with 29 additions and 20 deletions
+4 -1
View File
@@ -44,7 +44,10 @@ fn strip_front_matter(text: &str) -> (String, String) {
parts.push("**QA:** human review required".to_string()); parts.push("**QA:** human review required".to_string());
} }
} else if line.starts_with("merge_failure:") { } else if line.starts_with("merge_failure:") {
let val = line.trim_start_matches("merge_failure:").trim().trim_matches('"'); let val = line
.trim_start_matches("merge_failure:")
.trim()
.trim_matches('"');
if !val.is_empty() { if !val.is_empty() {
parts.push(format!("**Merge failure:** {val}")); parts.push(format!("**Merge failure:** {val}"));
} }
+25 -19
View File
@@ -320,7 +320,9 @@ pub async fn gateway_mcp_post_handler(
.unwrap_or(""); .unwrap_or("");
if GATEWAY_TOOLS.contains(&tool_name) { if GATEWAY_TOOLS.contains(&tool_name) {
to_json_response(handle_gateway_tool(tool_name, &rpc.params, &state).await) to_json_response(
handle_gateway_tool(tool_name, &rpc.params, &state, rpc.id.clone()).await,
)
} else { } else {
// Proxy to active project's container. // Proxy to active project's container.
match proxy_mcp_call(&state, &bytes).await { match proxy_mcp_call(&state, &bytes).await {
@@ -482,18 +484,22 @@ async fn handle_gateway_tool(
tool_name: &str, tool_name: &str,
params: &Value, params: &Value,
state: &GatewayState, state: &GatewayState,
id: Option<Value>,
) -> JsonRpcResponse { ) -> JsonRpcResponse {
let id = None; // The caller wraps this in a proper response.
match tool_name { match tool_name {
"switch_project" => handle_switch_project(params, state).await, "switch_project" => handle_switch_project(params, state, id).await,
"gateway_status" => handle_gateway_status(state).await, "gateway_status" => handle_gateway_status(state, id).await,
"gateway_health" => handle_gateway_health(state).await, "gateway_health" => handle_gateway_health(state, id).await,
_ => JsonRpcResponse::error(id, -32601, format!("Unknown gateway tool: {tool_name}")), _ => JsonRpcResponse::error(id, -32601, format!("Unknown gateway tool: {tool_name}")),
} }
} }
/// Switch the active project. /// Switch the active project.
async fn handle_switch_project(params: &Value, state: &GatewayState) -> JsonRpcResponse { async fn handle_switch_project(
params: &Value,
state: &GatewayState,
id: Option<Value>,
) -> JsonRpcResponse {
let project = params let project = params
.get("arguments") .get("arguments")
.and_then(|a| a.get("project")) .and_then(|a| a.get("project"))
@@ -502,7 +508,7 @@ async fn handle_switch_project(params: &Value, state: &GatewayState) -> JsonRpcR
.unwrap_or(""); .unwrap_or("");
if project.is_empty() { if project.is_empty() {
return JsonRpcResponse::error(None, -32602, "missing required parameter: project".into()); return JsonRpcResponse::error(id, -32602, "missing required parameter: project".into());
} }
let url = { let url = {
@@ -510,7 +516,7 @@ async fn handle_switch_project(params: &Value, state: &GatewayState) -> JsonRpcR
if !projects.contains_key(project) { if !projects.contains_key(project) {
let available: Vec<&str> = projects.keys().map(|s| s.as_str()).collect(); let available: Vec<&str> = projects.keys().map(|s| s.as_str()).collect();
return JsonRpcResponse::error( return JsonRpcResponse::error(
None, id,
-32602, -32602,
format!( format!(
"unknown project '{project}'. Available: {}", "unknown project '{project}'. Available: {}",
@@ -524,7 +530,7 @@ async fn handle_switch_project(params: &Value, state: &GatewayState) -> JsonRpcR
*state.active_project.write().await = project.to_string(); *state.active_project.write().await = project.to_string();
JsonRpcResponse::success( JsonRpcResponse::success(
None, id,
json!({ json!({
"content": [{ "content": [{
"type": "text", "type": "text",
@@ -535,11 +541,11 @@ async fn handle_switch_project(params: &Value, state: &GatewayState) -> JsonRpcR
} }
/// Show pipeline status for the active project by proxying `get_pipeline_status`. /// Show pipeline status for the active project by proxying `get_pipeline_status`.
async fn handle_gateway_status(state: &GatewayState) -> JsonRpcResponse { async fn handle_gateway_status(state: &GatewayState, id: Option<Value>) -> JsonRpcResponse {
let active = state.active_project.read().await.clone(); let active = state.active_project.read().await.clone();
let url = match state.active_url().await { let url = match state.active_url().await {
Ok(u) => u, Ok(u) => u,
Err(e) => return JsonRpcResponse::error(None, -32603, e), Err(e) => return JsonRpcResponse::error(id.clone(), -32603, e),
}; };
let mcp_url = format!("{}/mcp", url.trim_end_matches('/')); let mcp_url = format!("{}/mcp", url.trim_end_matches('/'));
@@ -560,7 +566,7 @@ async fn handle_gateway_status(state: &GatewayState) -> JsonRpcResponse {
// Extract the result from the upstream response and wrap it. // Extract the result from the upstream response and wrap it.
let pipeline = upstream.get("result").cloned().unwrap_or(json!(null)); let pipeline = upstream.get("result").cloned().unwrap_or(json!(null));
JsonRpcResponse::success( JsonRpcResponse::success(
None, id,
json!({ json!({
"content": [{ "content": [{
"type": "text", "type": "text",
@@ -573,16 +579,16 @@ async fn handle_gateway_status(state: &GatewayState) -> JsonRpcResponse {
) )
} }
Err(e) => { Err(e) => {
JsonRpcResponse::error(None, -32603, format!("invalid upstream response: {e}")) JsonRpcResponse::error(id, -32603, format!("invalid upstream response: {e}"))
} }
} }
} }
Err(e) => JsonRpcResponse::error(None, -32603, format!("failed to reach {mcp_url}: {e}")), Err(e) => JsonRpcResponse::error(id, -32603, format!("failed to reach {mcp_url}: {e}")),
} }
} }
/// Aggregate health checks across all registered projects. /// Aggregate health checks across all registered projects.
async fn handle_gateway_health(state: &GatewayState) -> JsonRpcResponse { async fn handle_gateway_health(state: &GatewayState, id: Option<Value>) -> JsonRpcResponse {
let mut results = BTreeMap::new(); let mut results = BTreeMap::new();
let project_entries: Vec<(String, String)> = state let project_entries: Vec<(String, String)> = state
@@ -609,7 +615,7 @@ async fn handle_gateway_health(state: &GatewayState) -> JsonRpcResponse {
let active = state.active_project.read().await.clone(); let active = state.active_project.read().await.clone();
JsonRpcResponse::success( JsonRpcResponse::success(
None, id,
json!({ json!({
"content": [{ "content": [{
"type": "text", "type": "text",
@@ -1104,7 +1110,7 @@ pub async fn gateway_switch_handler(
body: Json<SwitchRequest>, body: Json<SwitchRequest>,
) -> Response { ) -> Response {
let params = json!({ "arguments": { "project": body.project } }); let params = json!({ "arguments": { "project": body.project } });
let resp = handle_switch_project(&params, &state).await; let resp = handle_switch_project(&params, &state, None).await;
let (ok, error) = if resp.result.is_some() { let (ok, error) = if resp.result.is_some() {
(true, None) (true, None)
@@ -1907,7 +1913,7 @@ url = "http://localhost:3002"
let state = GatewayState::new(config, PathBuf::from("."), 3000).unwrap(); let state = GatewayState::new(config, PathBuf::from("."), 3000).unwrap();
let params = json!({ "arguments": { "project": "beta" } }); let params = json!({ "arguments": { "project": "beta" } });
let resp = handle_switch_project(&params, &state).await; let resp = handle_switch_project(&params, &state, None).await;
assert!(resp.result.is_some()); assert!(resp.result.is_some());
let active = state.active_project.read().await.clone(); let active = state.active_project.read().await.clone();
@@ -1927,7 +1933,7 @@ url = "http://localhost:3002"
let state = GatewayState::new(config, PathBuf::from("."), 3000).unwrap(); let state = GatewayState::new(config, PathBuf::from("."), 3000).unwrap();
let params = json!({ "arguments": { "project": "nonexistent" } }); let params = json!({ "arguments": { "project": "nonexistent" } });
let resp = handle_switch_project(&params, &state).await; let resp = handle_switch_project(&params, &state, None).await;
assert!(resp.error.is_some()); assert!(resp.error.is_some());
} }