diff --git a/INSTALL.md b/INSTALL.md index aee25ef..e3da961 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -18,6 +18,7 @@ Ensure you have **at least one** of the supported CLI harnesses installed: * [Gemini CLI](https://geminicli.com/) (*being deprecated soon*) * [Antigravity CLI (agy)](https://antigravity.google/) * [Claude Code](https://github.com/anthropics/claude-code) +* [GitHub Copilot CLI](https://docs.github.com/en/copilot/how-tos/copilot-cli) --- @@ -34,6 +35,9 @@ just install-geminicli # Claude Code just install-claude + +# GitHub Copilot CLI +just install-copilot ``` --- diff --git a/README.md b/README.md index 3a83573..ef2268f 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ You also need `python` and `uv` installed. - **`investigation-entrypoint`**: Primary entrypoint for investigating production outages, orchestrating SRE response, and mitigating incidents. Start here when an incident occurs! - **`gcp-architecture-discovery`**: Discover and map GCP infrastructure architecture including compute, networking, storage, and service dependencies. - **`gcp-playbooks`**: Follows established SRE playbooks for GCP/GKE investigations, including infrastructure discovery and common mitigation steps. -- **`gcp-mcp-setup`**: Automates enabling services, Google Managed MCP (OneMCP) servers, generating API keys, and configuring `~/.gemini/settings.json`. +- **`gcp-mcp-setup`**: Automates enabling services, Google Managed MCP (OneMCP) servers, generating API keys, and configuring the MCP settings file for your CLI harness (Gemini, Antigravity, or Copilot). - **`gcp-slo-management`**: Discover Monitoring Services, list existing SLOs, or create new SLOs (Availability/Latency) via the REST API. - **`postmortem-generator`**: Creates a generated PostMortem given enough context about a resolved incident/outage. @@ -70,7 +70,7 @@ You also need `python` and `uv` installed. | :----------------------| :----------:| :--------------------------------------------------------:| :--------------------------------------------------------:| :--------------------------------------------------------:| :--------------------------------------------------------:| | **Type** | Extension | [Plugin](https://antigravity.google/docs/plugins) | [Plugin](https://code.claude.com/docs/en/plugins-reference) | [Plugin](https://developers.openai.com/codex/plugins) | [Plugin](https://docs.github.com/copilot/how-tos/copilot-cli/customize-copilot/plugins-creating) | | **Install** | ✅ | ✅ | ✅ | 🟢 | ✅ | -| **MCP Setup** | ✅ | ✅ | 🟢 | 🟢 | 🔴 | +| **MCP Setup** | ✅ | ✅ | 🟢 | 🟢 | ✅ | | **SRE Skills** | ✅ | ✅ | 🟢 | 🟢 | ✅ | | **GKE Investigation** | ✅ | ✅ | 🟢 | 🟢 | 🟢 | diff --git a/skills/gcp-mcp-setup/CHANGELOG.md b/skills/gcp-mcp-setup/CHANGELOG.md index 26eeb38..8634e53 100644 --- a/skills/gcp-mcp-setup/CHANGELOG.md +++ b/skills/gcp-mcp-setup/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this skill will be documented in this file. +## [0.0.10] - 2026-07-03 +### Added +- **GitHub Copilot CLI support**: `setup_onemcp.py` now accepts `--harness {gemini,antigravity,copilot}` and writes the correct MCP config format and path per harness (`~/.copilot/mcp-config.json` using Copilot's `type/url/tools` wire format). +- **Centralized harness registry**: `harness_registry.py` as the single source of truth for all harness configuration (config file paths, MCP entry builders, CLI commands). Both `setup_onemcp.py` and `verify_setup.py` import from it. +- **Typed harness config**: `harness_registry.py` exposes `HarnessName(str, Enum)`, `HarnessCommand(str, Enum)`, and a `HarnessConfig` frozen dataclass with a `.paths(scope)` method. +- **Harness-agnostic MCP verification**: `verify_setup.py` now accepts `--harness {gemini,antigravity,copilot}` and dispatches to the correct CLI command via `HarnessCommand.MCP_LIST`. Config file paths are derived from the registry. + ## [0.0.9] - 2026-05-28 ### Added - **Antigravity CLI Support**: Added automated configuration support for the modern `agy` CLI and Antigravity Editor. diff --git a/skills/gcp-mcp-setup/EVAL.md b/skills/gcp-mcp-setup/EVAL.md index 585adcc..be64075 100644 --- a/skills/gcp-mcp-setup/EVAL.md +++ b/skills/gcp-mcp-setup/EVAL.md @@ -28,6 +28,11 @@ - Prompt: "What does "ConnectionPoolExhausted: Unable to acquire aconnection from the pool within the timeout." error mean?" - Test: Should use MCP to search for this in the DKP knowledge base. +## Copilot CLI Setup + +- Prompt: "Please install OneMCP on my project my-gcp-project using Copilot CLI" +- Test: Should run `setup_onemcp.py my-gcp-project --global --harness copilot` and write to `~/.copilot/mcp-config.json` using the `type/url/tools` format. + ## Good Skill Structure diff --git a/skills/gcp-mcp-setup/SKILL.md b/skills/gcp-mcp-setup/SKILL.md index 466fc33..52ec0d7 100644 --- a/skills/gcp-mcp-setup/SKILL.md +++ b/skills/gcp-mcp-setup/SKILL.md @@ -1,9 +1,9 @@ --- name: gcp-mcp-setup -description: 🐉 [SRE] Use when the user wants to set up Google Managed MCP (OneMCP) servers for their Gemini CLI environment. Automates enabling services, MCP servers, generating API keys, and configuring ~/.gemini/settings.json. +description: 🐉 [SRE] Use when the user wants to set up Google Managed MCP (OneMCP) servers for their CLI environment. Automates enabling services, MCP servers, generating API keys, and configuring the MCP settings file for the active harness (Gemini, Antigravity, or Copilot CLI). metadata: author: Riccardo - version: 0.0.9 + version: 0.0.10 status: published --- @@ -23,16 +23,15 @@ Use this skill when the user asks to "install OneMCP", "setup OneMCP", or "confi 2. **Setup Script Execution**: Run the provided script to enable APIs, MCP services, and generate the required API Key. - By default, it installs a **Lean SRE toolset** (Logging, Monitoring, GKE, Run, Resource Manager, Developer Knowledge). - Use the `--all` flag to include optional services (Error Reporting, Databases, Vertex AI). - - Script: `skills/gcp-mcp-setup/scripts/setup_onemcp.py [--local | --global] [--all] [--google-maps-key YOUR_KEY]` -3. **Configure Settings**: The script automatically writes the `mcpServers` configuration to the correct location depending on which harness/CLI you are running: - - **If your harness is Gemini CLI**: The configuration is injected directly into `.gemini/settings.json` (if `--local` is provided) or `~/.gemini/settings.json` (if `--global` is provided). - - **If your harness is Antigravity CLI (agy)**: The configuration is written to both `.gemini/antigravity/mcp_config.json` and `.gemini/config/mcp_config.json` (if `--local` is provided) or the global equivalents under `~/.gemini/` (if `--global` is provided). + - Use `--harness` to target a specific CLI harness (`gemini`, `antigravity`, or `copilot`). Defaults to `gemini+antigravity`. + - Script: `skills/gcp-mcp-setup/scripts/setup_onemcp.py [--local | --global] [--harness {gemini,antigravity,copilot}] [--all] [--google-maps-key YOUR_KEY]` +3. **Configure Settings**: The script automatically writes the `mcpServers` configuration to the correct location depending on the target harness: + - **Gemini CLI**: `.gemini/settings.json` (local) or `~/.gemini/settings.json` (global). + - **Antigravity CLI (agy)**: `.gemini/antigravity/mcp_config.json` and `.gemini/config/mcp_config.json` (local) or the global equivalents under `~/.gemini/`. + - **GitHub Copilot CLI**: `~/.copilot/mcp-config.json` (always global — no workspace-level config yet). 4. **Verification & Diagnostics:** - - Script: `skills/gcp-mcp-setup/scripts/verify_setup.py` - - This script checks the configured settings across both Gemini and Antigravity paths, and tests actual endpoint connectivity. - - For direct endpoint diagnostics, use the curl-based tool: - - Script: `skills/gcp-mcp-setup/scripts/test_mcp_endpoint.sh [ENDPOINT_URL]` - - Example: `./skills/gcp-mcp-setup/scripts/test_mcp_endpoint.sh https://monitoring.googleapis.com/mcp` + - Script: `skills/gcp-mcp-setup/scripts/verify_setup.py [--harness {gemini,antigravity,copilot}]` + - This script checks identity consistency (gcloud vs ADC), runs the harness-specific MCP list command, and confirms all expected google-* servers are registered. ## Examples @@ -45,8 +44,8 @@ Note that the **gcloud CLI Identity** (how you are currently logged in via `gclo MCP servers typically use ADC to authenticate. If these identities do not match, you may encounter permission errors (e.g., `serviceusage.serviceUsageConsumer` denied) even if `gcloud` commands work correctly. -If MCP doesn't work, yhou can poropose to run this on user's behalf: `gcloud auth application-default login --account ` - +If MCP doesn't work, you can propose to run this on user's behalf: `gcloud auth application-default login --account ` + ### 💡 Path Forward If you encounter an identity mismatch: diff --git a/skills/gcp-mcp-setup/scripts/harness_registry.py b/skills/gcp-mcp-setup/scripts/harness_registry.py new file mode 100644 index 0000000..6b8f891 --- /dev/null +++ b/skills/gcp-mcp-setup/scripts/harness_registry.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 +"""Central harness registry for the gcp-mcp-setup skill. + +Single source of truth for: + - MCP config file paths (global and local) per harness + - MCP server entry format builders per harness + - CLI commands per harness (keyed by HarnessCommand enum) + +To add a new harness, add one HarnessConfig entry to HARNESS_REGISTRY below and to the HarnessName enum. +""" + +from dataclasses import dataclass +from enum import Enum +from typing import Callable + + +class HarnessCommand(str, Enum): + """Abstract command names supported across harnesses. + """ + MCP_LIST = "mcp_list" + + +class HarnessName(str, Enum): + """Known CLI harness identifiers. + """ + GEMINI = "gemini" + ANTIGRAVITY = "antigravity" + COPILOT = "copilot" + + +@dataclass(frozen=True) +class HarnessConfig: + """Typed configuration for a single CLI harness.""" + global_paths: list[str] + local_paths: list[str] + builder: Callable[..., dict] + commands: dict[HarnessCommand, list[str]] + + def paths(self, scope: str) -> list[str]: + """Returns config file paths for the given scope ('global' or 'local').""" + return self.global_paths if scope == "global" else self.local_paths + + def get_command(self, key: "HarnessCommand") -> list[str]: + """Returns the command list for the given key. + Raises ValueError if the key is absent, None, or maps to an empty list. + """ + cmd = self.commands.get(key) + if not cmd: + raise ValueError(f"No command defined for '{key}' in this harness config.") + return cmd + + +def _gemini_mcp_format(url, *, project_id=None, api_key=None): + """Returns an MCP server entry in the Gemini/Antigravity format.""" + if api_key: + return {"httpUrl": url, "serverUrl": url, "headers": {"X-Goog-Api-Key": api_key}} + return { + "httpUrl": url, + "serverUrl": url, + "authProviderType": "google_credentials", + "oauth": {"scopes": ["https://www.googleapis.com/auth/cloud-platform"]}, + "headers": {"X-goog-user-project": project_id}, + } + + +def _copilot_mcp_format(url, *, project_id=None, api_key=None): + """Returns an MCP server entry in the Copilot CLI format.""" + headers = {"X-Goog-Api-Key": api_key} if api_key else {"X-goog-user-project": project_id} + return {"type": "http", "url": url, "headers": headers, "tools": ["*"]} + + +HARNESS_REGISTRY: dict[HarnessName, HarnessConfig] = { + HarnessName.GEMINI: HarnessConfig( + global_paths=["~/.gemini/settings.json"], + local_paths=[".gemini/settings.json"], + builder=_gemini_mcp_format, + commands={HarnessCommand.MCP_LIST: ["gemini", "-p", "/mcp list"]}, + ), + HarnessName.ANTIGRAVITY: HarnessConfig( + global_paths=["~/.gemini/antigravity/mcp_config.json", "~/.gemini/config/mcp_config.json"], + local_paths=[".gemini/antigravity/mcp_config.json", ".gemini/config/mcp_config.json"], + builder=_gemini_mcp_format, + commands={HarnessCommand.MCP_LIST: ["agy", "-p", "/mcp list"]}, + ), + HarnessName.COPILOT: HarnessConfig( + # Copilot CLI has no workspace-level MCP config; both scopes resolve to + # the same global path, so --local and --global behave identically. + global_paths=["~/.copilot/mcp-config.json"], + local_paths=["~/.copilot/mcp-config.json"], + builder=_copilot_mcp_format, + commands={HarnessCommand.MCP_LIST: ["copilot", "mcp", "list"]}, + ), +} diff --git a/skills/gcp-mcp-setup/scripts/setup_onemcp.py b/skills/gcp-mcp-setup/scripts/setup_onemcp.py index 344138c..3d998b0 100755 --- a/skills/gcp-mcp-setup/scripts/setup_onemcp.py +++ b/skills/gcp-mcp-setup/scripts/setup_onemcp.py @@ -17,11 +17,14 @@ """ OneMCP Setup Script This script sets the gcloud project, installs beta components, enables required services -and MCP servers. +and MCP servers. By default, it installs a lean SRE toolset (Logging, Monitoring, GKE, Run, RM, ErrorReporting, DK). Use --all to include databases (SQL, Spanner, Firestore, BigQuery) and Vertex AI. +Use --harness to target a specific CLI harness (gemini, antigravity, copilot). +Defaults to gemini+antigravity when omitted. + # Thanks to Romin Irani guidance: https://github.com/rominirani/google-mcp-servers/blob/main/demos/README.md """ @@ -32,9 +35,19 @@ import sys import time +sys.path.insert(0, os.path.dirname(__file__)) +from harness_registry import HARNESS_REGISTRY, HarnessName + +# On Windows, the default encoding is cp1252 which cannot encode emojis. Use UTF-8 to avoid encoding errors. +if sys.stdout.encoding.lower() != 'utf-8': + sys.stdout.reconfigure(encoding='utf-8') +if sys.stderr.encoding.lower() != 'utf-8': + sys.stderr.reconfigure(encoding='utf-8') + + def run_command(command, check=True): print(f"Running: {' '.join(command)}") - result = subprocess.run(command, text=True, capture_output=True) + result = subprocess.run(command, text=True, encoding='utf-8', capture_output=True, shell=(sys.platform == "win32")) if check and result.returncode != 0: print(f"Command failed with error: {result.stderr}", file=sys.stderr) sys.exit(result.returncode) @@ -42,29 +55,40 @@ def run_command(command, check=True): def main(): parser = argparse.ArgumentParser( - description="Set up Google Managed MCP (OneMCP) for Gemini CLI." + description="Set up Google Managed MCP (OneMCP) for your CLI harness." ) parser.add_argument("project_id", help="The Google Cloud Project ID to use.") - - group = parser.add_mutually_exclusive_group(required=True) - group.add_argument("--local", action="store_true", help="Update the local .gemini/settings.json file.") - group.add_argument("--global", action="store_true", dest="global_config", help="Update the global ~/.gemini/settings.json file.") + group = parser.add_mutually_exclusive_group(required=True) + group.add_argument("--local", action="store_true", help="Update the local config file (workspace-scoped).") + group.add_argument("--global", action="store_true", dest="global_config", help="Update the global config file (user-scoped).") + + parser.add_argument( + "--harness", + choices=[h.value for h in HarnessName], + default=None, + help="Target CLI harness to configure (gemini, antigravity, copilot). Defaults to gemini+antigravity.", + ) parser.add_argument("--all", action="store_true", help="Enable all supported OneMCP servers, including databases and Vertex AI.") parser.add_argument("--google-maps-key", dest="google_maps_key", help="The Google Maps API Key to enable mapstools MCP.") args = parser.parse_args() project_id = args.project_id + scope = "local" if args.local else "global" - target_files = [] - if args.local: - target_files.append((os.path.join(os.getcwd(), ".gemini", "settings.json"), "gemini")) - target_files.append((os.path.join(os.getcwd(), ".gemini", "antigravity", "mcp_config.json"), "antigravity")) - target_files.append((os.path.join(os.getcwd(), ".gemini", "config", "mcp_config.json"), "antigravity")) + # Resolve target harnesses: explicit selection or default to gemini+antigravity. + if args.harness: + selected_harnesses = [args.harness] else: - target_files.append((os.path.expanduser("~/.gemini/settings.json"), "gemini")) - target_files.append((os.path.expanduser("~/.gemini/antigravity/mcp_config.json"), "antigravity")) - target_files.append((os.path.expanduser("~/.gemini/config/mcp_config.json"), "antigravity")) + selected_harnesses = [HarnessName.GEMINI, HarnessName.ANTIGRAVITY] + + # Resolve target config files from the registry. + target_files = [] + for harness in selected_harnesses: + harness_config = HARNESS_REGISTRY[harness] + for path in harness_config.paths(scope): + resolved = os.path.expanduser(path) if path.startswith("~") else os.path.join(os.getcwd(), path) + target_files.append((resolved, harness_config.builder)) # Core SRE Services (Default) base_services = [ @@ -135,36 +159,23 @@ def main(): 'bigquery.googleapis.com': ('google-bigquery', 'https://bigquery.googleapis.com/mcp'), } - def build_mcp_servers(target_type): + def build_mcp_servers(builder): servers = {} for service in base_services: if service == 'developerknowledge.googleapis.com': - cfg = { - 'httpUrl': 'https://developerknowledge.googleapis.com/mcp', - 'serverUrl': 'https://developerknowledge.googleapis.com/mcp', - 'headers': {'X-Goog-Api-Key': dev_key} - } - servers['google-developer-knowledge'] = cfg + servers['google-developer-knowledge'] = builder( + 'https://developerknowledge.googleapis.com/mcp', api_key=dev_key + ) elif service == 'mapstools.googleapis.com': - cfg = { - 'httpUrl': 'https://mapstools.googleapis.com/mcp', - 'serverUrl': 'https://mapstools.googleapis.com/mcp', - 'headers': {'X-Goog-Api-Key': args.google_maps_key} - } - servers['google-maps'] = cfg + servers['google-maps'] = builder( + 'https://mapstools.googleapis.com/mcp', api_key=args.google_maps_key + ) elif service in mcp_config_map: key, url = mcp_config_map[service] - cfg = { - 'httpUrl': url, - 'serverUrl': url, - 'authProviderType': 'google_credentials', - 'oauth': {'scopes': ['https://www.googleapis.com/auth/cloud-platform']}, - 'headers': {'X-goog-user-project': project_id} - } - servers[key] = cfg + servers[key] = builder(url, project_id=project_id) return servers - for config_file, target_type in target_files: + for config_file, builder in target_files: print(f"Updating {config_file}...") os.makedirs(os.path.dirname(config_file), exist_ok=True) @@ -179,8 +190,7 @@ def build_mcp_servers(target_type): if 'mcpServers' not in data: data['mcpServers'] = {} - # Merge tailored servers - data['mcpServers'].update(build_mcp_servers(target_type)) + data['mcpServers'].update(build_mcp_servers(builder)) with open(config_file, 'w') as f: json.dump(data, f, indent=2) diff --git a/skills/gcp-mcp-setup/scripts/verify_setup.py b/skills/gcp-mcp-setup/scripts/verify_setup.py index c000aed..5149f23 100755 --- a/skills/gcp-mcp-setup/scripts/verify_setup.py +++ b/skills/gcp-mcp-setup/scripts/verify_setup.py @@ -22,17 +22,27 @@ import urllib.request from unittest.mock import patch, MagicMock -# --- Utility Functions --- +sys.path.insert(0, os.path.dirname(__file__)) +from harness_registry import HARNESS_REGISTRY, HarnessCommand, HarnessName + +# On Windows, the default encoding is cp1252 which cannot encode emojis. Use UTF-8 to avoid encoding errors. +if sys.stdout.encoding.lower() != 'utf-8': + sys.stdout.reconfigure(encoding='utf-8') +if sys.stderr.encoding.lower() != 'utf-8': + sys.stderr.reconfigure(encoding='utf-8') + + +def run_command(command, check=True): + result = subprocess.run(command, text=True, encoding='utf-8', capture_output=True, shell=(sys.platform == "win32")) + if check and result.returncode != 0: + print(f"Command failed with error: {result.stderr}", file=sys.stderr) + sys.exit(result.returncode) + return result def get_gcloud_identity(): """Retrieves the current gcloud account email.""" try: - res = subprocess.run( - ["gcloud", "config", "get-value", "account"], - capture_output=True, - text=True, - check=True - ) + res = run_command(["gcloud", "config", "get-value", "account"], check=False) return res.stdout.strip() except Exception: return "Unknown" @@ -40,12 +50,8 @@ def get_gcloud_identity(): def get_adc_identity(): """Retrieves the email associated with Application Default Credentials.""" try: - # Get ADC token - token_res = subprocess.run( - ["gcloud", "auth", "application-default", "print-access-token"], - capture_output=True, - text=True, - check=True + token_res = run_command( + ["gcloud", "auth", "application-default", "print-access-token"], check=False ) token = token_res.stdout.strip() @@ -58,17 +64,16 @@ def get_adc_identity(): return "Unknown (ADC not configured)" def get_configured_servers(): - """Reads settings.json and mcp_config.json paths and returns configured MCP server keys.""" - settings_paths = [ - os.path.join(os.getcwd(), ".gemini", "settings.json"), - os.path.expanduser("~/.gemini/settings.json"), - os.path.join(os.getcwd(), ".gemini", "antigravity", "mcp_config.json"), - os.path.expanduser("~/.gemini/antigravity/mcp_config.json"), - os.path.join(os.getcwd(), ".gemini", "config", "mcp_config.json"), - os.path.expanduser("~/.gemini/config/mcp_config.json") - ] + """Reads all known harness MCP config paths (derived from HARNESS_REGISTRY) and returns configured MCP server keys.""" + all_paths = set() + for harness_config in HARNESS_REGISTRY.values(): + for scope in ("global", "local"): + for path in harness_config.paths(scope): + resolved = os.path.expanduser(path) if path.startswith("~") else os.path.join(os.getcwd(), path) + all_paths.add(resolved) + configured = [] - for path in settings_paths: + for path in all_paths: if os.path.exists(path): try: with open(path, 'r') as f: @@ -78,22 +83,25 @@ def get_configured_servers(): pass return list(set(configured)) -def run_gemini_command(prompt): - """Executes a gemini command in headless mode and returns the output.""" +# Maps harness name to the command that lists registered MCP servers. +def run_mcp_list(harness): + """Runs the MCP list command for the given harness and returns its output.""" + harness_config = HARNESS_REGISTRY.get(harness) + if not harness_config: + return f"Error: unknown harness '{harness}'" try: - result = subprocess.run( - ["gemini", "-p", prompt], - capture_output=True, - text=True, - timeout=60 - ) + cmd = harness_config.get_command(HarnessCommand.MCP_LIST) + result = run_command(cmd, check=False) return result.stdout + result.stderr + except ValueError as e: + return f"Error: {e}" except Exception as e: - return f"Error running gemini: {str(e)}" + return f"Error running mcp list for '{harness}': {str(e)}" # --- Integration / Live System Checks --- class TestOneMCPIntegration(unittest.TestCase): + harness = "gemini" # overridden by --unittest path before test run def test_identity_match(self): """Verifies that gcloud and ADC identities match.""" @@ -110,26 +118,26 @@ def test_identity_match(self): print(" ✅ Identities match.") def test_mcp_list_output(self): - """Verifies that /mcp list contains our expected OneMCP servers.""" - print("\n[Integration] Verifying MCP server configuration via Gemini CLI...") - output = run_gemini_command("/mcp list") + """Verifies that the MCP list command contains our expected OneMCP servers.""" + print(f"\n[Integration] Verifying MCP server configuration via {self.harness}...") + output = run_mcp_list(self.harness) configured_servers = get_configured_servers() # Only check for google-managed ones expected_servers = [s for s in configured_servers if s.startswith("google-")] if not expected_servers: - print(" ⚠️ No google-* MCP servers found in settings.json. Skipping list check.") + print(" ⚠️ No google-* MCP servers found in config. Skipping list check.") return for server in expected_servers: with self.subTest(server=server): - self.assertIn(server, output, f"MCP server '{server}' not found in /mcp list output") + self.assertIn(server, output, f"MCP server '{server}' not found in mcp list output") print(f" ✅ Found {server}") def test_mcp_status_emojis(self): """Checks if servers are showing the green status emoji if connected.""" - output = run_gemini_command("/mcp list") + output = run_mcp_list(self.harness) if "🟢" in output or "Ready" in output or "Connected" in output: print(" ✅ Green status (🟢) or 'Ready' detected for at least one server!") @@ -166,12 +174,7 @@ def test_help_output(self): def get_kubectl_context(): """Retrieves current kubectl context and user from config.""" try: - res = subprocess.run( - ["kubectl", "config", "view", "--minify", "-o", "json"], - capture_output=True, - text=True, - check=True - ) + res = run_command(["kubectl", "config", "view", "--minify", "-o", "json"], check=False) data = json.loads(res.stdout) contexts = data.get("contexts", []) if contexts: @@ -183,12 +186,7 @@ def get_kubectl_context(): def get_kubectl_whoami(): """Retrieves current kubectl authenticated identity from the server.""" try: - res = subprocess.run( - ["kubectl", "auth", "whoami", "-o", "json"], - capture_output=True, - text=True, - check=True - ) + res = run_command(["kubectl", "auth", "whoami", "-o", "json"], check=False) data = json.loads(res.stdout) # Kubernetes 1.25+ returns json with status fields username = data.get("status", {}).get("userInfo", {}).get("username", "Unknown") @@ -197,12 +195,7 @@ def get_kubectl_whoami(): except Exception: # Fallback to non-json parsing if -o json is not supported or fails try: - res = subprocess.run( - ["kubectl", "auth", "whoami"], - capture_output=True, - text=True, - check=True - ) + res = run_command(["kubectl", "auth", "whoami"], check=False) # Parse table output lines = res.stdout.strip().split("\n") username = "Unknown" @@ -235,9 +228,27 @@ def test_kubectl_context(self): if __name__ == "__main__": # If run with --unittest, execute the unit tests and integration tests if len(sys.argv) > 1 and sys.argv[1] == "--unittest": + # Parse --harness before handing off to unittest (which owns sys.argv) + harness_arg = "gemini" + if "--harness" in sys.argv: + harness_idx = sys.argv.index("--harness") + if harness_idx + 1 < len(sys.argv): + harness_arg = sys.argv.pop(harness_idx + 1) + sys.argv.pop(harness_idx) + TestOneMCPIntegration.harness = harness_arg unittest.main(argv=[sys.argv[0]]) else: - # Simple execution mode (original behavior) + import argparse + parser = argparse.ArgumentParser(description="Verify OneMCP setup for a given harness.") + parser.add_argument( + "--harness", + choices=[h.value for h in HarnessName], + default=HarnessName.GEMINI, + help="CLI harness to verify against (default: gemini).", + ) + args = parser.parse_args() + + # Simple execution mode print("Starting OneMCP Verification...") gcloud_id = get_gcloud_identity() @@ -266,8 +277,8 @@ def test_kubectl_context(self): else: print(" ✅ kubectl configured.") - output = run_gemini_command("/mcp list") - print("\n--- Gemini Output ---") + output = run_mcp_list(args.harness) + print(f"\n--- MCP List ({args.harness}) ---") print(output) print("----------------------\n") @@ -277,9 +288,7 @@ def test_kubectl_context(self): all_found = True for s in expected: if s in output: - # Check for 🟢 indicator in a simple way - status = "🟢" if "🟢" in output and s in output else "⚪" - print(f"{status} {s}: Found") + print(f"✅ {s}: Found") else: print(f"❌ {s}: MISSING") all_found = False