From 1dd3eadae5d7d45b7d9319d5e7ee14b3a114f789 Mon Sep 17 00:00:00 2001 From: Younes Abouelnagah Date: Thu, 29 Oct 2026 18:01:43 -0400 Subject: [PATCH 1/4] feat(mcp): record first-load evidence on the server and in the card Issue #335: in Claude web and ChatGPT web the host's own error replaced the card on first load and one Retry recovered it. The card's own failure texts never appeared, and the server logged every exchange as a bare `POST`, so a stale card read (JSON-RPC -32602 inside HTTP 200) looked like a successful one. - Server, always on: `mcp.request`/`mcp.response` lines joined by `rid`, with the JSON-RPC method, the card address read and whether it is current, the handshake's client and protocol, status, error code, bytes, timings and whether the client hung up. Handler errors log name and code only. - Card, always on: an early script records script start, errors, rejections, CSP violations and host message arrival (tool input/result/cancel, teardown request); the stack's hooks mark fonts, context, start outcome and the first rendered answer or problem. Each report is an `[ansari-view]` console line. - Diagnosed deployment only: MCP_VIEW_DIAGNOSTICS_ORIGIN opens connect-src to that one origin and adds `POST /diag/view`, which accepts a fixed report shape and logs it as `view.report`. Unset, the route does not exist and the card's permissions stay closed. - scripts/first-load-trial.sh and first-load-timeline.mjs turn host trials and Railway deployment/edge logs into one timeline per trial with findings. The browser suite found one violation on every load: zod's caught `new Function('')` probe under the card's script-src. It is the recorded baseline; any other error fails the check. Co-Authored-By: Claude Opus 5.5 --- apps/mcp/README.md | 43 +++++++ apps/mcp/scripts/first-load-timeline.mjs | 78 ++++++++++++ apps/mcp/scripts/first-load-trial.sh | 42 +++++++ apps/mcp/src/node/http-server.ts | 1 + apps/mcp/src/server/exchange-log.ts | 116 +++++++++++++++++ apps/mcp/src/server/fetch-app.ts | 52 ++++++-- apps/mcp/src/server/runtime-config.ts | 15 +++ apps/mcp/src/server/view-artifact.ts | 20 ++- apps/mcp/src/server/view-report.ts | 68 ++++++++++ apps/mcp/src/view/diagnostic-marks.ts | 17 +++ apps/mcp/src/view/diagnostics.ts | 94 ++++++++++++++ apps/mcp/src/view/main.tsx | 20 ++- apps/mcp/src/view/mcp-app-react.html | 31 +++++ apps/mcp/src/worker/index.ts | 1 + apps/mcp/test/browser.ts | 45 ++++++- apps/mcp/test/diagnostics.test.ts | 152 +++++++++++++++++++++++ apps/mcp/turbo.json | 2 + 17 files changed, 778 insertions(+), 19 deletions(-) create mode 100644 apps/mcp/scripts/first-load-timeline.mjs create mode 100644 apps/mcp/scripts/first-load-trial.sh create mode 100644 apps/mcp/src/server/exchange-log.ts create mode 100644 apps/mcp/src/server/view-report.ts create mode 100644 apps/mcp/src/view/diagnostic-marks.ts create mode 100644 apps/mcp/src/view/diagnostics.ts create mode 100644 apps/mcp/test/diagnostics.test.ts diff --git a/apps/mcp/README.md b/apps/mcp/README.md index b983f99b..a1b7291e 100644 --- a/apps/mcp/README.md +++ b/apps/mcp/README.md @@ -95,11 +95,54 @@ The Worker takes the variables in the table below as Wrangler `vars` or secrets, | `MCP_ALLOWED_ORIGINS` | Comma-separated allowed browser origin hostnames. A configured list replaces defaults; an explicitly empty list permits no Origin header value. Requests without Origin remain supported. | | `MCP_RESPONSE_MODE` | `json` for modern MCP final-answer-only responses; otherwise SSE, which supports client-requested progress notifications. Older-host compatibility responses may use SSE. | | `ANSARI_RELEASE` | Optional safe release identifier, embedded in served HTML and health/log records. | +| `MCP_VIEW_DIAGNOSTICS_ORIGIN` | Set only on a deployment being diagnosed: its own public origin, such as `https://ansari-mcp-development.up.railway.app`. The card may then connect to that one origin and reports its start-up milestones to `POST /diag/view`; this changes the card's bytes and so its address. Unset, the card, its address and its closed permissions are unchanged and the route does not exist. A value that is not a bare `https` origin (`http` only for localhost) stops startup. See [First-load diagnosis](#first-load-diagnosis). | `POST /mcp` serves stateless MCP requests through Hono and the official MCP request handler. Modern requests need no initialization; older hosts can use the handler’s stateless compatibility path. No MCP session IDs, session map, expiry timer or replay store are retained. `GET /mcp` and `DELETE /mcp` return 405. `/health` exposes the release. The HTML is supplied by the hosting target (read from `dist/` at Node start, bundled into the Worker), revision-stamped and fingerprinted once before the first request is served: the release is stamped first, so equal final bytes have equal identities and a release change legitimately changes the hash. Each request creates its own lightweight MCP server using that prepared view; resource bytes and identity stay fixed until restart. No demo, font server, alternative renderer or historical resource is served. Tool calls have a fresh 60-second budget; timeout yields a safe error and leaves subsequent calls usable. The same lifetime signal aborts the upstream fetch and response-body read when the request ends or the budget expires. Closing a request server releases its budget even if it was never connected to a transport. On SIGINT or SIGTERM the listener stops accepting requests, closes MCP handlers and HTTP connections, and exits; unfinished calls are interrupted, including older-host SSE calls. Fetch cancellation does not guarantee that the remote backend has stopped processing. Caller-disconnect propagation depends on the host transport; the local Workers runtime does not establish immediate upstream cancellation from a client disconnect. Actual fetch cancellation at the request deadline is covered on both hosting targets. +## First-load diagnosis + +Issue #335: in Claude web and ChatGPT web the host's own error replaced the card on first load, and one Retry +recovered it. The card's own failure texts never appeared, so the host gave up before the card showed anything. These +records tell the possible causes apart. + +**Server, always on.** Each `/mcp` exchange writes an `mcp.request` line when it arrives and an `mcp.response` line +when it ends, joined by `rid`. The request line names the JSON-RPC method; for a card read, the address read and +whether it is the one served now (`uriCurrent`); for the opening handshake, the client's name, version and protocol; +for a tool call, the tool name. The response line has the HTTP status, any JSON-RPC error code (a stale card read is +error `-32602` inside an HTTP 200), bytes sent, time to headers and total time, and whether the response was +`complete`, `cancelled` by the client or `failed`. Handler errors are logged by name and code. No question, answer or +other parameter value is logged. + +**Card, always on.** A small script runs before the bundle and records when it started, any error, unhandled +rejection or content-policy violation, and when the host's messages arrive (first message, tool input, tool result, +cancellation, teardown request), by method name only. The stack's hooks add when fonts loaded, when the host context +was delivered, how starting ended and when the first answer or problem was rendered. The card writes each report as an +`[ansari-view]` console line in its frame. One violation is expected on every load: zod's `new Function('')` +capability probe under the card's `script-src`, which it catches. + +**Card beacon, diagnosed deployment only.** With `MCP_VIEW_DIAGNOSTICS_ORIGIN` set, the card also sends each report +to `POST /diag/view` when it starts or fails, renders its first answer or problem, is put away, or has not started +after 15 seconds. The server accepts only the fixed report shape (milestones, enumerated failures, error names) and +logs it as `view.report`, with the host sandbox's origin name. + +**Trials.** Run one trial per new conversation, with the deployment live for hours (`aged`) and right after a +redeploy (`fresh`): + +```sh +bash scripts/first-load-trial.sh claude aged +bash scripts/first-load-trial.sh chatgpt fresh +# Railway: deployment and edge HTTP logs for the trial window, then one timeline per trial. +railway logs --deployment --json --since 1h > test-results/first-load/app.jsonl +railway logs --http --json --since 1h > test-results/first-load/edge.jsonl +node scripts/first-load-timeline.mjs test-results/first-load/trials.jsonl test-results/first-load/app.jsonl test-results/first-load/edge.jsonl +``` + +The timeline flags a card read that never reached the server, a card address that is not the current deploy's, a card +read that was cancelled or slow, a card whose host asked to tear it down before it was ready, a card that never heard +from the host, and a slow start. + ## Workspace and container This package lives at `apps/mcp` and is named `ansari-mcp`. The root `pnpm-lock.yaml` and `pnpm-workspace.yaml` are the only install inputs. `pnpm mcp