From a30a5760174259d108574b472da54c9f38b6e7aa Mon Sep 17 00:00:00 2001 From: cliffhall Date: Sat, 11 Jul 2026 23:14:51 -0400 Subject: [PATCH 1/2] docs: MCP App review recipe + root README pointer (#1578) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #1578 Adds docs/mcp-app-review.md — the CLI-first → one-shot-web recipe for automated MCP App review: `--app-info` probe → deep-link navigate → rendered widget, plus OAuth-gated servers (stored-auth handoff, now honoring refresh_token per #1665), SSH port-forward (both ports), and HTTP proxy support. Adapted from PR #1510's reference doc to only document what actually merged and to match the current implementation: - Correct driver contract: `data-testid="connection-status"` + `data-status` /`data-error-message`/`data-deeplink`, `data-testid="open-app"`, and the `data-app-status="ready"` app lifecycle (not the reference's stale selectors). - The zero-click `autoOpen` path (#1577) alongside the click-to-open variant. - `--use-stored-auth` now refreshes a stored refresh_token (#1665), so the "token is not refreshed" caveat is replaced. The per-client READMEs (web deep-link scheme + data-* contract, CLI handoff + refresh + proxy) were updated in their own Wave 4 PRs; this adds the root README pointer to docs/. Proxy/undici is already documented in the CLI README. Part of the #1579 decomposition (Wave 4, docs — lands last). Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw --- README.md | 16 ++-- docs/mcp-app-review.md | 206 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 215 insertions(+), 7 deletions(-) create mode 100644 docs/mcp-app-review.md diff --git a/README.md b/README.md index 1cc32ed6e..2e2b35419 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,8 @@ inspector/ Each client has its own README with client-specific detail: [web](./clients/web/README.md) · [cli](./clients/cli/README.md) · [tui](./clients/tui/README.md) · [launcher](./clients/launcher/README.md). +Task-oriented guides live under [`docs/`](./docs): [MCP server configuration](./docs/mcp-server-configuration.md) (the shared catalog/config/ad-hoc flags) and [Reviewing an MCP App](./docs/mcp-app-review.md) (the CLI-first → one-shot-web recipe for automated App-tool review: `--app-info` probe → deep-link navigate → rendered widget, plus OAuth handoff and proxy support). + ## Setup Requires Node `>=22.19.0`. @@ -120,13 +122,13 @@ Individual clients: `build:web`, `build:cli`, `build:tui`, `build:launcher`. The Each client self-validates from its own folder; the root scripts chain them. There is **no** aggregate root `test` script — use `validate` (fast) or `coverage` (the gate). -| Script | What it does | -| --- | --- | -| `npm run validate` | `format:check` + `lint` + `build` + fast unit tests, per client. The quick inner-loop check. | -| `npm run coverage` | The **per-file ≥90% gate** (lines/statements/functions/branches) under v8 instrumentation, per client. CI-enforced. For web this also runs the integration project. | -| `npm run smoke` | End-to-end smokes through the built launcher (`--help` dispatch + prod cli/tui/web). | -| `npm run ci` | **Mandatory pre-push command.** `validate` → `coverage` → `smoke` → Storybook. A true superset of GitHub CI. | -| `npm run pack:verify` | Publish smoke — see [Publishing](#publishing). | +| Script | What it does | +| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `npm run validate` | `format:check` + `lint` + `build` + fast unit tests, per client. The quick inner-loop check. | +| `npm run coverage` | The **per-file ≥90% gate** (lines/statements/functions/branches) under v8 instrumentation, per client. CI-enforced. For web this also runs the integration project. | +| `npm run smoke` | End-to-end smokes through the built launcher (`--help` dispatch + prod cli/tui/web). | +| `npm run ci` | **Mandatory pre-push command.** `validate` → `coverage` → `smoke` → Storybook. A true superset of GitHub CI. | +| `npm run pack:verify` | Publish smoke — see [Publishing](#publishing). | Per-client scripts exist too (`validate:web`, `coverage:cli`, `smoke:tui`, …). Run `npm run format` (per client) before committing — `validate` runs the non-fixing `format:check` and fails CI on any unformatted file. diff --git a/docs/mcp-app-review.md b/docs/mcp-app-review.md new file mode 100644 index 000000000..3e8ba6349 --- /dev/null +++ b/docs/mcp-app-review.md @@ -0,0 +1,206 @@ +# Reviewing an MCP App with the Inspector + +This recipe is for automated reviewers (CI, agents) that need to inspect an MCP +server's **App tools** — the tools that bind a UI widget to a tool result via +`_meta.ui.resourceUri`. It uses the **CLI for everything that has a JSON +answer** and drops to a browser **only** to render the widget itself. + +The flow: probe with `--app-info` → build a deep link → drive the rendered +widget, with no manual form-filling in between. It is the composition of the +Wave-2 (spec-conformant Apps host) and Wave-3/4 (programmatic review path) +work; each surface below links to the client README that owns its full contract. + +## 1. Probe: does the tool have an App, and what is its security posture? + +```bash +npx @modelcontextprotocol/inspector --cli \ + --transport http --server-url https://example.com/mcp \ + --method tools/call --tool-name --app-info +``` + +Stdout is one JSON line; the process exits **0** when the tool has an App and +**2** (`no_app`) when it does not, so a shell `&&` chain can short-circuit. +Example output for an App tool: + +```json +{ + "hasApp": true, + "toolName": "get_pros", + "resourceUri": "ui://pros/view.html", + "csp": { "connectDomains": ["https://api.example.com"] }, + "permissions": { "clipboard": false }, + "prefersBorder": true, + "resourceMimeType": "text/html" +} +``` + +`csp` / `permissions` / `domain` come from the UI **resource** (per the spec +they live on the resource, not the tool); `--app-info` reads the resource for +you. Nothing here invokes the tool. To probe every tool at once, use +`--method tools/list --app-info` (NDJSON, one line per tool, single +connection). See [clients/cli/README.md](../clients/cli/README.md) for the full +flag reference and exit-code map. + +## 2. Full result payload (still no browser) + +```bash +npx @modelcontextprotocol/inspector --cli \ + --transport http --server-url https://example.com/mcp \ + --method tools/call --tool-name --tool-args-json '{"zip":"10001"}' +``` + +Stdout is the pretty-printed `CallToolResult`; for App tools an +`--- MCP App Info ---` block is appended. Add `--format json` to emit a single +`{ "result": …, "appInfo": … }` object that pipes cleanly into `jq`. +`--tool-args-json` passes the arguments verbatim (no `key=value` coercion), so +`"012"` stays a string. + +## 3. Launch the web inspector once, loopback-only + +```bash +TOKEN="$(openssl rand -hex 24)" +HOST=127.0.0.1 CLIENT_PORT=6274 MCP_SANDBOX_PORT=6275 \ +MCP_AUTO_OPEN_ENABLED=false MCP_INSPECTOR_API_TOKEN="$TOKEN" \ +npx @modelcontextprotocol/inspector --web & +``` + +`HOST=127.0.0.1` binds the API and sandbox servers to loopback only. The token +is also the deep-link auth gate (next step). + +## 4. One deep-link navigate → a rendered widget + +The deep-link URL shape (owned by +[clients/web/README.md](../clients/web/README.md#deep-link-auto-connect)): + +``` +http://127.0.0.1:6274/?serverUrl= + &transport=http|sse + &autoConnect= + &openApp= + &appArgs= + &autoOpen= +``` + +`autoConnect` **must equal** `MCP_INSPECTOR_API_TOKEN`; the page rejects any +other value, so a link minted by a third party cannot drive the connect. +Loading the URL connects to the server, switches to the **Apps** tab, selects +`openApp`, and pre-fills `appArgs` (merged **over** the tool's schema defaults, +so a required-with-default field doesn't leave "Open App" disabled). + +`appArgs` is `base64url(JSON)`: + +```js +const args = Buffer.from(JSON.stringify({ zip: "10001" })) + .toString("base64") + .replace(/\+/g, "-") + .replace(/\//g, "_") + .replace(/=+$/, ""); +``` + +`autoOpen=` (same token gate) fires "Open App" automatically — a tool +call from a URL, which is why it is gated. Omit it to stop at the pre-filled +form and click "Open App" yourself. + +The inspector exposes a stable `data-testid` / `data-*` contract (documented in +[clients/web/README.md](../clients/web/README.md)) so a driver waits on +deterministic signals instead of sleeping: + +```js +// Playwright (or any driver). With autoOpen set, no click is needed. +const url = + `http://127.0.0.1:6274/?serverUrl=${encodeURIComponent(serverUrl)}` + + `&transport=http&autoConnect=${TOKEN}` + + `&openApp=${tool}&appArgs=${args}&autoOpen=${TOKEN}`; + +await page.goto(url); +// Connection surface: poll data-status; read data-error-message on failure. +await page.waitForSelector( + '[data-testid="connection-status"][data-status="connected"]', +); +// App render lifecycle: idle → loading → ready (or error). +await page.waitForSelector('[data-app-status="ready"]'); +await page.locator('[data-testid="apps-form"]').screenshot({ path: out }); +``` + +Without `autoOpen`, click the button first: +`await page.click('[data-testid="open-app"]')`, then wait on +`[data-app-status="ready"]`. If the connect is rejected (bad token / non-http +serverUrl), `[data-testid="connection-status"]` carries +`data-deeplink="rejected"` and `data-error-message` — read those instead of +scraping a toast. + +## 5. OAuth-gated servers + +When the target server requires OAuth, complete the flow once in the **web** +inspector — the credential is written through the backend to +`~/.mcp-inspector/storage/oauth.json`, so the **CLI** on the same machine can +reuse it: + +```bash +npx @modelcontextprotocol/inspector --cli \ + --transport http --server-url "$SERVER_URL" \ + --use-stored-auth --method tools/list +``` + +`--use-stored-auth` looks up the stored state for `$SERVER_URL` (keyed by +`new URL().href` normalization, so pass the same URL you gave the web +inspector). When a `refresh_token` is stored it runs the OAuth refresh grant +and injects the **fresh** access token, persisting the rotation back to the +state file; otherwise it injects the stored access token. A failed refresh +(revoked token) exits `3` (`auth_required`). Use `--wait-for-auth ` to +block until a human completes the flow in a browser, and `--print-handoff` to +emit the deep link + port-forward command a remote VM needs (see +[clients/cli/README.md](../clients/cli/README.md)). + +Connection state persists in `~/.mcp-inspector/` between runs. For a clean +slate between reviews of different servers, `rm -rf ~/.mcp-inspector` before +launching. + +## 6. Reaching the web inspector remotely (SSH port-forward) + +When the backend runs on another host and you want to complete OAuth or +visually inspect a rendered App from your local browser, forward **both** +ports: + +```bash +ssh -L 6274:127.0.0.1:6274 -L 6275:127.0.0.1:6275 +``` + +Then open `http://127.0.0.1:6274/?MCP_INSPECTOR_API_TOKEN=$TOKEN` locally. + +- Use `127.0.0.1`, **not** `localhost` — the backend's origin guard checks the + literal `Origin` header against the configured `HOST`. +- Forward `:6275` (`MCP_SANDBOX_PORT`) as well: the Apps tab renders widgets in + an iframe served from that second origin; without it the App frame stays + blank. +- Forward the **same** local port number you bind on the remote — the browser + sends `Origin: http://127.0.0.1:` and the backend compares it to + its own port. +- The OAuth credential ends up in `~/.mcp-inspector/storage/oauth.json` on the + **remote** host, so a CLI invocation there can pick it up with + `--use-stored-auth` (§ 5). + +## 7. Reaching the target server through an HTTP proxy + +The CLI's outbound connections honor the standard `HTTPS_PROXY` / `HTTP_PROXY` +/ `NO_PROXY` environment variables: + +```bash +HTTPS_PROXY=http://proxy.example:3128 \ +npx @modelcontextprotocol/inspector --cli \ + --transport http --server-url "$SERVER_URL" --method tools/list +``` + +Per-scheme routing and `NO_PROXY` exclusions are handled by undici's +`EnvHttpProxyAgent`, imported lazily only when a proxy variable is set — runs +without a proxy pay no cost. See +[clients/cli/README.md](../clients/cli/README.md#http-proxy-support). + +## Local fixture + +The bundled test server includes an `mcp_app_demo` tool (preset `mcp_app_demo` + +- resource preset `mcp_app_demo_widget`) whose widget reports `size-changed`, + sends one `ui/message`, emits one log notification, and renders its received + `hostContext` — useful for verifying the host side of the UI protocol + end-to-end without a remote server. From e6278aea0fd2251217f67643c72a7b22fcd73f46 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Sat, 11 Jul 2026 23:25:20 -0400 Subject: [PATCH 2/2] =?UTF-8?q?docs:=20address=20#1674=20review=20?= =?UTF-8?q?=E2=80=94=20fix=20broken=20link=20+=20list=20rendering?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Root README no longer links to docs/mcp-server-configuration.md (not yet ported to v2); keep only the mcp-app-review.md pointer (review #3). - Collapse the Local-fixture parenthetical onto one line so prettier no longer renders '- resource preset …' as a mid-sentence bullet (review #5). Findings #1/#2 (documents unmerged #1576/#1577/#1665 behavior) and #4 (the web-README #deep-link-auto-connect anchor) resolve in the Wave 4 rollup, which lands this doc together with those code PRs — the reviewer noted the same. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw --- README.md | 2 +- docs/mcp-app-review.md | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 2e2b35419..f9bf664de 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ inspector/ Each client has its own README with client-specific detail: [web](./clients/web/README.md) · [cli](./clients/cli/README.md) · [tui](./clients/tui/README.md) · [launcher](./clients/launcher/README.md). -Task-oriented guides live under [`docs/`](./docs): [MCP server configuration](./docs/mcp-server-configuration.md) (the shared catalog/config/ad-hoc flags) and [Reviewing an MCP App](./docs/mcp-app-review.md) (the CLI-first → one-shot-web recipe for automated App-tool review: `--app-info` probe → deep-link navigate → rendered widget, plus OAuth handoff and proxy support). +Task-oriented guides live under [`docs/`](./docs) — see [Reviewing an MCP App](./docs/mcp-app-review.md), the CLI-first → one-shot-web recipe for automated App-tool review: `--app-info` probe → deep-link navigate → rendered widget, plus OAuth handoff and proxy support. ## Setup diff --git a/docs/mcp-app-review.md b/docs/mcp-app-review.md index 3e8ba6349..c6ca251bd 100644 --- a/docs/mcp-app-review.md +++ b/docs/mcp-app-review.md @@ -198,9 +198,8 @@ without a proxy pay no cost. See ## Local fixture -The bundled test server includes an `mcp_app_demo` tool (preset `mcp_app_demo` - -- resource preset `mcp_app_demo_widget`) whose widget reports `size-changed`, - sends one `ui/message`, emits one log notification, and renders its received - `hostContext` — useful for verifying the host side of the UI protocol - end-to-end without a remote server. +The bundled test server includes an `mcp_app_demo` tool (presets `mcp_app_demo` +and `mcp_app_demo_widget`) whose widget reports `size-changed`, sends one +`ui/message`, emits one log notification, and renders its received `hostContext` +— useful for verifying the host side of the UI protocol end-to-end without a +remote server.