Community-maintained FDE reference. Not an official Vercel or Anthropic project. About this project
Prompt: resources-server
This prompt builds a resources-only MCP server, showing where resources and resource templates sit among the server primitives: application-controlled context, no tools at all.
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 resources-server that runs locally and deploys to Vercel: an MCP (Model Context Protocol) server that demonstrates resources and resource templates in isolation. It serves one fixed note and one templated note, and it deliberately has NO tools and NO prompts, because resources are application-controlled context the host attaches to the model.
STACK (exact, non-negotiable)- Next.js App Router: next ^16.2.12, react and react-dom ^19.0.0.- mcp-handler 2.1.1 with @modelcontextprotocol/server pinned EXACTLY to 2.0.0 as a dependency, and @modelcontextprotocol/client pinned EXACTLY to 2.0.0 as a devDependency (tests only).- zod ^4.2.0. This is a hard floor: SDK v2 requires zod 4.2.0 or newer; a ^3 range installs cleanly and then fails at runtime.- Dev dependencies: typescript ^5.9.0, vitest ^4.1.0, @types/node ^24.0.0.- Node 22 or newer ("engines": { "node": ">=22" }). In package.json set "type": "module", "private": true, an "exports" map of { ".": "./src/server.ts" }, and scripts: dev (next dev), test (vitest run), typecheck (tsc --noEmit).- 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.
LAYOUT- app/api/mcp/route.ts is a thin shell only (there is NO [transport] directory in v2). 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. No withMcpAuth (this example has no authentication). The public endpoint stays /api/mcp.- ALL protocol logic lives in src/server.ts, which exports configureServer(server) plus these constants: SERVER_NAME = "resources-server", SERVER_VERSION = "0.1.0", WELCOME_URI = "notes://welcome", WELCOME_TEXT = "Welcome to the resources-server example.", TOPIC_TEMPLATE = "notes://{topic}", and a helper topicText(topic) returning 'You asked about: ' followed by the topic.- 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) and never import 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 } }.
BEHAVIOR- Direct resource: register "welcome-note" at the fixed URI notes://welcome with metadata { description: "Static direct resource: same content for every read.", mimeType: "text/plain" }; every read returns contents [{ uri: uri.href, mimeType: "text/plain", text: WELCOME_TEXT }].- Resource template: register "topic-note" with a ResourceTemplate for notes://{topic}, constructed as new ResourceTemplate(TOPIC_TEMPLATE, { list: undefined }), with metadata { description: "Resource template: the {topic} segment is bound per read request.", mimeType: "text/plain" }. Both ResourceTemplate and McpServer are imported from @modelcontextprotocol/server in v2. The SDK requires you to spell out list: undefined; this template only answers reads and never enumerates concrete resources. The read callback receives (uri, variables), binds topic as String(variables.topic), and returns contents [{ uri: uri.href, mimeType: "text/plain", text: topicText(topic) }].- Register nothing else. No tools means the server never negotiates the tools capability.
TESTS (vitest, offline)tests/server.test.ts (describe "resources-server"): in each test build a fresh McpServer with SERVER_NAME and SERVER_VERSION, call configureServer, and connect a real Client from @modelcontextprotocol/client (name "test-client", version "0.0.0") over InMemoryTransport.createLinkedPair(), with both McpServer and InMemoryTransport imported from @modelcontextprotocol/server. Assert:1. listResources returns exactly one resource: uri notes://welcome, name welcome-note, mimeType text/plain.2. listResourceTemplates returns exactly one template: uriTemplate notes://{topic}, name topic-note, mimeType text/plain.3. readResource on notes://welcome returns exactly [{ uri, mimeType, text: WELCOME_TEXT }].4. Template binding: reading notes://mcp returns text "You asked about: mcp" and reading notes://lifecycle returns "You asked about: lifecycle".5. Precedence: notes://welcome also matches the template pattern, but the exact registration must win, so its text is WELCOME_TEXT and must NOT contain "You asked about".6. A read of other://nope REJECTS: readResource throws a JSON-RPC protocol error mentioning that URI. Unlike tool failures, unknown resource URIs throw; they are not isError results.7. The server does not advertise the tools capability at all: client.getServerCapabilities() has no tools property, and listTools resolves with an EMPTY list. SDK 2.0.0 reality check: this changed from v1, where a server with zero tools made tools/list reject with Method not found (-32601); in v2 it resolves with [].8. No partial state: after reading notes://ephemeral through the template, listResources still shows only notes://welcome and listResourceTemplates still shows only notes://{topic}.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.- npm run dev, then connect MCP Inspector (npx @modelcontextprotocol/inspector) with Streamable HTTP to http://localhost:3000/api/mcp, list resources and templates, and read notes://welcome and notes://anything-you-like by hand. The default allowlist already covers the local dev server.- Optionally vercel deploy; the endpoint is https://<deployment>/api/mcp with no environment variables required. The one optional variable is MCP_ALLOWED_ORIGINS (comma separated browser origins); a deployment that serves only non-browser clients can leave it unset.
SOURCES- https://modelcontextprotocol.io/specification/2026-07-28/server/resources- https://modelcontextprotocol.io/specification/2026-07-28/basic/versioning- 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/resources-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 it small: two registrations, 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/resources-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/resources
- Model Context Protocol Specification - https://modelcontextprotocol.io/specification/2026-07-28/basic/versioning
- 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/resources-server(in the repository)