npm.io
0.9.0 • Published 1 month ago

@tmcp/transport-http

Licence
MIT
Version
0.9.0
Deps
2
Size
57 kB
Vulns
0
Weekly
0
Stars
203

@tmcp/transport-http

An HTTP transport implementation for TMCP (TypeScript Model Context Protocol) servers. It supports both initialization-based MCP sessions and the sessionless per-request protocol introduced in MCP 2026-07-28.

Installation

pnpm add @tmcp/transport-http tmcp

Usage

Basic Setup
import { McpServer } from 'tmcp';
import { HttpTransport } from '@tmcp/transport-http';

// Create your MCP server
const server = new McpServer(
	{
		name: 'my-http-server',
		version: '1.0.0',
		description: 'My HTTP 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 HTTP!' }],
		};
	},
);

// Create the HTTP transport (defaults to '/mcp' path)
const transport = new HttpTransport(server);

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

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

httpServer.listen(3000, () => {
	console.log('MCP HTTP server listening on port 3000');
});
With Custom Configuration
const transport = new HttpTransport(server, {
	// Custom MCP endpoint path (default: '/mcp', use null to respond on every path)
	path: '/api/mcp',
	// Custom session ID generation for initialization-based clients
	getSessionId: () => {
		return `session-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
	},
});

When the transport runs in development mode and you omit the path option, a warning is emitted. Future releases will treat an undefined path as "respond on every path", so set the property explicitly (for example path: '/mcp' or path: null) to lock in the behavior you want today.

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.

// 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 HttpTransport(server);

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

Handlers can also observe request cancellation through server.ctx.signal. The HTTP transport aborts this signal when the incoming request is aborted or the client closes its request-scoped SSE response. Cancellation is cooperative, so handlers should pass the signal to cancellable work or check signal.aborted at suitable boundaries.

Session Management

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

In-Memory Sessions (Default)
import {
	InMemoryStreamSessionManager,
	InMemoryInfoSessionManager,
	InMemorySubscriptionManager,
} from '@tmcp/session-manager';

const transport = new HttpTransport(server, {
	sessionManager: {
		streams: new InMemoryStreamSessionManager(),
		info: new InMemoryInfoSessionManager(),
	},
	subscriptionManager: new InMemorySubscriptionManager(),
});
Redis Sessions (Multi-Server/Serverless)

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

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

const transport = new HttpTransport(server, {
	sessionManager: {
		streams: new RedisStreamSessionManager('redis://localhost:6379'),
		info: new RedisInfoSessionManager('redis://localhost:6379'),
	},
	subscriptionManager: new RedisSubscriptionManager('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, this means notification from the server will not reach the client after the shutdown)
  • Load balancing: When using load balancers that might route requests to different server instances

Features

  • HTTP/SSE Communication: Uses Server-Sent Events for real-time bidirectional communication
  • Dual Protocol Support: Supports both initialization-based sessions and sessionless per-request calls
  • Streaming Responses: Supports streaming responses through SSE
  • Configurable Path: Customizable MCP endpoint path with automatic filtering (set path to null to respond everywhere)
  • Framework Agnostic: Works with any HTTP server framework (Fastify, Bun, Deno, etc.)
  • Real-time Updates: Server can push notifications and updates to connected clients
  • Error Handling: Graceful error handling for malformed requests
  • Multiple HTTP Methods: Supports legacy GET/DELETE session behavior alongside POST-only per-request calls
  • Session Metadata: Automatically persists client capabilities, info, and log levels and exposes them via server.ctx.sessionInfo

API

HttpTransport
Constructor
new HttpTransport(server: McpServer, options?: HttpTransportOptions)

Creates a new HTTP transport instance.

Parameters:

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

Options:

interface HttpTransportOptions {
	getSessionId?: () => string; // Custom session ID generator
	path?: string | null; // MCP endpoint path (default: '/mcp', null responds on every path)
	oauth?: OAuth; // an oauth provider generated from @tmcp/auth
	cors?: CorsConfig | boolean; // CORS configuration
	allowedOrigins?: string | string[] | true; // Cross-origin request security policy
	sessionManager?: {
		streams?: StreamSessionManager;
		info?: InfoSessionManager;
	}; // Provide custom managers; defaults to in-memory implementations
	subscriptionManager?: SubscriptionManager; // Per-request subscription routing
	disableSse?: boolean; // Disable SSE stream endpoint (GET returns 405)
}

If you omit sessionManager the transport creates InMemoryStreamSessionManager and InMemoryInfoSessionManager instances for initialization-based clients. Per-request clients never create, read, or return session IDs. You can override either field independently (for example, Redis streams with in-memory metadata during development).

If you omit subscriptionManager, the transport creates an InMemorySubscriptionManager. Supply a distributed implementation when subscriptions/listen requests and change publication may reach different server instances.

Requests without an Origin header and same-origin browser requests are accepted. When allowedOrigins is omitted, cross-origin requests are also accepted and the transport warns on the first one. Set an explicit origin or origin list to restrict access, [] to reject every cross-origin request, or true to explicitly allow every origin without a warning. This request security policy is intentionally independent from cors, which only controls response headers.

Disabling SSE Streams

If your deployment doesn't support long-lived SSE connections (e.g., some serverless environments), you can disable the GET endpoint:

const transport = new HttpTransport(server, {
	disableSse: true,
});

When disableSse is true, GET requests to the MCP endpoint return 405 Method Not Allowed, so legacy session-negotiated clients cannot receive server-initiated notifications through a GET stream. Per-request clients can still open modern subscriptions/listen streams through POST.

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 MCP path.

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 MCP endpoint

HTTP Methods:

  • POST: Processes MCP messages and returns short-lived event stream responses
  • GET: Establishes long-lived connections for server notifications
  • DELETE: Disconnects sessions and cleans up resources
closeSubscription(response)

Gracefully complete the per-request subscription represented by the exact Response returned from respond():

const response = await transport.respond(listen_request);
await transport.closeSubscription(response);

Each listen POST receives an opaque internal routing origin. Mcp-Session-Id is never used as modern subscription identity, even when the caller provides it. Closing the response body cancels the stream; HTTP clients do not send notifications/cancelled for 2026-07-28 subscriptions.

close()

Cancel every active per-request subscription owned by this transport.

Protocol Details

Per-Request Protocol (2026-07-28)

Each request or notification uses its own POST. A conforming request includes:

  • MCP-Protocol-Version, matching params._meta.io.modelcontextprotocol/protocolVersion.
  • Mcp-Method, matching the JSON-RPC method.
  • Mcp-Name for tools/call, prompts/get, and resources/read, matching params.name or params.uri.
  • Any recognized Mcp-Param-* values declared by x-mcp-header annotations in a tool's input JSON Schema.

Mcp-Name and parameter headers use the specification's =?base64?...?= sentinel when a value cannot be represented safely as plain ASCII. Header names are compared case-insensitively; values remain case-sensitive.

Successful requests use request-scoped SSE and include X-Accel-Buffering: no. Accepted notifications return HTTP 202 without a body. Header mismatches and unsupported versions return HTTP 400; unknown or unavailable methods return HTTP 404. Per-request traffic ignores Mcp-Session-Id and Last-Event-ID, never returns a session ID, and returns HTTP 405 for GET and DELETE.

Initialization-based clients continue to use the behavior documented below on the same transport. Their session header, GET stream, DELETE lifecycle, and JSON-RPC response POSTs remain supported.

HTTP Methods

The transport supports three HTTP methods:

POST - Message Processing

Clients send JSON-RPC messages via HTTP POST requests. Each POST accepts one JSON-RPC message; batch arrays are rejected with -32600 Invalid Request. The following example shows initialization-based session behavior:

POST /mcp HTTP/1.1
Content-Type: application/json
mcp-session-id: optional-session-id

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

Response: Short-lived event stream that closes after sending the response:

HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
mcp-session-id: generated-or-provided-session-id

data: {"jsonrpc":"2.0","id":1,"result":{"tools":[...]}}
GET - Notification Stream

Establishes long-lived connections for server notifications:

GET /mcp HTTP/1.1
mcp-session-id: optional-session-id

Response: Long-lived event stream for server notifications:

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

data: {"jsonrpc":"2.0","method":"notifications/initialized","params":{}}
DELETE - Session Disconnect

Disconnects a session and cleans up resources:

DELETE /mcp HTTP/1.1
mcp-session-id: session-to-disconnect

Response:

HTTP/1.1 200 OK
mcp-session-id: session-to-disconnect
Legacy Session Management
  • Session ID Header: mcp-session-id
  • 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 all active sessions

Framework Examples

Bun
import { McpServer } from 'tmcp';
import { HttpTransport } from '@tmcp/transport-http';

const server = new McpServer(/* ... */);
const transport = new HttpTransport(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
import { McpServer } from 'tmcp';
import { HttpTransport } from '@tmcp/transport-http';

const server = new McpServer(/* ... */);
const transport = new HttpTransport(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.

import { McpServer } from 'tmcp';
import { HttpTransport } from '@tmcp/transport-http';
import { serve } from 'srvx';

const server = new McpServer(/* ... */);
const transport = new HttpTransport(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
  • Session Management: Automatic cleanup of disconnected sessions
  • Server Errors: Server processing errors are propagated to clients

Development

# 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.)

Acknowledgments

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

License

MIT

Keywords