From ddd8261d5583a25f241796b6aac42f76855a7016 Mon Sep 17 00:00:00 2001 From: jpoehnelt-bot Date: Thu, 5 Mar 2026 18:29:12 -0700 Subject: [PATCH 1/4] fix(mcp): switch tool names from underscore to hyphen separator Fixes #235, fixes #162 - Switch separator from '_' to '-' (e.g., drive-files-list) - Use service alias instead of Discovery doc name for tool prefix - Hyphens are unambiguous since Google API names never contain them - Update workflow tool names and gws-discover meta-tool - Update tests --- .changeset/mcp-hyphen-tool-names.md | 7 +++++++ src/mcp_server.rs | 30 ++++++++++++++--------------- 2 files changed, 22 insertions(+), 15 deletions(-) create mode 100644 .changeset/mcp-hyphen-tool-names.md diff --git a/.changeset/mcp-hyphen-tool-names.md b/.changeset/mcp-hyphen-tool-names.md new file mode 100644 index 0000000..edb8a43 --- /dev/null +++ b/.changeset/mcp-hyphen-tool-names.md @@ -0,0 +1,7 @@ +--- +"@googleworkspace/cli": minor +--- + +Switch MCP tool names from underscore to hyphen separator (e.g., `drive-files-list` instead of `drive_files_list`). This resolves parsing ambiguity for services/resources with underscores in their names like `admin_reports`. Also fixes the alias mismatch where `tools/list` used Discovery doc names instead of configured service aliases. + +**Breaking:** MCP tool names have changed format. Well-behaved clients that discover tools via `tools/list` will pick up new names automatically. diff --git a/src/mcp_server.rs b/src/mcp_server.rs index 4fb8e3e..590cfaa 100644 --- a/src/mcp_server.rs +++ b/src/mcp_server.rs @@ -243,7 +243,7 @@ async fn build_tools_list(config: &ServerConfig) -> Result, GwsError> let (api_name, version) = crate::parse_service_and_version(&[svc_name.to_string()], svc_name)?; if let Ok(doc) = crate::discovery::fetch_discovery_document(&api_name, &version).await { - walk_resources(&doc.name, &doc.resources, &mut tools); + walk_resources(svc_name, &doc.resources, &mut tools); } else { eprintln!("[gws mcp] Warning: Failed to load discovery document for service '{}'. It will not be available as a tool.", svc_name); } @@ -327,7 +327,7 @@ async fn build_compact_tools_list(config: &ServerConfig) -> Result, G // Add gws_discover meta-tool tools.push(json!({ - "name": "gws_discover", + "name": "gws-discover", "description": "Query available resources, methods, and parameter schemas for any enabled service. Call with service only to list resources; add resource to list methods; add method to get full parameter schema.", "inputSchema": { "type": "object", @@ -359,7 +359,7 @@ async fn build_compact_tools_list(config: &ServerConfig) -> Result, G fn append_workflow_tools(tools: &mut Vec) { tools.push(json!({ - "name": "workflow_standup_report", + "name": "workflow-standup-report", "description": "Today's meetings + open tasks as a standup summary", "inputSchema": { "type": "object", @@ -369,7 +369,7 @@ fn append_workflow_tools(tools: &mut Vec) { } })); tools.push(json!({ - "name": "workflow_meeting_prep", + "name": "workflow-meeting-prep", "description": "Prepare for your next meeting: agenda, attendees, and linked docs", "inputSchema": { "type": "object", @@ -379,7 +379,7 @@ fn append_workflow_tools(tools: &mut Vec) { } })); tools.push(json!({ - "name": "workflow_email_to_task", + "name": "workflow-email-to-task", "description": "Convert a Gmail message into a Google Tasks entry", "inputSchema": { "type": "object", @@ -391,7 +391,7 @@ fn append_workflow_tools(tools: &mut Vec) { } })); tools.push(json!({ - "name": "workflow_weekly_digest", + "name": "workflow-weekly-digest", "description": "Weekly summary: this week's meetings + unread email count", "inputSchema": { "type": "object", @@ -401,7 +401,7 @@ fn append_workflow_tools(tools: &mut Vec) { } })); tools.push(json!({ - "name": "workflow_file_announce", + "name": "workflow-file-announce", "description": "Announce a Drive file in a Chat space", "inputSchema": { "type": "object", @@ -417,10 +417,10 @@ fn append_workflow_tools(tools: &mut Vec) { fn walk_resources(prefix: &str, resources: &HashMap, tools: &mut Vec) { for (res_name, res) in resources { - let new_prefix = format!("{}_{}", prefix, res_name); + let new_prefix = format!("{}-{}", prefix, res_name); for (method_name, method) in &res.methods { - let tool_name = format!("{}_{}", new_prefix, method_name); + let tool_name = format!("{}-{}", new_prefix, method_name); let mut description = method.description.clone().unwrap_or_default(); if description.is_empty() { description = format!("Execute the {} Google API method", tool_name); @@ -664,13 +664,13 @@ async fn handle_tools_call(params: &Value, config: &ServerConfig) -> Result Result = tool_name.split('_').collect(); + // Full mode: tool_name encodes service-resource-method (e.g., drive-files-list) + let parts: Vec<&str> = tool_name.split('-').collect(); if parts.len() < 3 { return Err(GwsError::Validation(format!( "Invalid API tool name: {}", @@ -1146,7 +1146,7 @@ mod tests { let mut tools = Vec::new(); append_workflow_tools(&mut tools); assert_eq!(tools.len(), 5); - assert_eq!(tools[0]["name"], "workflow_standup_report"); - assert_eq!(tools[4]["name"], "workflow_file_announce"); + assert_eq!(tools[0]["name"], "workflow-standup-report"); + assert_eq!(tools[4]["name"], "workflow-file-announce"); } } From 422496585f78fe8bd9cbf08b12a6aa9478b289e5 Mon Sep 17 00:00:00 2001 From: jpoehnelt-bot Date: Thu, 5 Mar 2026 18:54:19 -0700 Subject: [PATCH 2/4] fix: use prefix-matching for service alias in tool name parsing Addresses review comment: naive split on '-' broke hyphenated aliases like admin-reports. Now finds the enabled service that is a prefix of the tool name, then splits only the remainder for resource/method. --- src/mcp_server.rs | 35 ++++++++++++++++++++++------------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/src/mcp_server.rs b/src/mcp_server.rs index 590cfaa..980e2b6 100644 --- a/src/mcp_server.rs +++ b/src/mcp_server.rs @@ -715,20 +715,29 @@ async fn handle_tools_call(params: &Value, config: &ServerConfig) -> Result = tool_name.split('-').collect(); - if parts.len() < 3 { - return Err(GwsError::Validation(format!( - "Invalid API tool name: {}", + // Find the enabled service that is a prefix of the tool name. + // This correctly handles hyphenated aliases like "admin-reports". + let (svc_alias, rest) = config + .services + .iter() + .find_map(|s| { tool_name - ))); - } - - let svc_alias = parts[0]; + .strip_prefix(s.as_str()) + .and_then(|r| r.strip_prefix('-')) + .map(|remainder| (s.as_str(), remainder)) + }) + .ok_or_else(|| { + GwsError::Validation(format!( + "Could not determine service from tool name '{}'. No enabled service is a prefix.", + tool_name + )) + })?; - if !config.services.contains(&svc_alias.to_string()) { + let parts: Vec<&str> = rest.split('-').collect(); + if parts.is_empty() { return Err(GwsError::Validation(format!( - "Service '{}' is not enabled in this MCP session", - svc_alias + "Invalid API tool name: {}", + tool_name ))); } @@ -739,8 +748,8 @@ async fn handle_tools_call(params: &Value, config: &ServerConfig) -> Result Date: Thu, 5 Mar 2026 19:00:36 -0700 Subject: [PATCH 3/4] fix: use longest prefix match for service alias disambiguation Addresses review: find_map returns first match, but admin could match before admin-reports. Use filter_map + max_by_key to always pick the longest (most specific) service alias prefix. --- src/mcp_server.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/mcp_server.rs b/src/mcp_server.rs index 980e2b6..c57282e 100644 --- a/src/mcp_server.rs +++ b/src/mcp_server.rs @@ -720,12 +720,13 @@ async fn handle_tools_call(params: &Value, config: &ServerConfig) -> Result Date: Thu, 5 Mar 2026 19:07:05 -0700 Subject: [PATCH 4/4] fix: validate tool name has resource and method parts Addresses review: parts.is_empty() was dead code since split never returns empty. Now checks parts.len() >= 2 (resource + method) and rejects empty segments from double hyphens. --- src/mcp_server.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/mcp_server.rs b/src/mcp_server.rs index c57282e..758f3ac 100644 --- a/src/mcp_server.rs +++ b/src/mcp_server.rs @@ -735,9 +735,9 @@ async fn handle_tools_call(params: &Value, config: &ServerConfig) -> Result = rest.split('-').collect(); - if parts.is_empty() { + if parts.len() < 2 || parts.iter().any(|p| p.is_empty()) { return Err(GwsError::Validation(format!( - "Invalid API tool name: {}", + "Invalid API tool name: '{}'. Expected format: --", tool_name ))); }