From 4c855a841064a118022472b1ed576a9260a5ae40 Mon Sep 17 00:00:00 2001 From: Rohit Agarwal Date: Sat, 22 Nov 2025 02:37:04 +0530 Subject: [PATCH] Add guide: Converting STDIO MCP Servers to Streamable HTTP - Created comprehensive guide at guides/converting-stdio-to-streamable-http.mdx - Added guide to MCP Gateway section in docs.json navigation - Added contextual links from mcp-gateway.mdx, quickstart.mdx, and architecture.mdx - Added notes on firebase-mcp-server.mdx and postman-mcp-server.mdx pointing to conversion guide - Guide covers: STDIO vs HTTP transports, conversion steps with FastMCP/FastAPI/TypeScript examples, auth patterns, testing with Hoot, deployment, and troubleshooting - Updated GROUP_2_MIGRATION_PLAN.md --- docs.json | 3 +- .../converting-stdio-to-streamable-http.mdx | 834 ++++++++++++++++++ product/mcp-gateway.mdx | 2 + product/mcp-gateway/architecture.mdx | 4 + product/mcp-gateway/quickstart.mdx | 2 + 5 files changed, 844 insertions(+), 1 deletion(-) create mode 100644 guides/converting-stdio-to-streamable-http.mdx diff --git a/docs.json b/docs.json index 8eacbbf0..b6be5cf3 100644 --- a/docs.json +++ b/docs.json @@ -116,7 +116,8 @@ "product/mcp-gateway/architecture", "product/mcp-gateway/deployment", "product/mcp-gateway/mcp-hub", - "product/mcp-gateway/mcp-clients" + "product/mcp-gateway/mcp-clients", + "guides/converting-stdio-to-streamable-http" ] }, { diff --git a/guides/converting-stdio-to-streamable-http.mdx b/guides/converting-stdio-to-streamable-http.mdx new file mode 100644 index 00000000..ce8ff091 --- /dev/null +++ b/guides/converting-stdio-to-streamable-http.mdx @@ -0,0 +1,834 @@ +--- +title: 'Converting STDIO to Remote MCP Servers' +description: 'Step-by-step guide to converting local STDIO MCP servers to production-ready Streamable HTTP servers' +icon: 'globe' +--- + + +This guide covers converting STDIO MCP servers to Streamable HTTP, the current standard for remote MCP deployments (protocol version 2025-03-26). All code examples follow correct initialization patterns to avoid common errors. + + +## Why Convert to Remote? + + + + Host your server on any cloud platform and make it globally accessible + + + Handle multiple concurrent client connections simultaneously + + + Easier integration with web apps, mobile apps, and distributed systems + + + Deploy behind load balancers and scale as needed + + + +--- + +## Understanding MCP Transports + +### STDIO Transport + +**Best for:** Local development, single client + +```plaintext +Client spawns server as subprocess → stdin/stdout communication +``` + +**Pros:** Zero network overhead, simple setup +**Cons:** Same machine only, no multi-client support + +### Streamable HTTP (Recommended) + +**Best for:** Production, cloud hosting, multiple clients + +```plaintext +Server runs independently → Clients connect via HTTP +``` + +**Pros:** Single endpoint, bidirectional, optional sessions +**Cons:** Requires web server configuration + + +Streamable HTTP is the current standard (protocol version 2025-03-26). Use this for all new projects! + + +### SSE Transport (Legacy) + +**Status:** Superseded by Streamable HTTP + + +SSE is no longer the standard. Only use for backward compatibility with older clients. + + +--- + +## Prerequisites + + +```bash Python +# Check Python version (need 3.10+) +python --version + +# Install dependencies +pip install mcp fastapi uvicorn + +# Optional: FastMCP for rapid development +pip install fastmcp +``` + +```bash TypeScript +# Check Node version (need 18+) +node --version + +# Install dependencies +npm install @modelcontextprotocol/sdk express +npm install --save-dev @types/express +``` + + +--- + +## 1️⃣ Your Original STDIO Server + +Let's start with a typical STDIO server that runs locally: + + +```python Python +# stdio_server.py +import asyncio +from mcp.server import Server +from mcp.server.stdio import stdio_server +from mcp.types import Tool, TextContent + +server = Server("weather-server", version="1.0.0") + +@server.list_tools() +async def list_tools() -> list[Tool]: + return [ + Tool( + name="get_weather", + description="Get weather for a location", + inputSchema={ + "type": "object", + "properties": { + "location": {"type": "string"} + }, + "required": ["location"] + } + ) + ] + +@server.call_tool() +async def call_tool(name: str, arguments: dict) -> list[TextContent]: + if name == "get_weather": + location = arguments.get("location", "Unknown") + return [TextContent( + type="text", + text=f"Weather in {location}: Sunny, 72°F" + )] + raise ValueError(f"Unknown tool: {name}") + +async def main(): + # STDIO transport - runs as subprocess + async with stdio_server() as (read_stream, write_stream): + await server.run( + read_stream, + write_stream, + server.create_initialization_options() + ) + +if __name__ == "__main__": + asyncio.run(main()) +``` + +```typescript TypeScript +// stdio_server.ts +import { Server } from "@modelcontextprotocol/sdk/server/index.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { + CallToolRequestSchema, + ListToolsRequestSchema, +} from "@modelcontextprotocol/sdk/types.js"; + +const server = new Server( + { name: "weather-server", version: "1.0.0" }, + { capabilities: { tools: {} } } +); + +server.setRequestHandler(ListToolsRequestSchema, async () => ({ + tools: [ + { + name: "get_weather", + description: "Get weather for a location", + inputSchema: { + type: "object", + properties: { + location: { type: "string" }, + }, + required: ["location"], + }, + }, + ], +})); + +server.setRequestHandler(CallToolRequestSchema, async (request) => { + if (request.params.name === "get_weather") { + const location = request.params.arguments?.location || "Unknown"; + return { + content: [ + { type: "text", text: `Weather in ${location}: Sunny, 72°F` }, + ], + }; + } + throw new Error(`Unknown tool: ${request.params.name}`); +}); + +async function main() { + const transport = new StdioServerTransport(); + await server.connect(transport); +} + +main(); +``` + + +--- + +## 2️⃣ Convert to Streamable HTTP + + +```python FastMCP expandable +# http_server.py +from fastmcp import FastMCP + +# Create MCP server at startup // [!code highlight] +mcp = FastMCP("weather-server") // [!code highlight] + +# Define your tool (same logic as before!) +@mcp.tool() +def get_weather(location: str) -> str: + """Get weather for a location.""" + return f"Weather in {location}: Sunny, 72°F" + +if __name__ == "__main__": + # FastMCP handles transport initialization // [!code highlight] + mcp.run( // [!code highlight] + transport="http", // [!code highlight] + host="0.0.0.0", // [!code highlight] + port=8000, // [!code highlight] + path="/mcp" // [!code highlight] + ) // [!code highlight] +``` + +```python FastAPI expandable +# http_server_fastapi.py +import contextlib +from fastapi import FastAPI +from fastmcp import FastMCP + +# Create MCP server at startup // [!code highlight] +mcp = FastMCP("weather-server", stateless_http=True) // [!code highlight] + +@mcp.tool() +def get_weather(location: str) -> str: + """Get weather for a location.""" + return f"Weather in {location}: Sunny, 72°F" + +# Lifespan manager initializes MCP // [!code highlight] +@contextlib.asynccontextmanager // [!code highlight] +async def lifespan(app: FastAPI): // [!code highlight] + async with contextlib.AsyncExitStack() as stack: // [!code highlight] + await stack.enter_async_context(mcp.session_manager.run()) // [!code highlight] + yield // [!code highlight] + +# Create FastAPI app with lifespan // [!code highlight] +app = FastAPI(lifespan=lifespan) // [!code highlight] + +# Mount MCP server at /weather endpoint +app.mount("/weather", mcp.streamable_http_app()) + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8000) +``` + +```typescript TypeScript expandable +// http_server.ts +import express from "express"; +import { Server } from "@modelcontextprotocol/sdk/server/index.js"; +import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import { + CallToolRequestSchema, + ListToolsRequestSchema, +} from "@modelcontextprotocol/sdk/types.js"; + +const app = express(); +app.use(express.json()); + +// Create MCP server at startup // [!code highlight] +const server = new Server( // [!code highlight] + { name: "weather-server", version: "1.0.0" }, // [!code highlight] + { capabilities: { tools: {} } } // [!code highlight] +); // [!code highlight] + +// Register handlers +server.setRequestHandler(ListToolsRequestSchema, async () => ({ + tools: [ + { + name: "get_weather", + description: "Get weather for a location", + inputSchema: { + type: "object", + properties: { location: { type: "string" } }, + required: ["location"], + }, + }, + ], +})); + +server.setRequestHandler(CallToolRequestSchema, async (request) => { + if (request.params.name === "get_weather") { + const location = request.params.arguments?.location || "Unknown"; + return { + content: [ + { type: "text", text: `Weather in ${location}: Sunny, 72°F` }, + ], + }; + } + throw new Error(`Unknown tool: ${request.params.name}`); +}); + +// Create transport at startup // [!code highlight] +const transport = new StreamableHTTPServerTransport({ // [!code highlight] + path: "/mcp", // [!code highlight] +}); // [!code highlight] + +// Initialize server with transport // [!code highlight] +async function initializeServer() { // [!code highlight] + await server.connect(transport); // [!code highlight] + console.log("✅ MCP server initialized"); // [!code highlight] +} // [!code highlight] + +// Register transport handler +app.use("/mcp", (req, res) => transport.handleRequest(req, res)); + +// Start server +const PORT = 8000; +app.listen(PORT, async () => { + await initializeServer(); + console.log(`🚀 Server running on http://0.0.0.0:${PORT}/mcp`); +}); +``` + + + +**FastMCP vs FastAPI:** FastMCP provides a simpler API for quick setups. Use FastAPI when integrating MCP into existing FastAPI applications or when you need more control over the web server configuration. + + +--- + +## 3️⃣ Add auth + +Most STDIO servers use environment variables for authentication. Convert these to HTTP-based auth patterns for remote servers. + +### Example: OAuth Credentials Pattern + +**STDIO Version** (environment variables): + +```json Claude Desktop Config +{ + "mcpServers": { + "google-calendar": { + "command": "npx", + "args": ["@cocal/google-calendar-mcp"], + "env": { + "GOOGLE_OAUTH_CREDENTIALS": "/path/to/gcp-oauth.keys.json" + } + } + } +} +``` + +**Remote Version** (request headers): + + +```python Python +from fastapi import Header, HTTPException, Depends +import base64 +import json + +def get_credentials(authorization: str = Header(None)) -> dict: + """Extract credentials from Authorization header.""" + if not authorization or not authorization.startswith("Bearer "): + raise HTTPException(status_code=401, detail="Invalid auth") + + token = authorization.replace("Bearer ", "") + try: + return json.loads(base64.b64decode(token)) + except Exception: + raise HTTPException(status_code=401, detail="Invalid token") + +@app.post("/mcp") +async def handle_mcp( + request: Request, + credentials: dict = Depends(get_credentials) +): + # Use credentials from request + pass +``` + +```typescript TypeScript +function extractCredentials(authHeader: string | undefined): any { + if (!authHeader || !authHeader.startsWith("Bearer ")) { + throw new Error("Invalid authorization"); + } + + const token = authHeader.replace("Bearer ", ""); + try { + return JSON.parse( + Buffer.from(token, "base64").toString("utf-8") + ); + } catch (error) { + throw new Error("Invalid token"); + } +} + +const authenticateMiddleware = (req: any, res: any, next: any) => { + try { + req.credentials = extractCredentials(req.headers.authorization); + next(); + } catch (error) { + res.status(401).json({ error: error.message }); + } +}; + +app.use("/mcp", authenticateMiddleware); +``` + + +### Simpler Pattern: API Keys + +For basic authentication, use API keys: + + +```python Python +from fastapi import Header, HTTPException, Depends +import os + +async def verify_api_key(authorization: str = Header(None)): + """Verify API key from header.""" + if not authorization: + raise HTTPException(status_code=401, detail="Missing API key") + + api_key = authorization.replace("Bearer ", "") + if api_key != os.getenv("API_KEY"): + raise HTTPException(status_code=401, detail="Invalid API key") + + return api_key + +@app.post("/mcp") +async def handle_mcp( + request: Request, + api_key: str = Depends(verify_api_key) +): + # Request is authenticated + pass +``` + +```typescript TypeScript +const authenticateApiKey = (req: any, res: any, next: any) => { + const authHeader = req.headers["authorization"]; + if (!authHeader) { + return res.status(401).json({ error: "Missing API key" }); + } + + const apiKey = authHeader.replace("Bearer ", ""); + if (apiKey !== process.env.API_KEY) { + return res.status(401).json({ error: "Invalid API key" }); + } + + next(); +}; + +app.use("/mcp", authenticateApiKey); +``` + + +--- + +## 4️⃣ Run Your MCP Server + +Start your converted server: + + +```bash FastMCP +python http_server.py +# Server runs at http://localhost:8000/mcp +``` + +```bash FastAPI +python http_server_fastapi.py +# Server runs at http://localhost:8000/weather/mcp +``` + +```bash TypeScript +ts-node http_server.ts +# Server runs at http://localhost:8000/mcp +``` + + +--- + +## 5️⃣ Testing with Hoot 🦉 + + + Like Postman, but specifically designed for testing MCP servers. Perfect for development! + + +### Quick Start + +```bash Install & Run +# Run directly (no installation needed!) +npx -y @portkey-ai/hoot + +# Or install globally +npm install -g @portkey-ai/hoot +hoot +``` + + +Hoot opens at `http://localhost:8009` + + +### Using Hoot + + + + ```bash + python http_server.py + # Server runs at http://localhost:8000/mcp + ``` + + + + Navigate to `http://localhost:8009` + + + + - Paste URL: `http://localhost:8000/mcp` + - Hoot auto-detects the transport type! + + + + - View all available tools + - Select `get_weather` + - Add parameters: `{"location": "San Francisco"}` + - Click "Execute" + - See the response! + + + +### Hoot Features + + + + Automatically detects HTTP vs SSE + + + View and test all server tools + + + Handles OAuth 2.1 authentication + + + 8 themes with light & dark modes + + + +--- + +## Optional: Session Management + + +Session management is optional in the MCP spec. FastMCP handles it automatically if you need stateful interactions. + + + +```python FastMCP +# FastMCP handles sessions automatically + +# Stateful mode (maintains session state) +mcp = FastMCP("weather-server", stateless_http=False) + +# Stateless mode (no session state) +mcp = FastMCP("weather-server", stateless_http=True) +``` + +```python FastAPI +# Manual session management with FastAPI +from fastmcp import FastMCP + +mcp = FastMCP("weather-server", stateless_http=True) + +@contextlib.asynccontextmanager +async def lifespan(app: FastAPI): + async with mcp.session_manager.run(): + yield + +app = FastAPI(lifespan=lifespan) +``` + +```typescript TypeScript +import { randomUUID } from "crypto"; + +const transports = new Map(); + +async function getOrCreateTransport( + sessionId: string | undefined, + isInitialize: boolean +): Promise { + + if (sessionId && transports.has(sessionId)) { + return transports.get(sessionId)!; + } + + if (!sessionId && isInitialize) { + const transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: () => randomUUID(), + }); + + transport.onSessionInitialized = (newSessionId) => { + transports.set(newSessionId, transport); + }; + + await server.connect(transport); + return transport; + } + + throw new Error("Invalid session"); +} + +app.post("/mcp", async (req, res) => { + const sessionId = req.headers["mcp-session-id"] as string | undefined; + const isInitialize = req.body?.method === "initialize"; + + try { + const transport = await getOrCreateTransport(sessionId, isInitialize); + await transport.handleRequest(req, res); + } catch (error) { + res.status(400).json({ error: error.message }); + } +}); +``` + + +--- + +## Optional: CORS Configuration + + +Only add CORS if you need to support browser-based clients. For server-to-server communication, CORS isn't necessary. + + + +```python FastMCP +# CORS with FastMCP +mcp.run( + transport="http", + host="0.0.0.0", + port=8000, + cors_allow_origins=["https://yourdomain.com"] +) +``` + +```python FastAPI +from fastapi.middleware.cors import CORSMiddleware + +app.add_middleware( + CORSMiddleware, + allow_origins=["https://yourdomain.com"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + expose_headers=["Mcp-Session-Id"], +) +``` + +```typescript TypeScript +import cors from "cors"; + +app.use(cors({ + origin: ["https://yourdomain.com"], + credentials: true, + exposedHeaders: ["Mcp-Session-Id"], +})); +``` + + +--- + +## Deployment + + + + Containerize for any platform + + + Deploy in seconds + + + Serverless on GCP + + + +### Docker + + +```dockerfile Python +FROM python:3.11-slim +WORKDIR /app +COPY requirements.txt . +RUN pip install -r requirements.txt +COPY . . +EXPOSE 8000 +CMD ["python", "http_server.py"] +``` + +```dockerfile TypeScript +FROM node:18-alpine +WORKDIR /app +COPY package*.json ./ +RUN npm ci --production +COPY . . +RUN npm run build +EXPOSE 8000 +CMD ["node", "dist/http_server.js"] +``` + + +### Quick Deploy + + +```bash Fly.io +# Install Fly CLI +curl -L https://fly.io/install.sh | sh + +# Deploy +fly launch +fly deploy +``` + +```bash Cloud Run +gcloud run deploy mcp-server \ + --source . \ + --platform managed \ + --region us-central1 \ + --allow-unauthenticated +``` + + +--- + +## Troubleshooting + + + + **Check:** + - Server is running on the correct port + - Firewall allows connections + - URL is correct (including `/mcp` path) + + **Test with curl:** + ```bash + curl -X POST http://localhost:8000/mcp \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' + ``` + + + + **Solution:** Ensure tool handlers are registered before the server starts + + ```python Correct Order + # Register handlers FIRST + @mcp.tool() + def my_tool(): + pass + + # THEN run server + mcp.run(transport="http") + ``` + + + + **Solution:** Client must store and send session ID correctly + + ```python Session Handling + # Extract from initialization response + session_id = response.headers.get("Mcp-Session-Id") + + # Include in all subsequent requests + headers = {"Mcp-Session-Id": session_id} + ``` + + + +--- + +## Summary + + +**You've successfully converted your STDIO server to a remote Streamable HTTP server!** + + +### Key Principles + + + + Replace STDIO with Streamable HTTP for remote access + + + Convert environment variables to HTTP headers + + + Server and transport created once at startup + + + Use Hoot to verify all tools work correctly + + + +### What We Covered + +1. ✅ Original STDIO server structure +2. ✅ Converting to Streamable HTTP +3. ✅ Auth conversion from env vars to headers +4. ✅ Running your converted server +5. ✅ Testing with Hoot + +### Resources + + + + Official protocol documentation + + + Examples and source code + + + Examples and source code + + + Test your MCP servers + + + High-level Python framework + + + + +**Building something cool?** Share it with the MCP community and let us know how this guide helped! + \ No newline at end of file diff --git a/product/mcp-gateway.mdx b/product/mcp-gateway.mdx index 17507618..78d6af30 100644 --- a/product/mcp-gateway.mdx +++ b/product/mcp-gateway.mdx @@ -10,6 +10,8 @@ MCP Gateway is a universal adapter for the Model Context Protocol ecosystem. It **Remote MCP servers only**: Portkey's MCP Gateway exclusively supports remote MCP servers over HTTP/SSE transport protocols. Local STDIO-based MCP servers are not supported. + +If you have a STDIO-based MCP server, see our guide on [Converting STDIO MCP Servers to Streamable HTTP](/guides/converting-stdio-to-streamable-http). diff --git a/product/mcp-gateway/architecture.mdx b/product/mcp-gateway/architecture.mdx index d8eed97d..b9b556fe 100644 --- a/product/mcp-gateway/architecture.mdx +++ b/product/mcp-gateway/architecture.mdx @@ -11,6 +11,10 @@ title: Architecture + +The MCP Gateway supports remote MCP servers using HTTP/SSE transport protocols (StreamableHTTP and SSE). If you have a local STDIO-based MCP server that you want to use with the gateway, see our guide on [Converting STDIO MCP Servers to Streamable HTTP](/guides/converting-stdio-to-streamable-http). + + ## Security Architecture The gateway implements defense-in-depth security: diff --git a/product/mcp-gateway/quickstart.mdx b/product/mcp-gateway/quickstart.mdx index 1aa49397..12896764 100644 --- a/product/mcp-gateway/quickstart.mdx +++ b/product/mcp-gateway/quickstart.mdx @@ -8,6 +8,8 @@ MCP servers connect to the private hub through the UI or API. The hub enables sh **Remote servers only**: Portkey exclusively supports remote MCP servers over HTTP/SSE transport protocols. Local STDIO-based MCP servers are not supported. + +If you have a STDIO-based MCP server, see our guide on [Converting STDIO MCP Servers to Streamable HTTP](/guides/converting-stdio-to-streamable-http). Portkey supports remote MCP servers via StreamableHTTP and SSE transports. StreamableHTTP servers are recommended—SSE will be deprecated in the future.