Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
dist
node_modules
package-lock.json
.DS_Store
46 changes: 46 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ options:
- `--endpoint`: If `server` is set to `sse` or `stream`, this option sets the endpoint path (default: `/sse` or `/mcp`)
- `--sseEndpoint`: Set the SSE endpoint path (default: `/sse`). Overrides `--endpoint` if `server` is set to `sse`.
- `--streamEndpoint`: Set the streamable HTTP endpoint path (default: `/mcp`). Overrides `--endpoint` if `server` is set to `stream`.
- `--stateless`: Enable stateless mode for HTTP streamable transport (no session management). In this mode, each request creates a new server instance instead of maintaining persistent sessions.
- `--port`: Specify the port to listen on (default: 8080)
- `--debug`: Enable debug logging
- `--shell`: Spawn the server via the user's shell
Expand All @@ -51,6 +52,37 @@ npx mcp-proxy --port 8080 my-command -v
npx mcp-proxy --port 8080 -- my-command -v
```

### Stateless Mode

By default, MCP Proxy maintains persistent sessions for HTTP streamable transport, where each client connection is associated with a server instance that stays alive for the duration of the session.

Stateless mode (`--stateless`) changes this behavior:

- **No session management**: Each request creates a new server instance instead of maintaining persistent sessions
- **Simplified deployment**: Useful for serverless environments or when you want to minimize memory usage
- **Request isolation**: Each request is completely independent, which can be beneficial for certain use cases

Example usage:

```bash
# Enable stateless mode
npx mcp-proxy --port 8080 --stateless tsx server.js

# Stateless mode with stream-only transport
npx mcp-proxy --port 8080 --stateless --server stream tsx server.js
```

> [!NOTE]
> Stateless mode only affects HTTP streamable transport (`/mcp` endpoint). SSE transport behavior remains unchanged.

**When to use stateless mode:**

- **Serverless environments**: When deploying to platforms like AWS Lambda, Vercel, or similar
- **Load balancing**: When requests need to be distributed across multiple instances
- **Memory optimization**: When you want to minimize server memory usage
- **Request isolation**: When you need complete independence between requests
- **Simple deployments**: When you don't need to maintain connection state

### Node.js SDK

The Node.js SDK provides several utilities that are used to create a proxy.
Expand Down Expand Up @@ -90,11 +122,25 @@ const { close } = await startHTTPServer({
},
eventStore: new InMemoryEventStore(),
port: 8080,
stateless: false, // Optional: enable stateless mode for streamable HTTP transport
});

close();
```

Options:

- `createServer`: Function that creates a new server instance for each connection
- `eventStore`: Event store for streamable HTTP transport (optional)
- `port`: Port number to listen on
- `host`: Host to bind to (default: "::")
- `sseEndpoint`: SSE endpoint path (default: "/sse", set to null to disable)
- `streamEndpoint`: Streamable HTTP endpoint path (default: "/mcp", set to null to disable)
- `stateless`: Enable stateless mode for HTTP streamable transport (default: false)
- `onConnect`: Callback when a server connects (optional)
- `onClose`: Callback when a server disconnects (optional)
- `onUnhandledRequest`: Callback for unhandled HTTP requests (optional)

#### `startStdioServer`

Starts a proxy that listens on a `stdio`, and sends messages to the attached `sse` or `streamable` server.
Expand Down
6 changes: 6 additions & 0 deletions src/bin/mcp-proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,11 @@ const argv = await yargs(hideBin(process.argv))
describe: "The SSE endpoint to listen on",
type: "string",
},
stateless: {
default: false,
describe: "Enable stateless mode for HTTP streamable transport (no session management)",
type: "boolean",
},
streamEndpoint: {
default: "/mcp",
describe: "The stream endpoint to listen on",
Expand Down Expand Up @@ -160,6 +165,7 @@ const proxy = async () => {
argv.server && argv.server !== "sse"
? null
: (argv.sseEndpoint ?? argv.endpoint),
stateless: argv.stateless,
streamEndpoint:
argv.server && argv.server !== "stream"
? null
Expand Down
89 changes: 89 additions & 0 deletions src/startHTTPServer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,3 +238,92 @@ it("proxies messages between SSE and stdio servers", async () => {

expect(onClose).toHaveBeenCalled();
});

it("supports stateless HTTP streamable transport", async () => {
const stdioTransport = new StdioClientTransport({
args: ["src/fixtures/simple-stdio-server.ts"],
command: "tsx",
});

const stdioClient = new Client(
{
name: "mcp-proxy",
version: "1.0.0",
},
{
capabilities: {},
},
);

await stdioClient.connect(stdioTransport);

const serverVersion = stdioClient.getServerVersion() as {
name: string;
version: string;
};

const serverCapabilities = stdioClient.getServerCapabilities() as {
capabilities: Record<string, unknown>;
};

const port = await getRandomPort();

const onConnect = vi.fn().mockResolvedValue(undefined);
const onClose = vi.fn().mockResolvedValue(undefined);

const httpServer = await startHTTPServer({
createServer: async () => {
const mcpServer = new Server(serverVersion, {
capabilities: serverCapabilities,
});

await proxyServer({
client: stdioClient,
server: mcpServer,
serverCapabilities,
});

return mcpServer;
},
onClose,
onConnect,
port,
stateless: true, // Enable stateless mode
});

// Create a stateless streamable HTTP client
const streamTransport = new StreamableHTTPClientTransport(
new URL(`http://localhost:${port}/mcp`),
);

const streamClient = new Client(
{
name: "stream-client-stateless",
version: "1.0.0",
},
{
capabilities: {},
},
);

await streamClient.connect(streamTransport);

// Test that we can still make requests in stateless mode
const result = await streamClient.listResources();
expect(result).toEqual({
resources: [
{
name: "Example Resource",
uri: "file:///example.txt",
},
],
});

await streamClient.close();
await httpServer.close();
await stdioClient.close();

expect(onConnect).toHaveBeenCalled();
// Note: in stateless mode, onClose behavior may differ since there's no persistent session
await delay(100);
});
Loading