Community-maintained FDE reference. Not an official Vercel or Anthropic project. About this project
Prompt: minimal-server
This prompt builds the smallest end-to-end MCP server: one echo tool over Streamable HTTP, the 10-minute path from getting started and the structural template every other example copies.
Wire status: the pinned stack (mcp-handler 2.1.1 on @modelcontextprotocol/server 2.0.0) serves the 2026-07-28 contract natively over Streamable HTTP and falls back to stateless 2025-11-25 Streamable HTTP for legacy clients; the SDK Client defaults to that legacy handshake unless you opt in to modern version negotiation, so the examples’ in-memory test suites exercise only the legacy path. The prompt’s TESTS section pins the behaviors the in-memory harness actually exhibits, so the project it produces is green today.
Copy everything in the block below into your AI coding agent as one message. See the prompt index for what makes these prompts reliable and how they were tested.
GOALBuild me a small TypeScript project called minimal-server that runs locally and deploys to Vercel: an MCP (Model Context Protocol) server that exposes exactly one tool, echo, which returns the caller's message back unchanged. This is the smallest possible end-to-end MCP server, so keep everything minimal and readable.
STACK (exact, non-negotiable)- Next.js App Router: next ^16.2.12, react and react-dom ^19.0.0.- mcp-handler 2.1.1 plus @modelcontextprotocol/server pinned EXACTLY to 2.0.0 as regular dependencies (mcp-handler 2.1.1 peer-requires @modelcontextprotocol/server ^2.0.0). Do NOT add @modelcontextprotocol/sdk; that is the v1 package.- zod ^4.2.0. HARD FLOOR: the v2 SDK requires zod 4.2.0 or newer; zod ^3 installs cleanly and then fails typecheck and tests, so never downgrade it.- Dev dependencies: typescript ^5.9.0, vitest ^4.1.0, @types/node ^24.0.0, @types/react 19.2.18, and @modelcontextprotocol/client pinned EXACTLY to 2.0.0 (the client package is for tests only).- Node 22 or newer ("engines": { "node": ">=22" }). In package.json set "type": "module", "private": true, an "exports" map of { ".": "./src/server.ts" } (so a sibling package such as orchestrator-host can import configureServer by package name), and scripts: dev (next dev), test (vitest run), typecheck (tsc --noEmit).
LAYOUT- app/api/mcp/route.ts is a thin shell only. It builds the handler as withOriginCheck(createMcpHandler(configureServer, { serverInfo: { name: SERVER_NAME, version: SERVER_VERSION } }), parseAllowedOrigins(process.env[ALLOWED_ORIGINS_ENV])) and exports that one handler as GET, POST, and DELETE. createMcpHandler comes from mcp-handler; withOriginCheck, parseAllowedOrigins, and ALLOWED_ORIGINS_ENV come from src/origin.ts. There is NO withMcpAuth here (this example has no authentication), v2 has no [transport] directory and no basePath option, and the public endpoint is /api/mcp. Nothing else lives in the route file.- ALL protocol logic lives in src/server.ts, which exports configureServer(server) (typed against McpServer from @modelcontextprotocol/server) plus the constants SERVER_NAME = "minimal-server" and SERVER_VERSION = "0.1.0".- src/origin.ts is a framework-free Origin allowlist (the Streamable HTTP transport spec says servers MUST validate Origin and answer 403 when it is not allowed; mcp-handler 2.x does not do this itself). It exports: ALLOWED_ORIGINS_ENV = "MCP_ALLOWED_ORIGINS"; DEFAULT_ALLOWED_ORIGINS = ["http://localhost:3000", "http://127.0.0.1:3000"]; parseAllowedOrigins(raw) which splits the comma separated variable, trims, normalizes each entry to its serialized origin via new URL(entry).origin, drops entries that do not parse (fail closed, never widen), de-duplicates, and falls back to DEFAULT_ALLOWED_ORIGINS only when raw is undefined or blank; assertAllowedOrigin(request, allowlist) which returns null when the request has no Origin header (non-browser clients) or an allowlisted one, and otherwise returns a 403 Response with body "Forbidden: Origin not allowed" and content-type text/plain (the literal "null" origin and unparseable values are refused; the body never echoes the allowlist); and withOriginCheck(handler, allowlist), a generic wrapper that runs assertAllowedOrigin first, returns the refusal when there is one, and otherwise forwards the request plus any extra arguments (Next.js passes a route context) to the wrapped handler.- Tests live in tests/ as three files: tests/server.test.ts, tests/origin.test.ts, and tests/vercel-config.test.ts. They import only from src/ (and vercel.json as data). They must never import anything from Next.js.- vercel.json contains the $schema key ("https://openapi.vercel.sh/vercel.json") plus a functions block: { "app/api/mcp/route.ts": { "maxDuration": 30 } }, so a runaway tool call is bounded by the platform.
BEHAVIOR- Register one tool named echo with server.registerTool. Description: "Echo a message back to the caller."- Its input schema is a full zod object schema: inputSchema: z.object({ message: z.string() }). SDK v2 takes the object schema itself, not the raw shape v1 used.- The handler returns { content: [{ type: "text", text: message }] }, echoing the input exactly. No other tools, resources, or prompts.
TESTS (vitest, offline)tests/server.test.ts (describe "minimal-server"): in each test, build a fresh McpServer (from @modelcontextprotocol/server) with SERVER_NAME and SERVER_VERSION, call configureServer on it, then connect a real Client from @modelcontextprotocol/client (name "test-client", version "0.0.0") over InMemoryTransport.createLinkedPair() (InMemoryTransport also comes from @modelcontextprotocol/server); connect server and client with Promise.all. Assert:1. listTools returns exactly one tool named echo whose inputSchema is an object with properties.message of type string.2. callTool with { message: "ping" } succeeds (isError is falsy) and content equals [{ type: "text", text: "ping" }].3. callTool with a wrong-typed argument, message set to the number 42, comes back as a tool RESULT with isError: true. SDK v2 reality check: callTool does NOT throw for schema-invalid arguments on a known tool.4. callTool with an unknown tool name like "nope" REJECTS with a ProtocolError whose message matches /not found/i. This CHANGED from v1, which returned isError results for unknown tools; v2 matches the spec's protocol-error semantics.5. In-memory wire note: these tests run over InMemoryTransport, where a bare McpServer answers server/discover with -32601 and the Client defaults to the legacy initialize handshake at protocol version 2025-11-25, so on this path results carry no resultType and list results no ttlMs/cacheScope. That is a property of the harness, not of the server: the same configureServer behind createMcpHandler serves the 2026-07-28 frames over HTTP. If you want to assert those fields, do it in an HTTP-level check against the handler, not in these in-memory tests.tests/origin.test.ts exercises src/origin.ts directly with a Fetch Request helper (POST to https://server.example/api/mcp with an optional origin header) and an allowlist of ["https://app.example"], in three describe blocks: "assertAllowedOrigin" (a disallowed Origin gets 403 whose body matches /origin/i and does not contain "app.example"; no Origin header passes; an allowlisted Origin passes; the literal "null" origin and "not a url" are refused; scheme, host, and port all count, so http://app.example, https://app.example:8443, and https://app.example.evil are refused while HTTPS://APP.example passes after normalization; an empty allowlist refuses every Origin but still passes an origin-less request), "parseAllowedOrigins" (undefined and blank fall back to DEFAULT_ALLOWED_ORIGINS; " https://app.example/ , https://admin.example:8443/path, nope, ," yields ["https://app.example", "https://admin.example:8443"]; an all-junk value such as "garbage" yields [] rather than the default), and "withOriginCheck" (a refused request returns 403 before the wrapped handler runs, counted as zero calls; allowed and origin-less requests are forwarded with their extra arguments intact).tests/vercel-config.test.ts (describe "vercel.json") reads vercel.json from disk as data and asserts that functions["app/api/mcp/route.ts"].maxDuration is a positive integer.
DEFINITION OF DONE- npm install, npm run typecheck, and npm test all pass with no errors.- npm run dev starts the server; connect MCP Inspector (npx @modelcontextprotocol/inspector) using the Streamable HTTP transport to http://localhost:3000/api/mcp, list tools, and call echo by hand. The default allowlist already covers the local dev server.- Optionally run vercel deploy; the endpoint is https://<deployment>/api/mcp and needs no environment variables. The one optional variable is MCP_ALLOWED_ORIGINS (comma separated browser origins); a deployment that serves only non-browser clients can leave it unset, since those never send Origin.
SOURCES- https://modelcontextprotocol.io/specification/2026-07-28/server/tools- https://modelcontextprotocol.io/specification/2026-07-28/basic/versioning- https://modelcontextprotocol.io/specification/2026-07-28/basic/transports- https://vercel.com/docs/mcp/deploy-mcp-servers-to-vercel- https://github.com/vercel/mcp-handler- Reference implementation, compare against it if you get stuck: examples/minimal-server/ in this repository (a path relative to the repo root; the GitHub repository is private)
GUARDRAILS- Tests must pass fully offline with no network access and no Vercel account.- No dependencies beyond the stack list above.- Keep the project small: one tool, one source file of protocol logic (src/server.ts) plus the origin helper (src/origin.ts), and the three test files named above.Where to look now
- Prompt index - all thirteen prompts and the reliability notes.
examples/minimal-server(in the repository) - the reference implementation this prompt rebuilds.- Examples index - what each example demonstrates.
Bibliography
- localhost:3000 - http://localhost:3000/api/mcp
- <https:// /api/mcp> - Model Context Protocol Specification - https://modelcontextprotocol.io/specification/2026-07-28/server/tools
- Model Context Protocol Specification - https://modelcontextprotocol.io/specification/2026-07-28/basic/versioning
- Model Context Protocol Specification - https://modelcontextprotocol.io/specification/2026-07-28/basic/transports
- Vercel Documentation - https://vercel.com/docs/mcp/deploy-mcp-servers-to-vercel
- vercel/mcp-handler, source code - https://github.com/vercel/mcp-handler
- Reference implementation, source code (this repository) -
examples/minimal-server(in the repository)