launch-check
by atokim4
README.md
# launch-check: a website launch checklist as an MCP App
A remote MCP server (Streamable HTTP) that checks a public website before launch and returns a scored
pass/warn/fail report with a fix for every problem. It is an MCP App, so ChatGPT, Claude and other hosts that
support MCP Apps show the result as an interactive scorecard. Hosts without UI support get the same report as
text and structured content.
This is a personal portfolio project by Sunnatulla E. It is not a hosted product and has no users yet.

The screenshots come from `npm run e2e`, which checks a made-up sample shop served on 127.0.0.1, so the
address in the header is a local one. Full-length versions: [light](docs/scorecard-light-full.png),
[dark](docs/scorecard-dark-full.png).
## Tools
| Tool | What it does | Hints |
| --- | --- | --- |
| `check_site(url)` | Runs every check below on one public URL. Returns `structuredContent` (score, grade, metrics, `checks[{id, status, why, fix}]`) plus a plain-text summary for the model. Linked to the scorecard widget. | `readOnlyHint`, `openWorldHint` |
| `explain_fix(check_id)` | Why a check matters, plus copy-paste fixes for nginx, Netlify, Vercel, Next.js or plain HTML. The widget calls it when you open a fix. | `readOnlyHint` |
## What it checks
| Area | Checks |
| --- | --- |
| Transport | HTTPS, http:// → https:// redirect (301/308), redirect chain length, TLS certificate trust and expiry (warns at 14 days) |
| Security headers | Content-Security-Policy (flags `'unsafe-inline'` without nonces), HSTS max-age, X-Frame-Options / `frame-ancestors`, `nosniff`, Referrer-Policy |
| Page | Mixed content (scripts, styles, frames, forms, images on http://), title, meta description, canonical, Open Graph, Twitter card, `<html lang>`, viewport |
| Site files | robots.txt (catches a leftover `Disallow: /`), sitemap (from robots.txt or /sitemap.xml), favicon |
| Speed | Time to first byte from the server, HTML size and compression |
The score is a weighted average of the checks that ran (pass = full weight, warn = half, fail = zero). HTTPS and
the certificate weigh the most and a missing Twitter card the least. Weights are in `src/checks/catalog.ts`.
## The widget
`widget/` is a small React app built by Vite into one self-contained HTML file (`dist/widget/scorecard.html`)
and served as the MCP App resource `ui://launch-check/scorecard-v1.html` (`text/html;profile=mcp-app`).
- Grade, score and a strip with one segment per check, grouped by category, so the result reads at a glance.
- Filters for failed, warnings and passed. Failed checks open by default with their fix.
- "Show copy-paste fix" calls `explain_fix` through the host (`app.callServerTool`) and lists snippets with a
Copy button. If the host blocks the clipboard, the snippet is selected for a manual copy instead.
- Follows the host theme and style variables (light and dark), with its own fallbacks for hosts that send none.
- Tool results are validated with the same zod schema the server uses before anything is rendered.
- No network access: the resource declares an empty CSP domain list and the bundle loads nothing from outside.
The E2E run fails if the widget makes any request.
- Keyboard and screen reader friendly: real buttons with `aria-expanded` / `aria-pressed`, labelled status
icons, visible focus, a text summary for the status strip.
The bundle is about 460 KB (135 KB gzipped), mostly React DOM and the MCP client SDK. Hosts fetch it once and
cache it by URI, which is why the URI carries a version.
## Safety
The server fetches URLs that a model passes to it, so it treats every URL as hostile:
- Only `http:` and `https:`, no embedded credentials.
- SSRF guard: hosts that are, or resolve to, loopback, private, link-local (cloud metadata), CGNAT, multicast or
other reserved ranges are refused. The check runs inside the socket's DNS lookup, so it also covers every
redirect hop and DNS rebinding between check and connect. IPv4-mapped IPv6 and NAT64 forms are handled too.
- Limits per request: 8 s timeout, 2 MB body cap (also applied after gzip/brotli decompression), 5 redirects,
redirect loops detected. At most 4 checks run at once (`MAX_CONCURRENT_CHECKS`).
- GET requests only. The server stores nothing and does not log the URLs it checks.
- `/mcp` validates the Host and Origin headers (DNS rebinding protection) through `@modelcontextprotocol/hono`.
## Run it locally
Requires Node.js 22.19 or newer.
```bash
npm ci
npm run build # widget (Vite) + server (tsc)
npm start # http://127.0.0.1:8787/mcp
```
Settings are environment variables, see `.env.example`. Try it with the MCP Inspector:
```bash
npx @modelcontextprotocol/inspector@2.14.0 --cli http://127.0.0.1:8787/mcp --transport http --method tools/list
npx @modelcontextprotocol/inspector@2.14.0 --cli http://127.0.0.1:8787/mcp --transport http \
--method tools/call --tool-name check_site --tool-arg url=https://example.com
```
The server also serves `/privacy`, `/terms` and `/support` (static pages from `static/`) and `/health`.
## Connect it to Claude or ChatGPT
Both apps connect from their own cloud, so the server needs a public HTTPS URL. Deploy it (below) or, for a quick
test, put a tunnel in front of a local server and add the tunnel hostname to `ALLOWED_HOSTS`:
```bash
ALLOWED_HOSTS=your-tunnel-hostname.example npm start
```
**Claude** (web or desktop, any plan; Free allows one custom connector): Customize → Connectors → "+ Add" →
"Add custom connector", enter a name and `https://<your host>/mcp`, choose "No sign in", then Add. On Team and
Enterprise plans an owner adds it under Organization settings → Connectors.
**ChatGPT**: open chatgpt.com/plugins, press "+" → "Add custom MCP server", enter a name, description and
`https://<your host>/mcp` under Connection, choose no authentication, confirm, then "Create as a plugin" and
review the two tools. Account and workspace policies decide whether custom MCP servers are allowed.
Then ask something like "check example.com before launch". ChatGPT reads the standard `_meta.ui.resourceUri`
and the server also sets the older `openai/outputTemplate` alias.
## Deploy
`Dockerfile` builds the widget and server in a build stage, then runs `node dist/main.js` as the non-root `node` user
with only production dependencies. Set `HOST=0.0.0.0` (the image does) and `ALLOWED_HOSTS` to your public
hostname, or every request to `/mcp` is refused.
`render.yaml` is a Blueprint for a Render free web service built from the Dockerfile. The free tier spins down
after 15 minutes without traffic and takes about a minute to wake, so the first call after a pause is slow.
Check the current free-tier terms before you rely on it. Any host that runs a Node container works.
Cloudflare Workers does not fit as is: the SSRF guard needs Node's `dns.lookup` hook on the socket.
## Publishing to the ChatGPT app directory
Not done yet. The pieces the submission form asks for are in place: a public HTTPS server, read-only tool
annotations, a privacy policy (`/privacy`), terms (`/terms`) and support (`/support`). The remaining steps
belong to the account owner:
1. Deploy the server and set `ALLOWED_HOSTS`.
2. Finish identity verification for the OpenAI developer account (individual verification).
3. Test the app in ChatGPT with the custom server, both the widget and the text result.
4. Submit through the OpenAI platform dashboard with the server URL, the three page URLs, screenshots from
`docs/` and test prompts such as "check example.com before launch".
## Tests
```bash
npm run check # type check (server, widget, e2e), eslint, unit tests, build
npm run e2e # after a build; needs Chrome or Chromium (set CHROME_PATH if it is not found)
```
`npm test` runs 84 Vitest tests and never touches the internet:
- server checks against local fixture sites: a well-configured HTTPS site (certificate from a throwaway CA made
with openssl at test time), a careless HTTP site, a certificate that expires in 5 days, a self-signed
certificate, a 301 redirect, a redirect loop, a slow server, a 5 MB body, a gzip bomb, a closed port;
- SSRF cases (literal private IPs, decimal and hex IP forms, mapped IPv6, names that resolve to private addresses);
- the MCP endpoint: tool list and annotations, the `ui://` resource and its CSP and permissions, tool calls,
Host/Origin rejection, the static pages;
- the widget in jsdom: header and strip, default open rows, filters, the `explain_fix` round trip and copy,
the clipboard fallback, error and retry states, result parsing.
`npm run e2e` runs the whole thing: it starts the app and a local HTTPS sample shop, drives `/mcp` with the
MCP Inspector CLI (`tools/list`, `tools/call`, `--app-info`), reads the `ui://` resource with the MCP client,
renders it in headless Chrome inside a small host built on the ext-apps `AppBridge` (`e2e/host/`), sends the
tool result, opens a fix (a real `explain_fix` call through the bridge), copies a snippet, uses a filter, and
fails on any console error or network request from the widget. It saves the screenshots in `docs/`.
`SHOT_URL=https://example.com npm run e2e` checks a live site instead.
## Limits
- One page per call (the URL you pass), not a crawl.
- Response time is measured from wherever the server runs, so it is a rough signal, not a Lighthouse score.
- The HTML is not rendered, so tags added by client-side JavaScript after load are not seen.
- No authentication. Anyone with the URL can run checks, within the concurrency limit.
## Stack
TypeScript on Node 22. Server: `@modelcontextprotocol/server` 2.4 (MCP TypeScript SDK v2, stateless
`createMcpHandler`), `@modelcontextprotocol/ext-apps` 2.0.4 (`registerAppTool` / `registerAppResource`), Hono
with `@modelcontextprotocol/hono`, undici, cheerio, zod. Widget: React 19, `@modelcontextprotocol/ext-apps/react`
(`useApp`, `useHostStyles`), Vite 8 with `vite-plugin-singlefile`. Tests: Vitest, Testing Library, jsdom,
playwright-core, MCP Inspector CLI.
## License
MIT
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessOutdated data