> ## Documentation Index
> Fetch the complete documentation index at: https://docs.portkey.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Converting STDIO to Remote MCP Servers

> Step-by-step guide to converting local STDIO MCP servers to production-ready Streamable HTTP servers

> [!NOTE]
>
> 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?

> [card] Cloud Deployment
>
> Host your server on any cloud platform and make it globally accessible

> [card] Multi-Client Support
>
> Handle multiple concurrent client connections simultaneously

> [card] Better Integration
>
> Easier integration with web apps, mobile apps, and distributed systems

> [card] Horizontal Scaling
>
> Deploy behind load balancers and scale as needed

***

## Understanding MCP Transports

### STDIO Transport

**Best for:** Local development, single client

```plaintext theme={"system"}
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 theme={"system"}
Server runs independently → Clients connect via HTTP
```

**Pros:** Single endpoint, bidirectional, optional sessions\
**Cons:** Requires web server configuration

> [!TIP]
>
> 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

> [!WARNING]
>
> SSE is no longer the standard. Only use for backward compatibility with older clients.

***

## Prerequisites

```bash Python theme={"system"}
# 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 theme={"system"}
# 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 theme={"system"}
# 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 theme={"system"}
// 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 theme={"system"}
# 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 theme={"system"}
# 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 theme={"system"}
// 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`);
});
```

> [!NOTE]
>
> **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 theme={"system"}
{
  "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 theme={"system"}
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 theme={"system"}
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 theme={"system"}
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 theme={"system"}
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 theme={"system"}
python http_server.py
# Server runs at http://localhost:8000/mcp
```

```bash FastAPI theme={"system"}
python http_server_fastapi.py
# Server runs at http://localhost:8000/weather/mcp
```

```bash TypeScript theme={"system"}
ts-node http_server.ts
# Server runs at http://localhost:8000/mcp
```

***

## 5️⃣ Testing with Hoot 🦉

> [card] [Hoot - MCP Testing Tool](https://github.com/Portkey-AI/hoot)
>
> Like Postman, but specifically designed for testing MCP servers. Perfect for development!

### Quick Start

```bash Install & Run theme={"system"}
# Run directly (no installation needed!)
npx -y @portkey-ai/hoot

# Or install globally
npm install -g @portkey-ai/hoot
hoot
```

> [!NOTE]
>
> Hoot opens at `http://localhost:8009`

### Using Hoot

**1. Start your server**

```bash theme={"system"}
python http_server.py
# Server runs at http://localhost:8000/mcp
```

**2. Open Hoot**

Navigate to `http://localhost:8009`

**3. Connect to your server**

* Paste URL: `http://localhost:8000/mcp`
* Hoot auto-detects the transport type!

**4. Test your tools**

* View all available tools
* Select `get_weather`
* Add parameters: `{"location": "San Francisco"}`
* Click "Execute"
* See the response!

### Hoot Features

> [card] Auto-Detection
>
> Automatically detects HTTP vs SSE

> [card] Tool Explorer
>
> View and test all server tools

> [card] OAuth Support
>
> Handles OAuth 2.1 authentication

> [card] Beautiful Themes
>
> 8 themes with light & dark modes

***

## Optional: Session Management

> [!NOTE]
>
> Session management is optional in the MCP spec. FastMCP handles it automatically if you need stateful interactions.

```python FastMCP theme={"system"}
# 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 theme={"system"}
# 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 theme={"system"}
import { randomUUID } from "crypto";

const transports = new Map<string, StreamableHTTPServerTransport>();

async function getOrCreateTransport(
  sessionId: string | undefined,
  isInitialize: boolean
): Promise<StreamableHTTPServerTransport> {
  
  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

> [!WARNING]
>
> Only add CORS if you need to support browser-based clients. For server-to-server communication, CORS isn't necessary.

```python FastMCP theme={"system"}
# CORS with FastMCP
mcp.run(
    transport="http",
    host="0.0.0.0",
    port=8000,
    cors_allow_origins=["https://yourdomain.com"]
)
```

```python FastAPI theme={"system"}
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 theme={"system"}
import cors from "cors";

app.use(cors({
  origin: ["https://yourdomain.com"],
  credentials: true,
  exposedHeaders: ["Mcp-Session-Id"],
}));
```

***

## Deployment

> [card] [Docker](#docker)
>
> Containerize for any platform

> [card] [Fly.io](#flyio)
>
> Deploy in seconds

> [card] [Cloud Run](#cloud-run)
>
> Serverless on GCP

### Docker

```dockerfile Python theme={"system"}
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 theme={"system"}
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 theme={"system"}
# Install Fly CLI
curl -L https://fly.io/install.sh | sh

# Deploy
fly launch
fly deploy
```

```bash Cloud Run theme={"system"}
gcloud run deploy mcp-server \
  --source . \
  --platform managed \
  --region us-central1 \
  --allow-unauthenticated
```

***

## Troubleshooting

> [accordion] Can't connect to server
>
> **Check:**
>
> * Server is running on the correct port
> * Firewall allows connections
> * URL is correct (including `/mcp` path)
>
> **Test with curl:**
>
> ```bash theme={"system"}
> curl -X POST http://localhost:8000/mcp \
>   -H "Content-Type: application/json" \
>   -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}'
> ```

> [accordion] Tools not showing up
>
> **Solution:** Ensure tool handlers are registered before the server starts
>
> ```python Correct Order theme={"system"}
> # Register handlers FIRST
> @mcp.tool()
> def my_tool():
>     pass
>
> # THEN run server
> mcp.run(transport="http")
> ```

> [accordion] Session errors
>
> **Solution:** Client must store and send session ID correctly
>
> ```python Session Handling theme={"system"}
> # Extract from initialization response
> session_id = response.headers.get("Mcp-Session-Id")
>
> # Include in all subsequent requests
> headers = {"Mcp-Session-Id": session_id}
> ```

***

## Summary

> [!TIP]
>
> **You've successfully converted your STDIO server to a remote Streamable HTTP server!**

### Key Principles

> [card] Use HTTP Transport
>
> Replace STDIO with Streamable HTTP for remote access

> [card] Header-Based Auth
>
> Convert environment variables to HTTP headers

> [card] Initialize at Startup
>
> Server and transport created once at startup

> [card] Test Thoroughly
>
> 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

> [card] [MCP Specification](https://modelcontextprotocol.io/specification)
>
> Official protocol documentation

> [card] [Python SDK](https://github.com/modelcontextprotocol/python-sdk)
>
> Examples and source code

> [card] [TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk)
>
> Examples and source code

> [card] [Hoot Testing Tool](https://github.com/Portkey-AI/hoot)
>
> Test your MCP servers

> [card] [FastMCP](https://github.com/jlowin/fastmcp)
>
> High-level Python framework

> [!TIP]
>
> **Building something cool?** Share it with the MCP community and let us know how this guide helped!

***

> [card] [Portkey is now PRISMA AIRS AI Gateway. See it in action.](https://www.paloaltonetworks.in/ai-security/ai-gateway?utm_source=portkey\&utm_medium=referral\&utm_campaign=prisma_airs\&utm_content=docs_nav#contact)
>
> Contact Us


## Related topics

- [Atlassian Remote MCP server](https://docs.portkey.ai/docs/integrations/mcp-servers/atlassian-mcp-server.md)
- [May](https://docs.portkey.ai/docs/changelog/2025/may.md)
- [Using Private MCP Servers with Responses API](https://docs.portkey.ai/docs/guides/use-cases/private-mcp-servers.md)
- [MCP Registry](https://docs.portkey.ai/docs/product/mcp-gateway/mcp-registry.md)
- [Remote MCP](https://docs.portkey.ai/docs/product/ai-gateway/remote-mcp.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.