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
/
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
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
No line in this hunk matches that.