Tutorial 6 min read

How to Deploy an MCP Server to Production

How to deploy an MCP server: one HTTPS POST endpoint on Streamable HTTP, a localhost bind until TLS and bearer auth are in front, and a host that can hold the process.

Tayyab JavedFreelance AI agent developer
How to Deploy an MCP Server to Production
On this page 6 sections
  1. The transport you deploy
  2. TypeScript on Express
  3. Python on Streamable HTTP
  4. Where to run it
  5. Frequently Asked Questions
  6. Conclusion

Deploy an MCP server as one HTTPS POST endpoint, usually /mcp, on Streamable HTTP. Leave stdio on the machine that launched it. Revision 2026-07-28 deprecates the old HTTP+SSE transport. Put TLS and a bearer token in front, and bind to localhost until both exist.

By Tayyab Javed, AI agent developer.

Quick answer

  • Transport: Streamable HTTP. Each client message is its own POST. The server replies with JSON or a request-scoped SSE stream.
  • Bind: 127.0.0.1 until a TLS proxy sits in front. On 0.0.0.0, set an allowlist for Host and Origin.
  • Auth: OAuth 2.1 bearer tokens on every HTTP request. The audience is the public URL, not the internal port. See MCP server authentication.
  • Host: any place that can run a long-lived process and terminate TLS. A container, Fly.io, Render, or a VM all qualify.

The transport you deploy

The Streamable HTTP spec is one path. The client POSTs one JSON-RPC message with Accept: application/json, text/event-stream, MCP-Protocol-Version, and Mcp-Method. A tools/call also sends Mcp-Name. That revision removed the standalone GET stream and protocol sessions. Closing the response stream cancels the request.

The SDKs still answer older clients. A 2025-style tools/list with no protocol headers, sent to @modelcontextprotocol/express 2.0.2, returned 200 text/event-stream. A 2026-07-28 tools/call on the same app came back as JSON. The server under this is how to build an MCP server.

TypeScript on Express

Installed October 12, 2026: server 2.3.1, @modelcontextprotocol/express 2.0.2, @modelcontextprotocol/node 2.1.1. The adapter is not numbered with the server package. createMcpExpressApp checks Host and Origin on 127.0.0.1. Binding 0.0.0.0 turns those checks off unless you pass both allowlists. Hostnames only, no port.

TypeScript
import { createMcpExpressApp } from '@modelcontextprotocol/express';
import { toNodeHandler } from '@modelcontextprotocol/node';
import { createMcpHandler, McpServer } from '@modelcontextprotocol/server';
import * as z from 'zod/v4';

const handler = createMcpHandler(() => {
    const server = new McpServer({ name: 'notes', version: '1.0.0' });
    server.registerTool(
        'add-note',
        {
            description: 'Save a note and echo it back',
            inputSchema: z.object({ text: z.string() })
        },
        async ({ text }) => ({
            content: [{ type: 'text', text: 'Saved: ' + text }]
        })
    );
    return server;
});

const app = createMcpExpressApp({
    host: '0.0.0.0',
    allowedHosts: ['api.example.com'],
    allowedOrigins: ['app.example.com']
});
const node = toNodeHandler(handler);
app.all('/mcp', (req, res) => void node(req, res, req.body));
app.listen(3000, '0.0.0.0');

I ran this app on an ephemeral port. add-note returned Saved: ship it. On the default localhost app, Origin: https://evil.example got 403 Invalid Origin: evil.example. A request with no Origin got 200, which is how a non-browser client looks, so Origin checks do not replace bearer auth. With the allowlists above, Host: evil.example got 403 Invalid Host, and Host: api.example.com with Origin: https://app.example.com got 200. Pass req.body into toNodeHandler. The helper already parsed it.

Python on Streamable HTTP

On mcp 2.3.0 the defaults are 127.0.0.1:8000/mcp. I read them off run_streamable_http_async. stateless_http=True uses a fresh transport per request and skips session tracking. Calling streamable_http_app(stateless_http=True, json_response=True) returned Saved: ship it as JSON. run() forwards those arguments.

Python
from mcp.server import MCPServer

mcp = MCPServer("notes")

@mcp.tool()
def add_note(text: str) -> str:
    """Save a note and echo it back."""
    return "Saved: " + text

if __name__ == "__main__":
    mcp.run(
        transport="streamable-http",
        host="127.0.0.1",
        port=8000,
        stateless_http=True,
        json_response=True,
    )

Leave the bind on loopback. That default enables DNS-rebinding checks for 127.0.0.1, localhost, and ::1. If the platform requires 0.0.0.0, set TransportSecuritySettings.allowed_hosts to the public hostname. resource_server_url must be the public https:// URL, or a loopback audience fails the check.

Where to run it

A container, Fly.io, Render, or a VM will do, as long as it keeps the process up and terminates TLS. On Render, bind 0.0.0.0 and PORT, with the allowlists set. I ran the fetch handler from the authentication post in Node, not on Cloudflare. Stdio stays off these hosts.

Before the URL is public: TLS, bearer auth with that audience, allowlists on a public bind, a rate limit, and no secrets in tool descriptions. Schema drift and retried writes are in the MCP production guide.

Frequently Asked Questions

What transport should a production MCP server use?

Streamable HTTP, one POST endpoint behind TLS. Stdio stays local. HTTP+SSE is deprecated as of revision 2026-07-28.

Can I deploy an MCP server to Render or a container?

Yes. Run the HTTP process, terminate TLS at the edge, and set the public https URL as the OAuth resource. On Render, bind 0.0.0.0 and PORT, with Host and Origin allowlists.

Does the SDK authenticate the server for me?

The Express helper checks Host and Origin. Bearer tokens are a separate gate. A request with no Origin is allowed, so non-browser clients still need it.

Conclusion

Production is Streamable HTTP on a public name, with TLS, a bearer audience that matches that name, and a bind that refuses other hosts. Start local, add the gate, then point a proxy at it. Hire me if you want that path checked against the tools you already run.

Hire Tayyab to deploy the server

Free 30-minute scoping call. We will pick the host, the public URL, and the checks in front of /mcp.

Book a Scoping Call
Share
Tayyab Javed
About the author

Tayyab Javed

Tayyab is a freelance AI agent developer and founder of Workly. He does research, spec, architecture, UX, and the build — solo, no handoff failures. Ex-Principal PM behind a Fortune 500 AI contact center (40% CSAT lift). He helps founders and SMBs ship production-grade agentic systems end to end.