Skip to main content
Glama
README.md
# MCP Host Canary

[![CI](https://github.com/sjh9714/mcp-host-canary/actions/workflows/ci.yml/badge.svg)](https://github.com/sjh9714/mcp-host-canary/actions/workflows/ci.yml)

Create a disposable remote MCP server, connect it to Claude, ChatGPT, Cursor, or another managed host, and get a safe receipt of the last protocol boundary it actually reached.

[Run a 30-minute canary — no signup or install](https://mcp-host-canary-sg.onrender.com) · [View a finalized sample receipt](docs/examples/finalized-limits-receipt.md)

## Waterfall demo

![MCP Host Canary waterfall progressing from run creation through an executed tool callback](docs/assets/mcp-host-canary-waterfall.gif)

MCP Host Canary creates a known test server and records only the protocol boundaries it observes. It distinguishes no recognized MCP traffic, an incomplete manifest response, a returned tool list without a sentinel call, and a call request without registered callback execution. It records optional modern discovery and legacy initialization only when either actually arrives. It does not infer success from an event it did not receive.

**[MCP Inspector](https://github.com/modelcontextprotocol/inspector) and [MCPJam](https://github.com/MCPJam/inspector) connect to a server for direct debugging. MCP Host Canary records the last protocol boundary a known test server observed from Claude, ChatGPT, Cursor, or another host.**

No tokens, prompts, request bodies, tool arguments, IP addresses, or raw User-Agent strings are retained.

A finalized receipt is designed to be shared only after its disposable endpoint is closed:

```text
profile        limits
status         finalized
last observed  tools/call.executed
tools listed   258
called tool    sentinel_257
```

[View a complete privacy-safe Markdown receipt](docs/examples/finalized-limits-receipt.md). It is a controlled local example, not managed-host evidence.

Start with the matching symptom: [Claude connected but tools do not appear](https://mcp-host-canary-sg.onrender.com/diagnose/claude-mcp-connected-no-tools), [connected but no tool call](https://mcp-host-canary-sg.onrender.com/diagnose/mcp-connected-but-no-tool-call), [`tools/list` without `tools/call`](https://mcp-host-canary-sg.onrender.com/diagnose/tools-list-without-tools-call), [the 258-tool boundary](https://mcp-host-canary-sg.onrender.com/diagnose/mcp-tool-limit), or [MCP Inspector vs MCP Host Canary](https://mcp-host-canary-sg.onrender.com/compare/mcp-inspector-vs-mcp-host-canary).

1. Create a `baseline` run for the normal path or a `limits` run for the 258-tool boundary.
2. Give the temporary MCP URL only to the managed host you are testing; follow the [Claude, ChatGPT, or Cursor setup guide](docs/connect-managed-hosts.md).
3. Watch the server-observed waterfall, then finalize and export the safe receipt.

To contribute an independent host observation, follow the [safe receipt contribution path](CONTRIBUTING.md).

This is an experimental, free, single-instance pilot—not an MCP conformance suite, security scanner, proxy, or production gateway. A restart or free-instance suspension removes active runs and receipts.

## What it observes

Each run has one of two profiles:

- `baseline`: three compact, zero-input tools for a normal discovery and call path.
- `limits`: 258 tools, including `sentinel_257` at the ordering boundary and one schema-boundary fixture with a 16,385-byte description.

The waterfall contains only observed no-auth MCP facts. It covers two protocol eras rather than requiring one universal sequence:

```text
2026-07-28: server/discover? (optional)
2025-11-25 and earlier: initialize.request -> initialize.response -> initialized
tools/list.request -> tools/list.response
tools/call.request -> tools/call.executed
```

MCP `2026-07-28` removed the `initialize` / `initialized` handshake, and a modern client is not required to call `server/discover`. Earlier revisions use the handshake path. Interpret absent discovery or initialization against the observed protocol version when available; neither is a prerequisite for a modern `tools/list` or `tools/call` observation.

Request receipt and actual callback execution are separate observations. A tool count is recorded only after a manifest response is successfully produced.

## Initial managed-host evidence

The initial limits runs reached two different server-observed boundaries: Claude Web fetched the 258-tool manifest without a sentinel callback; ChatGPT Developer mode reached actual `sentinel_257` callback execution. Attempt counts were not uniform, so these are boundary case notes—not head-to-head rates, compatibility grades, or claims about a host's internal cause.

Read the [managed-host boundary evidence page](https://mcp-host-canary-sg.onrender.com/evidence/managed-host-boundaries) for the scope and limitations. Its machine-readable source, [`managed-host-observations.v1.json`](docs/evidence/managed-host-observations.v1.json), is a normalized small-sample summary, not a set of reconstructed receipts.

| Managed host | Profile | Retained operator protocol | Server-observed result |
| --- | --- | --- | --- |
| Claude Web | `baseline` | direct ×1; indirect ×1; negative ≥1 (exact negative count not retained) | `tools/call.executed`; total callback ×1; last tool `canary_ping` |
| ChatGPT Developer mode | `baseline` | direct ×3; indirect ×3; negative ≥1 (exact negative count not retained) | `tools/call.executed`; total callback ×6; last tool `canary_ping` |
| Claude Web | `limits` | explicit `sentinel_257` request ×2 | `tools/list.response` ×2 with 258 tools; total callback ×0 |
| ChatGPT Developer mode | `limits` | exact `sentinel_257` request count not retained | `tools/call.executed`; 258 tools listed; total callback ×3; last tool `sentinel_257` |

The negative control left callback counts unchanged in both baseline runs, but its exact repetition count was not retained. Do not calculate host success rates from these four historical rows.

## Reproduce a host boundary

1. Create a fresh `baseline` run and connect its disposable URL using the [Claude, ChatGPT, or Cursor setup guide](docs/connect-managed-hosts.md).
2. In three fresh host conversations, send each prompt exactly once: direct — `Call canary_ping exactly once and return only its tool result.`; indirect — `Use the connected MCP server to retrieve its fixed canary value without naming a tool yourself. Return only that value.`; negative — `Do not use any MCP tool. Reply with exactly: control-only`.
3. Record the callback-count delta after every prompt. A direct or indirect delta records host dispatch; the negative delta should remain zero. These are observations, not pass/fail grades.
4. For the ordering boundary, create a separate `limits` run and host connection using that run's new disposable URL, and send once in a fresh conversation: `Call sentinel_257 exactly once and return only its tool result.`
5. Finalize each run and export its safe receipt. Finalize disables the disposable MCP endpoint.

The same exact prompts are available as copy actions in the live UI after a run is created.

If the canary produces a reproducible boundary, use the [safe support-ticket template](docs/when-to-use-host-canary.md#safe-support-ticket-template) or [managed-host receipt form](https://github.com/sjh9714/mcp-host-canary/issues/new?template=host-receipt.yml). If it saved debugging time, [star the repository](https://github.com/sjh9714/mcp-host-canary) so other MCP developers can find it. Never include a live capability URL or owner-only receipt URL.

## Local quickstart

Use Node.js 22.x.

```sh
npm ci
npm run typecheck
npm test
npm run build
npm start
```

Open [http://127.0.0.1:4317](http://127.0.0.1:4317). The local non-production configuration uses a development-only session secret; production refuses to start without an explicit secret and HTTPS public URL.

With the server still running, exercise two controlled local runs from another terminal with the official MCP client:

```sh
npm run demo
```

The demo lists and calls the final sentinel in one run, then lists tools and stops in the other. That validates receipt differences without claiming compatibility with a managed host.

## Deploy an isolated pilot

[![Deploy to Render](https://render.com/images/deploy-to-render-button.svg)](https://render.com/deploy?repo=https://github.com/sjh9714/mcp-host-canary)

The Blueprint creates one Free instance in Singapore, generates a 256-bit session secret, runs the complete verification build, and keeps auto-deploy disabled. It uses the same 30-minute, 50-run, memory-only pilot boundary as the public beta. A Free compute instance does not guarantee a `$0` workspace bill: usage beyond included bandwidth or build-pipeline allowances can be charged when billing is enabled. Review [Render's Free instance limits](https://render.com/docs/free), check workspace usage, and configure the [build-pipeline spend limit](https://render.com/docs/build-pipeline) before deploying. A cold start or restart can erase active runs and receipts.

## API

All `/api/*` run operations are isolated to a signed anonymous browser session. Other owners receive the same `404` as a missing run. Only the high-entropy `/mcp/:id` capability URL is intentionally usable without that session.

| Method | Path | Purpose |
| --- | --- | --- |
| `POST` | `/api/session` | Issue or refresh an anonymous session |
| `DELETE` | `/api/session` | End the browser session |
| `POST` | `/api/tests` | Create `{ "profile": "baseline" | "limits" }` |
| `GET` | `/api/tests?limit=12&cursor=...` | List the current owner's runs |
| `GET` | `/api/tests/:id` | Read an owner-only run; supports `ETag` and `304` |
| `GET` | `/api/tests/:id/receipt?format=json|markdown` | Export a safe receipt |
| `POST` | `/api/tests/:id/finalize` | Disable MCP access and freeze the receipt |
| `DELETE` | `/api/tests/:id` | Revoke and remove a run immediately |
| `GET` | `/healthz` | Process health |
| `GET` | `/readyz` | Readiness |

The browser conditionally polls at most three selected active runs. Polling stops when the tab is hidden or a run is finalized or expired, and errors use exponential backoff.

## Pilot boundaries

- Runs live in one process and expire after at most 30 minutes.
- A deploy, restart, or free-instance suspension removes active runs and receipts.
- There are at most 50 active runs, three per owner, six creations per owner per hour, and 60 creations globally per hour.
- MCP traffic is limited per run; `tools/list` has a tighter limit and the limits fixture has a four-request execution semaphore.
- Capacity pressure returns `429` or `503` with `Retry-After`; an existing run is never evicted to admit a new one.
- `/api/*` bodies are limited to 2 KiB, `/mcp/*` bodies to 64 KiB, and JSON-RPC batches to 16 entries before application processing.
- MCP subscriptions and OAuth are intentionally absent. Tools are read-only, idempotent, and advertised with [no-auth compatibility metadata](https://developers.openai.com/plugins/build/auth).
- Results show the last boundary observed by this server. They cannot establish the root cause inside a closed host.

See [PRIVACY.md](PRIVACY.md) for retained fields and [SECURITY.md](SECURITY.md) for capability-URL and reporting guidance.

## Production configuration

The intended pilot shape is exactly one Node 22.x process behind HTTPS. In production:

- bind to `0.0.0.0` with `CANARY_BIND_HOST`;
- derive the public base URL and allowed host from `RENDER_EXTERNAL_URL`, or set the explicit public URL supported by the server;
- set `CANARY_SESSION_SECRET` to a randomly generated value of at least 32 bytes;
- optionally set `CANARY_INDEXNOW_KEY` to a separate, randomly generated 8-128 character ASCII letter, digit, or hyphen value to expose `/<key>.txt` for manual [IndexNow ownership verification](https://www.indexnow.org/documentation);
- set the TTL to `1800000` ms and maximum tests to `50`;
- keep auto-deploy disabled so a source push cannot silently erase active runs.

Never commit the session secret or an IndexNow key. Copy `.env.example` only for local configuration. The key file is not listed in `robots.txt` or the sitemap, and this service does not submit URLs automatically. Notify IndexNow only when a same-origin public page is added, updated, redirected, or deleted.

## Commands

```sh
npm run typecheck
npm test
npm run build
npm start
npm run demo
npm audit --audit-level=low
```

## Validation gate

Local tests are necessary but not sufficient. The pilot continues only if, within at most seven days:

- two independent managed hosts produce distinct receipts;
- five independent developers complete a run from their own host;
- two receipts are used in a real support ticket or GitHub issue; and
- secret non-retention checks remain passing.

The project does not expand if managed-host connection fails, evidence requires retaining secrets, users primarily request a proxy or automatic repair, or an official tool supplies the same managed-host evidence flow. Paid hosting and a versioned release happen only if the pilot evidence warrants them.

## License

[MIT](LICENSE)