# How to Deploy an MCP Server to Production

> How to deploy an MCP server: Streamable HTTP, a safe bind, Origin checks, bearer auth, and where to host the process.

By Tayyab Javed, freelance AI agent developer (https://hireagentic.dev) · Published October 12, 2026 · Tutorial · 6 min read
Canonical URL: https://hireagentic.dev/blog/deploy-mcp-server

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](https://hireagentic.dev/blog/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](https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http) 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](https://hireagentic.dev/blog/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](https://hireagentic.dev/blog/mcp-server-authentication). 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](https://hireagentic.dev/blog/mcp-model-context-protocol-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](https://cal.com/tayyab-javed-ubhldo/30min)
