Recipes
docs/draft/tools/inspector/recipes
Nearest release: v2.1.246, published 8 hours before this site recorded the change. Shown because the two are within 24 hours of each other. Nothing here says the release caused the edit.
docs/draft/tools/inspector/recipes New page · 207 lines, new page
# Recipes ## Connecting stdio vs. HTTP servers ### stdio ### HTTP and SSE ## Importing an existing client config ## Reviewing an MCP App ## Docker ## Hosting on a network ## Development workflow
A whole new page. There's nothing to diff it against, so here is what it says.
# Recipes
> Practical guides for transports, importing configs, reviewing MCP Apps, Docker, and network hosting
## Connecting stdio vs. HTTP servers
### stdio
A stdio server is a process the Inspector spawns. Everything positional is the command line:
```bash theme={null}
mcp-inspector node build/index.js -- --verbose --config /etc/myserver.conf
```
Put `--` before any arguments meant for your server. Without the separator, `--verbose` would be
parsed by the Inspector and never reach the server.
Give the process environment variables with `-e` and a working directory with `--cwd`:
```bash theme={null}
mcp-inspector -e API_KEY=abc123 -e REGION=us-east-1 --cwd ~/projects/my-server \
node build/index.js
```
The server's `stderr` lands in the **Console** tab (web) or the Console tab (`o`, TUI), which is where most stdio servers put their diagnostics, so check there first when a connection fails for no visible reason.
### HTTP and SSE
```bash theme={null}
mcp-inspector --server-url https://api.example.com/mcp --transport http \
--header "X-Tenant: acme"
```
`--transport` accepts `http` (Streamable HTTP) and `sse`. If the server is protected, see [Authorization](/docs/draft/tools/inspector/authorization): no setup is needed in advance, because when the server answers `401` the Inspector runs the OAuth flow described there and retries the connection.
For an HTTP server, also decide its [protocol era](/docs/draft/tools/inspector/protocol-eras). The default is `legacy`; set `modern` or `auto` in Server Settings (or `protocolEra` in the catalog file) to exercise the 2026-07-28 behavior.
## Importing an existing client config
On the Servers screen, **Add Servers** can import MCP servers you have already configured
elsewhere instead of retyping them. It parses Claude Desktop, Cursor, Cline, and VS Code client
configs directly, and it also reads a server's own [MCP Registry](/registry/about) `server.json`.
Import merges into the active [catalog](/docs/draft/tools/inspector/configuration#choosing-servers)
(the Inspector's writable server list), so existing entries aren't clobbered. If you'd rather
not touch your catalog at all, launch against the foreign file read-only instead:
```bash theme={null}
mcp-inspector --config ~/Library/Application\ Support/Claude/claude_desktop_config.json
```
`--config` guarantees the file is served as-is and never written, seeded, or migrated.
<Frame caption="Add Servers offers import from an existing client config or from a registry server.json.">
<img src="https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/import-config.png?fit=max&auto=format&n=gk28X8wi_tbRYzej&q=85&s=c9f5229c2d827f4bcab37938879f21f2" width="3840" height="2160" data-path="images/inspector/import-config.png" />
</Frame>
## Reviewing an MCP App
[MCP Apps](/extensions/apps/overview) are tools that carry a UI widget. For an automated reviewer (CI or an agent), use the CLI for every check that returns JSON, and open a browser only to inspect the rendered widget.
<Steps>
<Step title="Probe the security posture without calling the tool">
```bash theme={null}
mcp-inspector --cli --transport http --server-url https://example.com/mcp \
--method tools/call --tool-name <tool> --app-info
```
One JSON line on stdout; exit `0` if the tool has an app, `2` if not, so an `&&` chain short-circuits:
```json theme={null}
{
"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` and `permissions` (and `domain`, when the resource declares one) live on the UI **resource** rather than the tool, so `--app-info` reads that resource. The tool is never called.
</Step>
<Step title="Get the full result payload, still with no browser">
```bash theme={null}
mcp-inspector --cli --transport http --server-url https://example.com/mcp \
--method tools/call --tool-name <tool> --tool-args-json '{"zip":"10001"}' --format json
```
</Step>
<Step title="Launch the web Inspector once, loopback-only">
```bash theme={null}
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" \
mcp-inspector --web &
```
Pinning `MCP_SANDBOX_PORT` matters here: the app's UI is served from a separate sandbox port that is dynamic by default, and your automation needs a fixed address to reach it.
</Step>
<Step title="Navigate one deep link to a rendered widget">
```
http://127.0.0.1:6274/?serverUrl=<encoded url>&transport=http&autoConnect=<TOKEN>&openApp=<tool>&appArgs=<base64url(JSON)>&autoOpen=<TOKEN>
```
`appArgs` is the tool's arguments as base64url-encoded JSON, and every deep-link parameter is described under [Deep links](/docs/draft/tools/inspector/web#deep-links). `autoConnect` and `autoOpen` must both equal the session token, since `autoOpen` fires a tool call straight from the URL and needs the same gate as `autoConnect`.
</Step>
<Step title="Wait on a deterministic signal instead of sleeping">
The Apps screen exposes a stable automation contract. Poll these attributes instead of sleeping:
| Selector | Attribute | Values |
| ----------------------------------- | ----------------- | ------------------------------------------------------------------------------------------------------- |
| `[data-testid="apps-form"]` | `data-app-status` | `ready` (on failure, `data-app-error` carries the reason) |
| `[data-testid="connection-status"]` | `data-status` | `connecting`, then `connected` or `error` (`data-error-message` has the detail) |
| `[data-testid="connection-status"]` | `data-deeplink` | `parsed`, `rejected`, or `none` (`none` means no deep link was given, `rejected` means one was refused) |
</Step>
</Steps>
## Docker
A container image is published to GitHub Container Registry for `linux/amd64` and `linux/arm64`:
```bash theme={null}
docker run --rm -p 6274:6274 ghcr.io/modelcontextprotocol/inspector
```
Read the [session token](/docs/draft/tools/inspector/web#the-session-token) from the container logs, or pin it with `-e MCP_INSPECTOR_API_TOKEN=<value>`.
The image defaults to `--web`, bound to `0.0.0.0:6274` with browser auto-open off, and runs as a non-root user. It sets `DANGEROUSLY_BIND_ALL_INTERFACES=true` because a container must bind the wildcard address to be reachable through `-p`.
Its `HEALTHCHECK` probes the web UI, so add `--no-healthcheck` when running `--cli` or `--tui` (neither has a web server). `<target>` below is an [ad-hoc target](/docs/draft/tools/inspector/configuration#ad-hoc-targets): a positional stdio command, or `--server-url <url> --transport http`.
```bash theme={null}
docker run --rm --no-healthcheck ghcr.io/modelcontextprotocol/inspector --cli <target> --method tools/list
```
<Warning>
**If you remap the published port, set `ALLOWED_ORIGINS`.** With `-p
8080:6274` the browser's origin becomes `http://localhost:8080`, which no
longer matches the in-container port, and connects will `403`. Either run `-e
CLIENT_PORT=8080 -p 8080:8080`, or set `-e
ALLOWED_ORIGINS=http://localhost:8080,http://127.0.0.1:8080`.
</Warning>
## Hosting on a network
The Inspector binds `localhost` by default and its backend spawns processes, so treat exposing it to a network as a deliberate decision.
The Inspector refuses to bind the **wildcard** all-interfaces addresses (`0.0.0.0`, `::`, and every equivalent spelling) unless you set `DANGEROUSLY_BIND_ALL_INTERFACES=true`. Binding a **specific** address is allowed with no opt-in, because that's one deliberate exposure rather than every interface at once, which is the shape DNS-rebinding attacks target.
| Goal | What to do |
| -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Reach it from another machine on the LAN** | `HOST=192.168.1.50`. The default origin allow-list follows the bind host, so `http://192.168.1.50:6274` is accepted with no further config. |
| **Behind TLS or a reverse proxy** | The browser's `Origin` becomes the public origin, which won't match the bind host. Set `ALLOWED_ORIGINS=https://inspector.example.com`. |
| **Wildcard bind (containers)** | Set `DANGEROUSLY_BIND_ALL_INTERFACES=true`. Loopback access still works out of the box; reaching it at a non-loopback address needs `ALLOWED_ORIGINS`. |
<Warning>
`ALLOWED_ORIGINS` **replaces** the default list rather than merging with it. List every origin you'll browse from, including the loopback forms you want to keep:
```
ALLOWED_ORIGINS=http://localhost:6274,http://127.0.0.1:6274,http://192.168.1.50:6274
```
Each entry must include the scheme; a scheme-less value is dropped with a warning. A blank value does **not** disable the check; it falls back to the default. There is no knob to turn origin validation off.
</Warning>
Two further caveats when going off loopback:
* **MCP Apps need their sandbox port reachable too.** It's a separate, dynamic-by-default port; pin it with `MCP_SANDBOX_PORT` and expose or forward it. The Docker image publishes only `6274`.
* **MCP Apps can't render over TLS or at a bare IPv6 literal.** The sandbox URL is always plain `http`, so an `https://` page blocks the iframe as mixed content; and a bracketed IPv6 literal isn't a valid CSP host-source, so browse at a name or an IPv4 address.
Whatever the shape: keep authentication on. Do not set `DANGEROUSLY_OMIT_AUTH` on anything reachable by anyone but you.
## Development workflow
A loop that works well in practice:
<Steps>
<Step title="Start with the CLI">
`--method initialize` confirms the server starts, handshakes, and reports
the capabilities you expect, in one second, with a machine-readable answer.
Most "it doesn't work" turns out to be here.
</Step>
<Step title="Move to the web client for exploration">
Schema-driven forms, rendered results, and the Protocol tab beside them make
it fast to find the case where a tool misbehaves.
</Step>
<Step title="Test the edges">
Invalid inputs, missing required prompt arguments, concurrent calls, and,
for HTTP servers, both protocol eras. Verify the *errors* are as intentional
as the successes.
</Step>
<Step title="Lock it in with the CLI">
Turn what you found into a CI assertion: pipe the CLI's `--format json`
output to `jq -e` with `--stored-auth-only`, so a missing token fails fast
instead of starting interactive OAuth. See [Verify a server in
CI](/docs/draft/tools/inspector/cli#verify-a-server-in-ci) for the full
command.
</Step>
</Steps>