# MCP smoke test A standalone harness that proves the Microsoft first-party MCP servers behind a connector gateway actually work end-to-end: ``` connect → initialize → tools/list → a safe tools/call ``` It imports the `connector-namespaces` extension's real pipeline (`install.mjs`, `catalog.mjs`, `armClient.mjs`) and connects through the same native Streamable HTTP endpoint that the extension writes to the GitHub Copilot MCP config. The probe uses the configured `X-API-Key`, follows `Mcp-Session-Id`, and accepts standard JSON or SSE JSON-RPC responses. The whole point: it runs with **Node and a browser sign-in**. No Copilot app or canvas is required. Hand it to anyone (e.g. Arjun) and they can reproduce an MCP server issue locally. ## Prerequisites 1. **A browser for Microsoft Entra sign-in.** The harness opens the same interactive Azure sign-in as the extension at the start of each process. 2. **A gateway already picked once.** The harness reads gateway coordinates from `~/.copilot/extensions/connector-namespaces/artifacts/gateway-config.json` (`{ subscriptionId, resourceGroup, gatewayName }`). Pick a gateway once in the connector-namespaces canvas, or write that file by hand. 3. **Node 20+** (developed on Node 24). 4. **Extension dependencies installed.** From the repository root, run: ```bash npm install --prefix extensions/connector-namespaces ``` ## Run it ```bash node extensions/connector-namespaces/test/smoke.mjs ``` Options: | flag | effect | |---|---| | `--only=a,b` | only test these `apiName`s (comma-separated) | | `--limit=N` | stop after N connectable servers | | `--open-consent` | open consent URLs in the browser for OAuth servers that need it | | `--no-cleanup` | leave fresh keyless installs in place (default: uninstall them) | Examples: ```bash # just the three already-connected WorkIQ servers node extensions/connector-namespaces/test/smoke.mjs --only=WorkIQMail,WorkIQSharePoint,WorkIQTeams # first 5 connectable servers, open any consent prompts node extensions/connector-namespaces/test/smoke.mjs --limit=5 --open-consent ``` ## One-time connector consent OAuth-backed servers (most of them) need a human to consent **once** in a browser. Azure ARM sign-in is process-memory only and is repeated for each harness process; the one-time behavior below applies to the connector's own consent. The model: 1. **First run** hits a server that needs consent → the harness prints a consent URL and marks it `NEEDS_CONSENT`. It saves a pending record to `~/.copilot/extensions/connector-namespaces/artifacts/smoke-pending-consent.json` (not in the repo). No tool call is attempted. 2. **You open that URL once** and sign in / consent. After sign-in the browser may show "this site can't be reached" on a `127.0.0.1:7333/auth/callback/` page — **that is expected and harmless.** Consent completes gateway-side; the loopback page is just a redirect target and nothing is listening on it. 3. **Re-run the harness.** It sees the pending record, confirms the gateway connection is now `Connected`, finishes the install (mints the API key, writes the Copilot MCP entry), and probes it headless. From then on the connector is reused without repeating its consent. So the server taxonomy is: - **Already connected** (e.g. the three WorkIQ servers) → probed immediately. - **Keyless / SP / AAD** (e.g. Microsoft Learn Docs) → installed + probed + cleaned up immediately, no consent. - **Consent-once OAuth** → surfaced on run 1, converts to headless on run 2. That's why the **first** run may probe fewer than 10 servers — the rest are waiting on their one-time consent. Consent the URLs it prints, re-run, and the count climbs. This is inherent to the consent model, not a harness bug. ## Tool-call safety The harness never blindly calls the first tool a server advertises (mutation risk). `safe-tools.mjs` picks a tool to call by: 1. a **curated map** of known-safe read tools per server (e.g. Microsoft Learn Docs → `microsoft_docs_search`, WorkIQ Teams → `ListTeams`), then 2. a **read-only-name heuristic** fallback — the first tool whose name starts with `list`/`get`/`search`/`read`/`find`/… **and** whose required arguments are empty or trivially fillable with benign values. If nothing looks safe, it does `tools/list` only and records the call as `SKIPPED` (tools proven to load, no call made). Expand the curated map in `safe-tools.mjs` as you learn each server. ## Reading the report Each run prints a summary and writes two files to `test/reports/` (gitignored — they contain live endpoint URLs): - `mcp-smoke-.log` — human-readable table. **This is the handoff artifact** — attach it to a bug or send it to whoever needs to repro. - `mcp-smoke-.json` — machine-readable, same data. Per server you get: classification, `initialize` pass/fail + latency, tool count, which tool was called and why, the call result preview or error, and a direct transport error on failure. API keys are redacted; endpoint URLs are not, which is why the reports stay out of git. Exit code is **non-zero if any probed server failed a step**, so it's CI-usable. ## Files | file | role | |---|---| | `smoke.mjs` | orchestrator — bootstrap, classify each server, probe, report | | `mcp-probe.mjs` | drives the native Streamable HTTP JSON-RPC handshake | | `safe-tools.mjs` | curated safe-read-tool map + read-only heuristic + arg filler | | `reports/` | generated `.log` + `.json` artifacts (gitignored) | ## Scope Microsoft first-party servers only (`category === "Microsoft"` in the catalog). Partner servers (Box, Celonis, …) are filtered out — they need partner accounts and OAuth we can't automate.