diff --git a/.changeset/mcp-hyphen-tool-names.md b/.changeset/mcp-hyphen-tool-names.md new file mode 100644 index 00000000..edb8a43c --- /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 4fb8e3e6..758f3ac1 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(); - if parts.len() < 3 { - return Err(GwsError::Validation(format!( - "Invalid API tool name: {}", + // Full mode: tool_name encodes service-resource-method (e.g., drive-files-list) + // 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() + .filter_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)) + }) + .max_by_key(|(s, _)| s.len()) + .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.len() < 2 || parts.iter().any(|p| p.is_empty()) { return Err(GwsError::Validation(format!( - "Service '{}' is not enabled in this MCP session", - svc_alias + "Invalid API tool name: '{}'. Expected format: --", + tool_name ))); } @@ -739,8 +749,8 @@ async fn handle_tools_call(params: &Value, config: &ServerConfig) -> Result