# How to Test an MCP Server With the MCP Inspector in a Cloud Sandbox

> Install @modelcontextprotocol/inspector with npm in a sandbox, run its web UI as a systemd service on 127.0.0.1:6274 with ALLOWED_ORIGINS set to the hosted origin, and host it with sbx host --localhost. Keep that URL private. The Inspector page hands its API token to anyone who loads it, and its backend runs any stdio command it is given.

Source: https://sandboxer.com/guides/test-an-mcp-server-with-the-mcp-inspector. Updated 2026-10-27.

**Short answer:** Install @modelcontextprotocol/inspector with npm in a sandbox, run its web UI as a systemd service on 127.0.0.1:6274 with ALLOWED_ORIGINS set to the hosted origin, and host it with sbx host --localhost. Keep that URL private. The Inspector page hands its API token to anyone who loads it, and its backend runs any stdio command it is given.

The [MCP Inspector](https://github.com/modelcontextprotocol/inspector) is the MCP project's own test client. This guide runs it next to the server you are building, in a throwaway VM, and opens it in your browser. If you want an agent to *use* Sandboxer through MCP instead, see [Local or hosted MCP for Cursor, Claude Code and Codex](/guides/local-or-hosted-mcp-for-cursor-claude-code-codex).

## What we ran

Everything below ran on production on October 27, 2026, in one `small` sandbox (2 vCPU, 4 GB): Inspector 2.12.0, the reference [everything](https://github.com/modelcontextprotocol/servers/tree/main/src/everything) and [filesystem](https://github.com/modelcontextprotocol/servers/tree/main/src/filesystem) servers 2.0.1, Node 22.23.3. Timings are single runs; browser timings were measured from our machine with headless Chromium.

| Check | Result |
| - | - |
| `npm i -g` of the Inspector and both servers | 16.6 s |
| CLI `initialize` over stdio, process start included | 0.47 s |
| Hosted URL without its token | `401` |
| Hosted URL with its token, before setting `ALLOWED_ORIGINS` | `403` from the Inspector: host not allowed |
| Same, after setting it | `200`, the Inspector page |
| API call with a foreign `Origin` header | `403`, "Invalid origin" |
| Connect from the browser, stdio or Streamable HTTP | 0.82 s |
| Run the `echo` tool from the browser | 0.22 s |
| `sbx stop --wait`, then `sbx resume` | 2.2 s and 1.3 s. Memory restored: same PIDs, page answered `200` |

A `small` sandbox costs $0.018/hour while it runs and nothing while it is stopped.

## 1. Install the Inspector and two real servers

Inspector 2.x needs Node 22.19 or later. The base image has Node 22.23.3. The two reference servers declare Node 24 in their `package.json`; everything in this guide ran on Node 22 anyway.

```bash
sbx new --name mcp-inspector --type small
id=sb_8qpbsenfin    # the id that sbx new printed

sbx exec --timeout 600 $id "sudo npm i -g --silent @modelcontextprotocol/inspector@2.12.0 \
  @modelcontextprotocol/server-everything@2.0.1 @modelcontextprotocol/server-filesystem@2.0.1 && \
  mcp-inspector --version && which mcp-server-everything mcp-server-filesystem"
```

```text
2.12.0
/usr/bin/mcp-server-everything
/usr/bin/mcp-server-filesystem
```

Pin the versions. The Inspector's own [smoke-testing guide](https://github.com/modelcontextprotocol/inspector/blob/main/docs/cli-smoke-testing.md) warns that `npx @modelcontextprotocol/inspector` resolves the latest release on every run. Swap in your own server the same way: install it in the sandbox, or `git clone` and build it there.

## 2. Script a check with the CLI first

The CLI mode needs no browser and nothing hosted. It connects, runs one method, prints the result and exits with a code you can branch on. Run these inside the sandbox (`sbx ssh $id`, or wrap each in `sbx exec $id "…"`).

Over stdio, the Inspector spawns the server itself:

```bash
mcp-inspector --cli mcp-server-everything --method tools/list --format json | jq -r '.result.tools[].name' | tr '\n' ' '
mcp-inspector --cli mcp-server-everything --method tools/call --tool-name echo \
  --tool-arg message="hello from a sandbox" --format json
```

```text
echo get-annotated-message get-env get-header-params get-resource-links … get-roots-list
{"result":{"content":[{"type":"text","text":"Echo: hello from a sandbox"}]}}
```

The filesystem server takes the directories it may touch as arguments. A read outside them is a tool error, and the CLI exits with `5`:

```bash
mcp-inspector --cli mcp-server-filesystem /home/user/project --method tools/call \
  --tool-name read_text_file --tool-arg path=/etc/hostname --format json; echo "exit=$?"
```

```text
{"result":{"content":[{"type":"text","text":"Access denied - path outside allowed directories: /etc/hostname not in /home/user/project"}],"isError":true}}
…
{"error":{"code":"tool_is_error","message":"Tool 'read_text_file' returned isError:true."}}
exit=5
```

Over Streamable HTTP (the server from step 3 on port 3001), point the CLI at the URL. A path ending in `/mcp` selects the transport:

```bash
mcp-inspector --cli --server-url http://127.0.0.1:3001/mcp --transport http --method initialize --format json \
  | jq -c '{server: .result.serverInfo.name, protocol: .result.protocolVersion}'
mcp-inspector --cli http://127.0.0.1:3001/mcp --method tools/call --tool-name get-sum \
  --tool-arg a=2 --tool-arg b=40 --format json | jq -r '.result.content[0].text'
```

```text
{"server":"mcp-servers/everything","protocol":"2025-11-25"}
The sum of 2 and 40 is 42.
```

A tool that doesn't exist also exits `5`, with `{"error":{"code":"tool_not_found",…}}` on stderr. The full exit-code table is in the [CLI README](https://github.com/modelcontextprotocol/inspector/blob/main/clients/cli/README.md#exit-codes--error-envelopes). That makes the CLI a good gate for an agent loop: change the server, run the check, read the exit code.

## 3. Run the server and the web UI as services

Write a catalog with the servers you want in the UI. Passing `--catalog` also keeps the web UI from seeding its default sample list, which includes a remote server.

```json
{
  "mcpServers": {
    "everything-stdio": { "type": "stdio", "command": "mcp-server-everything", "args": ["stdio"] },
    "filesystem-stdio": { "type": "stdio", "command": "mcp-server-filesystem", "args": ["/home/user/project"] },
    "everything-http": { "type": "streamable-http", "url": "http://127.0.0.1:3001/mcp" }
  }
}
```

Save it as `/home/user/inspector/mcp.json`. Then add two systemd units, so both come back if the sandbox ever boots from disk ([long-running docs](/docs/long-running#services-that-should-always-run)). Use your own sandbox's subdomain in `ALLOWED_ORIGINS`; `sbx new` printed it in the sandbox URL.

```ini
# /etc/systemd/system/mcp-everything.service
[Unit]
Description=MCP everything server (Streamable HTTP)
After=network-online.target

[Service]
User=user
Environment=PORT=3001
ExecStart=/usr/bin/mcp-server-everything streamableHttp
Restart=always

[Install]
WantedBy=multi-user.target
```

```ini
# /etc/systemd/system/mcp-inspector.service
[Unit]
Description=MCP Inspector web UI
After=network-online.target

[Service]
User=user
WorkingDirectory=/home/user/inspector
Environment=HOME=/home/user
Environment=MCP_AUTO_OPEN_ENABLED=false
Environment=ALLOWED_ORIGINS=https://minty-vernal-plum-6274.box.sandboxer.com
ExecStart=/usr/bin/mcp-inspector --web --catalog /home/user/inspector/mcp.json
Restart=always

[Install]
WantedBy=multi-user.target
```

We put the catalog and both units in one script, `setup.sh`, and piped it in with `sbx exec -i`:

```bash
sbx exec -i --timeout 120 $id "bash -s" < setup.sh
# setup.sh ends with:
#   sudo systemctl daemon-reload && sudo systemctl enable --now mcp-everything mcp-inspector
#   sudo ss -ltnp | grep -E ':(3001|6274|6275|6278) '
```

```text
LISTEN 0  511  127.0.0.1:6274  0.0.0.0:*  users:(("node",pid=864,fd=26))
LISTEN 0  511  127.0.0.1:6275  0.0.0.0:*  users:(("node",pid=864,fd=21))
LISTEN 0  511  127.0.0.1:6278  0.0.0.0:*  users:(("node",pid=864,fd=22))
LISTEN 0  511          *:3001        *:*  users:(("node",pid=863,fd=21))
```

The Inspector binds `127.0.0.1` by default and refuses an all-interfaces bind unless you set `DANGEROUSLY_BIND_ALL_INTERFACES`. Leave it on loopback. The everything server has no bind option and listens on all interfaces. That is fine here: Sandboxer's proxy reaches only ports you host, and the sandbox firewall closes its public IPv6 address except SSH ([Hosting](/docs/hosting#how-it-works)).

There is no keychain in a headless VM, so the Inspector logs that it keeps secrets (OAuth tokens, stdio `env` values) unencrypted in `~/.mcp-inspector/secrets.json`. Its [secret storage doc](https://github.com/modelcontextprotocol/inspector/blob/main/docs/secret-storage.md) covers encrypting that file. Read the startup banner with `sudo journalctl -u mcp-inspector`. It prints the API token, so don't paste it anywhere.

## 4. Host the UI on a private URL

```bash
sbx host $id 6274 --localhost --title "MCP Inspector"
```

```text
https://minty-vernal-plum-6274.box.sandboxer.com?_token=<token>
```

`--localhost` makes the sandbox relay the connection to `127.0.0.1:6274` ([apps that listen on localhost](/docs/hosting#apps-that-listen-on-localhost)). Without the hosted token, Sandboxer's proxy answers `401` and the Inspector never sees the request. With it, the first response sets a cookie and redirects to `/`.

Our first run had no `ALLOWED_ORIGINS` line. The token URL got past Sandboxer's proxy, and then the Inspector refused the page:

```text
403
Forbidden: Host "minty-vernal-plum-6274.box.sandboxer.com" is not allowed. To serve the Inspector under this name, add it to MCP_INSPECTOR_ALLOWED_HOSTS (or to ALLOWED_ORIGINS).
```

The proxy passes your browser's `Host` header through, and the Inspector checks it to block DNS rebinding. `ALLOWED_ORIGINS` fixes both checks at once: its hosts are admitted to the Host allow-list, and the origin is accepted on the `/api/*` calls a browser makes. Use the `https://` origin with no port, exactly as the browser sends it. The variable replaces the default list, so add `http://localhost:6274` too if you also reach the UI through an SSH tunnel ([origin allow-list](https://github.com/modelcontextprotocol/inspector/blob/main/clients/web/README.md#host-binding--the-origin-allow-list)).

After a restart, the page loaded (`200`, `<title>MCP Inspector</title>`). We then called the Inspector's API directly, with the hosted cookie:

| Request to `/api/servers` | Status |
| - | - |
| No Inspector token | `401` |
| Inspector token, no `Origin` | `200` |
| Inspector token, `Origin: https://evil.example` | `403` `Invalid origin. Request blocked to prevent DNS rebinding attacks.` |
| Inspector token, `Origin` set to the hosted origin | `200` |

## 5. Connect and call tools from the browser

Open the full token URL. The Servers screen lists the three catalog entries. Flip the switch on `everything-stdio` and the backend spawns `mcp-server-everything stdio` inside the sandbox. In our runs:

- **stdio:** connected in 0.82 s, MCP 2025-11-25. The Tools tab listed 21 tools; the CLI saw 17. The web client advertises more client capabilities (sampling, elicitation), and the server adds tools for them. `echo` returned `Echo: hello from the hosted Inspector` in 0.22 s.
- **Streamable HTTP:** `everything-http` connected in 0.82 s and `echo` returned `Echo: over Streamable HTTP`. The Inspector's backend, not your browser, makes this connection, so the server sees no browser `Origin`. The everything server refuses browser origins other than localhost.
- **Filesystem over stdio:** 14 tools. `list_allowed_directories` returned `/home/user/project`.

The Logs, Protocol and Console panels on the right show the JSON-RPC traffic and the server's log messages. Out of scope here: the MCP Apps tab. It renders in a second listener on port 6275, which this setup doesn't host.

## Why the Inspector must never be public

Don't run `sbx host $id 6274 --public`, and never set `DANGEROUSLY_OMIT_AUTH`. Two facts from the Inspector's own docs and our run explain why:

1. **The page gives away the token.** The server injects its API token into `index.html` on every page load ([auth token](https://github.com/modelcontextprotocol/inspector/blob/main/clients/web/README.md#auth-token)). We found it in the HTML we fetched. The token stops other websites from calling your Inspector. It does not stop anyone who can open the page.
2. **The backend launches commands.** A stdio server is a command line, and the backend runs it as `user`, who has `sudo` in a sandbox. Anyone who can reach the UI can add a "server" whose command is any shell command.

So a public Inspector URL is a remote shell on that VM. The private hosted URL is the lock. Treat the full URL like a password ([Hosting](/docs/hosting#private-and-public)). Run `sbx host hide $id 6274` when you are done, or delete the sandbox. A sandbox for testing also keeps the blast radius small: an unknown MCP server you try runs in the VM, not on your laptop.

## After stop and resume

```bash
sbx stop --wait $id
sbx resume $id
```

Stop took 2.2 s and resume 1.3 s, and resume reported `memoryRestored: true`. Both services kept their PIDs (948 and 863), and the hosted page answered `200` with the same cookie. The hosted URL and its token are kept across stop and resume. If a resume boots from disk instead (it can on a different CPU type), the processes are gone but the enabled units start again. Your browser tab then needs a reload, because the Inspector makes a new API token each time it starts.

## FAQ

### Does the MCP Inspector need two hosted ports?

Not in version 2. One port, 6274, serves the UI and the `/api/*` backend, so one hosted URL is enough. Version 1 had a separate MCP proxy on 6277 with `MCP_PROXY_AUTH_TOKEN`; version 2 has no proxy, and that variable is a deprecated name for `MCP_INSPECTOR_API_TOKEN` ([v1 to v2 migration](https://github.com/modelcontextprotocol/inspector/blob/main/docs/v1-to-v2-migration.md)).

### Can I use the Inspector's CLI against a server outside the sandbox?

Yes. `--server-url` takes any Streamable HTTP or SSE URL, and `--header` adds headers such as an API key. We only tested servers inside the sandbox.

### How do I test my own MCP server instead of the reference ones?

Clone and build it in the sandbox, then add it to `mcp.json` as a stdio command or an HTTP URL. Or pass it to the CLI directly, as in step 2.

### Is it safe to run an MCP server I don't trust this way?

It runs in a microVM, not on your laptop, which is the point. Don't give that sandbox secrets you wouldn't hand the server; create it with `sbx new --no-env` so it gets none of your secrets ([CLI](/docs/cli)).

## Related

- [Local or hosted MCP for Cursor, Claude Code and Codex](/guides/local-or-hosted-mcp-for-cursor-claude-code-codex)
- [Give a sandbox port an HTTPS URL](/guides/give-a-sandbox-port-an-https-url)
- [Run JupyterLab on an HTTPS URL](/guides/run-jupyterlab-on-an-https-url)
- [Hosting docs](/docs/hosting)
