
Services
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.

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.
127.0.0.1 until a TLS proxy sits in front. On 0.0.0.0, set an allowlist for Host and Origin.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.
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.
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.
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.
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.
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.
Streamable HTTP, one POST endpoint behind TLS. Stdio stays local. HTTP+SSE is deprecated as of revision 2026-07-28.
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.
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.
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.
Free 30-minute scoping call. We will pick the host, the public URL, and the checks in front of /mcp.
Book a Scoping CallTayyab 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.