From 7f4c283573a4fcb505e6189935d501c48927646a Mon Sep 17 00:00:00 2001 From: Shreyas Karnik Date: Thu, 5 Mar 2026 13:55:50 -0800 Subject: [PATCH 1/4] fix(mcp): conditionally include body/upload in full-mode tool schemas and drop empty body on execution Full-mode tool schemas now only include `body` when the Discovery Document method defines a request body, and `upload` when `supportsMediaUpload` is true. This prevents LLMs from hallucinating these fields on GET-only methods. Additionally, empty body objects (`{}`) are filtered out before execution in both compact and full modes, and empty upload strings are ignored. LLMs commonly send "body": {} on read-only methods, which causes Google APIs to return HTTP 400. --- src/mcp_server.rs | 51 ++++++++++++++++++++++++++++------------------- 1 file changed, 30 insertions(+), 21 deletions(-) diff --git a/src/mcp_server.rs b/src/mcp_server.rs index 331e70d..ba579e2 100644 --- a/src/mcp_server.rs +++ b/src/mcp_server.rs @@ -426,27 +426,32 @@ fn walk_resources(prefix: &str, resources: &HashMap, tools description = format!("Execute the {} Google API method", tool_name); } - // Generate JSON Schema for MCP input + // 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" + })); + } + if method.supports_media_upload { + properties.insert("upload".to_string(), json!({ + "type": "string", + "description": "Local file path to upload as media content" + })); + } + properties.insert("page_all".to_string(), json!({ + "type": "boolean", + "description": "Auto-paginate, returning all pages" + })); let input_schema = json!({ "type": "object", - "properties": { - "params": { - "type": "object", - "description": "Query or path parameters (e.g. fileId, q, pageSize)" - }, - "body": { - "type": "object", - "description": "Request body API object" - }, - "upload": { - "type": "string", - "description": "Local file path to upload as media content" - }, - "page_all": { - "type": "boolean", - "description": "Auto-paginate, returning all pages" - } - } + "properties": properties }); tools.push(json!({ @@ -756,13 +761,17 @@ 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!( From dc44f5d71eba65b0948152740284e88474e65414 Mon Sep 17 00:00:00 2001 From: Shreyas Karnik Date: Thu, 5 Mar 2026 15:05:34 -0800 Subject: [PATCH 2/4] style: cargo fmt and add changeset for MCP tool schema fix --- .changeset/mcp-tool-schema-body-upload.md | 5 ++ src/auth.rs | 28 +++++++++--- src/mcp_server.rs | 56 +++++++++++++++-------- 3 files changed, 62 insertions(+), 27 deletions(-) create mode 100644 .changeset/mcp-tool-schema-body-upload.md diff --git a/.changeset/mcp-tool-schema-body-upload.md b/.changeset/mcp-tool-schema-body-upload.md new file mode 100644 index 0000000..b38b6d3 --- /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` and `upload` properties only when the underlying Discovery Document method supports them. Also drops empty `body: {}` objects that LLMs commonly send on GET methods, preventing 400 errors from Google APIs. diff --git a/src/auth.rs b/src/auth.rs index 10af238..19c6a5c 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -225,20 +225,31 @@ async fn get_token_inner( /// - anything else (including `"authorized_user"`) → [`Credential::AuthorizedUser`] /// /// Uses the already-parsed `serde_json::Value` to avoid a second string parse. -async fn parse_credential_file(path: &std::path::Path, content: &str) -> anyhow::Result { +async fn parse_credential_file( + path: &std::path::Path, + content: &str, +) -> anyhow::Result { let json: serde_json::Value = serde_json::from_str(content) .with_context(|| format!("Failed to parse credentials JSON at {}", path.display()))?; if json.get("type").and_then(|v| v.as_str()) == Some("service_account") { - let key = yup_oauth2::parse_service_account_key(content) - .with_context(|| format!("Failed to parse service account key from {}", path.display()))?; + let key = yup_oauth2::parse_service_account_key(content).with_context(|| { + format!( + "Failed to parse service account key from {}", + path.display() + ) + })?; return Ok(Credential::ServiceAccount(key)); } // Deserialize from the Value we already have — avoids a second string parse. - let secret: yup_oauth2::authorized_user::AuthorizedUserSecret = - serde_json::from_value(json) - .with_context(|| format!("Failed to parse authorized user credentials from {}", path.display()))?; + let secret: yup_oauth2::authorized_user::AuthorizedUserSecret = serde_json::from_value(json) + .with_context(|| { + format!( + "Failed to parse authorized user credentials from {}", + path.display() + ) + })?; Ok(Credential::AuthorizedUser(secret)) } @@ -422,7 +433,10 @@ mod tests { match res.unwrap() { Credential::ServiceAccount(key) => { - assert_eq!(key.client_email, "adc-sa@test-project.iam.gserviceaccount.com"); + assert_eq!( + key.client_email, + "adc-sa@test-project.iam.gserviceaccount.com" + ); } _ => panic!("Expected ServiceAccount from ADC"), } diff --git a/src/mcp_server.rs b/src/mcp_server.rs index ba579e2..93667f0 100644 --- a/src/mcp_server.rs +++ b/src/mcp_server.rs @@ -429,26 +429,38 @@ fn walk_resources(prefix: &str, resources: &HashMap, tools // 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!({ + properties.insert( + "params".to_string(), + json!({ "type": "object", - "description": "Request body API 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" + }), + ); } if method.supports_media_upload { - properties.insert("upload".to_string(), json!({ - "type": "string", - "description": "Local file path to upload as media content" - })); + properties.insert( + "upload".to_string(), + json!({ + "type": "string", + "description": "Local file path to upload as media content" + }), + ); } - properties.insert("page_all".to_string(), json!({ - "type": "boolean", - "description": "Auto-paginate, returning all pages" - })); + properties.insert( + "page_all".to_string(), + json!({ + "type": "boolean", + "description": "Auto-paginate, returning all pages" + }), + ); let input_schema = json!({ "type": "object", "properties": properties @@ -763,15 +775,19 @@ async fn execute_mcp_method( // 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_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()).filter(|s| !s.is_empty()) { + 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!( From aeb658c1e264211b088b1c01599f749e47214b2e Mon Sep 17 00:00:00 2001 From: Shreyas Karnik Date: Thu, 5 Mar 2026 15:25:04 -0800 Subject: [PATCH 3/4] fix(mcp): conditionally include page_all only for paginated methods Only include the page_all property in full-mode tool schemas when the method has a pageToken parameter, preventing LLMs from attempting pagination on non-paginable methods. --- src/mcp_server.rs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/mcp_server.rs b/src/mcp_server.rs index 93667f0..9e8e6ad 100644 --- a/src/mcp_server.rs +++ b/src/mcp_server.rs @@ -454,13 +454,15 @@ fn walk_resources(prefix: &str, resources: &HashMap, tools }), ); } - properties.insert( - "page_all".to_string(), - json!({ - "type": "boolean", - "description": "Auto-paginate, returning all pages" - }), - ); + 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 From a073e1d36ec3650eddfb5d3f0f5f0dd9dec7bd42 Mon Sep 17 00:00:00 2001 From: Shreyas Karnik Date: Thu, 5 Mar 2026 15:26:44 -0800 Subject: [PATCH 4/4] docs: update changeset to include page_all conditional change --- .changeset/mcp-tool-schema-body-upload.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/mcp-tool-schema-body-upload.md b/.changeset/mcp-tool-schema-body-upload.md index b38b6d3..4d1b883 100644 --- a/.changeset/mcp-tool-schema-body-upload.md +++ b/.changeset/mcp-tool-schema-body-upload.md @@ -2,4 +2,4 @@ "@googleworkspace/cli": patch --- -Fix MCP tool schemas to conditionally include `body` and `upload` properties only when the underlying Discovery Document method supports them. Also drops empty `body: {}` objects that LLMs commonly send on GET methods, preventing 400 errors from Google APIs. +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.