Community-maintained FDE reference. Not an official Vercel or Anthropic project. About this project
Prompt: sampling-server
Deprecated pattern (SEP-2577). The 2026-07-28 spec revision deprecates sampling; the suggested migration is to call LLM provider APIs directly from the server - on Vercel, the AI SDK or AI Gateway. The surface stays functional during the deprecation window, and this prompt is kept deliberately to teach the flow and its trust boundary. See the deprecated-features registry.
You get an MCP server whose summarize tool asks the host’s own model for a completion instead of bundling one; it teaches the server-to-host sampling flow from sampling-request handling.
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 sampling-server that runs locally and deploys to Vercel: an MCP (Model Context Protocol) server whose one tool, summarize, does not run a model itself. Instead it turns around and asks the connected host's model to write the summary via the MCP sampling feature (sampling/createMessage), then returns that completion as the tool result. The server never holds a model or an API key. Note: the 2026-07-28 spec revision deprecates sampling (SEP-2577, migrate to direct provider APIs), but the SDK keeps it functional; this project deliberately teaches the flow.
STACK (exact, non-negotiable)Next.js App Router (next ^16.2.12, react and react-dom ^19.0.0), mcp-handler 2.1.1, @modelcontextprotocol/server pinned EXACTLY to 2.0.0 as a dependency (mcp-handler 2.1.1 peer-requires ^2.0.0), @modelcontextprotocol/client pinned EXACTLY to 2.0.0 as a devDependency (tests only), 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), devDeps typescript ^5.9.0, vitest ^4.1.0, @types/node ^24.0.0. Node 22 or newer ("engines": { "node": ">=22" }). package.json has "type": "module", "private": true, an "exports" map of { ".": "./src/server.ts" }, and scripts dev (next dev), test (vitest run), typecheck (tsc --noEmit).
LAYOUTapp/api/mcp/route.ts is a thin shell. 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, DELETE. createMcpHandler comes from 'mcp-handler'; withOriginCheck, parseAllowedOrigins, and ALLOWED_ORIGINS_ENV come from src/origin.ts. No withMcpAuth (this example has no authentication). There is NO [transport] directory and NO basePath option and NO three-argument createMcpHandler call (that v1 signature no longer exists in mcp-handler 2.x; the createMcpHandler name survives with the two-argument form used here); the public endpoint is /api/mcp. ALL protocol logic lives in src/server.ts exporting configureServer(server), typed against McpServer from '@modelcontextprotocol/server'.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, tests/vercel-config.test.ts), import 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 } }.
BEHAVIORsrc/server.ts exports SERVER_NAME 'sampling-server', SERVER_VERSION '0.1.0', and MAX_TOKENS = 512 (sampling spends the user's tokens through the host, so the tool bounds its own request).configureServer registers exactly one tool, summarize, with description "Summarize text by asking the host's model for a completion via sampling." and inputSchema z.object({ text: z.string() }) (in SDK v2 inputSchema is a full zod object schema, NOT the bare shape v1 used).The handler calls server.server.createMessage (the underlying Server instance in @modelcontextprotocol/server 2.0.0; it is marked deprecated there but works) with: messages = one user message whose content is a single text block reading 'Summarize the following text in one or two sentences:' followed by a blank line and then the caller's text; maxTokens: MAX_TOKENS; modelPreferences: { intelligencePriority: 0.8 } (a hint only, the host may ignore it).If the returned completion content has type 'text', return exactly that text as the tool result. Otherwise do not crash: return a text block saying 'Host returned non-text sampling content of type X.' where X is the actual type.Do not catch capability errors yourself: if the connected client never advertised the sampling capability, the SDK throws before sending and McpServer surfaces that as an isError tool result on its own.
TESTS (vitest, deterministic, fully offline, no real model anywhere)tests/server.test.ts (describe "sampling-server"): connect a real Client (from '@modelcontextprotocol/client', name 'test-client', version '0.0.0') over InMemoryTransport.createLinkedPair() (InMemoryTransport comes from '@modelcontextprotocol/server'). The client stands in for the host plus model: BEFORE connect, call client.registerCapabilities({ sampling: {} }) and then client.setRequestHandler('sampling/createMessage', handler) - in SDK v2 request handlers are registered by method name string, not by the v1 zod request schema. The handler records every incoming request's params into an array typed CreateMessageRequest['params'][] (reset in beforeEach) and returns a canned CreateMessageResult: { role: 'assistant', content: { type: 'text', text: <a fixed canned summary string> }, model: 'stub-model', stopReason: 'endTurn' } (types CreateMessageRequest and CreateMessageResult from '@modelcontextprotocol/client'). Capability first, handler second, both before connect: the capability is what the server checks before sending. Give the connect helper options { withSampling?: boolean; result?: () => CreateMessageResult } so individual tests can skip the capability or swap the canned result.Assert all of these:1. listTools shows exactly one tool named summarize whose inputSchema is an object with a string text property.2. Calling summarize returns isError falsy and content equal to exactly one text block containing the canned summary (proves the round trip). The recorded request has maxTokens = 512, modelPreferences matching intelligencePriority 0.8, and one user message whose single text block contains the caller's input text.3. When the stub returns image content instead of text ({ type: 'image', data: 'aGVsbG8=', mimeType: 'image/png' }), the tool returns isError falsy with the 'Host returned non-text sampling content of type image.' text instead of crashing.4. Calling summarize with text: 42 (wrong type) returns an isError true tool RESULT and ZERO recorded sampling requests (SDK v2 reality: schema-invalid args on a KNOWN tool do not throw from callTool, they come back as isError true results, and the invalid call must never reach the stub model).5. Calling an unknown tool name REJECTS: callTool throws a protocol error whose message matches /not found/i, with zero sampling requests. This CHANGED from the v1 SDK, which returned isError results for unknown tools; v2 restores the spec's protocol-error semantics.6. A client connected WITHOUT registering the sampling capability gets an isError true result from summarize and zero sampling requests were sent (the SDK refuses to send to a client that cannot handle it).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 DONEnpm install, npm run typecheck, npm test all green. Then npm run dev and connect MCP Inspector (npx @modelcontextprotocol/inspector) with Streamable HTTP to http://localhost:3000/api/mcp; you can list and call summarize, and the call completes when the connected host brokers sampling. 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.
SOURCEShttps://modelcontextprotocol.io/specification/2026-07-28/client/sampling for the sampling flow and https://modelcontextprotocol.io/specification/2026-07-28/deprecated for its deprecation status; https://vercel.com/docs/mcp/deploy-mcp-servers-to-vercel and https://github.com/vercel/mcp-handler for the hosting pieces; the reference implementation at examples/sampling-server/ in this repository (a path relative to the repo root; the GitHub repository is private), compare against it if you get stuck.
GUARDRAILSTests must pass with no network access, no API keys, and no Vercel account. No dependencies beyond the stack list. Keep it 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/sampling-server(in the repository) - the reference implementation this prompt rebuilds.- Examples index - what each example demonstrates.
Bibliography
- localhost:3000 - http://localhost:3000/api/mcp
- Model Context Protocol Specification, Sampling - https://modelcontextprotocol.io/specification/2026-07-28/client/sampling
- Model Context Protocol Specification, Deprecated Features - https://modelcontextprotocol.io/specification/2026-07-28/deprecated
- 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/sampling-server(in the repository)