A production-style MCP (Model Context Protocol) server packaged as a full-stack web app. It pairs a portfolio/landing page with a live, bearer-authenticated MCP endpoint that exposes six typed utility tools.
- Framework: TanStack Start (React 19 + Vite)
- MCP integration:
mcp-tanstack-start+@modelcontextprotocol/sdk - Validation: Zod (tool input schemas are the contract)
- Transport: JSON-RPC 2.0 over Streamable HTTP (MCP spec
2025-06-18) - Deploy target: Cloudflare Workers (via Wrangler)
Contributor: Muhammad Zeeshan.
This project is two things at once: a web page you open in a browser, and an MCP server that AI clients (Cursor, Claude Desktop, etc.) connect to. Here is the end-to-end happy path.
bun install # or: npm install --legacy-peer-depsThe dev server reads the bearer secret from the shell environment, so export it on the same line that starts the server (see the local-dev note for why):
# macOS / Linux
MCP_SECRET=local-dev-secret-3f9a2c7b6e1d4805 bun run dev# Windows PowerShell
$env:MCP_SECRET="local-dev-secret-3f9a2c7b6e1d4805"; bun run devThe terminal prints the URL (currently http://localhost:8080/).
Visit http://localhost:8080/ — it documents the live endpoint and the full tool registry.
curl -X POST http://localhost:8080/api/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "Authorization: Bearer local-dev-secret-3f9a2c7b6e1d4805" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'You should get back the six tools and their JSON schemas.
Add this to your MCP client config — in Cursor that's .cursor/mcp.json (project)
or ~/.cursor/mcp.json (global):
{
"mcpServers": {
"mcp-core-engine": {
"url": "http://localhost:8080/api/mcp",
"headers": {
"Authorization": "Bearer local-dev-secret-3f9a2c7b6e1d4805"
}
}
}
}Reload the client; mcp-core-engine should appear with 6 tools you can call from chat.
For a deployed server, swap the URL and use your real MCP_SECRET.
Heads-up:
.cursor/mcp.jsoncontains the bearer token — don't commit it.
Local-dev note.
bun run devserves SSR from the Node process, so it readsMCP_SECRETfromprocess.envand does not auto-load.dev.vars(that's awrangler devbehavior). If you start the server without the env var, every MCP request returns401. In production,wrangler secret put
nodejs_compatpopulatesprocess.envfor you.
For everything beyond this happy path — deployment, security model, adding tools, testing — see the sections below.
| Route | File | Purpose |
|---|---|---|
/ |
src/routes/index.tsx |
Portfolio landing page documenting the MCP capabilities |
/api/mcp |
src/routes/api/mcp.ts |
Authenticated MCP endpoint. POST only (GET/DELETE → 405) |
Each tool lives in its own file under src/lib/mcp/tools/ — a Zod schema plus a
handler, registered in src/routes/api/mcp.ts.
| Tool | File | Description |
|---|---|---|
greet |
greeting.ts |
Greet a user by name. |
echo |
echo.ts |
Echo a message back (connectivity test). |
current_time |
time.ts |
Current time in ISO 8601 + a chosen IANA timezone. |
calculate |
math.ts |
Safe arithmetic evaluator (+ - * / %, parentheses, decimals). |
fetch_url |
fetch-url.ts |
Fetch a public http(s) URL with an SSRF guard, 10s timeout, byte cap. |
generate_uuid |
uuid.ts |
Generate 1–50 RFC 4122 v4 UUIDs. |
- Node.js 20+ (developed against v22)
- Bun is the project's package manager (
bun.lock,bunfig.toml). npm also works but requires--legacy-peer-depsbecausemcp-tanstack-startdeclares azod@^3peer while this project useszod@^4.
# Preferred
bun install
# Or with npm (note the flag — peer dep mismatch with zod)
npm install --legacy-peer-depsEnvironment variables are validated with Zod in src/lib/server/env.ts
(memoized, fail-closed). The MCP endpoint is gated by a bearer token read from
MCP_SECRET. If MCP_SECRET is unset, all MCP requests are rejected.
| Variable | Required | Default | Purpose |
|---|---|---|---|
MCP_SECRET |
yes* | — | Bearer token for /api/mcp (min 16 chars) |
RATE_LIMIT_MAX |
no | 60 |
Requests allowed per window, per client IP |
RATE_LIMIT_WINDOW_MS |
no | 60000 |
Rate-limit window size in milliseconds |
LOG_LEVEL |
no | info |
debug | info | warn | error |
NODE_ENV |
no | development |
Standard environment marker |
* Without it the server runs but rejects every MCP request (fail-closed).
For local development with Wrangler, create a .dev.vars file (gitignored) in
the project root:
# .dev.vars
MCP_SECRET=your-long-random-secret# Export the secret on the same line (see the local-dev note in "How to use")
MCP_SECRET=local-dev-secret-3f9a2c7b6e1d4805 bun run dev # Vite dev serverVisit the landing page at the dev URL printed in the terminal (currently
http://localhost:8080/), and POST to /api/mcp with your bearer token.
| Script | Command | Purpose |
|---|---|---|
bun run dev |
vite dev |
Local dev server |
bun run build |
vite build |
Production build |
bun run preview |
vite preview |
Preview the production build |
bun run lint |
eslint . |
Lint |
bun run typecheck |
tsc --noEmit |
Type-check the whole project |
bun run format |
prettier --write . |
Format |
bun run test |
vitest run |
Run the unit suite once |
bun run test:watch |
vitest |
Run unit tests in watch mode |
bun run test:e2e |
playwright test |
Run the Playwright e2e suite |
This server is hardened by construction (see architecture/
for the ADRs and tradeoffs):
- SSRF protection (
src/lib/server/ssrf.ts) —fetch_urlvalidates the protocol, a hostname blocklist, and every DNS-resolved A/AAAA address (via DNS-over-HTTPS, since Workers has nonode:dns), blocking DNS-rebinding to private/loopback/link-local/CGNAT ranges in both IPv4 and IPv6. - Bearer auth, fail-closed — missing/invalid
MCP_SECRET⇒401. - Rate limiting (
src/lib/server/rate-limit.ts) — per-client-IP sliding window withRateLimit-*+Retry-Afterheaders;429when exceeded. - Security headers (
src/lib/server/security-headers.ts) — content-aware CSP, HSTS,X-Content-Type-Options,X-Frame-Options,Referrer-Policy,Permissions-Policy, COOP, applied at a single choke point inserver.ts. - Structured logging (
src/lib/logger.ts) — one JSON object per line; ESLint forbids rawconsoleelsewhere.
All requests require an Authorization: Bearer <MCP_SECRET> header and must
accept both JSON and SSE.
curl -X POST http://localhost:8080/api/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "Authorization: Bearer $MCP_SECRET" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'curl -X POST http://localhost:8080/api/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "Authorization: Bearer $MCP_SECRET" \
-d '{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": { "name": "generate_uuid", "arguments": { "count": 3 } }
}'{
"mcpServers": {
"mcp-core-engine": {
"url": "https://your-deployment.example.com/api/mcp",
"headers": {
"Authorization": "Bearer YOUR_MCP_SECRET"
}
}
}
}The Worker entry is src/server.ts (see wrangler.jsonc), which wraps the
TanStack Start SSR handler and normalizes catastrophic SSR errors into a branded
500 page.
-
Authenticate Wrangler (one-time):
bunx wrangler login
-
Set the production secret (do not commit it):
bunx wrangler secret put MCP_SECRET
-
Build and deploy:
bun run build bunx wrangler deploy
wrangler.jsonc highlights:
name:tanstack-start-appmain:src/server.tscompatibility_flags:["nodejs_compat"]
-
Create
src/lib/mcp/tools/my-tool.ts:import { defineTool } from "mcp-tanstack-start"; import { z } from "zod"; export const myTool = defineTool({ name: "my_tool", description: "What it does.", parameters: z.object({ input: z.string().min(1).describe("..."), }), execute: async ({ input }) => `result for ${input}`, });
-
Register it in
src/routes/api/mcp.tsby importing it and adding it to thetools: [...]array. -
Add a test in
src/lib/mcp/tools/tools.test.ts.
Unit (Vitest) — src/**/*.test.ts, run in a plain Node environment
(vitest.config.ts). They cover each tool's execute handler and Zod schema,
the SSRF IP classifier + assertPublicUrl (with an injected resolver), the rate
limiter (with injected now()), env validation, and the security headers.
bun run test # 82 testsE2E (Playwright) — e2e/*.spec.ts, run against the dev server (which boots
the Worker in workerd, so headers/auth/rate-limit behave like production). They
assert the landing page renders + hydrates under the CSP, the 405/401
responses, the security headers, and that repeated POSTs get rate-limited.
bunx playwright install chromium # first time only
bun run test:e2ePoint the e2e suite at a deployed URL instead of the dev server:
E2E_BASE_URL=https://your-deployment.example.com bun run test:e2e.github/workflows/ci.yml runs on every push/PR:
- verify —
lint→typecheck→ unittest→build - e2e — Playwright (Chromium)
- deploy — on
main,wrangler deploy. Opt in by setting repo variableCLOUDFLARE_DEPLOY=trueplus secretsCLOUDFLARE_API_TOKEN/CLOUDFLARE_ACCOUNT_ID. Without the opt-in, the deploy job is skipped (so CI stays green).MCP_SECRETis provisioned separately viawrangler secret put.
The same gates run in a pinned container for local reproducibility:
docker build -f infra/Dockerfile -t mcp-core-engine .
docker run --rm mcp-core-engineThe Dockerfile is a build/CI artifact only — production runs on Cloudflare Workers via Wrangler (see ADR-0007).
architecture/ # ADRs (decisions + tradeoffs)
infra/Dockerfile # Hermetic build/CI image (not the deploy artifact)
e2e/ # Playwright specs
.github/workflows/ci.yml # lint → typecheck → unit → build → e2e → deploy
src/
├── server.ts # Worker entry: SSR + error normalize + security headers
├── start.ts # TanStack Start instance + error middleware
├── router.tsx # Router factory (React Query context)
├── routeTree.gen.ts # Auto-generated route tree (do not edit)
├── routes/
│ ├── __root.tsx # Root layout / providers / error + 404 pages
│ ├── index.tsx # Landing page
│ └── api/mcp.ts # MCP route: rate-limit → auth → handler
├── lib/
│ ├── logger.ts # Isomorphic structured-JSON logger
│ ├── server/ # Server-only trust boundary
│ │ ├── env.ts # Zod-validated, memoized env
│ │ ├── rate-limit.ts # Injectable sliding-window limiter
│ │ ├── security-headers.ts # CSP / HSTS / nosniff / frame-ancestors
│ │ └── ssrf.ts # IP classifier + DoH resolver + assertPublicUrl
│ ├── mcp/tools/ # One file per MCP tool (+ tools.test.ts)
│ ├── error-capture.ts # SSR error capture
│ ├── error-page.ts # Branded HTML error page
│ └── utils.ts # cn() helper
├── components/ui/ # shadcn/ui component library
└── hooks/ # React hooks
- Muhammad Zeeshan
No license file is present. Add one before public distribution.