FastMCP abstracts protocol plumbing into Python decorators; the official @modelcontextprotocol/sdk does the same for TypeScript. Both get you a working local server fast. The gap is what comes next: picking a transport that’s actually current, packaging the result so a clean container can start it, and pointing it at a production host.
Three layers, not one
Keep these separate or the guidance below won’t make sense:
- MCP protocol — the spec at modelcontextprotocol.io, currently version 2025-11-25. It defines transports, lifecycle, tools.
- Framework — FastMCP (Python) and
@modelcontextprotocol/sdk(TypeScript) are open-source implementations of that spec. Their APIs change between versions independent of the protocol itself. - Platform — MCPLambda hosts the container your framework code runs in. It has its own CLI flags and config file, which don’t always share vocabulary with the framework.
Option 1: FastMCP (Python)
server.py
from fastmcp import FastMCP
mcp = FastMCP("Database Tools")
@mcp.tool
def query_user_count(status: str = "active") -> str:
"""Fetch total user count filtered by account status."""
# Custom database or API logic here
return f"Total users with status '{status}': 1,420"
if __name__ == "__main__":
mcp.run(transport="http", host="0.0.0.0", port=8000)
transport="http" starts FastMCP’s Streamable HTTP server (mounted at /mcp by default). Bind host="0.0.0.0" — the container needs to accept connections from outside its own network namespace, not just 127.0.0.1. This requires FastMCP 2.0+; earlier 0.x releases predate the transport="http" argument.
requirements.txt
fastmcp>=2.0
Option 2: TypeScript SDK
src/index.ts
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
import express from "express";
const server = new Server(
{ name: "company-metrics-mcp", version: "1.0.0" },
{ capabilities: { tools: {} } }
);
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: "get_monthly_revenue",
description: "Get monthly revenue stats",
inputSchema: { type: "object", properties: {} },
},
],
}));
server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === "get_monthly_revenue") {
return { content: [{ type: "text", text: "Revenue: $128,400" }] };
}
throw new Error(`Unknown tool: ${request.params.name}`);
});
const app = express();
app.use(express.json());
app.post("/mcp", async (req, res) => {
// Stateless: a fresh transport per request. Simple, and any container
// replica can serve the request — no session affinity needed.
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
res.on("close", () => transport.close());
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
});
app.listen(8000, () => console.log("MCP server listening on port 8000"));
StreamableHTTPServerTransport lives at @modelcontextprotocol/sdk/server/streamableHttp.js. Passing sessionIdGenerator: undefined runs it stateless — the right default behind a load balancer or multiple replicas. If tools depend on server-side session state across calls, generate a session ID instead and route by the Mcp-Session-Id header (see the SDK’s session docs).
Transport: use Streamable HTTP, not legacy SSE
The 2025-11-25 transports spec defines stdio and Streamable HTTP as the standard transports. HTTP+SSE was the standard remote transport in the older 2024-11-05 spec; the current spec documents it only for backwards compatibility, not as an equal third option for new servers.
| Layer | How you say “Streamable HTTP” |
|---|---|
| FastMCP Python code | mcp.run(transport="http", ...) |
| TypeScript SDK code | new StreamableHTTPServerTransport({...}) |
| MCPLambda CLI flag | --transport streamable-http |
mcplambda.yaml field | transport: streamable-http |
Same protocol, four different spellings depending on which layer you’re touching. Set the wrong one and the framework starts correctly but the platform proxies traffic to a transport your process never opens.
Deploying to MCPLambda via CLI
curl -fsSL https://mcplambda.io/mcpl/install.sh | sh
mcpl login
Published package (PyPI/npm):
mcpl deploy uvx://my-fastmcp-server --name db-tools
mcpl deploy npx://@your-org/my-mcp-server --name company-metrics
Your own repo, while you’re still iterating on it — this is the flow most custom servers actually use, since uvx:///npx:// expect an already-published package:
mcpl deploy https://github.com/your-org/my-fastmcp-server --branch main --name db-tools --transport streamable-http
Full flag reference: CLI docs.
Deploying via GitOps (mcplambda.yaml)
Connect the repo in the dashboard and every push to the selected branch triggers a build and deploy. Commit mcplambda.yaml at the repo root so builds don’t depend on flags typed at deploy time.
FastMCP (Python, pip):
run: python server.py
build:
strategy: pip
install_command: pip install -r requirements.txt
port: 8000
transport: streamable-http
TypeScript SDK (npm):
run: node dist/index.js
build:
strategy: npm
install_command: npm ci
build_command: npm run build
port: 8000
transport: streamable-http
Neither example needs a Dockerfile — pip and npm are auto-detected build strategies. Full field list and override rules: mcplambda.yaml docs.
Security checklist for Streamable HTTP
The transport spec is explicit here, not just a platform recommendation:
- MUST validate the
Originheader on incoming connections to mitigate DNS rebinding; reject with403on mismatch - SHOULD implement authentication for all connections — don’t ship an unauthenticated tool endpoint to the public internet
- Validate tool inputs server-side; don’t trust the client’s declared schema alone
- Keep secrets (
DATABASE_URL, API keys) out ofenv_varsinmcplambda.yaml— use MCPLambda project secrets, injected at deploy time instead
See Securing remote MCP servers for the full checklist.
What breaks in production
| Symptom | Likely cause | Fix |
|---|---|---|
| Deploy succeeds, client can’t connect | FastMCP host="127.0.0.1" (the default) inside a container | Bind host="0.0.0.0" |
| Works locally, 403s once deployed | No Origin/host validation, or a proxy rewriting it unexpectedly | Add host-header validation middleware; check MCPLambda’s Deployment URL, not localhost |
TypeError: run() got unexpected keyword | FastMCP before 2.0 with the new mcp.run(transport=..., host=..., port=...) call shape | Upgrade fastmcp |
Deployment shows sse in the platform but framework serves /mcp | --transport/yaml set to sse while code runs transport="http" / StreamableHTTPServerTransport | Match all four layers to streamable-http |
| One replica handles all traffic and others 404 | Stateful sessionIdGenerator without session-aware routing across replicas | Use stateless mode, or route by Mcp-Session-Id |
Related guides
Packaging for production · mcplambda.yaml reference · Transports explained · Securing remote servers
Once it’s running, add it to a Gateway to group it with other servers behind one URL, and check MCP Server Registry if you’re looking for pre-built tools instead.
Sources
- Transports — specification 2025-11-25 (current standard: stdio + Streamable HTTP)
- Transports — specification 2024-11-05 (legacy HTTP+SSE definition)
- FastMCP — running the server
- FastMCP — upgrading from FastMCP 2 (transport args moved to
run()) - MCP TypeScript SDK — server docs
- MCPLambda CLI reference · mcplambda.yaml · Deployment strategies