diff --git a/CHANGELOG.md b/CHANGELOG.md index 8792ba9..6a7dfd9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,57 +13,30 @@ versions adhere to [Semantic Versioning](https://semver.org). ### Added -- **`GET /` now serves a small HTML landing page** with proper +- **`GET /` now serves a small HTML landing page** — a short + human-readable body pointing at `/mcp` and the GitHub repo, plus `` and `` tags in the - head, plus a short human-readable body pointing at `/mcp` and - the GitHub repo. Closes the browser-style favicon-discovery gap: - before this change, hitting `/` returned Express's default 404 - HTML with an empty `
`, so any client doing HTML-shaped - icon discovery (favicon checkers, some connector UIs) found no - hints and fell back to nothing. - - Pairs with the v1.6.6 URL-form `serverInfo.icons` fix so we now - cover BOTH icon-discovery channels: - - MCP protocol channel: `serverInfo.icons` returns a URL entry - first (data URI fallback second) — for clients that read the - MCP initialize response. - - HTML channel: `GET /` returns an HTML page with `` - tags pointing at `/icon.svg` — for clients that crawl the root - URL like a browser. - - Static bytes, no template interpolation, no XSS surface. 1h - cache. Generic content only (no deployment-specific URLs or - tenant info in the body). 6 new tests in `tests/http-app.test.ts` - cover: content-type, the two `` tags' shape, the `/mcp` - reference in the body, the cache-control header, and a - determinism check (two requests return identical bytes). + head. Before this, hitting the root URL in a browser returned + Express's default "Cannot GET /" — now a curious visitor (or a + favicon checker that parses HTML) gets something useful. + + Scope note: this does NOT change how Claude.ai's connector UI + displays the server icon. We empirically traced that mechanism + to a favicon crawler (AWS-hosted, hourly, browser user-agents) + that fetches `/favicon.ico` directly — it does not parse the + landing-page `` tags, and it only crawls **custom domains**, + not Public-Suffix-List platform subdomains like `*.run.app`. The + load-bearing route for that crawler (`/favicon.ico`) predates + this change; getting Claude.ai to show the icon requires mapping + the service to a custom domain, which is infra-side, not a code + change here. The landing page stands on its own DX merit. 6 new + tests in `tests/http-app.test.ts` cover content-type, the two + `` tags, the `/mcp` reference, cache-control, and a + determinism check. ### Fixed -- **`serverInfo.icons` now emits a URL-form entry first when - `PUBLIC_BASE_URL` is set (HTTP+OAuth deploys), with the existing - data-URI form retained as a second fallback entry.** Some client - UIs (observed: Claude.ai's connector list on the Cloud Run URL) - silently skip `data:` image srcs — plausibly a CSP / `img-src` - policy that disallows data URIs. A sibling MCP server fronted by - a custom domain rendered its icon correctly with the URL form - while capsulemcp on `*.run.app` didn't, even though the SVG bytes - were identical. The fix emits both shapes (URL first so clients - iterating the array pick it; data URI second for stdio + CSP- - tolerant clients). Stdio installs (no `PUBLIC_BASE_URL`) see no - behavioural change — still data-URI-only. - - Refactor: the icons array shape now lives in - `src/icon-builder.ts` (hand-edited) instead of inside the SVG - generator at `scripts/build-icon.mjs`. `src/icon.ts` is now pure - generated data (`ICON_SVG`, `ICON_DATA_URI`); the orchestration - (URL vs data URI ordering, sizes hints) is separate. The `ICONS` - named export from `src/icon.ts` is gone — replaced by - `buildIcons(publicBaseUrl?)` in `src/icon-builder.ts`. No external - consumer relied on `ICONS` (single internal call site in - `src/server.ts`). - - **Tool annotations now emit `{readOnlyHint, destructiveHint}` explicitly on every tool — never rely on MCP spec defaults.** Per spec, `destructiveHint` defaults to `true`. The pre-fix diff --git a/HOWTO.md b/HOWTO.md index 3d16753..dd89c61 100644 --- a/HOWTO.md +++ b/HOWTO.md @@ -11,7 +11,7 @@ npm install npm test ``` -523 tests, all mocked — no Capsule API calls happen, no token needed. The suite has three layers: +529 tests, all mocked — no Capsule API calls happen, no token needed. The suite has three layers: - **Per-tool unit tests** (e.g. `tests/parties.test.ts`): import the tool function, mock `undici.fetch`, assert on the URL, method, body, and response handling. Most tests live here. - **MCP-protocol integration tests** (`tests/mcp-integration.test.ts`): drive a real `McpServer` through the wire protocol via the SDK's in-memory transport pair, with `undici.fetch` still mocked. Catches the layer between "tool function works" and "MCP correctly registers and dispatches the tool". Includes the `get_attachment` content-type routing logic (which lives in `server.ts`, not the tool function). @@ -46,7 +46,7 @@ for the contributor-facing summary. npm run build ``` -Produces `dist/index.js` (stdio entry, ~165 KB, with `#!/usr/bin/env node` shebang and the executable bit set) and `dist/http.js` (HTTP entry, ~191 KB, no shebang). Each is fully self-contained — tsup runs as two separate configs so the stdio entry can be invoked directly via npx while the HTTP entry isn't a CLI. tsup target is Node 22 (undici 8 requires Node 22+ for the `webidl.util.markAsUncloneable` runtime API). +Produces `dist/index.js` (stdio entry, ~165 KB, with `#!/usr/bin/env node` shebang and the executable bit set) and `dist/http.js` (HTTP entry, ~193 KB, no shebang). Each is fully self-contained — tsup runs as two separate configs so the stdio entry can be invoked directly via npx while the HTTP entry isn't a CLI. tsup target is Node 22 (undici 8 requires Node 22+ for the `webidl.util.markAsUncloneable` runtime API). `npm run build` also chains `npm run build:icon` (`scripts/build-icon.mjs`), which regenerates `src/icon.ts` from the canonical `assets/icon.svg`. The TypeScript file is committed (so typecheck works without a build step) but is **generated** — edit the SVG, then run the build. A drift-guard test (`tests/icon-source.test.ts`) fails CI if the two ever fall out of sync. diff --git a/scripts/build-icon.mjs b/scripts/build-icon.mjs index 586b047..af2d893 100644 --- a/scripts/build-icon.mjs +++ b/scripts/build-icon.mjs @@ -43,23 +43,29 @@ const generated = `/** * edit this file directly** — edit the SVG and re-run \`npm run * build:icon\` (or \`npm run build\`, which chains it). * - * This file is generated DATA: the raw SVG plus its \`data:\` URI - * form. The \`serverInfo.icons\` ARRAY shape (URL form vs data URI - * form vs both, ordering, sizes hints) is hand-edited orchestration - * and lives in \`src/icon-builder.ts\` — kept out of this generator - * so the icon-array shape can evolve without touching the SVG. - * - * Exposed two ways at runtime: + * Exposed two ways: * - Embedded as a \`data:\` URI in the MCP \`serverInfo.icons\` array - * (spec-compliant; works without any HTTP route — stdio path). + * (spec-compliant; works without any HTTP route). * - Served at \`/icon.svg\` and \`/favicon.ico\` by the HTTP entry, - * so clients that prefer to fetch a URL get a real HTTPS resource - * (some UIs' CSP blocks \`data:\` image srcs — URL form survives). + * in case the consuming client prefers a URL it can fetch. */ export const ICON_SVG = \`${escapedSvg}\`; export const ICON_DATA_URI = \`data:image/svg+xml;base64,\${Buffer.from(ICON_SVG, "utf8").toString("base64")}\`; + +/** + * Shaped for MCP's \`serverInfo.icons\` field. Single 64x64 SVG that + * scales cleanly to any size; \`sizes: ["any"]\` tells the client it + * works at every render size. + */ +export const ICONS = [ + { + src: ICON_DATA_URI, + mimeType: "image/svg+xml", + sizes: ["64x64", "any"], + }, +]; `; writeFileSync(TS_PATH, generated, "utf8"); diff --git a/src/icon-builder.ts b/src/icon-builder.ts deleted file mode 100644 index 2a36ad6..0000000 --- a/src/icon-builder.ts +++ /dev/null @@ -1,65 +0,0 @@ -/** - * Builds the `serverInfo.icons` array shape for MCP's `initialize` - * response. Lives separately from the generated `src/icon.ts` so the - * shape can evolve (URL form, multiple sizes, fallback ordering) - * without touching the SVG generator. - * - * Why two forms (URL + data URI): - * - * - URL form (`https://