# @tmcp/transport-sse

A Server-Sent Events (SSE) transport implementation for TMCP (TypeScript Model Context Protocol) servers. This package provides SSE-based communication for MCP servers, enabling efficient real-time bidirectional communication between clients and servers through standard HTTP with Server-Sent Events.

## Installation

```bash
pnpm add @tmcp/transport-sse tmcp
```

## Usage

### Basic Setup

```javascript
import { McpServer } from 'tmcp';
import { SseTransport } from '@tmcp/transport-sse';

// Create your MCP server
const server = new McpServer(
	{
		name: 'my-sse-server',
		version: '1.0.0',
		description: 'My SSE MCP server',
	},
	{
		adapter: new YourSchemaAdapter(),
		capabilities: {
			tools: { listChanged: true },
			prompts: { listChanged: true },
			resources: { listChanged: true },
		},
	},
);

// Add your tools, prompts, and resources
server.tool(
	{
		name: 'example_tool',
		description: 'An example tool',
	},
	async () => {
		return {
			content: [{ type: 'text', text: 'Hello from SSE!' }],
		};
	},
);

// Create the SSE transport (defaults to '/sse' path for events, '/message' for messages)
const transport = new SseTransport(server);

// Use with your preferred HTTP server
// Example with Node.js built-in server:
import * as http from 'node:http';
import { createRequestListener } from '@remix-run/node-fetch-server';

const httpServer = http.createServer((request)=>{
	const response = await transport.respond(request);
	if(response){
		return response;
	}
	return new Response(null, { status: 404 });
}));

httpServer.listen(3000, () => {
	console.log('MCP SSE server listening on port 3000');
});
```

### With Custom Configuration

```javascript
const transport = new SseTransport(server, {
	// Custom SSE endpoint path (default: '/sse', use null to respond on every path)
	path: '/api/events',
	// Custom message endpoint path (default: '/message')
	endpoint: '/api/message',
	// Custom session ID generation
	getSessionId: () => {
		return `session-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
	},
	oauth: OAuth; // an oauth provider generated from @tmcp/auth
});

> [!NOTE]
> In development you'll see a warning when the `path` option is omitted. Upcoming releases will interpret an `undefined` path as "respond on every path", so set the field explicitly (for example `path: '/sse'` or `path: null`) to keep the behaviour you expect.
```

### With Custom Context

You can pass custom context data to your MCP server for each request. This is useful for authentication, user information, database connections, etc.

```javascript
// Define your custom context type
interface MyContext {
    userId: string;
    permissions: string[];
    database: DatabaseConnection;
}

// Create server with custom context
const server = new McpServer(serverInfo, options).withContext<MyContext>();

server.tool(
    {
        name: 'get-user-profile',
        description: 'Get the current user profile',
    },
    async () => {
        // Access custom context in your handler
        const { userId, database } = server.ctx.custom!;
        const profile = await database.users.findById(userId);

        return {
            content: [
                { type: 'text', text: `User profile: ${JSON.stringify(profile)}` }
            ],
        };
    },
);

// Create transport (it will be typed to accept your custom context)
const transport = new SseTransport(server);

// Create transport (it will be typed to accept your custom context)
const transport = new HttpTransport(server);

// then in the handler
const response = await transport.respond(req, {
	userId,
	permissions,
	database: req.locals.db,
});
```

### Session Management

The SSE transport supports custom session managers for different deployment scenarios:

#### In-Memory Sessions (Default)

```javascript
import {
	InMemoryStreamSessionManager,
	InMemoryInfoSessionManager,
} from '@tmcp/session-manager';

const transport = new SseTransport(server, {
	sessionManager: {
		streams: new InMemoryStreamSessionManager(),
		info: new InMemoryInfoSessionManager(),
	},
});
```

#### Redis Sessions (Multi-Server/Serverless)

For deployments across multiple servers or serverless environments where sessions need to be shared:

```javascript
import {
	RedisStreamSessionManager,
	RedisInfoSessionManager,
} from '@tmcp/session-manager-redis';

const transport = new SseTransport(server, {
	sessionManager: {
		streams: new RedisStreamSessionManager('redis://localhost:6379'),
		info: new RedisInfoSessionManager('redis://localhost:6379'),
	},
});
```

**When to use Redis sessions:**

- **Multi-server deployments**: When your application runs on multiple servers and clients might connect to different instances
- **Serverless deployments**: When your transport is deployed on serverless platforms where instances are ephemeral (attention, serverless environment generally kills SSE request after a not-so-long amount of time, it's generally preferable to use streaming-http)
- **Load balancing**: When using load balancers that might route requests to different server instances

No matter which backend you choose, the transport stores client capabilities, client info, and log level so you can access them later via `server.ctx.sessionInfo`.

## API

### `SseTransport`

#### Constructor

```typescript
new SseTransport(server: McpServer, options?: SseTransportOptions)
```

Creates a new SSE transport instance.

**Parameters:**

- `server` - A TMCP server instance to handle incoming requests
- `options` - Optional configuration for the transport

**Options:**

```typescript
interface SseTransportOptions {
	getSessionId?: () => string; // Custom session ID generator
	path?: string | null; // SSE endpoint path (default: '/sse', null responds on every path)
	endpoint?: string; // Message endpoint path (default: '/message')
	oauth?: OAuth; // an oauth provider generated from @tmcp/auth
	sessionManager?: {
		streams?: StreamSessionManager;
		info?: InfoSessionManager;
	}; // Provide custom managers; defaults to in-memory implementations
}
```

By default the transport instantiates the in-memory managers. You can mix different backends (for example, Durable Objects for streaming with Redis for metadata) by supplying only the field you want to override.

#### Methods

##### `respond(request: Request, customContext?: T): Promise<Response | null>`

Processes an HTTP request and returns a Response with Server-Sent Events, or null if the request path doesn't match the configured SSE paths.

**Parameters:**

- `request` - A Web API Request object containing the JSON-RPC message
- `customContext` - Optional custom context data to pass to the MCP server for this request

**Returns:**

- A Response object with SSE stream for ongoing communication, or null if the request path doesn't match the SSE endpoints

**HTTP Methods:**

- **GET**: Establishes SSE connection and returns event stream with endpoint information
- **POST**: Processes MCP messages and sends responses through the SSE stream
- **DELETE**: Disconnects sessions and cleans up resources
- **OPTIONS**: Handles CORS preflight requests

##### `close(): void`

Closes all active SSE sessions and cleans up resources.

## Protocol Details

### HTTP Methods

The transport supports four HTTP methods across two endpoints:

#### GET - SSE Event Stream (path: `/sse`)

Establishes a Server-Sent Events connection:

```http
GET /sse?session_id=optional-session-id HTTP/1.1
mcp-session-id: optional-session-id
```

Response: Long-lived SSE stream with endpoint information:

```http
HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
Access-Control-Allow-Origin: *
mcp-session-id: generated-or-provided-session-id

event: endpoint
data: /message?session_id=session-id

data: {"jsonrpc":"2.0","method":"notifications/initialized","params":{}}

```

#### POST - Message Processing (path: `/message`)

Clients send JSON-RPC messages via HTTP POST requests:

```http
POST /message?session_id=session-id HTTP/1.1
Content-Type: application/json
mcp-session-id: session-id

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/list",
  "params": {}
}
```

Response: Acknowledgment (response sent through SSE stream):

```http
HTTP/1.1 202 Accepted
Content-Type: application/json
mcp-session-id: session-id
```

#### DELETE - Session Disconnect

Disconnects a session and cleans up resources:

```http
DELETE /sse?session_id=session-id HTTP/1.1
mcp-session-id: session-to-disconnect
```

Response:

```http
HTTP/1.1 204 No Content
mcp-session-id: session-to-disconnect
```

### Session Management

- **Session ID**: Can be provided via query parameter or `mcp-session-id` header
- **Automatic Generation**: If no session ID is provided, one is generated automatically
- **Session Persistence**: Sessions persist across multiple requests until the client disconnects
- **Server Notifications**: Server can send notifications to active sessions through SSE

## Framework Examples

### Bun

```javascript
import { McpServer } from 'tmcp';
import { SseTransport } from '@tmcp/transport-sse';

const server = new McpServer(/* ... */);
const transport = new SseTransport(server);

Bun.serve({
	port: 3000,
	async fetch(req) {
		const response = await transport.respond(req);
		if (response === null) {
			return new Response('Not Found', { status: 404 });
		}
		return response;
	},
});
```

### Deno

```javascript
import { McpServer } from 'tmcp';
import { SseTransport } from '@tmcp/transport-sse';

const server = new McpServer(/* ... */);
const transport = new SseTransport(server);

Deno.serve({ port: 3000 }, async (req) => {
	const response = await transport.respond(req);
	if (response === null) {
		return new Response('Not Found', { status: 404 });
	}
	return response;
});
```

### `srvx`

If you want the same experience across Deno, Bun, and Node.js, you can use [srvx](https://srvx.h3.dev/).

```js
import { McpServer } from 'tmcp';
import { SseTransport } from '@tmcp/transport-sse';
import { serve } from 'srvx';

const server = new McpServer(/* ... */);
const transport = new SseTransport(server);

serve({
	async fetch(req) {
		const response = await transport.respond(req);
		if (response === null) {
			return new Response('Not Found', { status: 404 });
		}
		return response;
	},
});
```

## Error Handling

The transport includes comprehensive error handling:

- **Malformed JSON**: Invalid JSON requests return appropriate error responses
- **Content-Type Validation**: Ensures proper `application/json` content type
- **Session Management**: Automatic cleanup of disconnected sessions
- **CORS Handling**: Built-in CORS support for cross-origin requests
- **Server Errors**: Server processing errors are propagated to clients

## Development

```bash
# Install dependencies
pnpm install

# Generate TypeScript declarations
pnpm generate:types

# Lint the code
pnpm lint
```

## Requirements

- Node.js 16+ (for native ES modules and Web API support)
- A TMCP server instance
- An HTTP server framework or runtime
- A schema adapter (Zod, Valibot, etc.)

## Related Packages

- [`tmcp`](../tmcp) - Core TMCP server implementation
- [`@tmcp/transport-http`](../transport-http) - HTTP transport with SSE streaming
- [`@tmcp/transport-stdio`](../transport-stdio) - Standard I/O transport
- [`@tmcp/adapter-zod`](../adapter-zod) - Zod schema adapter
- [`@tmcp/adapter-valibot`](../adapter-valibot) - Valibot schema adapter

## Acknowledgments

Huge thanks to Sean O'Bannon that provided us with the `@tmcp` scope on npm.

## License

MIT
