fix(mcp): conditionally include body/upload in tool schemas, drop empty body on execution (#213)
* 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.
* style: cargo fmt and add changeset for MCP tool schema fix
* 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.
* docs: update changeset to include page_all conditional change
This commit is contained in:
@@ -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.
|
||||
+44
-17
@@ -426,27 +426,46 @@ fn walk_resources(prefix: &str, resources: &HashMap<String, RestResource>, 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!(
|
||||
|
||||
Reference in New Issue
Block a user