From 7c86e4ba60b0cf1d9ca10ce509ab87ae9cf87f4d Mon Sep 17 00:00:00 2001 From: Abhi16016 Date: Thu, 5 Mar 2026 18:02:48 +0530 Subject: [PATCH 1/3] fix(mcp): align tool list prefixes with service aliases --- src/mcp_server.rs | 163 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 162 insertions(+), 1 deletion(-) diff --git a/src/mcp_server.rs b/src/mcp_server.rs index 2b0752b..3968a5f 100644 --- a/src/mcp_server.rs +++ b/src/mcp_server.rs @@ -208,7 +208,7 @@ async fn build_tools_list(config: &ServerConfig) -> Result, GwsError> let (api_name, version) = crate::parse_service_and_version(std::slice::from_ref(svc_name), 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); } @@ -468,3 +468,164 @@ async fn handle_tools_call(params: &Value, config: &ServerConfig) -> Result, + } + + impl Drop for EnvVarGuard { + fn drop(&mut self) { + unsafe { + if let Some(old) = &self.old { + std::env::set_var(self.key, old); + } else { + std::env::remove_var(self.key); + } + } + } + } + + fn set_env_var_path(key: &'static str, value: &Path) -> EnvVarGuard { + let old = std::env::var_os(key); + unsafe { + std::env::set_var(key, value); + } + EnvVarGuard { key, old } + } + + fn write_cached_doc(cache_dir: &Path, service: &str, version: &str, doc: Value) { + let cache_file = cache_dir.join(format!("{service}_{version}.json")); + fs::write(cache_file, serde_json::to_string(&doc).unwrap()).unwrap(); + } + + #[tokio::test] + #[serial_test::serial] + async fn build_tools_list_uses_alias_prefixes_for_aliased_services() { + let tmp = tempfile::tempdir().unwrap(); + let _config_guard = set_env_var_path(TEST_CONFIG_DIR_ENV, tmp.path()); + let cache_dir = tmp.path().join("cache"); + fs::create_dir_all(&cache_dir).unwrap(); + + write_cached_doc( + &cache_dir, + "workspaceevents", + "v1", + json!({ + "name": "workspaceevents", + "version": "v1", + "rootUrl": "https://workspaceevents.googleapis.com/", + "resources": { + "subscriptions": { + "methods": { + "list": { + "httpMethod": "GET", + "path": "subscriptions", + "description": "Lists subscriptions" + } + } + } + } + }), + ); + write_cached_doc( + &cache_dir, + "script", + "v1", + json!({ + "name": "script", + "version": "v1", + "rootUrl": "https://script.googleapis.com/", + "resources": { + "projects": { + "methods": { + "list": { + "httpMethod": "GET", + "path": "projects", + "description": "Lists script projects" + } + } + } + } + }), + ); + write_cached_doc( + &cache_dir, + "admin", + "reports_v1", + json!({ + "name": "admin", + "version": "reports_v1", + "rootUrl": "https://admin.googleapis.com/", + "resources": { + "customerUsageReports": { + "methods": { + "get": { + "httpMethod": "GET", + "path": "customerUsageReports/{date}", + "description": "Gets customer usage report" + } + } + } + } + }), + ); + + let config = ServerConfig { + services: vec![ + "events".to_string(), + "apps-script".to_string(), + "admin-reports".to_string(), + ], + workflows: false, + _helpers: false, + }; + + let tools = build_tools_list(&config).await.unwrap(); + let names: HashSet = tools + .iter() + .filter_map(|t| t.get("name").and_then(|n| n.as_str()).map(str::to_string)) + .collect(); + + assert!(names.contains("events_subscriptions_list")); + assert!(names.contains("apps-script_projects_list")); + assert!(names.contains("admin-reports_customerUsageReports_get")); + + assert!(!names.contains("workspaceevents_subscriptions_list")); + assert!(!names.contains("script_projects_list")); + assert!(!names.contains("admin_customerUsageReports_get")); + } + + #[tokio::test] + #[serial_test::serial] + async fn handle_tools_call_rejects_discovery_prefix_when_alias_is_enabled() { + let config = ServerConfig { + services: vec!["events".to_string()], + workflows: false, + _helpers: false, + }; + + let params = json!({ + "name": "workspaceevents_subscriptions_list", + "arguments": {} + }); + + let err = handle_tools_call(¶ms, &config).await.unwrap_err(); + match err { + GwsError::Validation(msg) => { + assert!(msg.contains("Service 'workspaceevents' is not enabled in this MCP session")) + } + other => panic!("Expected validation error, got: {other}"), + } + } +} From 660b9e2694d4f04470a860069a245f5e437a4535 Mon Sep 17 00:00:00 2001 From: Abhi16016 Date: Thu, 5 Mar 2026 18:08:59 +0530 Subject: [PATCH 2/3] chore(changeset): add patch note for MCP alias tool fix --- .changeset/fix-mcp-alias-tools-mismatch.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/fix-mcp-alias-tools-mismatch.md diff --git a/.changeset/fix-mcp-alias-tools-mismatch.md b/.changeset/fix-mcp-alias-tools-mismatch.md new file mode 100644 index 0000000..84f7851 --- /dev/null +++ b/.changeset/fix-mcp-alias-tools-mismatch.md @@ -0,0 +1,5 @@ +--- +"@googleworkspace/cli": patch +--- + +Fix MCP alias tool namespace mismatch so names returned by `tools/list` are callable via `tools/call` for aliased services. From 569af9e32b15888773e365211cfa6d76db3a0e4a Mon Sep 17 00:00:00 2001 From: Abhi16016 Date: Thu, 5 Mar 2026 18:49:08 +0530 Subject: [PATCH 3/3] test(mcp): remove unnecessary unsafe env var wrappers --- src/mcp_server.rs | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/mcp_server.rs b/src/mcp_server.rs index 3968a5f..d98b8be 100644 --- a/src/mcp_server.rs +++ b/src/mcp_server.rs @@ -486,21 +486,17 @@ mod tests { impl Drop for EnvVarGuard { fn drop(&mut self) { - unsafe { - if let Some(old) = &self.old { - std::env::set_var(self.key, old); - } else { - std::env::remove_var(self.key); - } + if let Some(old) = &self.old { + std::env::set_var(self.key, old); + } else { + std::env::remove_var(self.key); } } } fn set_env_var_path(key: &'static str, value: &Path) -> EnvVarGuard { let old = std::env::var_os(key); - unsafe { - std::env::set_var(key, value); - } + std::env::set_var(key, value); EnvVarGuard { key, old } }