Follow Discord
Sweep 22 Oct 2026 · 04:32Z Build v2.1.308 527 read Stable v2.1.308 Latest v2.1.308 Next v2.1.308 Feeds RSS JSON llms.txt llms-full.txt Unofficial
One change · mcp

Recipes changeddocs/2026-07-28/tools/inspector/recipes

Nearest release: v2.1.295, published 5 hours after upstream edited the page. Shown because the two are within 24 hours of each other. Nothing here says the release caused the edit.

Upstream edited this page at 8 Oct 2026 12:58 UTC, give or take a minute or two: the time comes from Anthropic’s own sitemap rather than from a commit. This site recorded the change at 20 Oct 2026 20:07 UTC.

Upstream edited
Recorded here
Lines+148added
Lines−156removed
From line 52 where the diff opens
First seen 14 Aug 2026 this site's first read of the page
Recorded edits4to this page, all time

The whole hunk

from line 52, old and new numbered
/
lines
from line 52
5252 
5353`--config` guarantees the file is served as-is and never written, seeded, or migrated.
5454 
55<Frame caption="Add Servers offers import from an existing client config or from a registry server.json.">
56 <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" />
57</Frame>
55![](https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/import-config.png?fit=max\&auto=format\&n=gk28X8wi_tbRYzej\&q=85\&s=c9f5229c2d827f4bcab37938879f21f2)
5856 
5957## Reviewing an MCP App
6058 
6159[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.
6260 
63<Steps>
64 <Step title="Probe the security posture without calling the tool">
65 ```bash theme={null}
66 mcp-inspector --cli --transport http --server-url https://example.com/mcp \
67 --method tools/call --tool-name <tool> --app-info --advertise-apps
68 ```
61**1. Probe the security posture without calling the tool**
6962 
70 `--advertise-apps` makes the CLI claim MCP Apps support at `initialize`. It is off by default because the CLI cannot render an app, but a server that shows its app tools only to app-capable clients would otherwise report no app.
63```bash theme={null}
64mcp-inspector --cli --transport http --server-url https://example.com/mcp \
65 --method tools/call --tool-name <tool> --app-info --advertise-apps
66```
7167 
72 One JSON line on stdout; exit `0` if the tool has an app, `2` if not, so an `&&` chain short-circuits:
68`--advertise-apps` makes the CLI claim MCP Apps support at `initialize`. It is off by default because the CLI cannot render an app, but a server that shows its app tools only to app-capable clients would otherwise report no app.
7369 
74 ```json theme={null}
75 {
76 "hasApp": true,
77 "toolName": "get_pros",
78 "resourceUri": "ui://pros/view.html",
79 "csp": { "connectDomains": ["https://api.example.com"] },
80 "permissions": { "clipboard": false },
81 "prefersBorder": true,
82 "resourceMimeType": "text/html"
83 }
84 ```
70One JSON line on stdout; exit `0` if the tool has an app, `2` if not, so an `&&` chain short-circuits:
8571 
86 `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.
87 </Step>
72```json theme={null}
73{
74 "hasApp": true,
75 "toolName": "get_pros",
76 "resourceUri": "ui://pros/view.html",
77 "csp": { "connectDomains": ["https://api.example.com"] },
78 "permissions": { "clipboard": false },
79 "prefersBorder": true,
80 "resourceMimeType": "text/html"
81}
82```
8883 
89 <Step title="Get the full result payload, still with no browser">
90 ```bash theme={null}
91 mcp-inspector --cli --transport http --server-url https://example.com/mcp \
92 --method tools/call --tool-name <tool> --tool-args-json '{"zip":"10001"}' --format json
93 ```
94 </Step>
84`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.
9585 
96 <Step title="Launch the web Inspector once, loopback-only">
97 ```bash theme={null}
98 TOKEN="$(openssl rand -hex 24)"
99 HOST=127.0.0.1 CLIENT_PORT=6274 MCP_SANDBOX_PORT=6275 \
100 MCP_AUTO_OPEN_ENABLED=false MCP_INSPECTOR_API_TOKEN="$TOKEN" \
101 mcp-inspector --web &
102 ```
86**2. Get the full result payload, still with no browser**
10387 
104 Pinning `MCP_SANDBOX_PORT` keeps the address explicit: the app's UI is served from a separate sandbox port, and your automation needs to know where it is.
105 </Step>
88```bash theme={null}
89mcp-inspector --cli --transport http --server-url https://example.com/mcp \
90 --method tools/call --tool-name <tool> --tool-args-json '{"zip":"10001"}' --format json
91```
10692 
107 <Step title="Navigate one deep link to a rendered widget">
108 ```
109 http://127.0.0.1:6274/?serverUrl=<encoded url>&transport=http&autoConnect=<TOKEN>&openApp=<tool>&appArgs=<base64url(JSON)>&autoOpen=<TOKEN>
110 ```
93**3. Launch the web Inspector once, loopback-only**
11194 
112 `appArgs` is the tool's arguments as base64url-encoded JSON, and every deep-link parameter is described under [Deep links](/docs/2026-07-28/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`.
113 </Step>
95```bash theme={null}
96TOKEN="$(openssl rand -hex 24)"
97HOST=127.0.0.1 CLIENT_PORT=6274 MCP_SANDBOX_PORT=6275 \
98MCP_AUTO_OPEN_ENABLED=false MCP_INSPECTOR_API_TOKEN="$TOKEN" \
99mcp-inspector --web &
100```
114101 
115 <Step title="Wait on a deterministic signal instead of sleeping">
116 The Apps screen exposes a stable automation contract. Poll these attributes instead of sleeping:
102Pinning `MCP_SANDBOX_PORT` keeps the address explicit: the app's UI is served from a separate sandbox port, and your automation needs to know where it is.
117103 
118 | Selector | Attribute | Values |
119 | - | - | - |
120 | `[data-testid="apps-form"]` | `data-app-status` | `idle`, then `loading`, then `ready` or `error` (on `error`, `data-app-error` carries the reason) |
121 | `[data-testid="connection-status"]` | `data-status` | `disconnected`, `connecting`, then `connected` or `error` (`data-error-message` has the detail) |
122 | `[data-testid="connection-status"]` | `data-deeplink` | `parsed`, `rejected`, or `none` (`none` means no deep link was given, `rejected` means one was refused) |
123 </Step>
124</Steps>
104**4. Navigate one deep link to a rendered widget**
125105 
106```
107http://127.0.0.1:6274/?serverUrl=<encoded url>&transport=http&autoConnect=<TOKEN>&openApp=<tool>&appArgs=<base64url(JSON)>&autoOpen=<TOKEN>
108```
109 
110`appArgs` is the tool's arguments as base64url-encoded JSON, and every deep-link parameter is described under [Deep links](/docs/2026-07-28/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`.
111 
112**5. Wait on a deterministic signal instead of sleeping**
113 
114The Apps screen exposes a stable automation contract. Poll these attributes instead of sleeping:
115 
116| Selector | Attribute | Values |
117| - | - | - |
118| `[data-testid="apps-form"]` | `data-app-status` | `idle`, then `loading`, then `ready` or `error` (on `error`, `data-app-error` carries the reason) |
119| `[data-testid="connection-status"]` | `data-status` | `disconnected`, `connecting`, then `connected` or `error` (`data-error-message` has the detail) |
120| `[data-testid="connection-status"]` | `data-deeplink` | `parsed`, `rejected`, or `none` (`none` means no deep link was given, `rejected` means one was refused) |
121 
126122## Docker
127123 
128124A container image is published to GitHub Container Registry for `linux/amd64` and `linux/arm64`:
from line 131
135131 
136132The image defaults to `--web`, bound to `0.0.0.0:6274` with browser auto-open off, and runs as the non-root `node` user (uid `1000`). It sets `DANGEROUSLY_BIND_ALL_INTERFACES=true` because a container must bind the wildcard address to be reachable through `-p`.
137133 
138<Warning>
139 **Keep the `127.0.0.1:` prefix on every published port.** A bare `-p
140 6274:6274` publishes on every host interface, putting a backend that spawns
141 processes, and the page that discloses its token, on your local network. The
142 image's `DANGEROUSLY_BIND_ALL_INTERFACES` covers the container's interfaces,
143 not the host's. See [Publish the port on loopback
144 only](/docs/2026-07-28/tools/inspector/security#publish-the-port-on-loopback-only).
145</Warning>
134> [!WARNING]
135>
136> **Keep the `127.0.0.1:` prefix on every published port.** A bare `-p
137> 6274:6274` publishes on every host interface, putting a backend that spawns
138> processes, and the page that discloses its token, on your local network. The
139> image's `DANGEROUSLY_BIND_ALL_INTERFACES` covers the container's interfaces,
140> not the host's. See [Publish the port on loopback
141> only](/docs/2026-07-28/tools/inspector/security#publish-the-port-on-loopback-only).
146142 
147143To use the **Apps** tab, also publish the MCP Apps sandbox port, `6275`, and `6278` for an app that declares `_meta.ui.domain`. Publish each on the same port number inside and out, since the browser is handed the in-container port:
148144 
from line 166
170166 
171167If you bind-mount a host directory instead of a named volume, it keeps its host ownership, so on Linux add `--user "$(id -u):$(id -g)"` or `chown` it to uid `1000`, or saves fail with `EACCES`. Don't bind-mount the secrets file on its own: it isn't recognized as durable, and it can't be replaced atomically.
172168 
173<Warning>
174 **Mounting that volume turns on file storage of secrets, and without a key the file is plaintext.** Every OAuth token acquired, and every client secret and stdio `env:` value you save, is then written to `secrets.json` on the volume. It is readable by root and every member of the host's `docker` group, and by anyone who gets a backup, snapshot or copy of the volume.
169> [!WARNING]
170>
171> **Mounting that volume turns on file storage of secrets, and without a key the file is plaintext.** Every OAuth token acquired, and every client secret and stdio `env:` value you save, is then written to `secrets.json` on the volume. It is readable by root and every member of the host's `docker` group, and by anyone who gets a backup, snapshot or copy of the volume.
172>
173> Give it a key, generated into a file that only you can read and that sits outside the volume, its backups and any repository:
174>
175> ```bash theme={null}
176> mkdir -p ~/.config/mcp-inspector
177> (umask 077 && openssl rand -base64 32 > ~/.config/mcp-inspector/secret-key)
178> ```
179>
180> Even encrypted, secrets on disk carry moderate risk. See [what the file store protects against](/docs/2026-07-28/tools/inspector/security#what-the-file-store-protects-against).
175181 
176 Give it a key, generated into a file that only you can read and that sits outside the volume, its backups and any repository:
182Hand the key to the container **as a file** with `MCP_INSPECTOR_SECRET_KEY_FILE`, not as an environment variable. A key passed with `-e MCP_INSPECTOR_SECRET_KEY=…` is readable by anyone who can run `docker inspect` or `docker exec`.
177183 
178 ```bash theme={null}
179 mkdir -p ~/.config/mcp-inspector
180 (umask 077 && openssl rand -base64 32 > ~/.config/mcp-inspector/secret-key)
181 ```
184**docker run**
182185 
183 Even encrypted, secrets on disk carry moderate risk. See [what the file store protects against](/docs/2026-07-28/tools/inspector/security#what-the-file-store-protects-against).
184</Warning>
186```bash theme={null}
187docker run --rm -p 127.0.0.1:6274:6274 \
188 -v mcp-inspector-data:/home/node/.mcp-inspector \
189 -v "$HOME/.config/mcp-inspector/secret-key:/run/secrets/mcp_inspector_secret_key:ro" \
190 -e MCP_INSPECTOR_SECRET_KEY_FILE=/run/secrets/mcp_inspector_secret_key \
191 ghcr.io/modelcontextprotocol/inspector
192```
185193 
186Hand the key to the container **as a file** with `MCP_INSPECTOR_SECRET_KEY_FILE`, not as an environment variable. A key passed with `-e MCP_INSPECTOR_SECRET_KEY=…` is readable by anyone who can run `docker inspect` or `docker exec`.
194**Compose secrets**
187195 
188<Tabs>
189 <Tab title="docker run">
190 ```bash theme={null}
191 docker run --rm -p 127.0.0.1:6274:6274 \
192 -v mcp-inspector-data:/home/node/.mcp-inspector \
193 -v "$HOME/.config/mcp-inspector/secret-key:/run/secrets/mcp_inspector_secret_key:ro" \
194 -e MCP_INSPECTOR_SECRET_KEY_FILE=/run/secrets/mcp_inspector_secret_key \
195 ghcr.io/modelcontextprotocol/inspector
196 ```
197 </Tab>
196```yaml theme={null}
197services:
198 inspector:
199 image: ghcr.io/modelcontextprotocol/inspector
200 ports: ["127.0.0.1:6274:6274"]
201 volumes: ["mcp-inspector-data:/home/node/.mcp-inspector"]
202 environment:
203 MCP_INSPECTOR_SECRET_KEY_FILE: /run/secrets/mcp_inspector_secret_key
204 secrets: [mcp_inspector_secret_key]
205secrets:
206 mcp_inspector_secret_key:
207 file: ${HOME}/.config/mcp-inspector/secret-key
208volumes:
209 mcp-inspector-data:
210```
198211 
199 <Tab title="Compose secrets">
200 ```yaml theme={null}
201 services:
202 inspector:
203 image: ghcr.io/modelcontextprotocol/inspector
204 ports: ["127.0.0.1:6274:6274"]
205 volumes: ["mcp-inspector-data:/home/node/.mcp-inspector"]
206 environment:
207 MCP_INSPECTOR_SECRET_KEY_FILE: /run/secrets/mcp_inspector_secret_key
208 secrets: [mcp_inspector_secret_key]
209 secrets:
210 mcp_inspector_secret_key:
211 file: ${HOME}/.config/mcp-inspector/secret-key
212 volumes:
213 mcp-inspector-data:
214 ```
215 </Tab>
216</Tabs>
217 
218212Without Swarm, Compose secrets are bind mounts that keep the host file's owner and mode, so the `0600` key file must be owned by uid `1000`. On a Linux host where your uid is different, run `sudo chown 1000 ~/.config/mcp-inspector/secret-key` rather than loosening its mode. Supply the **same** key on every run.
219213 
220214If the key file is missing, unreadable or empty, or both key variables are set, the Inspector **refuses to read or write the secrets file** rather than falling back to plaintext, and says why in the log and in the settings dialogs' footer. Everything else about the store (selection order, location, permissions) is under [Where secrets are stored](/docs/2026-07-28/tools/inspector/configuration#where-secrets-are-stored).
from line 223
229223docker run --rm ghcr.io/modelcontextprotocol/inspector --cli <target> --method tools/list
230224```
231225 
232<Warning>
233 **If you remap the published port, set `ALLOWED_ORIGINS`.** With `-p
234 127.0.0.1:8080:6274` the browser's origin becomes `http://localhost:8080`,
235 which no longer matches the in-container port, and connects will `403`. Either
236 run `-e CLIENT_PORT=8080 -p 127.0.0.1:8080:8080`, or set `-e
237 ALLOWED_ORIGINS=http://localhost:8080,http://127.0.0.1:8080`.
238</Warning>
226> [!WARNING]
227>
228> **If you remap the published port, set `ALLOWED_ORIGINS`.** With `-p
229> 127.0.0.1:8080:6274` the browser's origin becomes `http://localhost:8080`,
230> which no longer matches the in-container port, and connects will `403`. Either
231> run `-e CLIENT_PORT=8080 -p 127.0.0.1:8080:8080`, or set `-e
232> ALLOWED_ORIGINS=http://localhost:8080,http://127.0.0.1:8080`.
239233 
240234## Hosting on a network
241235 
from line 243
249243| **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`, and for MCP Apps set `MCP_SANDBOX_FULL_ADDRESS` (see below). |
250244| **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`. |
251245 
252<Warning>
253 `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:
246> [!WARNING]
247>
248> `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:
249>
250> ```
251> ALLOWED_ORIGINS=http://localhost:6274,http://127.0.0.1:6274,http://192.168.1.50:6274
252> ```
253>
254> 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.
254255 
255 ```
256 ALLOWED_ORIGINS=http://localhost:6274,http://127.0.0.1:6274,http://192.168.1.50:6274
257 ```
258 
259 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.
260</Warning>
261 
262256Two further caveats when going off loopback:
263257 
264258* **MCP Apps need their sandbox port reachable too.** It's a separate listener (`6275` by default, set with `MCP_SANDBOX_PORT`), so expose or forward it alongside the web port.
from line 281
287281 
288282A loop that works well in practice:
289283 
290<Steps>
291 <Step title="Start with the CLI">
292 `--method initialize` confirms the server starts, handshakes, and reports
293 the capabilities you expect, in one second, with a machine-readable answer.
294 Most "it doesn't work" turns out to be here.
295 </Step>
284**1. Start with the CLI**
296285 
297 <Step title="Move to the web client for exploration">
298 Schema-driven forms, rendered results, and the Protocol tab beside them make
299 it fast to find the case where a tool misbehaves.
300 </Step>
286`--method initialize` confirms the server starts, handshakes, and reports
287the capabilities you expect, in one second, with a machine-readable answer.
288Most "it doesn't work" turns out to be here.
301289 
302 <Step title="Test the edges">
303 Invalid inputs, missing required prompt arguments, concurrent calls, and,
304 for HTTP servers, both protocol eras. Verify the *errors* are as intentional
305 as the successes.
306 </Step>
290**2. Move to the web client for exploration**
307291 
308 <Step title="Lock it in with the CLI">
309 Turn what you found into a CI assertion: pipe the CLI's `--format json`
310 output to `jq -e` with `--stored-auth-only`, so a missing token fails fast
311 instead of starting interactive OAuth. See [Verify a server in
312 CI](/docs/2026-07-28/tools/inspector/cli#verify-a-server-in-ci) for the full
313 command.
314 </Step>
315</Steps>
292Schema-driven forms, rendered results, and the Protocol tab beside them make
293it fast to find the case where a tool misbehaves.
294 
295**3. Test the edges**
296 
297Invalid inputs, missing required prompt arguments, concurrent calls, and,
298for HTTP servers, both protocol eras. Verify the *errors* are as intentional
299as the successes.
300 
301**4. Lock it in with the CLI**
302 
303Turn what you found into a CI assertion: pipe the CLI's `--format json`
304output to `jq -e` with `--stored-auth-only`, so a missing token fails fast
305instead of starting interactive OAuth. See [Verify a server in
306CI](/docs/2026-07-28/tools/inspector/cli#verify-a-server-in-ci) for the full
307command.
316308 
Feedback