How to Deploy a Remote MCP Server (HTTP/SSE Transport)
Local stdio transport gets you started quickly, but it has a fundamental limitation: only the local machine can connect. If you want multiple team members, multiple machines, or production AI agents to access the same MCP server, you need to deploy it for remote access using HTTP with Server-Sent Events (SSE) transport.
This tutorial walks through the full process: converting a local server to HTTP/SSE, adding authentication, and deploying to three different hosting platforms.
Prerequisites#
- Completed the build MCP server tutorial or have an existing MCP server
- Node.js 18+ and npm installed
- An account on at least one of: Railway, Vercel, or AWS
- Familiarity with environment variables and basic Express.js
Overview#
The deployment process has four stages:
- Convert your server from stdio to HTTP/SSE transport
- Add authentication to protect the remote endpoint
- Deploy to your chosen cloud platform
- Connect Claude Desktop and custom clients to the remote server
Step 1: Convert to HTTP/SSE Transport#
A stdio server and an HTTP/SSE server use the same tool definitions — you only change the transport layer. Here is a minimal stdio server:
// Before: stdio transport
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const server = new McpServer({ name: "my-server", version: "1.0.0" });
server.tool("get_status", "Get service status", {}, async () => ({
content: [{ type: "text", text: "Status: operational" }],
}));
const transport = new StdioServerTransport();
await server.connect(transport);
Convert it to HTTP/SSE by replacing the transport and adding Express:
// After: HTTP/SSE transport
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import express from "express";
import { z } from "zod";
const app = express();
app.use(express.json());
// Create the MCP server (same tools as before)
const mcpServer = new McpServer({ name: "my-server", version: "1.0.0" });
mcpServer.tool("get_status", "Get service status", {}, async () => ({
content: [{ type: "text", text: "Status: operational" }],
}));
// Track active SSE transports by session ID
const transports = new Map<string, SSEServerTransport>();
// SSE endpoint — clients connect here to receive server events
app.get("/sse", async (req, res) => {
const transport = new SSEServerTransport("/messages", res);
transports.set(transport.sessionId, transport);
res.on("close", () => {
transports.delete(transport.sessionId);
});
await mcpServer.connect(transport);
});
// POST endpoint — clients send messages here
app.post("/messages", async (req, res) => {
const sessionId = req.query.sessionId as string;
const transport = transports.get(sessionId);
if (!transport) {
res.status(404).json({ error: "Session not found" });
return;
}
await transport.handlePostMessage(req, res, req.body);
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`MCP server listening on port ${PORT}`);
});
Install the new dependency:
npm install express
npm install --save-dev @types/express
Test locally before deploying:
npx ts-node --esm src/server.ts
# In another terminal:
curl http://localhost:3000/sse
Step 2: Add Authentication#
An internet-accessible MCP server without authentication is a security liability. Add API key validation before proceeding to deployment. See MCP authentication for the full security picture.
// Authentication middleware
function requireApiKey(
req: express.Request,
res: express.Response,
next: express.NextFunction
) {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith("Bearer ")) {
res.status(401).json({
error: "Missing Authorization header. Use: Authorization: Bearer <api-key>",
});
return;
}
const apiKey = authHeader.slice(7);
const validKey = process.env.MCP_API_KEY;
if (!validKey || apiKey !== validKey) {
res.status(403).json({ error: "Invalid API key" });
return;
}
next();
}
// Apply authentication to both endpoints
app.get("/sse", requireApiKey, async (req, res) => { /* ... */ });
app.post("/messages", requireApiKey, async (req, res) => { /* ... */ });
Generate a secure API key:
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
Store this key in your environment — never commit it to version control. Add a .env file for local development:
MCP_API_KEY=your-generated-key-here
PORT=3000
Also add a health check endpoint — most platforms use this to verify deployment success:
app.get("/health", (req, res) => {
res.json({ status: "ok", server: "my-mcp-server", version: "1.0.0" });
});
Step 3: Deploy to Your Platform#
Option A: Railway (Recommended for Persistent MCP Servers)#
Railway supports long-lived HTTP connections without function timeouts, making it the best fit for MCP SSE servers.
Prepare your project:
// package.json
{
"scripts": {
"start": "node dist/server.js",
"build": "tsc"
},
"engines": {
"node": ">=18"
}
}
// tsconfig.json — ensure output goes to dist/
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"outDir": "./dist",
"rootDir": "./src"
}
}
Deploy:
- Push your code to a GitHub repository
- Go to railway.app and create a new project
- Select "Deploy from GitHub repo" and choose your repository
- In the project settings, add the environment variable
MCP_API_KEYwith your generated key - Railway auto-detects Node.js and runs
npm run build && npm start - Your server URL will be
https://your-project.up.railway.app
Test the deployment:
curl -H "Authorization: Bearer your-api-key" \
https://your-project.up.railway.app/health
Option B: Vercel (Good for Short-Lived Sessions)#
Vercel works well for MCP servers where each tool call is fast and sessions are short. Be aware of the execution timeout limits on free plans.
Create a vercel.json:
{
"functions": {
"api/sse.ts": {
"maxDuration": 60
},
"api/messages.ts": {
"maxDuration": 30
}
}
}
Structure your project as Vercel API routes:
// api/sse.ts
import { createMcpServer } from "../src/server-core";
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
const sessions = new Map<string, SSEServerTransport>();
export default async function handler(req: any, res: any) {
if (!validateApiKey(req)) {
res.status(401).json({ error: "Unauthorized" });
return;
}
const server = createMcpServer();
const transport = new SSEServerTransport("/api/messages", res);
sessions.set(transport.sessionId, transport);
await server.connect(transport);
}
Deploy:
npm install -g vercel
vercel --prod
# Set environment variable in Vercel dashboard: MCP_API_KEY
Option C: AWS with App Runner#
AWS App Runner is well-suited for containerized MCP servers that need auto-scaling.
Create a Dockerfile:
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY dist/ ./dist/
EXPOSE 3000
CMD ["node", "dist/server.js"]
Build and push to ECR, then create an App Runner service pointing to your image. Set the MCP_API_KEY environment variable in the App Runner service configuration.
Step 4: Connect Clients to the Remote Server#
Claude Desktop#
Recent versions of Claude Desktop support connecting directly to remote HTTP/SSE servers. Edit ~/Library/Application Support/Claude/claude_desktop_config.json:
{
"mcpServers": {
"my-remote-server": {
"url": "https://your-project.up.railway.app/sse",
"headers": {
"Authorization": "Bearer your-api-key-here"
}
}
}
}
Restart Claude Desktop and verify the server appears in the tools panel.
Custom TypeScript Client#
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
const transport = new SSEClientTransport(
new URL("https://your-project.up.railway.app/sse"),
{
requestInit: {
headers: {
Authorization: `Bearer ${process.env.MCP_API_KEY}`,
},
},
eventSourceInit: {
fetch: (url, init) =>
fetch(url, {
...init,
headers: {
...init?.headers,
Authorization: `Bearer ${process.env.MCP_API_KEY}`,
},
}),
},
}
);
const client = new Client(
{ name: "my-client", version: "1.0.0" },
{ capabilities: {} }
);
await client.connect(transport);
const tools = await client.listTools();
console.log("Remote tools:", tools.tools.map((t) => t.name));
Custom Python Client#
import asyncio
import os
from mcp import ClientSession
from mcp.client.sse import sse_client
async def main():
headers = {"Authorization": f"Bearer {os.environ['MCP_API_KEY']}"}
async with sse_client(
"https://your-project.up.railway.app/sse",
headers=headers
) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
tools = await session.list_tools()
print("Remote tools:", [t.name for t in tools.tools])
asyncio.run(main())
Common Issues and Solutions#
SSE connection closes immediately
Most likely an authentication failure — the server returns a 401/403, the HTTP layer closes the connection, and the client sees a disconnection. Check your API key and Authorization header format. Add request logging on the server to confirm the header is arriving.
Messages endpoint returns 404 for session
The session ID in the ?sessionId= query parameter does not match any active SSE session. This can happen if the SSE connection closed between the client connecting and sending the first message, or if you have multiple server instances without shared session storage. For multi-instance deployments, store sessions in Redis rather than an in-memory Map.
Deployment succeeds but tools time out
If your tools call external APIs or databases, the connection strings and API keys must be set as environment variables on the deployment platform. The server process cannot access your local .env file.
CORS errors in browser-based clients
Add CORS headers if your MCP client runs in a browser:
import cors from "cors";
app.use(cors({
origin: process.env.ALLOWED_ORIGINS?.split(",") || "*",
methods: ["GET", "POST"],
allowedHeaders: ["Content-Type", "Authorization"],
}));
Next Steps#
With your remote MCP server deployed:
- Implement comprehensive security controls from the MCP server security tutorial
- Set up monitoring and alerting — Railway and Vercel both expose metrics dashboards
- Explore advanced patterns including resource subscriptions in the advanced MCP patterns tutorial
- Test your deployment thoroughly using the test and debug MCP server guide
Frequently Asked Questions#
How much does hosting an MCP server cost?
Railway's free tier includes 500 hours/month of compute — enough for a personal MCP server running continuously. For team use, Railway's Starter plan at $5/month covers most needs. Vercel's free tier works for low-traffic servers. AWS App Runner starts at around $10-15/month for minimal compute. The main cost driver is not the MCP server itself but any external services your tools call.
Can multiple users share one remote MCP server?
Yes. An HTTP/SSE server handles multiple simultaneous SSE connections. Each connected client gets its own session ID and isolated tool call context. If your tools access user-specific data, implement per-session authentication that maps the API key or token to the user's data scope to prevent data leakage between users.
How do I update my deployed MCP server without downtime?
Railway and Vercel both support zero-downtime deployments. Push to your connected GitHub repository and the platform builds and deploys the new version, routing traffic to the new instance before terminating the old one. Active SSE sessions on the old instance will be disconnected during the cutover — design your clients to reconnect automatically when the SSE connection drops.