[v2] Decouple server from express and hono - http framework-agnostic MCP server (#1326)
This commit is contained in:
parent
5ce4b5ef08
commit
f495077e08
112 changed files with 8193 additions and 15757 deletions
4
.github/workflows/publish.yml
vendored
4
.github/workflows/publish.yml
vendored
|
|
@ -38,4 +38,6 @@ jobs:
|
|||
run: pnpm run build:all
|
||||
|
||||
- name: Publish preview packages
|
||||
run: pnpm dlx pkg-pr-new publish --packageManager=npm --pnpm './packages/server' './packages/client'
|
||||
run:
|
||||
pnpm dlx pkg-pr-new publish --packageManager=npm --pnpm './packages/server' './packages/client'
|
||||
'./packages/middleware/express' './packages/middleware/hono' './packages/middleware/node'
|
||||
|
|
|
|||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -133,6 +133,7 @@ dist/
|
|||
|
||||
# IDE
|
||||
.idea/
|
||||
.cursor/
|
||||
|
||||
# Conformance test results
|
||||
results/
|
||||
|
|
|
|||
|
|
@ -64,11 +64,15 @@ Transports (`packages/core/src/shared/transport.ts`) provide the communication l
|
|||
### Client-Side Features
|
||||
|
||||
- **Auth**: OAuth client support in `packages/client/src/client/auth.ts` and `packages/client/src/client/auth-extensions.ts`
|
||||
- **Middleware**: Request middleware in `packages/client/src/client/middleware.ts`
|
||||
- **Client middleware**: Request middleware in `packages/client/src/client/middleware.ts` (unrelated to the framework adapter packages below)
|
||||
- **Sampling**: Clients can handle `sampling/createMessage` requests from servers (LLM completions)
|
||||
- **Elicitation**: Clients can handle `elicitation/create` requests for user input (form or URL mode)
|
||||
- **Roots**: Clients can expose filesystem roots to servers via `roots/list`
|
||||
|
||||
### Middleware packages (framework/runtime adapters)
|
||||
|
||||
The repo also ships “middleware” packages under `packages/middleware/` (e.g. `@modelcontextprotocol/express`, `@modelcontextprotocol/hono`, `@modelcontextprotocol/node`). These are thin integration layers for specific frameworks/runtimes and should not add new MCP functionality.
|
||||
|
||||
### Experimental Features
|
||||
|
||||
Located in `packages/*/src/experimental/`:
|
||||
|
|
@ -224,7 +228,8 @@ mcpServer.tool('tool-name', { param: z.string() }, async ({ param }, extra) => {
|
|||
|
||||
```typescript
|
||||
// Server
|
||||
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID() });
|
||||
// (Node.js IncomingMessage/ServerResponse wrapper; exported by @modelcontextprotocol/node)
|
||||
const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID() });
|
||||
await server.connect(transport);
|
||||
|
||||
// Client
|
||||
|
|
|
|||
31
README.md
31
README.md
|
|
@ -1,7 +1,6 @@
|
|||
# MCP TypeScript SDK
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **This is the `main` branch which contains v2 of the SDK (currently in development, pre-alpha).**
|
||||
> [!IMPORTANT] **This is the `main` branch which contains v2 of the SDK (currently in development, pre-alpha).**
|
||||
>
|
||||
> We anticipate a stable v2 release in Q1 2026. Until then, **v1.x remains the recommended version** for production use. v1.x will continue to receive bug fixes and security updates for at least 6 months after v2 ships to give people time to upgrade.
|
||||
>
|
||||
|
|
@ -30,6 +29,7 @@ This repository contains the TypeScript SDK implementation of the MCP specificat
|
|||
|
||||
- MCP **server** libraries (tools/resources/prompts, Streamable HTTP, stdio, auth helpers)
|
||||
- MCP **client** libraries (transports, high-level helpers, OAuth helpers)
|
||||
- Optional **middleware packages** for specific runtimes/frameworks (Express, Hono, Node.js HTTP)
|
||||
- Runnable **examples** (under [`examples/`](examples/))
|
||||
|
||||
## Packages
|
||||
|
|
@ -41,6 +41,16 @@ This monorepo publishes split packages:
|
|||
|
||||
Both packages have a **required peer dependency** on `zod` for schema validation. The SDK internally imports from `zod/v4`, but remains compatible with projects using Zod v3.25+.
|
||||
|
||||
### Middleware packages (optional)
|
||||
|
||||
The SDK also publishes small “middleware” packages under [`packages/middleware/`](packages/middleware/) that help you **wire MCP into a specific runtime or web framework**.
|
||||
|
||||
They are intentionally thin adapters: they should not introduce new MCP functionality or business logic. See [`packages/middleware/README.md`](packages/middleware/README.md) for details.
|
||||
|
||||
- **`@modelcontextprotocol/node`**: Node.js Streamable HTTP transport wrapper for `IncomingMessage` / `ServerResponse`
|
||||
- **`@modelcontextprotocol/express`**: Express helpers (app defaults + Host header validation)
|
||||
- **`@modelcontextprotocol/hono`**: Hono helpers (app defaults + JSON body parsing hook + Host header validation)
|
||||
|
||||
## Installation
|
||||
|
||||
### Server
|
||||
|
|
@ -55,6 +65,23 @@ npm install @modelcontextprotocol/server zod
|
|||
npm install @modelcontextprotocol/client zod
|
||||
```
|
||||
|
||||
### Optional middleware packages
|
||||
|
||||
The SDK also publishes optional “middleware” packages that help you **wire MCP into a specific runtime or web framework** (for example Express, Hono, or Node.js `http`).
|
||||
|
||||
These packages are intentionally thin adapters and should not introduce additional MCP features or business logic. See [`packages/middleware/README.md`](packages/middleware/README.md) for details.
|
||||
|
||||
```bash
|
||||
# Node.js HTTP (IncomingMessage/ServerResponse) Streamable HTTP transport:
|
||||
npm install @modelcontextprotocol/node
|
||||
|
||||
# Express integration:
|
||||
npm install @modelcontextprotocol/express express
|
||||
|
||||
# Hono integration:
|
||||
npm install @modelcontextprotocol/hono hono
|
||||
```
|
||||
|
||||
## Quick Start (runnable examples)
|
||||
|
||||
The runnable examples live under `examples/` and are kept in sync with the docs.
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ export default defineConfig(
|
|||
'@typescript-eslint/consistent-type-imports': ['error', { disallowTypeAnnotations: false }],
|
||||
'simple-import-sort/imports': 'warn',
|
||||
'simple-import-sort/exports': 'warn',
|
||||
'import/consistent-type-specifier-style': ['error', 'prefer-top-level'],
|
||||
'import/no-extraneous-dependencies': [
|
||||
'error',
|
||||
{
|
||||
|
|
|
|||
|
|
@ -15,10 +15,10 @@ export default defineConfig({
|
|||
deps: {
|
||||
moduleDirectories: ['node_modules', path.resolve(__dirname, '../../packages'), path.resolve(__dirname, '../../common')]
|
||||
},
|
||||
poolOptions: {
|
||||
threads: {
|
||||
useAtomics: true
|
||||
}
|
||||
},
|
||||
poolOptions: {
|
||||
threads: {
|
||||
useAtomics: true
|
||||
}
|
||||
},
|
||||
plugins: [tsconfigPaths()]
|
||||
|
|
|
|||
|
|
@ -65,6 +65,15 @@ For production use, you can either:
|
|||
|
||||
The SDK ships several runnable server examples under `examples/server/src`. Start from the server examples index in [`examples/server/README.md`](../examples/server/README.md) and the entry-point quick start in the root [`README.md`](../README.md).
|
||||
|
||||
### Why did we remove `server` auth exports?
|
||||
|
||||
Server authentication & authorization is outside of the scope of the SDK, and the recommendation is to use packages that focus on this area specifically (or a full-fledged Authorization Server for those who use such). Example packages provide an example with `better-auth`.
|
||||
|
||||
### Why did we remove `server` SSE transport?
|
||||
|
||||
The SSE transport has been deprecated for a long time, and `v2` will not support it on the server side any more. Client side will keep supporting it in order to be able to connect to legacy SSE servers via the `v2` SDK, but serving SSE from `v2` will not be possible. Servers
|
||||
wanting to switch to `v2` and using SSE should migrate to Streamable HTTP.
|
||||
|
||||
## v1 (legacy)
|
||||
|
||||
### Where do v1 documentation and v1-specific fixes live?
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ For more detailed patterns (stateless vs stateful, JSON response mode, CORS, DNS
|
|||
MCP servers running on localhost are vulnerable to DNS rebinding attacks. Use `createMcpExpressApp()` to create an Express app with DNS rebinding protection enabled by default:
|
||||
|
||||
```typescript
|
||||
import { createMcpExpressApp } from '@modelcontextprotocol/server';
|
||||
import { createMcpExpressApp } from '@modelcontextprotocol/express';
|
||||
|
||||
// Protection auto-enabled (default host is 127.0.0.1)
|
||||
const app = createMcpExpressApp();
|
||||
|
|
@ -85,7 +85,7 @@ const app = createMcpExpressApp({ host: '0.0.0.0' });
|
|||
When binding to `0.0.0.0` / `::`, provide an allow-list of hosts:
|
||||
|
||||
```typescript
|
||||
import { createMcpExpressApp } from '@modelcontextprotocol/server';
|
||||
import { createMcpExpressApp } from '@modelcontextprotocol/express';
|
||||
|
||||
const app = createMcpExpressApp({
|
||||
host: '0.0.0.0',
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
# MCP TypeScript SDK Examples (Server)
|
||||
|
||||
This directory contains runnable MCP **server** examples built with `@modelcontextprotocol/server`.
|
||||
This directory contains runnable MCP **server** examples built with `@modelcontextprotocol/server` plus framework adapters:
|
||||
|
||||
- `@modelcontextprotocol/express`
|
||||
- `@modelcontextprotocol/hono`
|
||||
|
||||
For client examples, see [`../client/README.md`](../client/README.md). For guided docs, see [`../../docs/server.md`](../../docs/server.md).
|
||||
|
||||
|
|
@ -68,7 +71,7 @@ When deploying MCP servers in a horizontally scaled environment (multiple server
|
|||
|
||||
### Stateless mode
|
||||
|
||||
To enable stateless mode, configure the `StreamableHTTPServerTransport` with:
|
||||
To enable stateless mode, configure the `NodeStreamableHTTPServerTransport` with:
|
||||
|
||||
```typescript
|
||||
sessionIdGenerator: undefined;
|
||||
|
|
|
|||
|
|
@ -35,11 +35,15 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@hono/node-server": "catalog:runtimeServerOnly",
|
||||
"hono": "catalog:runtimeServerOnly",
|
||||
"@modelcontextprotocol/examples-shared": "workspace:^",
|
||||
"@modelcontextprotocol/node": "workspace:^",
|
||||
"@modelcontextprotocol/server": "workspace:^",
|
||||
"@modelcontextprotocol/express": "workspace:^",
|
||||
"@modelcontextprotocol/hono": "workspace:^",
|
||||
"better-auth": "^1.4.7",
|
||||
"cors": "catalog:runtimeServerOnly",
|
||||
"express": "catalog:runtimeServerOnly",
|
||||
"hono": "catalog:runtimeServerOnly",
|
||||
"zod": "catalog:runtimeShared"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
|||
|
|
@ -9,8 +9,10 @@
|
|||
|
||||
import { randomUUID } from 'node:crypto';
|
||||
|
||||
import { createMcpExpressApp, isInitializeRequest, McpServer, StreamableHTTPServerTransport } from '@modelcontextprotocol/server';
|
||||
import { type Request, type Response } from 'express';
|
||||
import { createMcpExpressApp } from '@modelcontextprotocol/express';
|
||||
import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node';
|
||||
import { isInitializeRequest, McpServer } from '@modelcontextprotocol/server';
|
||||
import type { Request, Response } from 'express';
|
||||
|
||||
// Create MCP server - it will automatically use AjvJsonSchemaValidator with sensible defaults
|
||||
// The validator supports format validation (email, date, etc.) if ajv-formats is installed
|
||||
|
|
@ -321,7 +323,7 @@ async function main() {
|
|||
const app = createMcpExpressApp();
|
||||
|
||||
// Map to store transports by session ID
|
||||
const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};
|
||||
const transports: { [sessionId: string]: NodeStreamableHTTPServerTransport } = {};
|
||||
|
||||
// MCP POST endpoint
|
||||
const mcpPostHandler = async (req: Request, res: Response) => {
|
||||
|
|
@ -331,13 +333,13 @@ async function main() {
|
|||
}
|
||||
|
||||
try {
|
||||
let transport: StreamableHTTPServerTransport;
|
||||
let transport: NodeStreamableHTTPServerTransport;
|
||||
if (sessionId && transports[sessionId]) {
|
||||
// Reuse existing transport for this session
|
||||
transport = transports[sessionId];
|
||||
} else if (!sessionId && isInitializeRequest(req.body)) {
|
||||
// New initialization request - create new transport
|
||||
transport = new StreamableHTTPServerTransport({
|
||||
transport = new NodeStreamableHTTPServerTransport({
|
||||
sessionIdGenerator: () => randomUUID(),
|
||||
onsessioninitialized: sessionId => {
|
||||
// Store the transport by session ID when session is initialized
|
||||
|
|
|
|||
|
|
@ -9,19 +9,16 @@
|
|||
|
||||
import { randomUUID } from 'node:crypto';
|
||||
|
||||
import { setupAuthServer } from '@modelcontextprotocol/examples-shared';
|
||||
import type { CallToolResult, ElicitRequestURLParams, ElicitResult, OAuthMetadata } from '@modelcontextprotocol/server';
|
||||
import {
|
||||
checkResourceAllowed,
|
||||
createMcpExpressApp,
|
||||
createProtectedResourceMetadataRouter,
|
||||
getOAuthProtectedResourceMetadataUrl,
|
||||
isInitializeRequest,
|
||||
mcpAuthMetadataRouter,
|
||||
McpServer,
|
||||
requireBearerAuth,
|
||||
StreamableHTTPServerTransport,
|
||||
UrlElicitationRequiredError
|
||||
} from '@modelcontextprotocol/server';
|
||||
setupAuthServer
|
||||
} from '@modelcontextprotocol/examples-shared';
|
||||
import { createMcpExpressApp } from '@modelcontextprotocol/express';
|
||||
import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node';
|
||||
import type { CallToolResult, ElicitRequestURLParams, ElicitResult } from '@modelcontextprotocol/server';
|
||||
import { isInitializeRequest, McpServer, UrlElicitationRequiredError } from '@modelcontextprotocol/server';
|
||||
import cors from 'cors';
|
||||
import type { Request, Response } from 'express';
|
||||
import express from 'express';
|
||||
|
|
@ -238,63 +235,17 @@ let authMiddleware = null;
|
|||
const mcpServerUrl = new URL(`http://localhost:${MCP_PORT}/mcp`);
|
||||
const authServerUrl = new URL(`http://localhost:${AUTH_PORT}`);
|
||||
|
||||
const oauthMetadata: OAuthMetadata = setupAuthServer({ authServerUrl, mcpServerUrl, strictResource: true });
|
||||
setupAuthServer({ authServerUrl, mcpServerUrl, strictResource: true, demoMode: true });
|
||||
|
||||
const tokenVerifier = {
|
||||
verifyAccessToken: async (token: string) => {
|
||||
const endpoint = oauthMetadata.introspection_endpoint;
|
||||
|
||||
if (!endpoint) {
|
||||
throw new Error('No token verification endpoint available in metadata');
|
||||
}
|
||||
|
||||
const response = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
token: token
|
||||
}).toString()
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text().catch(() => null);
|
||||
throw new Error(`Invalid or expired token: ${text}`);
|
||||
}
|
||||
|
||||
const data = (await response.json()) as { aud: string; client_id: string; scope: string; exp: number };
|
||||
|
||||
if (!data.aud) {
|
||||
throw new Error(`Resource Indicator (RFC8707) missing`);
|
||||
}
|
||||
if (!checkResourceAllowed({ requestedResource: data.aud, configuredResource: mcpServerUrl })) {
|
||||
throw new Error(`Expected resource indicator ${mcpServerUrl}, got: ${data.aud}`);
|
||||
}
|
||||
|
||||
// Convert the response to AuthInfo format
|
||||
return {
|
||||
token,
|
||||
clientId: data.client_id,
|
||||
scopes: data.scope ? data.scope.split(' ') : [],
|
||||
expiresAt: data.exp
|
||||
};
|
||||
}
|
||||
};
|
||||
// Add metadata routes to the main MCP server
|
||||
app.use(
|
||||
mcpAuthMetadataRouter({
|
||||
oauthMetadata,
|
||||
resourceServerUrl: mcpServerUrl,
|
||||
scopesSupported: ['mcp:tools'],
|
||||
resourceName: 'MCP Demo Server'
|
||||
})
|
||||
);
|
||||
// Add protected resource metadata route to the MCP server
|
||||
// This allows clients to discover the auth server
|
||||
app.use(createProtectedResourceMetadataRouter());
|
||||
|
||||
authMiddleware = requireBearerAuth({
|
||||
verifier: tokenVerifier,
|
||||
requiredScopes: [],
|
||||
resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(mcpServerUrl)
|
||||
resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(mcpServerUrl),
|
||||
strictResource: true,
|
||||
expectedResource: mcpServerUrl
|
||||
});
|
||||
|
||||
/**
|
||||
|
|
@ -594,7 +545,7 @@ app.post('/confirm-payment', express.urlencoded(), (req: Request, res: Response)
|
|||
});
|
||||
|
||||
// Map to store transports by session ID
|
||||
const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};
|
||||
const transports: { [sessionId: string]: NodeStreamableHTTPServerTransport } = {};
|
||||
|
||||
// Interface for a function that can send an elicitation request
|
||||
type ElicitationSender = (params: ElicitRequestURLParams) => Promise<ElicitResult>;
|
||||
|
|
@ -613,7 +564,7 @@ const mcpPostHandler = async (req: Request, res: Response) => {
|
|||
console.debug(`Received MCP POST for session: ${sessionId || 'unknown'}`);
|
||||
|
||||
try {
|
||||
let transport: StreamableHTTPServerTransport;
|
||||
let transport: NodeStreamableHTTPServerTransport;
|
||||
if (sessionId && transports[sessionId]) {
|
||||
// Reuse existing transport
|
||||
transport = transports[sessionId];
|
||||
|
|
@ -621,7 +572,7 @@ const mcpPostHandler = async (req: Request, res: Response) => {
|
|||
const server = getServer();
|
||||
// New initialization request
|
||||
const eventStore = new InMemoryEventStore();
|
||||
transport = new StreamableHTTPServerTransport({
|
||||
transport = new NodeStreamableHTTPServerTransport({
|
||||
sessionIdGenerator: () => randomUUID(),
|
||||
eventStore, // Enable resumability
|
||||
onsessioninitialized: sessionId => {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import { randomUUID } from 'node:crypto';
|
||||
|
||||
import { createMcpExpressApp } from '@modelcontextprotocol/express';
|
||||
import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node';
|
||||
import type { CallToolResult } from '@modelcontextprotocol/server';
|
||||
import { createMcpExpressApp, isInitializeRequest, McpServer, StreamableHTTPServerTransport } from '@modelcontextprotocol/server';
|
||||
import { isInitializeRequest, McpServer } from '@modelcontextprotocol/server';
|
||||
import type { Request, Response } from 'express';
|
||||
import * as z from 'zod/v4';
|
||||
|
||||
|
|
@ -96,21 +98,21 @@ const getServer = () => {
|
|||
const app = createMcpExpressApp();
|
||||
|
||||
// Map to store transports by session ID
|
||||
const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};
|
||||
const transports: { [sessionId: string]: NodeStreamableHTTPServerTransport } = {};
|
||||
|
||||
app.post('/mcp', async (req: Request, res: Response) => {
|
||||
console.log('Received MCP request:', req.body);
|
||||
try {
|
||||
// Check for existing session ID
|
||||
const sessionId = req.headers['mcp-session-id'] as string | undefined;
|
||||
let transport: StreamableHTTPServerTransport;
|
||||
let transport: NodeStreamableHTTPServerTransport;
|
||||
|
||||
if (sessionId && transports[sessionId]) {
|
||||
// Reuse existing transport
|
||||
transport = transports[sessionId];
|
||||
} else if (!sessionId && isInitializeRequest(req.body)) {
|
||||
// New initialization request - use JSON response mode
|
||||
transport = new StreamableHTTPServerTransport({
|
||||
transport = new NodeStreamableHTTPServerTransport({
|
||||
sessionIdGenerator: () => randomUUID(),
|
||||
enableJsonResponse: true, // Enable JSON response mode
|
||||
onsessioninitialized: sessionId => {
|
||||
|
|
|
|||
|
|
@ -1,174 +0,0 @@
|
|||
import type { CallToolResult } from '@modelcontextprotocol/server';
|
||||
import { createMcpExpressApp, McpServer, SSEServerTransport } from '@modelcontextprotocol/server';
|
||||
import type { Request, Response } from 'express';
|
||||
import * as z from 'zod/v4';
|
||||
|
||||
/**
|
||||
* This example server demonstrates the deprecated HTTP+SSE transport
|
||||
* (protocol version 2024-11-05). It mainly used for testing backward compatible clients.
|
||||
*
|
||||
* The server exposes two endpoints:
|
||||
* - /mcp: For establishing the SSE stream (GET)
|
||||
* - /messages: For receiving client messages (POST)
|
||||
*
|
||||
*/
|
||||
|
||||
// Create an MCP server instance
|
||||
const getServer = () => {
|
||||
const server = new McpServer(
|
||||
{
|
||||
name: 'simple-sse-server',
|
||||
version: '1.0.0'
|
||||
},
|
||||
{ capabilities: { logging: {} } }
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
'start-notification-stream',
|
||||
{
|
||||
description: 'Starts sending periodic notifications',
|
||||
inputSchema: {
|
||||
interval: z.number().describe('Interval in milliseconds between notifications').default(1000),
|
||||
count: z.number().describe('Number of notifications to send').default(10)
|
||||
}
|
||||
},
|
||||
async ({ interval, count }, extra): Promise<CallToolResult> => {
|
||||
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
||||
let counter = 0;
|
||||
|
||||
// Send the initial notification
|
||||
await server.sendLoggingMessage(
|
||||
{
|
||||
level: 'info',
|
||||
data: `Starting notification stream with ${count} messages every ${interval}ms`
|
||||
},
|
||||
extra.sessionId
|
||||
);
|
||||
|
||||
// Send periodic notifications
|
||||
while (counter < count) {
|
||||
counter++;
|
||||
await sleep(interval);
|
||||
|
||||
try {
|
||||
await server.sendLoggingMessage(
|
||||
{
|
||||
level: 'info',
|
||||
data: `Notification #${counter} at ${new Date().toISOString()}`
|
||||
},
|
||||
extra.sessionId
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error sending notification:', error);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Completed sending ${count} notifications every ${interval}ms`
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
);
|
||||
return server;
|
||||
};
|
||||
|
||||
const app = createMcpExpressApp();
|
||||
|
||||
// Store transports by session ID
|
||||
const transports: Record<string, SSEServerTransport> = {};
|
||||
|
||||
// SSE endpoint for establishing the stream
|
||||
app.get('/mcp', async (req: Request, res: Response) => {
|
||||
console.log('Received GET request to /sse (establishing SSE stream)');
|
||||
|
||||
try {
|
||||
// Create a new SSE transport for the client
|
||||
// The endpoint for POST messages is '/messages'
|
||||
const transport = new SSEServerTransport('/messages', res);
|
||||
|
||||
// Store the transport by session ID
|
||||
const sessionId = transport.sessionId;
|
||||
transports[sessionId] = transport;
|
||||
|
||||
// Set up onclose handler to clean up transport when closed
|
||||
transport.onclose = () => {
|
||||
console.log(`SSE transport closed for session ${sessionId}`);
|
||||
delete transports[sessionId];
|
||||
};
|
||||
|
||||
// Connect the transport to the MCP server
|
||||
const server = getServer();
|
||||
await server.connect(transport);
|
||||
|
||||
console.log(`Established SSE stream with session ID: ${sessionId}`);
|
||||
} catch (error) {
|
||||
console.error('Error establishing SSE stream:', error);
|
||||
if (!res.headersSent) {
|
||||
res.status(500).send('Error establishing SSE stream');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Messages endpoint for receiving client JSON-RPC requests
|
||||
app.post('/messages', async (req: Request, res: Response) => {
|
||||
console.log('Received POST request to /messages');
|
||||
|
||||
// Extract session ID from URL query parameter
|
||||
// In the SSE protocol, this is added by the client based on the endpoint event
|
||||
const sessionId = req.query.sessionId as string | undefined;
|
||||
|
||||
if (!sessionId) {
|
||||
console.error('No session ID provided in request URL');
|
||||
res.status(400).send('Missing sessionId parameter');
|
||||
return;
|
||||
}
|
||||
|
||||
const transport = transports[sessionId];
|
||||
if (!transport) {
|
||||
console.error(`No active transport found for session ID: ${sessionId}`);
|
||||
res.status(404).send('Session not found');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Handle the POST message with the transport
|
||||
await transport.handlePostMessage(req, res, req.body);
|
||||
} catch (error) {
|
||||
console.error('Error handling request:', error);
|
||||
if (!res.headersSent) {
|
||||
res.status(500).send('Error handling request');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Start the server
|
||||
const PORT = 3000;
|
||||
app.listen(PORT, error => {
|
||||
if (error) {
|
||||
console.error('Failed to start server:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`Simple SSE Server (deprecated protocol version 2024-11-05) listening on port ${PORT}`);
|
||||
});
|
||||
|
||||
// Handle server shutdown
|
||||
process.on('SIGINT', async () => {
|
||||
console.log('Shutting down server...');
|
||||
|
||||
// Close all active transports to properly clean up resources
|
||||
for (const sessionId in transports) {
|
||||
try {
|
||||
console.log(`Closing transport for session ${sessionId}`);
|
||||
await transports[sessionId]!.close();
|
||||
delete transports[sessionId];
|
||||
} catch (error) {
|
||||
console.error(`Error closing transport for session ${sessionId}:`, error);
|
||||
}
|
||||
}
|
||||
console.log('Server shutdown complete');
|
||||
process.exit(0);
|
||||
});
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
import { createMcpExpressApp } from '@modelcontextprotocol/express';
|
||||
import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node';
|
||||
import type { CallToolResult, GetPromptResult, ReadResourceResult } from '@modelcontextprotocol/server';
|
||||
import { createMcpExpressApp, McpServer, StreamableHTTPServerTransport } from '@modelcontextprotocol/server';
|
||||
import { McpServer } from '@modelcontextprotocol/server';
|
||||
import type { Request, Response } from 'express';
|
||||
import * as z from 'zod/v4';
|
||||
|
||||
|
|
@ -103,7 +105,7 @@ const app = createMcpExpressApp();
|
|||
app.post('/mcp', async (req: Request, res: Response) => {
|
||||
const server = getServer();
|
||||
try {
|
||||
const transport: StreamableHTTPServerTransport = new StreamableHTTPServerTransport({
|
||||
const transport: NodeStreamableHTTPServerTransport = new NodeStreamableHTTPServerTransport({
|
||||
sessionIdGenerator: undefined
|
||||
});
|
||||
await server.connect(transport);
|
||||
|
|
|
|||
|
|
@ -1,26 +1,26 @@
|
|||
import { randomUUID } from 'node:crypto';
|
||||
|
||||
import { setupAuthServer } from '@modelcontextprotocol/examples-shared';
|
||||
import {
|
||||
createProtectedResourceMetadataRouter,
|
||||
getOAuthProtectedResourceMetadataUrl,
|
||||
requireBearerAuth,
|
||||
setupAuthServer
|
||||
} from '@modelcontextprotocol/examples-shared';
|
||||
import { createMcpExpressApp } from '@modelcontextprotocol/express';
|
||||
import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node';
|
||||
import type {
|
||||
CallToolResult,
|
||||
GetPromptResult,
|
||||
OAuthMetadata,
|
||||
PrimitiveSchemaDefinition,
|
||||
ReadResourceResult,
|
||||
ResourceLink
|
||||
} from '@modelcontextprotocol/server';
|
||||
import {
|
||||
checkResourceAllowed,
|
||||
createMcpExpressApp,
|
||||
ElicitResultSchema,
|
||||
getOAuthProtectedResourceMetadataUrl,
|
||||
InMemoryTaskMessageQueue,
|
||||
InMemoryTaskStore,
|
||||
isInitializeRequest,
|
||||
mcpAuthMetadataRouter,
|
||||
McpServer,
|
||||
requireBearerAuth,
|
||||
StreamableHTTPServerTransport
|
||||
McpServer
|
||||
} from '@modelcontextprotocol/server';
|
||||
import type { Request, Response } from 'express';
|
||||
import * as z from 'zod/v4';
|
||||
|
|
@ -527,70 +527,22 @@ if (useOAuth) {
|
|||
const mcpServerUrl = new URL(`http://localhost:${MCP_PORT}/mcp`);
|
||||
const authServerUrl = new URL(`http://localhost:${AUTH_PORT}`);
|
||||
|
||||
const oauthMetadata: OAuthMetadata = setupAuthServer({ authServerUrl, mcpServerUrl, strictResource: strictOAuth });
|
||||
setupAuthServer({ authServerUrl, mcpServerUrl, strictResource: strictOAuth, demoMode: true });
|
||||
|
||||
const tokenVerifier = {
|
||||
verifyAccessToken: async (token: string) => {
|
||||
const endpoint = oauthMetadata.introspection_endpoint;
|
||||
|
||||
if (!endpoint) {
|
||||
throw new Error('No token verification endpoint available in metadata');
|
||||
}
|
||||
|
||||
const response = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
token: token
|
||||
}).toString()
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text().catch(() => null);
|
||||
throw new Error(`Invalid or expired token: ${text}`);
|
||||
}
|
||||
|
||||
const data = (await response.json()) as { aud: string; client_id: string; scope: string; exp: number };
|
||||
|
||||
if (strictOAuth) {
|
||||
if (!data.aud) {
|
||||
throw new Error(`Resource Indicator (RFC8707) missing`);
|
||||
}
|
||||
if (!checkResourceAllowed({ requestedResource: data.aud, configuredResource: mcpServerUrl })) {
|
||||
throw new Error(`Expected resource indicator ${mcpServerUrl}, got: ${data.aud}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Convert the response to AuthInfo format
|
||||
return {
|
||||
token,
|
||||
clientId: data.client_id,
|
||||
scopes: data.scope ? data.scope.split(' ') : [],
|
||||
expiresAt: data.exp
|
||||
};
|
||||
}
|
||||
};
|
||||
// Add metadata routes to the main MCP server
|
||||
app.use(
|
||||
mcpAuthMetadataRouter({
|
||||
oauthMetadata,
|
||||
resourceServerUrl: mcpServerUrl,
|
||||
scopesSupported: ['mcp:tools'],
|
||||
resourceName: 'MCP Demo Server'
|
||||
})
|
||||
);
|
||||
// Add protected resource metadata route to the MCP server
|
||||
// This allows clients to discover the auth server
|
||||
app.use(createProtectedResourceMetadataRouter());
|
||||
|
||||
authMiddleware = requireBearerAuth({
|
||||
verifier: tokenVerifier,
|
||||
requiredScopes: [],
|
||||
resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(mcpServerUrl)
|
||||
resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(mcpServerUrl),
|
||||
strictResource: strictOAuth,
|
||||
expectedResource: mcpServerUrl
|
||||
});
|
||||
}
|
||||
|
||||
// Map to store transports by session ID
|
||||
const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};
|
||||
const transports: { [sessionId: string]: NodeStreamableHTTPServerTransport } = {};
|
||||
|
||||
// MCP POST endpoint with optional auth
|
||||
const mcpPostHandler = async (req: Request, res: Response) => {
|
||||
|
|
@ -601,18 +553,18 @@ const mcpPostHandler = async (req: Request, res: Response) => {
|
|||
console.log('Request body:', req.body);
|
||||
}
|
||||
|
||||
if (useOAuth && req.auth) {
|
||||
console.log('Authenticated user:', req.auth);
|
||||
if (useOAuth && req.app.locals.auth) {
|
||||
console.log('Authenticated user:', req.app.locals.auth);
|
||||
}
|
||||
try {
|
||||
let transport: StreamableHTTPServerTransport;
|
||||
let transport: NodeStreamableHTTPServerTransport;
|
||||
if (sessionId && transports[sessionId]) {
|
||||
// Reuse existing transport
|
||||
transport = transports[sessionId];
|
||||
} else if (!sessionId && isInitializeRequest(req.body)) {
|
||||
// New initialization request
|
||||
const eventStore = new InMemoryEventStore();
|
||||
transport = new StreamableHTTPServerTransport({
|
||||
transport = new NodeStreamableHTTPServerTransport({
|
||||
sessionIdGenerator: () => randomUUID(),
|
||||
eventStore, // Enable resumability
|
||||
onsessioninitialized: sessionId => {
|
||||
|
|
@ -685,8 +637,8 @@ const mcpGetHandler = async (req: Request, res: Response) => {
|
|||
return;
|
||||
}
|
||||
|
||||
if (useOAuth && req.auth) {
|
||||
console.log('Authenticated SSE connection from user:', req.auth);
|
||||
if (useOAuth && req.app.locals.auth) {
|
||||
console.log('Authenticated SSE connection from user:', req.app.locals.auth);
|
||||
}
|
||||
|
||||
// Check for Last-Event-ID header for resumability
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@
|
|||
|
||||
import { randomUUID } from 'node:crypto';
|
||||
|
||||
import { createMcpExpressApp } from '@modelcontextprotocol/express';
|
||||
import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node';
|
||||
import type {
|
||||
CallToolResult,
|
||||
CreateMessageRequest,
|
||||
|
|
@ -35,15 +37,13 @@ import type {
|
|||
} from '@modelcontextprotocol/server';
|
||||
import {
|
||||
CallToolRequestSchema,
|
||||
createMcpExpressApp,
|
||||
GetTaskPayloadRequestSchema,
|
||||
GetTaskRequestSchema,
|
||||
InMemoryTaskStore,
|
||||
isTerminal,
|
||||
ListToolsRequestSchema,
|
||||
RELATED_TASK_META_KEY,
|
||||
Server,
|
||||
StreamableHTTPServerTransport
|
||||
Server
|
||||
} from '@modelcontextprotocol/server';
|
||||
import type { Request, Response } from 'express';
|
||||
|
||||
|
|
@ -642,7 +642,7 @@ const createServer = (): Server => {
|
|||
const app = createMcpExpressApp();
|
||||
|
||||
// Map to store transports by session ID
|
||||
const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};
|
||||
const transports: { [sessionId: string]: NodeStreamableHTTPServerTransport } = {};
|
||||
|
||||
// Helper to check if request is initialize
|
||||
const isInitializeRequest = (body: unknown): boolean => {
|
||||
|
|
@ -654,12 +654,12 @@ app.post('/mcp', async (req: Request, res: Response) => {
|
|||
const sessionId = req.headers['mcp-session-id'] as string | undefined;
|
||||
|
||||
try {
|
||||
let transport: StreamableHTTPServerTransport;
|
||||
let transport: NodeStreamableHTTPServerTransport;
|
||||
|
||||
if (sessionId && transports[sessionId]) {
|
||||
transport = transports[sessionId];
|
||||
} else if (!sessionId && isInitializeRequest(req.body)) {
|
||||
transport = new StreamableHTTPServerTransport({
|
||||
transport = new NodeStreamableHTTPServerTransport({
|
||||
sessionIdGenerator: () => randomUUID(),
|
||||
onsessioninitialized: sid => {
|
||||
console.log(`Session initialized: ${sid}`);
|
||||
|
|
|
|||
|
|
@ -1,258 +0,0 @@
|
|||
import { randomUUID } from 'node:crypto';
|
||||
|
||||
import type { CallToolResult } from '@modelcontextprotocol/server';
|
||||
import {
|
||||
createMcpExpressApp,
|
||||
isInitializeRequest,
|
||||
McpServer,
|
||||
SSEServerTransport,
|
||||
StreamableHTTPServerTransport
|
||||
} from '@modelcontextprotocol/server';
|
||||
import type { Request, Response } from 'express';
|
||||
import * as z from 'zod/v4';
|
||||
|
||||
import { InMemoryEventStore } from './inMemoryEventStore.js';
|
||||
|
||||
/**
|
||||
* This example server demonstrates backwards compatibility with both:
|
||||
* 1. The deprecated HTTP+SSE transport (protocol version 2024-11-05)
|
||||
* 2. The Streamable HTTP transport (protocol version 2025-11-25)
|
||||
*
|
||||
* It maintains a single MCP server instance but exposes two transport options:
|
||||
* - /mcp: The new Streamable HTTP endpoint (supports GET/POST/DELETE)
|
||||
* - /sse: The deprecated SSE endpoint for older clients (GET to establish stream)
|
||||
* - /messages: The deprecated POST endpoint for older clients (POST to send messages)
|
||||
*/
|
||||
|
||||
const getServer = () => {
|
||||
const server = new McpServer(
|
||||
{
|
||||
name: 'backwards-compatible-server',
|
||||
version: '1.0.0'
|
||||
},
|
||||
{ capabilities: { logging: {} } }
|
||||
);
|
||||
|
||||
// Register a simple tool that sends notifications over time
|
||||
server.registerTool(
|
||||
'start-notification-stream',
|
||||
{
|
||||
description: 'Starts sending periodic notifications for testing resumability',
|
||||
inputSchema: {
|
||||
interval: z.number().describe('Interval in milliseconds between notifications').default(100),
|
||||
count: z.number().describe('Number of notifications to send (0 for 100)').default(50)
|
||||
}
|
||||
},
|
||||
async ({ interval, count }, extra): Promise<CallToolResult> => {
|
||||
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
||||
let counter = 0;
|
||||
|
||||
while (count === 0 || counter < count) {
|
||||
counter++;
|
||||
try {
|
||||
await server.sendLoggingMessage(
|
||||
{
|
||||
level: 'info',
|
||||
data: `Periodic notification #${counter} at ${new Date().toISOString()}`
|
||||
},
|
||||
extra.sessionId
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error sending notification:', error);
|
||||
}
|
||||
// Wait for the specified interval
|
||||
await sleep(interval);
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Started sending periodic notifications every ${interval}ms`
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
);
|
||||
return server;
|
||||
};
|
||||
|
||||
// Create Express application
|
||||
const app = createMcpExpressApp();
|
||||
|
||||
// Store transports by session ID
|
||||
const transports: Record<string, StreamableHTTPServerTransport | SSEServerTransport> = {};
|
||||
|
||||
//=============================================================================
|
||||
// STREAMABLE HTTP TRANSPORT (PROTOCOL VERSION 2025-11-25)
|
||||
//=============================================================================
|
||||
|
||||
// Handle all MCP Streamable HTTP requests (GET, POST, DELETE) on a single endpoint
|
||||
app.all('/mcp', async (req: Request, res: Response) => {
|
||||
console.log(`Received ${req.method} request to /mcp`);
|
||||
|
||||
try {
|
||||
// Check for existing session ID
|
||||
const sessionId = req.headers['mcp-session-id'] as string | undefined;
|
||||
let transport: StreamableHTTPServerTransport;
|
||||
|
||||
if (sessionId && transports[sessionId]) {
|
||||
// Check if the transport is of the correct type
|
||||
const existingTransport = transports[sessionId];
|
||||
if (existingTransport instanceof StreamableHTTPServerTransport) {
|
||||
// Reuse existing transport
|
||||
transport = existingTransport;
|
||||
} else {
|
||||
// Transport exists but is not a StreamableHTTPServerTransport (could be SSEServerTransport)
|
||||
res.status(400).json({
|
||||
jsonrpc: '2.0',
|
||||
error: {
|
||||
code: -32000,
|
||||
message: 'Bad Request: Session exists but uses a different transport protocol'
|
||||
},
|
||||
id: null
|
||||
});
|
||||
return;
|
||||
}
|
||||
} else if (!sessionId && req.method === 'POST' && isInitializeRequest(req.body)) {
|
||||
const eventStore = new InMemoryEventStore();
|
||||
transport = new StreamableHTTPServerTransport({
|
||||
sessionIdGenerator: () => randomUUID(),
|
||||
eventStore, // Enable resumability
|
||||
onsessioninitialized: sessionId => {
|
||||
// Store the transport by session ID when session is initialized
|
||||
console.log(`StreamableHTTP session initialized with ID: ${sessionId}`);
|
||||
transports[sessionId] = transport;
|
||||
}
|
||||
});
|
||||
|
||||
// Set up onclose handler to clean up transport when closed
|
||||
transport.onclose = () => {
|
||||
const sid = transport.sessionId;
|
||||
if (sid && transports[sid]) {
|
||||
console.log(`Transport closed for session ${sid}, removing from transports map`);
|
||||
delete transports[sid];
|
||||
}
|
||||
};
|
||||
|
||||
// Connect the transport to the MCP server
|
||||
const server = getServer();
|
||||
await server.connect(transport);
|
||||
} else {
|
||||
// Invalid request - no session ID or not initialization request
|
||||
res.status(400).json({
|
||||
jsonrpc: '2.0',
|
||||
error: {
|
||||
code: -32000,
|
||||
message: 'Bad Request: No valid session ID provided'
|
||||
},
|
||||
id: null
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle the request with the transport
|
||||
await transport.handleRequest(req, res, req.body);
|
||||
} catch (error) {
|
||||
console.error('Error handling MCP request:', error);
|
||||
if (!res.headersSent) {
|
||||
res.status(500).json({
|
||||
jsonrpc: '2.0',
|
||||
error: {
|
||||
code: -32603,
|
||||
message: 'Internal server error'
|
||||
},
|
||||
id: null
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
//=============================================================================
|
||||
// DEPRECATED HTTP+SSE TRANSPORT (PROTOCOL VERSION 2024-11-05)
|
||||
//=============================================================================
|
||||
|
||||
app.get('/sse', async (req: Request, res: Response) => {
|
||||
console.log('Received GET request to /sse (deprecated SSE transport)');
|
||||
const transport = new SSEServerTransport('/messages', res);
|
||||
transports[transport.sessionId] = transport;
|
||||
res.on('close', () => {
|
||||
delete transports[transport.sessionId];
|
||||
});
|
||||
const server = getServer();
|
||||
await server.connect(transport);
|
||||
});
|
||||
|
||||
app.post('/messages', async (req: Request, res: Response) => {
|
||||
const sessionId = req.query.sessionId as string;
|
||||
let transport: SSEServerTransport;
|
||||
const existingTransport = transports[sessionId];
|
||||
if (existingTransport instanceof SSEServerTransport) {
|
||||
// Reuse existing transport
|
||||
transport = existingTransport;
|
||||
} else {
|
||||
// Transport exists but is not a SSEServerTransport (could be StreamableHTTPServerTransport)
|
||||
res.status(400).json({
|
||||
jsonrpc: '2.0',
|
||||
error: {
|
||||
code: -32000,
|
||||
message: 'Bad Request: Session exists but uses a different transport protocol'
|
||||
},
|
||||
id: null
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (transport) {
|
||||
await transport.handlePostMessage(req, res, req.body);
|
||||
} else {
|
||||
res.status(400).send('No transport found for sessionId');
|
||||
}
|
||||
});
|
||||
|
||||
// Start the server
|
||||
const PORT = 3000;
|
||||
app.listen(PORT, error => {
|
||||
if (error) {
|
||||
console.error('Failed to start server:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`Backwards compatible MCP server listening on port ${PORT}`);
|
||||
console.log(`
|
||||
==============================================
|
||||
SUPPORTED TRANSPORT OPTIONS:
|
||||
|
||||
1. Streamable Http(Protocol version: 2025-11-25)
|
||||
Endpoint: /mcp
|
||||
Methods: GET, POST, DELETE
|
||||
Usage:
|
||||
- Initialize with POST to /mcp
|
||||
- Establish SSE stream with GET to /mcp
|
||||
- Send requests with POST to /mcp
|
||||
- Terminate session with DELETE to /mcp
|
||||
|
||||
2. Http + SSE (Protocol version: 2024-11-05)
|
||||
Endpoints: /sse (GET) and /messages (POST)
|
||||
Usage:
|
||||
- Establish SSE stream with GET to /sse
|
||||
- Send requests with POST to /messages?sessionId=<id>
|
||||
==============================================
|
||||
`);
|
||||
});
|
||||
|
||||
// Handle server shutdown
|
||||
process.on('SIGINT', async () => {
|
||||
console.log('Shutting down server...');
|
||||
|
||||
// Close all active transports to properly clean up resources
|
||||
for (const sessionId in transports) {
|
||||
try {
|
||||
console.log(`Closing transport for session ${sessionId}`);
|
||||
await transports[sessionId]!.close();
|
||||
delete transports[sessionId];
|
||||
} catch (error) {
|
||||
console.error(`Error closing transport for session ${sessionId}:`, error);
|
||||
}
|
||||
}
|
||||
console.log('Server shutdown complete');
|
||||
process.exit(0);
|
||||
});
|
||||
|
|
@ -14,8 +14,10 @@
|
|||
*/
|
||||
import { randomUUID } from 'node:crypto';
|
||||
|
||||
import { createMcpExpressApp } from '@modelcontextprotocol/express';
|
||||
import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node';
|
||||
import type { CallToolResult } from '@modelcontextprotocol/server';
|
||||
import { createMcpExpressApp, McpServer, StreamableHTTPServerTransport } from '@modelcontextprotocol/server';
|
||||
import { McpServer } from '@modelcontextprotocol/server';
|
||||
import cors from 'cors';
|
||||
import type { Request, Response } from 'express';
|
||||
|
||||
|
|
@ -111,7 +113,7 @@ app.use(cors());
|
|||
const eventStore = new InMemoryEventStore();
|
||||
|
||||
// Track transports by session ID for session reuse
|
||||
const transports = new Map<string, StreamableHTTPServerTransport>();
|
||||
const transports = new Map<string, NodeStreamableHTTPServerTransport>();
|
||||
|
||||
// Handle all MCP requests
|
||||
app.all('/mcp', async (req: Request, res: Response) => {
|
||||
|
|
@ -121,7 +123,7 @@ app.all('/mcp', async (req: Request, res: Response) => {
|
|||
let transport = sessionId ? transports.get(sessionId) : undefined;
|
||||
|
||||
if (!transport) {
|
||||
transport = new StreamableHTTPServerTransport({
|
||||
transport = new NodeStreamableHTTPServerTransport({
|
||||
sessionIdGenerator: () => randomUUID(),
|
||||
eventStore,
|
||||
retryInterval: 2000, // Default retry interval for priming events
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import { randomUUID } from 'node:crypto';
|
||||
|
||||
import { createMcpExpressApp } from '@modelcontextprotocol/express';
|
||||
import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node';
|
||||
import type { ReadResourceResult } from '@modelcontextprotocol/server';
|
||||
import { createMcpExpressApp, isInitializeRequest, McpServer, StreamableHTTPServerTransport } from '@modelcontextprotocol/server';
|
||||
import { isInitializeRequest, McpServer } from '@modelcontextprotocol/server';
|
||||
import type { Request, Response } from 'express';
|
||||
|
||||
// Create an MCP server with implementation details
|
||||
|
|
@ -11,7 +13,7 @@ const server = new McpServer({
|
|||
});
|
||||
|
||||
// Store transports by session ID to send notifications
|
||||
const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};
|
||||
const transports: { [sessionId: string]: NodeStreamableHTTPServerTransport } = {};
|
||||
|
||||
const addResource = (name: string, content: string) => {
|
||||
const uri = `https://mcp-example.com/dynamic/${encodeURIComponent(name)}`;
|
||||
|
|
@ -41,14 +43,14 @@ app.post('/mcp', async (req: Request, res: Response) => {
|
|||
try {
|
||||
// Check for existing session ID
|
||||
const sessionId = req.headers['mcp-session-id'] as string | undefined;
|
||||
let transport: StreamableHTTPServerTransport;
|
||||
let transport: NodeStreamableHTTPServerTransport;
|
||||
|
||||
if (sessionId && transports[sessionId]) {
|
||||
// Reuse existing transport
|
||||
transport = transports[sessionId];
|
||||
} else if (!sessionId && isInitializeRequest(req.body)) {
|
||||
// New initialization request
|
||||
transport = new StreamableHTTPServerTransport({
|
||||
transport = new NodeStreamableHTTPServerTransport({
|
||||
sessionIdGenerator: () => randomUUID(),
|
||||
onsessioninitialized: sessionId => {
|
||||
// Store the transport by session ID when session is initialized
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue