Move to remote HTTP safely
Turn the factory into an HTTP handler
Serve the same capability factory over remote HTTP with the v2 web-standard handler instead of duplicating server definitions.
Stdio works for one local client that launches the server as a child process. A remote client can’t do that. It needs a URL. So let’s give the same server an HTTP endpoint.
This is where the factory pays off. We wrote createServer() once. Now we hand it to a different transport and nothing else changes.
Create src/worker.ts:
import { createMcpHandler } from '@modelcontextprotocol/server'
import { createServer } from './server.js'
const handler = createMcpHandler(createServer)
export default {
async fetch(request: Request) {
const url = new URL(request.url)
if (url.pathname !== '/mcp') {
return new Response('Not found', { status: 404 })
}
return handler.fetch(request)
}
}
createMcpHandler() returns a web-standard handler object with { fetch, close, notify, bus }. We only use fetch. It takes a standard Request and returns a standard Response, so it runs anywhere the Fetch API exists.
The default export is the Worker-style shape: an object with a fetch method. Our wrapper does one thing before calling the SDK. It checks the path and mounts the handler at /mcp only. Anything else gets a 404.
One server per request
Just like serveStdio(), this handler calls our factory. The difference is when. Stdio creates one server per connection. createMcpHandler() creates one per HTTP request. That’s why we never stored caller state inside the McpServer instance. It wouldn’t survive to the next request anyway.
Which protocol versions it speaks
The handler serves modern 2026-07-28 requests. By default it also provides a stateless fallback for 2025-era clients, so older tools keep working. If you want a modern-only endpoint, pass { legacy: 'reject' } as the second argument. Then test every client you intend to support against it, because some will stop connecting.
What it does not do
createMcpHandler() doesn’t authenticate anyone. It doesn’t check the Origin or Host header for your deployment. It doesn’t rate limit. All of that has to sit in front of handler.fetch(), in your wrapper or at the edge, before the endpoint goes public. We’ll add authentication in a couple of lessons.
Test it locally
For now, run this handler only in a local development runtime. The deploy lesson shows the exact command for a Worker-style runtime. Once it’s running, point the Inspector at the local /mcp URL using the HTTP transport. List capabilities, call both tools, read the resource, render the prompt.
The result must match stdio exactly: two tools, one resource, one prompt, same schemas. If anything differs, you’ve somehow registered a capability outside the factory.
One common mistake: connecting the Inspector to the root URL instead of /mcp. You’ll get Not found and a failed connection. That’s our wrapper doing its job. Add the path and try again.
Lesson completed