diff --git a/.changeset/mcp-tool-schema-body-upload.md b/.changeset/mcp-tool-schema-body-upload.md new file mode 100644 index 0000000..4d1b883 --- /dev/null +++ b/.changeset/mcp-tool-schema-body-upload.md @@ -0,0 +1,5 @@ +--- +"@googleworkspace/cli": patch +--- + +Fix MCP tool schemas to conditionally include `body`, `upload`, and `page_all` properties only when the underlying Discovery Document method supports them. `body` is included only when a request body is defined, `upload` only when `supportsMediaUpload` is true, and `page_all` only when the method has a `pageToken` parameter. Also drops empty `body: {}` objects that LLMs commonly send on GET methods, preventing 400 errors from Google APIs. diff --git a/src/mcp_server.rs b/src/mcp_server.rs index 331e70d..9e8e6ad 100644 --- a/src/mcp_server.rs +++ b/src/mcp_server.rs @@ -426,27 +426,46 @@ fn walk_resources(prefix: &str, resources: &HashMap, tools description = format!("Execute the {} Google API method", tool_name); } - // Generate JSON Schema for MCP input - let input_schema = json!({ - "type": "object", - "properties": { - "params": { - "type": "object", - "description": "Query or path parameters (e.g. fileId, q, pageSize)" - }, - "body": { + // Generate JSON Schema for MCP input — only include body/upload + // when the Discovery Document method actually supports them. + let mut properties = serde_json::Map::new(); + properties.insert( + "params".to_string(), + json!({ + "type": "object", + "description": "Query or path parameters (e.g. fileId, q, pageSize)" + }), + ); + if method.request.is_some() { + properties.insert( + "body".to_string(), + json!({ "type": "object", "description": "Request body API object" - }, - "upload": { + }), + ); + } + if method.supports_media_upload { + properties.insert( + "upload".to_string(), + json!({ "type": "string", "description": "Local file path to upload as media content" - }, - "page_all": { + }), + ); + } + if method.parameters.contains_key("pageToken") { + properties.insert( + "page_all".to_string(), + json!({ "type": "boolean", "description": "Auto-paginate, returning all pages" - } - } + }), + ); + } + let input_schema = json!({ + "type": "object", + "properties": properties }); tools.push(json!({ @@ -756,13 +775,21 @@ async fn execute_mcp_method( .transpose() .map_err(|e| GwsError::Validation(format!("Failed to serialize params: {e}")))?; - let body_json_val = arguments.get("body"); + // Drop empty body objects — LLMs commonly send "body": {} even on GET + // methods, which causes Google APIs to return 400. + let body_json_val = arguments + .get("body") + .filter(|v| !v.as_object().is_some_and(|m| m.is_empty())); let body_str = body_json_val .map(serde_json::to_string) .transpose() .map_err(|e| GwsError::Validation(format!("Failed to serialize body: {e}")))?; - let upload_path = if let Some(raw) = arguments.get("upload").and_then(|v| v.as_str()) { + let upload_path = if let Some(raw) = arguments + .get("upload") + .and_then(|v| v.as_str()) + .filter(|s| !s.is_empty()) + { let p = std::path::Path::new(raw); if p.is_absolute() || p.components().any(|c| c == std::path::Component::ParentDir) { return Err(GwsError::Validation(format!(