diff --git a/.changeset/rich-teams-cough.md b/.changeset/rich-teams-cough.md new file mode 100644 index 000000000..4ba2077e3 --- /dev/null +++ b/.changeset/rich-teams-cough.md @@ -0,0 +1,8 @@ +--- +'@modelcontextprotocol/express': patch +'@modelcontextprotocol/node': patch +'@modelcontextprotocol/test-integration': patch +'@modelcontextprotocol/server': patch +--- + +add middleware packages for node and express diff --git a/examples/server/package.json b/examples/server/package.json index a3a3d14c7..d40803a82 100644 --- a/examples/server/package.json +++ b/examples/server/package.json @@ -38,6 +38,8 @@ "hono": "catalog:runtimeServerOnly", "@modelcontextprotocol/examples-shared": "workspace:^", "@modelcontextprotocol/server": "workspace:^", + "@modelcontextprotocol/express": "workspace:^", + "@modelcontextprotocol/node": "workspace:^", "cors": "catalog:runtimeServerOnly", "express": "catalog:runtimeServerOnly", "zod": "catalog:runtimeShared" diff --git a/examples/server/src/elicitationFormExample.ts b/examples/server/src/elicitationFormExample.ts index f8863c17b..d770722bc 100644 --- a/examples/server/src/elicitationFormExample.ts +++ b/examples/server/src/elicitationFormExample.ts @@ -9,7 +9,9 @@ import { randomUUID } from 'node:crypto'; -import { createMcpExpressApp, isInitializeRequest, McpServer, StreamableHTTPServerTransport } from '@modelcontextprotocol/server'; +import { createMcpExpressApp } from '@modelcontextprotocol/express'; +import { StreamableHTTPServerTransport } from '@modelcontextprotocol/node'; +import { isInitializeRequest, McpServer } from '@modelcontextprotocol/server'; import { type Request, type Response } from 'express'; // Create MCP server - it will automatically use AjvJsonSchemaValidator with sensible defaults diff --git a/examples/server/src/elicitationUrlExample.ts b/examples/server/src/elicitationUrlExample.ts index 99f85d079..c1f87b6d2 100644 --- a/examples/server/src/elicitationUrlExample.ts +++ b/examples/server/src/elicitationUrlExample.ts @@ -10,18 +10,15 @@ import { randomUUID } from 'node:crypto'; import { setupAuthServer } from '@modelcontextprotocol/examples-shared'; -import type { CallToolResult, ElicitRequestURLParams, ElicitResult, OAuthMetadata } from '@modelcontextprotocol/server'; import { - checkResourceAllowed, createMcpExpressApp, getOAuthProtectedResourceMetadataUrl, - isInitializeRequest, mcpAuthMetadataRouter, - McpServer, - requireBearerAuth, - StreamableHTTPServerTransport, - UrlElicitationRequiredError -} from '@modelcontextprotocol/server'; + requireBearerAuth +} from '@modelcontextprotocol/express'; +import { StreamableHTTPServerTransport } from '@modelcontextprotocol/node'; +import type { CallToolResult, ElicitRequestURLParams, ElicitResult, OAuthMetadata } from '@modelcontextprotocol/server'; +import { checkResourceAllowed, isInitializeRequest, McpServer, UrlElicitationRequiredError } from '@modelcontextprotocol/server'; import cors from 'cors'; import type { Request, Response } from 'express'; import express from 'express'; diff --git a/examples/server/src/honoWebStandardStreamableHttp.ts b/examples/server/src/honoWebStandardStreamableHttp.ts index aef1e99e2..7d6012526 100644 --- a/examples/server/src/honoWebStandardStreamableHttp.ts +++ b/examples/server/src/honoWebStandardStreamableHttp.ts @@ -9,7 +9,7 @@ import { serve } from '@hono/node-server'; import type { CallToolResult } from '@modelcontextprotocol/server'; -import { McpServer, WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/server'; +import { HTTPServerTransport, McpServer } from '@modelcontextprotocol/server'; import { Hono } from 'hono'; import { cors } from 'hono/cors'; import * as z from 'zod/v4'; @@ -36,7 +36,7 @@ server.registerTool( ); // Create a stateless transport (no options = no session management) -const transport = new WebStandardStreamableHTTPServerTransport(); +const transport = new HTTPServerTransport(); // Create the Hono app const app = new Hono(); diff --git a/examples/server/src/jsonResponseStreamableHttp.ts b/examples/server/src/jsonResponseStreamableHttp.ts index 2199ebfbe..0caf98610 100644 --- a/examples/server/src/jsonResponseStreamableHttp.ts +++ b/examples/server/src/jsonResponseStreamableHttp.ts @@ -1,7 +1,9 @@ import { randomUUID } from 'node:crypto'; +import { createMcpExpressApp } from '@modelcontextprotocol/express'; +import { StreamableHTTPServerTransport } 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'; diff --git a/examples/server/src/simpleSseServer.ts b/examples/server/src/simpleSseServer.ts index 90561c62f..ee7f5386e 100644 --- a/examples/server/src/simpleSseServer.ts +++ b/examples/server/src/simpleSseServer.ts @@ -1,5 +1,7 @@ +import { createMcpExpressApp } from '@modelcontextprotocol/express'; +import { SSEServerTransport } from '@modelcontextprotocol/node'; import type { CallToolResult } from '@modelcontextprotocol/server'; -import { createMcpExpressApp, McpServer, SSEServerTransport } from '@modelcontextprotocol/server'; +import { McpServer } from '@modelcontextprotocol/server'; import type { Request, Response } from 'express'; import * as z from 'zod/v4'; diff --git a/examples/server/src/simpleStatelessStreamableHttp.ts b/examples/server/src/simpleStatelessStreamableHttp.ts index 3aee2c212..e7040984e 100644 --- a/examples/server/src/simpleStatelessStreamableHttp.ts +++ b/examples/server/src/simpleStatelessStreamableHttp.ts @@ -1,5 +1,7 @@ +import { createMcpExpressApp } from '@modelcontextprotocol/express'; +import { StreamableHTTPServerTransport } 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'; diff --git a/examples/server/src/simpleStreamableHttp.ts b/examples/server/src/simpleStreamableHttp.ts index 7613e3786..dc5c1e257 100644 --- a/examples/server/src/simpleStreamableHttp.ts +++ b/examples/server/src/simpleStreamableHttp.ts @@ -1,6 +1,13 @@ import { randomUUID } from 'node:crypto'; import { setupAuthServer } from '@modelcontextprotocol/examples-shared'; +import { + createMcpExpressApp, + getOAuthProtectedResourceMetadataUrl, + mcpAuthMetadataRouter, + requireBearerAuth +} from '@modelcontextprotocol/express'; +import { StreamableHTTPServerTransport } from '@modelcontextprotocol/node'; import type { CallToolResult, GetPromptResult, @@ -11,16 +18,11 @@ import type { } 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'; diff --git a/examples/server/src/simpleTaskInteractive.ts b/examples/server/src/simpleTaskInteractive.ts index 956c33f8e..17311c34d 100644 --- a/examples/server/src/simpleTaskInteractive.ts +++ b/examples/server/src/simpleTaskInteractive.ts @@ -11,6 +11,8 @@ import { randomUUID } from 'node:crypto'; +import { createMcpExpressApp } from '@modelcontextprotocol/express'; +import { StreamableHTTPServerTransport } 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'; diff --git a/examples/server/src/sseAndStreamableHttpCompatibleServer.ts b/examples/server/src/sseAndStreamableHttpCompatibleServer.ts index 335802d0a..6ed3d06e7 100644 --- a/examples/server/src/sseAndStreamableHttpCompatibleServer.ts +++ b/examples/server/src/sseAndStreamableHttpCompatibleServer.ts @@ -1,13 +1,9 @@ import { randomUUID } from 'node:crypto'; +import { createMcpExpressApp } from '@modelcontextprotocol/express'; +import { SSEServerTransport, StreamableHTTPServerTransport } from '@modelcontextprotocol/node'; import type { CallToolResult } from '@modelcontextprotocol/server'; -import { - createMcpExpressApp, - isInitializeRequest, - McpServer, - SSEServerTransport, - StreamableHTTPServerTransport -} from '@modelcontextprotocol/server'; +import { isInitializeRequest, McpServer } from '@modelcontextprotocol/server'; import type { Request, Response } from 'express'; import * as z from 'zod/v4'; diff --git a/examples/server/src/ssePollingExample.ts b/examples/server/src/ssePollingExample.ts index 4e3d36328..f45e71a97 100644 --- a/examples/server/src/ssePollingExample.ts +++ b/examples/server/src/ssePollingExample.ts @@ -14,8 +14,10 @@ */ import { randomUUID } from 'node:crypto'; +import { createMcpExpressApp } from '@modelcontextprotocol/express'; +import { StreamableHTTPServerTransport } 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'; diff --git a/examples/server/src/standaloneSseWithGetStreamableHttp.ts b/examples/server/src/standaloneSseWithGetStreamableHttp.ts index cceb24299..7bc1f28b6 100644 --- a/examples/server/src/standaloneSseWithGetStreamableHttp.ts +++ b/examples/server/src/standaloneSseWithGetStreamableHttp.ts @@ -1,7 +1,9 @@ import { randomUUID } from 'node:crypto'; +import { createMcpExpressApp } from '@modelcontextprotocol/express'; +import { StreamableHTTPServerTransport } 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 diff --git a/examples/server/tsconfig.json b/examples/server/tsconfig.json index 98d3a5b3f..cc85c5066 100644 --- a/examples/server/tsconfig.json +++ b/examples/server/tsconfig.json @@ -6,6 +6,8 @@ "paths": { "*": ["./*"], "@modelcontextprotocol/server": ["./node_modules/@modelcontextprotocol/server/src/index.ts"], + "@modelcontextprotocol/node": ["./node_modules/@modelcontextprotocol/node/src/index.ts"], + "@modelcontextprotocol/express": ["./node_modules/@modelcontextprotocol/express/src/index.ts"], "@modelcontextprotocol/core": [ "./node_modules/@modelcontextprotocol/server/node_modules/@modelcontextprotocol/core/src/index.ts" ], diff --git a/examples/shared/package.json b/examples/shared/package.json index 8287ca552..6dfcc2f68 100644 --- a/examples/shared/package.json +++ b/examples/shared/package.json @@ -35,6 +35,7 @@ }, "dependencies": { "@modelcontextprotocol/server": "workspace:^", + "@modelcontextprotocol/express": "workspace:^", "express": "catalog:runtimeServerOnly" }, "devDependencies": { diff --git a/examples/shared/src/demoInMemoryOAuthProvider.ts b/examples/shared/src/demoInMemoryOAuthProvider.ts index bcf11dd0c..02fc3e915 100644 --- a/examples/shared/src/demoInMemoryOAuthProvider.ts +++ b/examples/shared/src/demoInMemoryOAuthProvider.ts @@ -1,15 +1,9 @@ import { randomUUID } from 'node:crypto'; -import type { - AuthInfo, - AuthorizationParams, - OAuthClientInformationFull, - OAuthMetadata, - OAuthRegisteredClientsStore, - OAuthServerProvider, - OAuthTokens -} from '@modelcontextprotocol/server'; -import { createOAuthMetadata, InvalidRequestError, mcpAuthRouter, resourceUrlFromServerUrl } from '@modelcontextprotocol/server'; +import type { AuthorizationParams, OAuthRegisteredClientsStore, OAuthServerProvider } from '@modelcontextprotocol/express'; +import { createOAuthMetadata, mcpAuthRouter } from '@modelcontextprotocol/express'; +import type { AuthInfo, OAuthClientInformationFull, OAuthMetadata, OAuthTokens } from '@modelcontextprotocol/server'; +import { InvalidRequestError, resourceUrlFromServerUrl } from '@modelcontextprotocol/server'; import type { Request, Response } from 'express'; import express from 'express'; diff --git a/examples/shared/test/demoInMemoryOAuthProvider.test.ts b/examples/shared/test/demoInMemoryOAuthProvider.test.ts index 4018dddbe..f0d4bb4d9 100644 --- a/examples/shared/test/demoInMemoryOAuthProvider.test.ts +++ b/examples/shared/test/demoInMemoryOAuthProvider.test.ts @@ -1,5 +1,5 @@ -import type { OAuthClientInformationFull } from '@modelcontextprotocol/core'; -import type { AuthorizationParams } from '@modelcontextprotocol/server'; +import type { AuthorizationParams } from '@modelcontextprotocol/express'; +import type { OAuthClientInformationFull } from '@modelcontextprotocol/server'; import { InvalidRequestError } from '@modelcontextprotocol/server'; import { createExpressResponseMock } from '@modelcontextprotocol/test-helpers'; import type { Response } from 'express'; diff --git a/examples/shared/tsconfig.json b/examples/shared/tsconfig.json index aa994f939..69d2f966a 100644 --- a/examples/shared/tsconfig.json +++ b/examples/shared/tsconfig.json @@ -6,6 +6,7 @@ "paths": { "*": ["./*"], "@modelcontextprotocol/server": ["./node_modules/@modelcontextprotocol/server/src/index.ts"], + "@modelcontextprotocol/express": ["./node_modules/@modelcontextprotocol/express/src/index.ts"], "@modelcontextprotocol/core": [ "./node_modules/@modelcontextprotocol/server/node_modules/@modelcontextprotocol/core/src/index.ts" ], diff --git a/packages/middleware/express/README.md b/packages/middleware/express/README.md new file mode 100644 index 000000000..6e9fb2a82 --- /dev/null +++ b/packages/middleware/express/README.md @@ -0,0 +1,35 @@ +# @modelcontextprotocol/express + +Express middleware and OAuth server support for MCP. + +## Installation + +```bash +npm install @modelcontextprotocol/express +``` + +## Exports + +- `createMcpExpressApp()` - Create an Express app with MCP-compatible middleware +- `mcpAuthRouter()` - OAuth 2.0 authorization server router +- `requireBearerAuth()` - Bearer token authentication middleware +- `mcpAuthMetadataRouter()` - Protected resource metadata endpoints + +## Usage + +```typescript +import { createMcpExpressApp, mcpAuthRouter } from '@modelcontextprotocol/express'; +import { StreamableHTTPServerTransport } from '@modelcontextprotocol/node'; +import { McpServer } from '@modelcontextprotocol/server'; + +const app = createMcpExpressApp(); +const server = new McpServer({ name: 'my-server', version: '1.0.0' }); + +app.post('/mcp', async (req, res) => { + const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => crypto.randomUUID() }); + await server.connect(transport); + await transport.handleRequest(req, res, req.body); +}); + +app.listen(3000); +``` diff --git a/packages/middleware/express/eslint.config.mjs b/packages/middleware/express/eslint.config.mjs new file mode 100644 index 000000000..a3c43f6ac --- /dev/null +++ b/packages/middleware/express/eslint.config.mjs @@ -0,0 +1,12 @@ +// @ts-check + +import baseConfig from '@modelcontextprotocol/eslint-config'; + +export default [ + ...baseConfig, + { + settings: { + 'import/internal-regex': '^@modelcontextprotocol/' + } + } +]; diff --git a/packages/middleware/express/package.json b/packages/middleware/express/package.json new file mode 100644 index 000000000..ee1372c82 --- /dev/null +++ b/packages/middleware/express/package.json @@ -0,0 +1,80 @@ +{ + "name": "@modelcontextprotocol/express", + "version": "2.0.0-alpha.0", + "description": "Express adapter for Model Context Protocol servers", + "license": "MIT", + "author": "Anthropic, PBC (https://anthropic.com)", + "homepage": "https://modelcontextprotocol.io", + "bugs": "https://github.com/modelcontextprotocol/typescript-sdk/issues", + "type": "module", + "repository": { + "type": "git", + "url": "git+https://github.com/modelcontextprotocol/typescript-sdk.git" + }, + "engines": { + "node": ">=20", + "pnpm": ">=10.24.0" + }, + "packageManager": "pnpm@10.24.0", + "keywords": [ + "modelcontextprotocol", + "mcp", + "express" + ], + "exports": { + ".": { + "types": "./dist/index.d.mts", + "import": "./dist/index.mjs" + } + }, + "files": [ + "dist" + ], + "scripts": { + "typecheck": "tsgo -p tsconfig.json --noEmit", + "build": "tsdown", + "build:watch": "tsdown --watch", + "prepack": "npm run build", + "lint": "eslint src/ && prettier --ignore-path ../../../.prettierignore --check .", + "lint:fix": "eslint src/ --fix && prettier --ignore-path ../../../.prettierignore --write .", + "check": "npm run typecheck && npm run lint", + "test": "vitest run", + "test:watch": "vitest" + }, + "dependencies": { + "cors": "catalog:runtimeServerOnly", + "express": "catalog:runtimeServerOnly", + "express-rate-limit": "catalog:runtimeServerOnly", + "pkce-challenge": "catalog:runtimeShared", + "zod": "catalog:runtimeShared" + }, + "peerDependencies": { + "@modelcontextprotocol/server": "workspace:*", + "@modelcontextprotocol/node": "workspace:*" + }, + "devDependencies": { + "@modelcontextprotocol/core": "workspace:^", + "@modelcontextprotocol/server": "workspace:^", + "@modelcontextprotocol/node": "workspace:^", + "@modelcontextprotocol/tsconfig": "workspace:^", + "@modelcontextprotocol/vitest-config": "workspace:^", + "@modelcontextprotocol/eslint-config": "workspace:^", + "@modelcontextprotocol/test-helpers": "workspace:^", + "@eslint/js": "catalog:devTools", + "@types/cors": "catalog:devTools", + "@types/express": "catalog:devTools", + "@types/express-serve-static-core": "catalog:devTools", + "@types/supertest": "catalog:devTools", + "@typescript/native-preview": "catalog:devTools", + "eslint": "catalog:devTools", + "eslint-config-prettier": "catalog:devTools", + "eslint-plugin-n": "catalog:devTools", + "prettier": "catalog:devTools", + "supertest": "catalog:devTools", + "tsdown": "catalog:devTools", + "tsx": "catalog:devTools", + "typescript": "catalog:devTools", + "typescript-eslint": "catalog:devTools", + "vitest": "catalog:devTools" + } +} diff --git a/packages/server/src/server/auth/clients.ts b/packages/middleware/express/src/auth/clients.ts similarity index 100% rename from packages/server/src/server/auth/clients.ts rename to packages/middleware/express/src/auth/clients.ts diff --git a/packages/server/src/server/auth/handlers/authorize.ts b/packages/middleware/express/src/auth/handlers/authorize.ts similarity index 100% rename from packages/server/src/server/auth/handlers/authorize.ts rename to packages/middleware/express/src/auth/handlers/authorize.ts diff --git a/packages/middleware/express/src/auth/handlers/index.ts b/packages/middleware/express/src/auth/handlers/index.ts new file mode 100644 index 000000000..66d87e043 --- /dev/null +++ b/packages/middleware/express/src/auth/handlers/index.ts @@ -0,0 +1,5 @@ +export * from './authorize.js'; +export * from './metadata.js'; +export * from './register.js'; +export * from './revoke.js'; +export * from './token.js'; diff --git a/packages/server/src/server/auth/handlers/metadata.ts b/packages/middleware/express/src/auth/handlers/metadata.ts similarity index 100% rename from packages/server/src/server/auth/handlers/metadata.ts rename to packages/middleware/express/src/auth/handlers/metadata.ts diff --git a/packages/server/src/server/auth/handlers/register.ts b/packages/middleware/express/src/auth/handlers/register.ts similarity index 100% rename from packages/server/src/server/auth/handlers/register.ts rename to packages/middleware/express/src/auth/handlers/register.ts diff --git a/packages/server/src/server/auth/handlers/revoke.ts b/packages/middleware/express/src/auth/handlers/revoke.ts similarity index 100% rename from packages/server/src/server/auth/handlers/revoke.ts rename to packages/middleware/express/src/auth/handlers/revoke.ts diff --git a/packages/server/src/server/auth/handlers/token.ts b/packages/middleware/express/src/auth/handlers/token.ts similarity index 100% rename from packages/server/src/server/auth/handlers/token.ts rename to packages/middleware/express/src/auth/handlers/token.ts diff --git a/packages/middleware/express/src/auth/index.ts b/packages/middleware/express/src/auth/index.ts new file mode 100644 index 000000000..2616d645e --- /dev/null +++ b/packages/middleware/express/src/auth/index.ts @@ -0,0 +1,6 @@ +export * from './clients.js'; +export * from './handlers/index.js'; +export * from './middleware/index.js'; +export * from './provider.js'; +export * from './providers/index.js'; +export * from './router.js'; diff --git a/packages/server/src/server/auth/middleware/allowedMethods.ts b/packages/middleware/express/src/auth/middleware/allowedMethods.ts similarity index 100% rename from packages/server/src/server/auth/middleware/allowedMethods.ts rename to packages/middleware/express/src/auth/middleware/allowedMethods.ts diff --git a/packages/server/src/server/auth/middleware/bearerAuth.ts b/packages/middleware/express/src/auth/middleware/bearerAuth.ts similarity index 100% rename from packages/server/src/server/auth/middleware/bearerAuth.ts rename to packages/middleware/express/src/auth/middleware/bearerAuth.ts diff --git a/packages/server/src/server/auth/middleware/clientAuth.ts b/packages/middleware/express/src/auth/middleware/clientAuth.ts similarity index 100% rename from packages/server/src/server/auth/middleware/clientAuth.ts rename to packages/middleware/express/src/auth/middleware/clientAuth.ts diff --git a/packages/middleware/express/src/auth/middleware/index.ts b/packages/middleware/express/src/auth/middleware/index.ts new file mode 100644 index 000000000..c4fbbe419 --- /dev/null +++ b/packages/middleware/express/src/auth/middleware/index.ts @@ -0,0 +1,3 @@ +export * from './allowedMethods.js'; +export * from './bearerAuth.js'; +export * from './clientAuth.js'; diff --git a/packages/server/src/server/auth/provider.ts b/packages/middleware/express/src/auth/provider.ts similarity index 100% rename from packages/server/src/server/auth/provider.ts rename to packages/middleware/express/src/auth/provider.ts diff --git a/packages/middleware/express/src/auth/providers/index.ts b/packages/middleware/express/src/auth/providers/index.ts new file mode 100644 index 000000000..1311989cd --- /dev/null +++ b/packages/middleware/express/src/auth/providers/index.ts @@ -0,0 +1 @@ +export * from './proxyProvider.js'; diff --git a/packages/server/src/server/auth/providers/proxyProvider.ts b/packages/middleware/express/src/auth/providers/proxyProvider.ts similarity index 100% rename from packages/server/src/server/auth/providers/proxyProvider.ts rename to packages/middleware/express/src/auth/providers/proxyProvider.ts diff --git a/packages/server/src/server/auth/router.ts b/packages/middleware/express/src/auth/router.ts similarity index 100% rename from packages/server/src/server/auth/router.ts rename to packages/middleware/express/src/auth/router.ts diff --git a/packages/server/src/server/express.ts b/packages/middleware/express/src/express.ts similarity index 100% rename from packages/server/src/server/express.ts rename to packages/middleware/express/src/express.ts diff --git a/packages/middleware/express/src/index.ts b/packages/middleware/express/src/index.ts new file mode 100644 index 000000000..a61abc600 --- /dev/null +++ b/packages/middleware/express/src/index.ts @@ -0,0 +1,6 @@ +// Express app factory and middleware +export * from './express.js'; +export * from './middleware/index.js'; + +// OAuth/Auth - Express middleware and routers +export * from './auth/index.js'; diff --git a/packages/server/src/server/middleware/hostHeaderValidation.ts b/packages/middleware/express/src/middleware/hostHeaderValidation.ts similarity index 100% rename from packages/server/src/server/middleware/hostHeaderValidation.ts rename to packages/middleware/express/src/middleware/hostHeaderValidation.ts diff --git a/packages/middleware/express/src/middleware/index.ts b/packages/middleware/express/src/middleware/index.ts new file mode 100644 index 000000000..2945a8768 --- /dev/null +++ b/packages/middleware/express/src/middleware/index.ts @@ -0,0 +1 @@ +export * from './hostHeaderValidation.js'; diff --git a/packages/server/test/server/auth/handlers/authorize.test.ts b/packages/middleware/express/test/auth/handlers/authorize.test.ts similarity index 97% rename from packages/server/test/server/auth/handlers/authorize.test.ts rename to packages/middleware/express/test/auth/handlers/authorize.test.ts index b84de3bc3..874230e42 100644 --- a/packages/server/test/server/auth/handlers/authorize.test.ts +++ b/packages/middleware/express/test/auth/handlers/authorize.test.ts @@ -4,10 +4,10 @@ import type { Response } from 'express'; import express from 'express'; import supertest from 'supertest'; -import type { OAuthRegisteredClientsStore } from '../../../../src/server/auth/clients.js'; -import type { AuthorizationHandlerOptions } from '../../../../src/server/auth/handlers/authorize.js'; -import { authorizationHandler } from '../../../../src/server/auth/handlers/authorize.js'; -import type { AuthorizationParams, OAuthServerProvider } from '../../../../src/server/auth/provider.js'; +import type { OAuthRegisteredClientsStore } from '../../../src/auth/clients.js'; +import type { AuthorizationHandlerOptions } from '../../../src/auth/handlers/authorize.js'; +import { authorizationHandler } from '../../../src/auth/handlers/authorize.js'; +import type { AuthorizationParams, OAuthServerProvider } from '../../../src/auth/provider.js'; describe('Authorization Handler', () => { // Mock client data diff --git a/packages/server/test/server/auth/handlers/metadata.test.ts b/packages/middleware/express/test/auth/handlers/metadata.test.ts similarity index 97% rename from packages/server/test/server/auth/handlers/metadata.test.ts rename to packages/middleware/express/test/auth/handlers/metadata.test.ts index 0dc51e51d..223769371 100644 --- a/packages/server/test/server/auth/handlers/metadata.test.ts +++ b/packages/middleware/express/test/auth/handlers/metadata.test.ts @@ -2,7 +2,7 @@ import type { OAuthMetadata } from '@modelcontextprotocol/core'; import express from 'express'; import supertest from 'supertest'; -import { metadataHandler } from '../../../../src/server/auth/handlers/metadata.js'; +import { metadataHandler } from '../../../src/auth/handlers/metadata.js'; describe('Metadata Handler', () => { const exampleMetadata: OAuthMetadata = { diff --git a/packages/server/test/server/auth/handlers/register.test.ts b/packages/middleware/express/test/auth/handlers/register.test.ts similarity index 98% rename from packages/server/test/server/auth/handlers/register.test.ts rename to packages/middleware/express/test/auth/handlers/register.test.ts index b10e048ed..ccf7df0e9 100644 --- a/packages/server/test/server/auth/handlers/register.test.ts +++ b/packages/middleware/express/test/auth/handlers/register.test.ts @@ -3,9 +3,9 @@ import express from 'express'; import supertest from 'supertest'; import type { MockInstance } from 'vitest'; -import type { OAuthRegisteredClientsStore } from '../../../../src/server/auth/clients.js'; -import type { ClientRegistrationHandlerOptions } from '../../../../src/server/auth/handlers/register.js'; -import { clientRegistrationHandler } from '../../../../src/server/auth/handlers/register.js'; +import type { OAuthRegisteredClientsStore } from '../../../src/auth/clients.js'; +import type { ClientRegistrationHandlerOptions } from '../../../src/auth/handlers/register.js'; +import { clientRegistrationHandler } from '../../../src/auth/handlers/register.js'; describe('Client Registration Handler', () => { // Mock client store with registration support diff --git a/packages/server/test/server/auth/handlers/revoke.test.ts b/packages/middleware/express/test/auth/handlers/revoke.test.ts similarity index 96% rename from packages/server/test/server/auth/handlers/revoke.test.ts rename to packages/middleware/express/test/auth/handlers/revoke.test.ts index 61ff51b24..f13c9ced0 100644 --- a/packages/server/test/server/auth/handlers/revoke.test.ts +++ b/packages/middleware/express/test/auth/handlers/revoke.test.ts @@ -5,10 +5,10 @@ import express from 'express'; import supertest from 'supertest'; import type { MockInstance } from 'vitest'; -import type { OAuthRegisteredClientsStore } from '../../../../src/server/auth/clients.js'; -import type { RevocationHandlerOptions } from '../../../../src/server/auth/handlers/revoke.js'; -import { revocationHandler } from '../../../../src/server/auth/handlers/revoke.js'; -import type { AuthorizationParams, OAuthServerProvider } from '../../../../src/server/auth/provider.js'; +import type { OAuthRegisteredClientsStore } from '../../../src/auth/clients.js'; +import type { RevocationHandlerOptions } from '../../../src/auth/handlers/revoke.js'; +import { revocationHandler } from '../../../src/auth/handlers/revoke.js'; +import type { AuthorizationParams, OAuthServerProvider } from '../../../src/auth/provider.js'; describe('Revocation Handler', () => { // Mock client data diff --git a/packages/server/test/server/auth/handlers/token.test.ts b/packages/middleware/express/test/auth/handlers/token.test.ts similarity index 97% rename from packages/server/test/server/auth/handlers/token.test.ts rename to packages/middleware/express/test/auth/handlers/token.test.ts index 02eab891f..5f67576c5 100644 --- a/packages/server/test/server/auth/handlers/token.test.ts +++ b/packages/middleware/express/test/auth/handlers/token.test.ts @@ -6,11 +6,11 @@ import * as pkceChallenge from 'pkce-challenge'; import supertest from 'supertest'; import { type Mock } from 'vitest'; -import type { OAuthRegisteredClientsStore } from '../../../../src/server/auth/clients.js'; -import type { TokenHandlerOptions } from '../../../../src/server/auth/handlers/token.js'; -import { tokenHandler } from '../../../../src/server/auth/handlers/token.js'; -import type { AuthorizationParams, OAuthServerProvider } from '../../../../src/server/auth/provider.js'; -import { ProxyOAuthServerProvider } from '../../../../src/server/auth/providers/proxyProvider.js'; +import type { OAuthRegisteredClientsStore } from '../../../src/auth/clients.js'; +import type { TokenHandlerOptions } from '../../../src/auth/handlers/token.js'; +import { tokenHandler } from '../../../src/auth/handlers/token.js'; +import type { AuthorizationParams, OAuthServerProvider } from '../../../src/auth/provider.js'; +import { ProxyOAuthServerProvider } from '../../../src/auth/providers/proxyProvider.js'; // Mock pkce-challenge vi.mock('pkce-challenge', () => ({ diff --git a/packages/server/test/server/auth/middleware/allowedMethods.test.ts b/packages/middleware/express/test/auth/middleware/allowedMethods.test.ts similarity index 96% rename from packages/server/test/server/auth/middleware/allowedMethods.test.ts rename to packages/middleware/express/test/auth/middleware/allowedMethods.test.ts index 40e9c3b1f..c71fc0d21 100644 --- a/packages/server/test/server/auth/middleware/allowedMethods.test.ts +++ b/packages/middleware/express/test/auth/middleware/allowedMethods.test.ts @@ -2,7 +2,7 @@ import type { Request, Response } from 'express'; import express from 'express'; import request from 'supertest'; -import { allowedMethods } from '../../../../src/server/auth/middleware/allowedMethods.js'; +import { allowedMethods } from '../../../src/auth/middleware/allowedMethods.js'; describe('allowedMethods', () => { let app: express.Express; diff --git a/packages/server/test/server/auth/middleware/bearerAuth.test.ts b/packages/middleware/express/test/auth/middleware/bearerAuth.test.ts similarity index 99% rename from packages/server/test/server/auth/middleware/bearerAuth.test.ts rename to packages/middleware/express/test/auth/middleware/bearerAuth.test.ts index 7b464bbff..6ab3304a3 100644 --- a/packages/server/test/server/auth/middleware/bearerAuth.test.ts +++ b/packages/middleware/express/test/auth/middleware/bearerAuth.test.ts @@ -4,8 +4,8 @@ import { createExpressResponseMock } from '@modelcontextprotocol/test-helpers'; import type { Request, Response } from 'express'; import type { Mock } from 'vitest'; -import { requireBearerAuth } from '../../../../src/server/auth/middleware/bearerAuth.js'; -import type { OAuthTokenVerifier } from '../../../../src/server/auth/provider.js'; +import { requireBearerAuth } from '../../../src/auth/middleware/bearerAuth.js'; +import type { OAuthTokenVerifier } from '../../../src/auth/provider.js'; // Mock verifier const mockVerifyAccessToken = vi.fn(); diff --git a/packages/server/test/server/auth/middleware/clientAuth.test.ts b/packages/middleware/express/test/auth/middleware/clientAuth.test.ts similarity index 95% rename from packages/server/test/server/auth/middleware/clientAuth.test.ts rename to packages/middleware/express/test/auth/middleware/clientAuth.test.ts index 55a00f0c2..8ce681554 100644 --- a/packages/server/test/server/auth/middleware/clientAuth.test.ts +++ b/packages/middleware/express/test/auth/middleware/clientAuth.test.ts @@ -2,9 +2,9 @@ import type { OAuthClientInformationFull } from '@modelcontextprotocol/core'; import express from 'express'; import supertest from 'supertest'; -import type { OAuthRegisteredClientsStore } from '../../../../src/server/auth/clients.js'; -import type { ClientAuthenticationMiddlewareOptions } from '../../../../src/server/auth/middleware/clientAuth.js'; -import { authenticateClient } from '../../../../src/server/auth/middleware/clientAuth.js'; +import type { OAuthRegisteredClientsStore } from '../../../src/auth/clients.js'; +import type { ClientAuthenticationMiddlewareOptions } from '../../../src/auth/middleware/clientAuth.js'; +import { authenticateClient } from '../../../src/auth/middleware/clientAuth.js'; describe('clientAuth middleware', () => { // Mock client store diff --git a/packages/server/test/server/auth/providers/proxyProvider.test.ts b/packages/middleware/express/test/auth/providers/proxyProvider.test.ts similarity index 98% rename from packages/server/test/server/auth/providers/proxyProvider.test.ts rename to packages/middleware/express/test/auth/providers/proxyProvider.test.ts index 375179e5b..6a3606df2 100644 --- a/packages/server/test/server/auth/providers/proxyProvider.test.ts +++ b/packages/middleware/express/test/auth/providers/proxyProvider.test.ts @@ -3,8 +3,8 @@ import { InsufficientScopeError, InvalidTokenError, ServerError } from '@modelco import type { Response } from 'express'; import { type Mock } from 'vitest'; -import type { ProxyOptions } from '../../../../src/server/auth/providers/proxyProvider.js'; -import { ProxyOAuthServerProvider } from '../../../../src/server/auth/providers/proxyProvider.js'; +import type { ProxyOptions } from '../../../src/auth/providers/proxyProvider.js'; +import { ProxyOAuthServerProvider } from '../../../src/auth/providers/proxyProvider.js'; describe('Proxy OAuth Server Provider', () => { // Mock client data diff --git a/packages/server/test/server/auth/router.test.ts b/packages/middleware/express/test/auth/router.test.ts similarity index 98% rename from packages/server/test/server/auth/router.test.ts rename to packages/middleware/express/test/auth/router.test.ts index 250fca4c4..72c32ef83 100644 --- a/packages/server/test/server/auth/router.test.ts +++ b/packages/middleware/express/test/auth/router.test.ts @@ -5,10 +5,10 @@ import type { Response } from 'express'; import express from 'express'; import supertest from 'supertest'; -import type { OAuthRegisteredClientsStore } from '../../../src/server/auth/clients.js'; -import type { AuthorizationParams, OAuthServerProvider } from '../../../src/server/auth/provider.js'; -import type { AuthMetadataOptions, AuthRouterOptions } from '../../../src/server/auth/router.js'; -import { mcpAuthMetadataRouter, mcpAuthRouter } from '../../../src/server/auth/router.js'; +import type { OAuthRegisteredClientsStore } from '../../src/auth/clients.js'; +import type { AuthorizationParams, OAuthServerProvider } from '../../src/auth/provider.js'; +import type { AuthMetadataOptions, AuthRouterOptions } from '../../src/auth/router.js'; +import { mcpAuthMetadataRouter, mcpAuthRouter } from '../../src/auth/router.js'; describe('MCP Auth Router', () => { // Setup mock provider with full capabilities diff --git a/packages/middleware/express/tsconfig.json b/packages/middleware/express/tsconfig.json new file mode 100644 index 000000000..87a06faf4 --- /dev/null +++ b/packages/middleware/express/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "@modelcontextprotocol/tsconfig", + "include": ["./"], + "exclude": ["node_modules", "dist"], + "compilerOptions": { + "paths": { + "*": ["./*"], + "@modelcontextprotocol/core": ["./node_modules/@modelcontextprotocol/core/src/index.ts"], + "@modelcontextprotocol/server": ["./node_modules/@modelcontextprotocol/server/src/index.ts"], + "@modelcontextprotocol/node": ["./node_modules/@modelcontextprotocol/node/src/index.ts"], + "@modelcontextprotocol/test-helpers": ["./node_modules/@modelcontextprotocol/test-helpers/src/index.ts"] + } + } +} diff --git a/packages/middleware/express/tsdown.config.ts b/packages/middleware/express/tsdown.config.ts new file mode 100644 index 000000000..30fd89f61 --- /dev/null +++ b/packages/middleware/express/tsdown.config.ts @@ -0,0 +1,36 @@ +import { defineConfig } from 'tsdown'; + +export default defineConfig({ + // 1. Entry Points + // Directly matches package.json include/exclude globs + entry: ['src/index.ts'], + + // 2. Output Configuration + format: ['esm'], + outDir: 'dist', + clean: true, // Recommended: Cleans 'dist' before building + sourcemap: true, + + // 3. Platform & Target + target: 'esnext', + platform: 'node', + shims: true, // Polyfills common Node.js shims (__dirname, etc.) + + // 4. Type Definitions + // Bundles d.ts files into a single output + dts: { + resolver: 'tsc', + // override just for DTS generation: + compilerOptions: { + baseUrl: '.', + paths: { + '@modelcontextprotocol/core': ['../../core/src/index.ts'], + '@modelcontextprotocol/server': ['../../server/src/index.ts'], + '@modelcontextprotocol/node': ['../node/src/index.ts'] + } + } + }, + // 5. Vendoring Strategy - Bundle the code for this specific package into the output, + // but treat all other dependencies as external (require/import). + noExternal: ['@modelcontextprotocol/core', '@modelcontextprotocol/server', '@modelcontextprotocol/node'] +}); diff --git a/packages/middleware/express/vitest.config.js b/packages/middleware/express/vitest.config.js new file mode 100644 index 000000000..496fca320 --- /dev/null +++ b/packages/middleware/express/vitest.config.js @@ -0,0 +1,3 @@ +import baseConfig from '@modelcontextprotocol/vitest-config'; + +export default baseConfig; diff --git a/packages/middleware/node/README.md b/packages/middleware/node/README.md new file mode 100644 index 000000000..1d8ea372c --- /dev/null +++ b/packages/middleware/node/README.md @@ -0,0 +1,33 @@ +# @modelcontextprotocol/node + +Node.js HTTP adapters for MCP servers. Wraps the Web Standards `HTTPServerTransport` for compatibility with Node.js HTTP types (`IncomingMessage`/`ServerResponse`). + +## Installation + +```bash +npm install @modelcontextprotocol/node +``` + +## Exports + +- `StreamableHTTPServerTransport` - HTTP transport for Node.js servers +- `SSEServerTransport` - Legacy SSE transport (protocol version 2024-11-05) + +## Usage + +```typescript +import { StreamableHTTPServerTransport } from '@modelcontextprotocol/node'; +import { McpServer } from '@modelcontextprotocol/server'; +import http from 'node:http'; + +const server = new McpServer({ name: 'my-server', version: '1.0.0' }); +const transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: () => crypto.randomUUID() +}); + +await server.connect(transport); + +http.createServer((req, res) => { + transport.handleRequest(req, res); +}).listen(3000); +``` diff --git a/packages/middleware/node/eslint.config.js b/packages/middleware/node/eslint.config.js new file mode 100644 index 000000000..7c44076fc --- /dev/null +++ b/packages/middleware/node/eslint.config.js @@ -0,0 +1,3 @@ +import config from '@modelcontextprotocol/eslint-config'; + +export default config; diff --git a/packages/middleware/node/package.json b/packages/middleware/node/package.json new file mode 100644 index 000000000..1bc32b9cf --- /dev/null +++ b/packages/middleware/node/package.json @@ -0,0 +1,74 @@ +{ + "name": "@modelcontextprotocol/node", + "version": "2.0.0-alpha.0", + "description": "Node.js HTTP adapter for Model Context Protocol server transports", + "license": "MIT", + "author": "Anthropic, PBC (https://anthropic.com)", + "homepage": "https://modelcontextprotocol.io", + "bugs": "https://github.com/modelcontextprotocol/typescript-sdk/issues", + "type": "module", + "repository": { + "type": "git", + "url": "git+https://github.com/modelcontextprotocol/typescript-sdk.git" + }, + "engines": { + "node": ">=20", + "pnpm": ">=10.24.0" + }, + "packageManager": "pnpm@10.24.0", + "keywords": [ + "modelcontextprotocol", + "mcp", + "node" + ], + "exports": { + ".": { + "types": "./dist/index.d.mts", + "import": "./dist/index.mjs" + } + }, + "files": [ + "dist" + ], + "scripts": { + "typecheck": "tsgo -p tsconfig.json --noEmit", + "build": "tsdown", + "build:watch": "tsdown --watch", + "prepack": "npm run build", + "lint": "eslint src/ && prettier --ignore-path ../../../.prettierignore --check .", + "lint:fix": "eslint src/ --fix && prettier --ignore-path ../../../.prettierignore --write .", + "check": "npm run typecheck && npm run lint", + "test": "vitest run", + "test:watch": "vitest" + }, + "dependencies": { + "@hono/node-server": "catalog:runtimeServerOnly", + "content-type": "catalog:runtimeServerOnly", + "raw-body": "catalog:runtimeServerOnly", + "@modelcontextprotocol/core": "workspace:^" + }, + "peerDependencies": { + "@modelcontextprotocol/server": "workspace:*" + }, + "devDependencies": { + "@modelcontextprotocol/server": "workspace:^", + "@modelcontextprotocol/tsconfig": "workspace:^", + "@modelcontextprotocol/vitest-config": "workspace:^", + "@modelcontextprotocol/eslint-config": "workspace:^", + "@modelcontextprotocol/test-helpers": "workspace:^", + "@eslint/js": "catalog:devTools", + "@types/content-type": "catalog:devTools", + "@types/supertest": "catalog:devTools", + "@typescript/native-preview": "catalog:devTools", + "eslint": "catalog:devTools", + "eslint-config-prettier": "catalog:devTools", + "eslint-plugin-n": "catalog:devTools", + "prettier": "catalog:devTools", + "supertest": "catalog:devTools", + "tsdown": "catalog:devTools", + "tsx": "catalog:devTools", + "typescript": "catalog:devTools", + "typescript-eslint": "catalog:devTools", + "vitest": "catalog:devTools" + } +} diff --git a/packages/server/src/server/streamableHttp.ts b/packages/middleware/node/src/http.ts similarity index 76% rename from packages/server/src/server/streamableHttp.ts rename to packages/middleware/node/src/http.ts index f9ee07ca8..a5eeb7c91 100644 --- a/packages/server/src/server/streamableHttp.ts +++ b/packages/middleware/node/src/http.ts @@ -1,32 +1,31 @@ /** * Node.js HTTP Streamable HTTP Server Transport * - * This is a thin wrapper around `WebStandardStreamableHTTPServerTransport` that provides + * This is a thin wrapper around `HTTPServerTransport` that provides * compatibility with Node.js HTTP server (IncomingMessage/ServerResponse). * - * For web-standard environments (Cloudflare Workers, Deno, Bun), use `WebStandardStreamableHTTPServerTransport` directly. + * For web-standard environments (Cloudflare Workers, Deno, Bun), use `HTTPServerTransport` directly from `@modelcontextprotocol/server`. */ import type { IncomingMessage, ServerResponse } from 'node:http'; import { getRequestListener } from '@hono/node-server'; import type { AuthInfo, JSONRPCMessage, MessageExtraInfo, RequestId, Transport } from '@modelcontextprotocol/core'; - -import type { WebStandardStreamableHTTPServerTransportOptions } from './webStandardStreamableHttp.js'; -import { WebStandardStreamableHTTPServerTransport } from './webStandardStreamableHttp.js'; +import type { HTTPServerTransportOptions } from '@modelcontextprotocol/server'; +import { HTTPServerTransport } from '@modelcontextprotocol/server'; /** * Configuration options for StreamableHTTPServerTransport * - * This is an alias for WebStandardStreamableHTTPServerTransportOptions for backward compatibility. + * This is an alias for HTTPServerTransportOptions for backward compatibility. */ -export type StreamableHTTPServerTransportOptions = WebStandardStreamableHTTPServerTransportOptions; +export type StreamableHTTPServerTransportOptions = HTTPServerTransportOptions; /** * Server transport for Streamable HTTP: this implements the MCP Streamable HTTP transport specification. * It supports both SSE streaming and direct HTTP responses. * - * This is a wrapper around `WebStandardStreamableHTTPServerTransport` that provides Node.js HTTP compatibility. + * This is a wrapper around `HTTPServerTransport` that provides Node.js HTTP compatibility. * It uses the `@hono/node-server` library to convert between Node.js HTTP and Web Standard APIs. * * Usage example: @@ -60,20 +59,20 @@ export type StreamableHTTPServerTransportOptions = WebStandardStreamableHTTPServ * - No session validation is performed */ export class StreamableHTTPServerTransport implements Transport { - private _webStandardTransport: WebStandardStreamableHTTPServerTransport; + private _httpServerTransport: HTTPServerTransport; private _requestListener: ReturnType; // Store auth and parsedBody per request for passing through to handleRequest private _requestContext: WeakMap = new WeakMap(); constructor(options: StreamableHTTPServerTransportOptions = {}) { - this._webStandardTransport = new WebStandardStreamableHTTPServerTransport(options); + this._httpServerTransport = new HTTPServerTransport(options); // Create a request listener that wraps the web standard transport // getRequestListener converts Node.js HTTP to Web Standard and properly handles SSE streaming this._requestListener = getRequestListener(async (webRequest: Request) => { // Get context if available (set during handleRequest) const context = this._requestContext.get(webRequest); - return this._webStandardTransport.handleRequest(webRequest, { + return this._httpServerTransport.handleRequest(webRequest, { authInfo: context?.authInfo, parsedBody: context?.parsedBody }); @@ -84,40 +83,40 @@ export class StreamableHTTPServerTransport implements Transport { * Gets the session ID for this transport instance. */ get sessionId(): string | undefined { - return this._webStandardTransport.sessionId; + return this._httpServerTransport.sessionId; } /** * Sets callback for when the transport is closed. */ set onclose(handler: (() => void) | undefined) { - this._webStandardTransport.onclose = handler; + this._httpServerTransport.onclose = handler; } get onclose(): (() => void) | undefined { - return this._webStandardTransport.onclose; + return this._httpServerTransport.onclose; } /** * Sets callback for transport errors. */ set onerror(handler: ((error: Error) => void) | undefined) { - this._webStandardTransport.onerror = handler; + this._httpServerTransport.onerror = handler; } get onerror(): ((error: Error) => void) | undefined { - return this._webStandardTransport.onerror; + return this._httpServerTransport.onerror; } /** * Sets callback for incoming messages. */ set onmessage(handler: ((message: JSONRPCMessage, extra?: MessageExtraInfo) => void) | undefined) { - this._webStandardTransport.onmessage = handler; + this._httpServerTransport.onmessage = handler; } get onmessage(): ((message: JSONRPCMessage, extra?: MessageExtraInfo) => void) | undefined { - return this._webStandardTransport.onmessage; + return this._httpServerTransport.onmessage; } /** @@ -125,28 +124,28 @@ export class StreamableHTTPServerTransport implements Transport { * for the Streamable HTTP transport as connections are managed per-request. */ async start(): Promise { - return this._webStandardTransport.start(); + return this._httpServerTransport.start(); } /** * Closes the transport and all active connections. */ async close(): Promise { - return this._webStandardTransport.close(); + return this._httpServerTransport.close(); } /** * Sends a JSON-RPC message through the transport. */ async send(message: JSONRPCMessage, options?: { relatedRequestId?: RequestId }): Promise { - return this._webStandardTransport.send(message, options); + return this._httpServerTransport.send(message, options); } /** * Handles an incoming HTTP request, whether GET or POST. * * This method converts Node.js HTTP objects to Web Standard Request/Response - * and delegates to the underlying WebStandardStreamableHTTPServerTransport. + * and delegates to the underlying HTTPServerTransport. * * @param req - Node.js IncomingMessage, optionally with auth property from middleware * @param res - Node.js ServerResponse @@ -159,7 +158,7 @@ export class StreamableHTTPServerTransport implements Transport { // Create a custom handler that includes our context const handler = getRequestListener(async (webRequest: Request) => { - return this._webStandardTransport.handleRequest(webRequest, { + return this._httpServerTransport.handleRequest(webRequest, { authInfo, parsedBody }); @@ -176,7 +175,7 @@ export class StreamableHTTPServerTransport implements Transport { * client will reconnect after the retry interval specified in the priming event. */ closeSSEStream(requestId: RequestId): void { - this._webStandardTransport.closeSSEStream(requestId); + this._httpServerTransport.closeSSEStream(requestId); } /** @@ -184,6 +183,6 @@ export class StreamableHTTPServerTransport implements Transport { * Use this to implement polling behavior for server-initiated notifications. */ closeStandaloneSSEStream(): void { - this._webStandardTransport.closeStandaloneSSEStream(); + this._httpServerTransport.closeStandaloneSSEStream(); } } diff --git a/packages/middleware/node/src/index.ts b/packages/middleware/node/src/index.ts new file mode 100644 index 000000000..b46825543 --- /dev/null +++ b/packages/middleware/node/src/index.ts @@ -0,0 +1,11 @@ +/** + * @modelcontextprotocol/node + * + * Node.js HTTP adapters for Model Context Protocol servers. + * These transports wrap the Web Standards HTTPServerTransport to provide + * compatibility with Node.js HTTP server types (IncomingMessage/ServerResponse). + */ + +// Node.js transports +export * from './http.js'; +export * from './sse.js'; diff --git a/packages/server/src/server/sse.ts b/packages/middleware/node/src/sse.ts similarity index 94% rename from packages/server/src/server/sse.ts rename to packages/middleware/node/src/sse.ts index 4fd0fa1d6..56addbbbd 100644 --- a/packages/server/src/server/sse.ts +++ b/packages/middleware/node/src/sse.ts @@ -16,24 +16,24 @@ export interface SSEServerTransportOptions { /** * List of allowed host header values for DNS rebinding protection. * If not specified, host validation is disabled. - * @deprecated Use the `hostHeaderValidation` middleware from `@modelcontextprotocol/sdk/server/middleware/hostHeaderValidation.js` instead, - * or use `createMcpExpressApp` from `@modelcontextprotocol/sdk/server/express.js` which includes localhost protection by default. + * @deprecated Use the `hostHeaderValidation` middleware from `@modelcontextprotocol/express` instead, + * or use `createMcpExpressApp` from `@modelcontextprotocol/express` which includes localhost protection by default. */ allowedHosts?: string[]; /** * List of allowed origin header values for DNS rebinding protection. * If not specified, origin validation is disabled. - * @deprecated Use the `hostHeaderValidation` middleware from `@modelcontextprotocol/sdk/server/middleware/hostHeaderValidation.js` instead, - * or use `createMcpExpressApp` from `@modelcontextprotocol/sdk/server/express.js` which includes localhost protection by default. + * @deprecated Use the `hostHeaderValidation` middleware from `@modelcontextprotocol/express` instead, + * or use `createMcpExpressApp` from `@modelcontextprotocol/express` which includes localhost protection by default. */ allowedOrigins?: string[]; /** * Enable DNS rebinding protection (requires allowedHosts and/or allowedOrigins to be configured). * Default is false for backwards compatibility. - * @deprecated Use the `hostHeaderValidation` middleware from `@modelcontextprotocol/sdk/server/middleware/hostHeaderValidation.js` instead, - * or use `createMcpExpressApp` from `@modelcontextprotocol/sdk/server/express.js` which includes localhost protection by default. + * @deprecated Use the `hostHeaderValidation` middleware from `@modelcontextprotocol/express` instead, + * or use `createMcpExpressApp` from `@modelcontextprotocol/express` which includes localhost protection by default. */ enableDnsRebindingProtection?: boolean; } diff --git a/packages/server/test/server/streamableHttp.test.ts b/packages/middleware/node/test/http.test.ts similarity index 99% rename from packages/server/test/server/streamableHttp.test.ts rename to packages/middleware/node/test/http.test.ts index d8c6388e4..62a90b04b 100644 --- a/packages/server/test/server/streamableHttp.test.ts +++ b/packages/middleware/node/test/http.test.ts @@ -12,12 +12,12 @@ import type { JSONRPCResultResponse, RequestId } from '@modelcontextprotocol/core'; -import { listenOnRandomPort } from '@modelcontextprotocol/test-helpers'; +import type { EventId, EventStore, StreamId } from '@modelcontextprotocol/server'; +import { McpServer } from '@modelcontextprotocol/server'; +import type { ZodMatrixEntry } from '@modelcontextprotocol/test-helpers'; +import { listenOnRandomPort, zodTestMatrix } from '@modelcontextprotocol/test-helpers'; -import { McpServer } from '../../src/server/mcp.js'; -import { StreamableHTTPServerTransport } from '../../src/server/streamableHttp.js'; -import type { EventId, EventStore, StreamId } from '../../src/server/webStandardStreamableHttp.js'; -import { type ZodMatrixEntry, zodTestMatrix } from './__fixtures__/zodTestMatrix.js'; +import { StreamableHTTPServerTransport } from '../src/http.js'; async function getFreePort() { return new Promise(res => { diff --git a/packages/server/test/server/sse.test.ts b/packages/middleware/node/test/sse.test.ts similarity index 99% rename from packages/server/test/server/sse.test.ts rename to packages/middleware/node/test/sse.test.ts index 0fc9eebc8..8fe8ed4e5 100644 --- a/packages/server/test/server/sse.test.ts +++ b/packages/middleware/node/test/sse.test.ts @@ -2,12 +2,12 @@ import type http from 'node:http'; import { createServer, type Server } from 'node:http'; import type { CallToolResult, JSONRPCMessage } from '@modelcontextprotocol/core'; +import { McpServer } from '@modelcontextprotocol/server'; import type { ZodMatrixEntry } from '@modelcontextprotocol/test-helpers'; import { listenOnRandomPort, zodTestMatrix } from '@modelcontextprotocol/test-helpers'; import { type Mocked } from 'vitest'; -import { McpServer } from '../../src/server/mcp.js'; -import { SSEServerTransport } from '../../src/server/sse.js'; +import { SSEServerTransport } from '../src/sse.js'; const createMockResponse = () => { const res = { diff --git a/packages/middleware/node/tsconfig.json b/packages/middleware/node/tsconfig.json new file mode 100644 index 000000000..4a869bec4 --- /dev/null +++ b/packages/middleware/node/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "@modelcontextprotocol/tsconfig", + "include": ["./"], + "exclude": ["node_modules", "dist"], + "compilerOptions": { + "paths": { + "*": ["./*"], + "@modelcontextprotocol/core": ["./node_modules/@modelcontextprotocol/core/src/index.ts"], + "@modelcontextprotocol/server": ["./node_modules/@modelcontextprotocol/server/src/index.ts"], + "@modelcontextprotocol/test-helpers": ["./node_modules/@modelcontextprotocol/test-helpers/src/index.ts"] + } + } +} diff --git a/packages/middleware/node/tsdown.config.ts b/packages/middleware/node/tsdown.config.ts new file mode 100644 index 000000000..cba220b76 --- /dev/null +++ b/packages/middleware/node/tsdown.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'tsdown'; + +export default defineConfig({ + entry: ['src/index.ts'], + outDir: 'dist', + format: 'esm', + clean: true, + dts: true, + sourcemap: true, + external: ['@modelcontextprotocol/server', '@modelcontextprotocol/core'] +}); diff --git a/packages/middleware/node/vitest.config.js b/packages/middleware/node/vitest.config.js new file mode 100644 index 000000000..496fca320 --- /dev/null +++ b/packages/middleware/node/vitest.config.js @@ -0,0 +1,3 @@ +import baseConfig from '@modelcontextprotocol/vitest-config'; + +export default baseConfig; diff --git a/packages/server/package.json b/packages/server/package.json index b039751f6..00197bb2a 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -44,14 +44,6 @@ "client": "tsx scripts/cli.ts client" }, "dependencies": { - "content-type": "catalog:runtimeServerOnly", - "cors": "catalog:runtimeServerOnly", - "@hono/node-server": "catalog:runtimeServerOnly", - "hono": "catalog:runtimeServerOnly", - "express": "catalog:runtimeServerOnly", - "express-rate-limit": "catalog:runtimeServerOnly", - "raw-body": "catalog:runtimeServerOnly", - "pkce-challenge": "catalog:runtimeShared", "zod": "catalog:runtimeShared", "zod-to-json-schema": "catalog:runtimeShared" }, @@ -75,12 +67,8 @@ "@modelcontextprotocol/test-helpers": "workspace:^", "@cfworker/json-schema": "catalog:runtimeShared", "@eslint/js": "catalog:devTools", - "@types/content-type": "catalog:devTools", - "@types/cors": "catalog:devTools", "@types/cross-spawn": "catalog:devTools", "@types/eventsource": "catalog:devTools", - "@types/express": "catalog:devTools", - "@types/express-serve-static-core": "catalog:devTools", "@types/supertest": "catalog:devTools", "@typescript/native-preview": "catalog:devTools", "eslint": "catalog:devTools", diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 4b0c42053..1d7d38198 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -1,14 +1,8 @@ export * from './server/completable.js'; -export * from './server/express.js'; +export * from './server/http.js'; export * from './server/mcp.js'; export * from './server/server.js'; -export * from './server/sse.js'; export * from './server/stdio.js'; -export * from './server/streamableHttp.js'; -export * from './server/webStandardStreamableHttp.js'; - -// auth exports -export * from './server/auth/index.js'; // experimental exports export * from './experimental/index.js'; diff --git a/packages/server/src/server/auth/index.ts b/packages/server/src/server/auth/index.ts deleted file mode 100644 index 5369224cf..000000000 --- a/packages/server/src/server/auth/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -export * from './clients.js'; -export * from './handlers/authorize.js'; -export * from './handlers/metadata.js'; -export * from './handlers/register.js'; -export * from './handlers/revoke.js'; -export * from './handlers/token.js'; -export * from './middleware/allowedMethods.js'; -export * from './middleware/bearerAuth.js'; -export * from './middleware/clientAuth.js'; -export * from './provider.js'; -export * from './providers/proxyProvider.js'; -export * from './router.js'; diff --git a/packages/server/src/server/webStandardStreamableHttp.ts b/packages/server/src/server/http.ts similarity index 97% rename from packages/server/src/server/webStandardStreamableHttp.ts rename to packages/server/src/server/http.ts index 082c904e1..034add49d 100644 --- a/packages/server/src/server/webStandardStreamableHttp.ts +++ b/packages/server/src/server/http.ts @@ -1,10 +1,11 @@ /** - * Web Standards Streamable HTTP Server Transport + * HTTP Server Transport (Web Standards) * * This is the core transport implementation using Web Standard APIs (Request, Response, ReadableStream). * It can run on any runtime that supports Web Standards: Node.js 18+, Cloudflare Workers, Deno, Bun, etc. * - * For Node.js Express/HTTP compatibility, use `StreamableHTTPServerTransport` which wraps this transport. + * For Node.js HTTP compatibility, use `StreamableHTTPServerTransport` from `@modelcontextprotocol/node`. + * For Express-specific features, use `@modelcontextprotocol/express`. */ import { TextEncoder } from 'node:util'; @@ -70,9 +71,9 @@ interface StreamMapping { } /** - * Configuration options for WebStandardStreamableHTTPServerTransport + * Configuration options for HTTPServerTransport */ -export interface WebStandardStreamableHTTPServerTransportOptions { +export interface HTTPServerTransportOptions { /** * Function that generates a session ID for the transport. * The session ID SHOULD be globally unique and cryptographically secure (e.g., a securely generated UUID, a JWT, or a cryptographic hash) @@ -96,7 +97,7 @@ export interface WebStandardStreamableHTTPServerTransportOptions { * Useful in cases when you need to clean up resources associated with the session. * Note that this is different from the transport closing, if you are handling * HTTP requests from multiple nodes you might want to close each - * WebStandardStreamableHTTPServerTransport after a request is completed while still keeping the + * HTTPServerTransport after a request is completed while still keeping the * session open/running. * @param sessionId The session ID that was closed */ @@ -161,8 +162,8 @@ export interface HandleRequestOptions { } /** - * Server transport for Web Standards Streamable HTTP: this implements the MCP Streamable HTTP transport specification - * using Web Standard APIs (Request, Response, ReadableStream). + * Server transport for Streamable HTTP using Web Standard APIs (Request, Response, ReadableStream). + * This implements the MCP Streamable HTTP transport specification. * * This transport works on any runtime that supports Web Standards: Node.js 18+, Cloudflare Workers, Deno, Bun, etc. * @@ -170,12 +171,12 @@ export interface HandleRequestOptions { * * ```typescript * // Stateful mode - server sets the session ID - * const statefulTransport = new WebStandardStreamableHTTPServerTransport({ + * const statefulTransport = new HTTPServerTransport({ * sessionIdGenerator: () => crypto.randomUUID(), * }); * * // Stateless mode - explicitly set session ID to undefined - * const statelessTransport = new WebStandardStreamableHTTPServerTransport({ + * const statelessTransport = new HTTPServerTransport({ * sessionIdGenerator: undefined, * }); * @@ -203,7 +204,7 @@ export interface HandleRequestOptions { * - No Session ID is included in any responses * - No session validation is performed */ -export class WebStandardStreamableHTTPServerTransport implements Transport { +export class HTTPServerTransport implements Transport { // when sessionId is not set (undefined), it means the transport is in stateless mode private sessionIdGenerator: (() => string) | undefined; private _started: boolean = false; @@ -226,7 +227,7 @@ export class WebStandardStreamableHTTPServerTransport implements Transport { onerror?: (error: Error) => void; onmessage?: (message: JSONRPCMessage, extra?: MessageExtraInfo) => void; - constructor(options: WebStandardStreamableHTTPServerTransportOptions = {}) { + constructor(options: HTTPServerTransportOptions = {}) { this.sessionIdGenerator = options.sessionIdGenerator; this._enableJsonResponse = options.enableJsonResponse ?? false; this._eventStore = options.eventStore; @@ -992,3 +993,13 @@ export class WebStandardStreamableHTTPServerTransport implements Transport { } } } + +/** + * @deprecated Use `HTTPServerTransport` instead. This alias is provided for backwards compatibility. + */ +export const WebStandardStreamableHTTPServerTransport = HTTPServerTransport; + +/** + * @deprecated Use `HTTPServerTransportOptions` instead. This alias is provided for backwards compatibility. + */ +export type WebStandardStreamableHTTPServerTransportOptions = HTTPServerTransportOptions; diff --git a/packages/server/test/server/__fixtures__/zodTestMatrix.ts b/packages/server/test/server/__fixtures__/zodTestMatrix.ts deleted file mode 100644 index fc4ee63db..000000000 --- a/packages/server/test/server/__fixtures__/zodTestMatrix.ts +++ /dev/null @@ -1,22 +0,0 @@ -import * as z3 from 'zod/v3'; -import * as z4 from 'zod/v4'; - -// Shared Zod namespace type that exposes the common surface area used in tests. -export type ZNamespace = typeof z3 & typeof z4; - -export const zodTestMatrix = [ - { - zodVersionLabel: 'Zod v3', - z: z3 as ZNamespace, - isV3: true as const, - isV4: false as const - }, - { - zodVersionLabel: 'Zod v4', - z: z4 as ZNamespace, - isV3: false as const, - isV4: true as const - } -] as const; - -export type ZodMatrixEntry = (typeof zodTestMatrix)[number]; diff --git a/packages/server/test/server/completable.test.ts b/packages/server/test/server/completable.test.ts index ff94b641b..615192ac4 100644 --- a/packages/server/test/server/completable.test.ts +++ b/packages/server/test/server/completable.test.ts @@ -1,6 +1,7 @@ +import type { ZodMatrixEntry } from '@modelcontextprotocol/test-helpers'; +import { zodTestMatrix } from '@modelcontextprotocol/test-helpers'; + import { completable, getCompleter } from '../../src/server/completable.js'; -import type { ZodMatrixEntry } from './__fixtures__/zodTestMatrix.js'; -import { zodTestMatrix } from './__fixtures__/zodTestMatrix.js'; describe.each(zodTestMatrix)('completable with $zodVersionLabel', (entry: ZodMatrixEntry) => { const { z } = entry; diff --git a/packages/server/test/server/http.test.ts b/packages/server/test/server/http.test.ts new file mode 100644 index 000000000..c8d6c3025 --- /dev/null +++ b/packages/server/test/server/http.test.ts @@ -0,0 +1,757 @@ +import { randomUUID } from 'node:crypto'; + +import type { CallToolResult, JSONRPCErrorResponse, JSONRPCMessage } from '@modelcontextprotocol/core'; +import type { ZodMatrixEntry } from '@modelcontextprotocol/test-helpers'; +import { zodTestMatrix } from '@modelcontextprotocol/test-helpers'; + +import type { EventId, EventStore, StreamId } from '../../src/server/http.js'; +import { HTTPServerTransport } from '../../src/server/http.js'; +import { McpServer } from '../../src/server/mcp.js'; + +/** + * Common test messages + */ +const TEST_MESSAGES = { + initialize: { + jsonrpc: '2.0', + method: 'initialize', + params: { + clientInfo: { name: 'test-client', version: '1.0' }, + protocolVersion: '2025-11-25', + capabilities: {} + }, + id: 'init-1' + } as JSONRPCMessage, + + initializeOldVersion: { + jsonrpc: '2.0', + method: 'initialize', + params: { + clientInfo: { name: 'test-client', version: '1.0' }, + protocolVersion: '2025-06-18', + capabilities: {} + }, + id: 'init-1' + } as JSONRPCMessage, + + toolsList: { + jsonrpc: '2.0', + method: 'tools/list', + params: {}, + id: 'tools-1' + } as JSONRPCMessage +}; + +/** + * Helper to create a Web Standard Request + */ +function createRequest( + method: string, + body?: JSONRPCMessage | JSONRPCMessage[], + options?: { + sessionId?: string; + accept?: string; + contentType?: string; + extraHeaders?: Record; + } +): Request { + const headers: Record = {}; + + if (options?.accept) { + headers['Accept'] = options.accept; + } else if (method === 'POST') { + headers['Accept'] = 'application/json, text/event-stream'; + } else if (method === 'GET') { + headers['Accept'] = 'text/event-stream'; + } + + if (options?.contentType) { + headers['Content-Type'] = options.contentType; + } else if (body) { + headers['Content-Type'] = 'application/json'; + } + + if (options?.sessionId) { + headers['mcp-session-id'] = options.sessionId; + headers['mcp-protocol-version'] = '2025-11-25'; + } + + if (options?.extraHeaders) { + Object.assign(headers, options.extraHeaders); + } + + return new Request('http://localhost/mcp', { + method, + headers, + body: body ? JSON.stringify(body) : undefined + }); +} + +/** + * Helper to extract text from SSE response + */ +async function readSSEEvent(response: Response): Promise { + const reader = response.body?.getReader(); + const { value } = await reader!.read(); + return new TextDecoder().decode(value); +} + +/** + * Helper to parse SSE data line + */ +function parseSSEData(text: string): unknown { + const eventLines = text.split('\n'); + const dataLine = eventLines.find(line => line.startsWith('data:')); + if (!dataLine) { + throw new Error('No data line found in SSE event'); + } + return JSON.parse(dataLine.substring(5).trim()); +} + +function expectErrorResponse(data: unknown, expectedCode: number, expectedMessagePattern: RegExp): void { + expect(data).toMatchObject({ + jsonrpc: '2.0', + error: expect.objectContaining({ + code: expectedCode, + message: expect.stringMatching(expectedMessagePattern) + }) + }); +} + +describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { + const { z } = entry; + + describe('HTTPServerTransport', () => { + let transport: HTTPServerTransport; + let mcpServer: McpServer; + let sessionId: string; + + beforeEach(async () => { + mcpServer = new McpServer({ name: 'test-server', version: '1.0.0' }, { capabilities: { logging: {} } }); + + mcpServer.tool( + 'greet', + 'A simple greeting tool', + { name: z.string().describe('Name to greet') }, + async ({ name }): Promise => { + return { content: [{ type: 'text', text: `Hello, ${name}!` }] }; + } + ); + + transport = new HTTPServerTransport({ + sessionIdGenerator: () => randomUUID() + }); + + await mcpServer.connect(transport); + }); + + afterEach(async () => { + await transport.close(); + }); + + async function initializeServer(): Promise { + const request = createRequest('POST', TEST_MESSAGES.initialize); + const response = await transport.handleRequest(request); + + expect(response.status).toBe(200); + const newSessionId = response.headers.get('mcp-session-id'); + expect(newSessionId).toBeDefined(); + return newSessionId as string; + } + + describe('Initialization', () => { + it('should initialize server and generate session ID', async () => { + const request = createRequest('POST', TEST_MESSAGES.initialize); + const response = await transport.handleRequest(request); + + expect(response.status).toBe(200); + expect(response.headers.get('content-type')).toBe('text/event-stream'); + expect(response.headers.get('mcp-session-id')).toBeDefined(); + }); + + it('should reject second initialization request', async () => { + sessionId = await initializeServer(); + expect(sessionId).toBeDefined(); + + const secondInitMessage = { + ...TEST_MESSAGES.initialize, + id: 'second-init' + }; + + const request = createRequest('POST', secondInitMessage); + const response = await transport.handleRequest(request); + + expect(response.status).toBe(400); + const errorData = await response.json(); + expectErrorResponse(errorData, -32600, /Server already initialized/); + }); + + it('should reject batch initialize request', async () => { + const batchInitMessages: JSONRPCMessage[] = [ + TEST_MESSAGES.initialize, + { + jsonrpc: '2.0', + method: 'initialize', + params: { + clientInfo: { name: 'test-client-2', version: '1.0' }, + protocolVersion: '2025-03-26' + }, + id: 'init-2' + } + ]; + + const request = createRequest('POST', batchInitMessages); + const response = await transport.handleRequest(request); + + expect(response.status).toBe(400); + const errorData = await response.json(); + expectErrorResponse(errorData, -32600, /Only one initialization request is allowed/); + }); + }); + + describe('POST Requests', () => { + it('should handle post requests via SSE response correctly', async () => { + sessionId = await initializeServer(); + + const request = createRequest('POST', TEST_MESSAGES.toolsList, { sessionId }); + const response = await transport.handleRequest(request); + + expect(response.status).toBe(200); + + const text = await readSSEEvent(response); + const eventData = parseSSEData(text); + + expect(eventData).toMatchObject({ + jsonrpc: '2.0', + result: expect.objectContaining({ + tools: expect.arrayContaining([ + expect.objectContaining({ + name: 'greet', + description: 'A simple greeting tool' + }) + ]) + }), + id: 'tools-1' + }); + }); + + it('should call a tool and return the result', async () => { + sessionId = await initializeServer(); + + const toolCallMessage: JSONRPCMessage = { + jsonrpc: '2.0', + method: 'tools/call', + params: { + name: 'greet', + arguments: { + name: 'Test User' + } + }, + id: 'call-1' + }; + + const request = createRequest('POST', toolCallMessage, { sessionId }); + const response = await transport.handleRequest(request); + + expect(response.status).toBe(200); + + const text = await readSSEEvent(response); + const eventData = parseSSEData(text); + + expect(eventData).toMatchObject({ + jsonrpc: '2.0', + result: { + content: [ + { + type: 'text', + text: 'Hello, Test User!' + } + ] + }, + id: 'call-1' + }); + }); + + it('should reject requests without a valid session ID', async () => { + const request = createRequest('POST', TEST_MESSAGES.toolsList); + const response = await transport.handleRequest(request); + + expect(response.status).toBe(400); + const errorData = (await response.json()) as JSONRPCErrorResponse; + expectErrorResponse(errorData, -32000, /Bad Request/); + expect(errorData.id).toBeNull(); + }); + + it('should reject invalid session ID', async () => { + await initializeServer(); + + const request = createRequest('POST', TEST_MESSAGES.toolsList, { sessionId: 'invalid-session-id' }); + const response = await transport.handleRequest(request); + + expect(response.status).toBe(404); + const errorData = await response.json(); + expectErrorResponse(errorData, -32001, /Session not found/); + }); + + it('should reject request with wrong Accept header', async () => { + const request = createRequest('POST', TEST_MESSAGES.initialize, { accept: 'application/json' }); + const response = await transport.handleRequest(request); + + expect(response.status).toBe(406); + const errorData = await response.json(); + expectErrorResponse(errorData, -32000, /Not Acceptable/); + }); + + it('should reject request with wrong Content-Type header', async () => { + const request = new Request('http://localhost/mcp', { + method: 'POST', + headers: { + Accept: 'application/json, text/event-stream', + 'Content-Type': 'text/plain' + }, + body: JSON.stringify(TEST_MESSAGES.initialize) + }); + const response = await transport.handleRequest(request); + + expect(response.status).toBe(415); + const errorData = await response.json(); + expectErrorResponse(errorData, -32000, /Unsupported Media Type/); + }); + + it('should reject invalid JSON', async () => { + const request = new Request('http://localhost/mcp', { + method: 'POST', + headers: { + Accept: 'application/json, text/event-stream', + 'Content-Type': 'application/json' + }, + body: 'not valid json' + }); + const response = await transport.handleRequest(request); + + expect(response.status).toBe(400); + const errorData = await response.json(); + expectErrorResponse(errorData, -32700, /Parse error.*Invalid JSON/); + }); + + it('should accept notifications without session and return 202', async () => { + sessionId = await initializeServer(); + + const notification: JSONRPCMessage = { + jsonrpc: '2.0', + method: 'notifications/initialized' + }; + + const request = createRequest('POST', notification, { sessionId }); + const response = await transport.handleRequest(request); + + expect(response.status).toBe(202); + }); + }); + + describe('GET Requests (SSE Stream)', () => { + it('should establish standalone SSE stream', async () => { + sessionId = await initializeServer(); + + const request = createRequest('GET', undefined, { sessionId }); + const response = await transport.handleRequest(request); + + expect(response.status).toBe(200); + expect(response.headers.get('content-type')).toBe('text/event-stream'); + expect(response.headers.get('mcp-session-id')).toBe(sessionId); + }); + + it('should reject GET without Accept: text/event-stream', async () => { + sessionId = await initializeServer(); + + const request = createRequest('GET', undefined, { sessionId, accept: 'application/json' }); + const response = await transport.handleRequest(request); + + expect(response.status).toBe(406); + const errorData = await response.json(); + expectErrorResponse(errorData, -32000, /Not Acceptable/); + }); + + it('should reject second standalone SSE stream', async () => { + sessionId = await initializeServer(); + + // First SSE stream + const request1 = createRequest('GET', undefined, { sessionId }); + const response1 = await transport.handleRequest(request1); + expect(response1.status).toBe(200); + + // Second SSE stream should be rejected + const request2 = createRequest('GET', undefined, { sessionId }); + const response2 = await transport.handleRequest(request2); + + expect(response2.status).toBe(409); + const errorData = await response2.json(); + expectErrorResponse(errorData, -32000, /Conflict/); + }); + }); + + describe('DELETE Requests', () => { + it('should handle DELETE to close session', async () => { + sessionId = await initializeServer(); + + const request = createRequest('DELETE', undefined, { sessionId }); + const response = await transport.handleRequest(request); + + expect(response.status).toBe(200); + }); + + it('should reject DELETE without valid session', async () => { + await initializeServer(); + + const request = createRequest('DELETE', undefined, { sessionId: 'invalid-session' }); + const response = await transport.handleRequest(request); + + expect(response.status).toBe(404); + }); + }); + + describe('Unsupported Methods', () => { + it('should reject PUT requests', async () => { + const request = new Request('http://localhost/mcp', { method: 'PUT' }); + const response = await transport.handleRequest(request); + + expect(response.status).toBe(405); + expect(response.headers.get('Allow')).toBe('GET, POST, DELETE'); + }); + + it('should reject PATCH requests', async () => { + const request = new Request('http://localhost/mcp', { method: 'PATCH' }); + const response = await transport.handleRequest(request); + + expect(response.status).toBe(405); + }); + }); + }); + + describe('HTTPServerTransport - Stateless Mode', () => { + let transport: HTTPServerTransport; + let mcpServer: McpServer; + + beforeEach(async () => { + mcpServer = new McpServer({ name: 'test-server', version: '1.0.0' }, { capabilities: { logging: {} } }); + + mcpServer.tool('echo', 'Echo tool', { message: z.string() }, async ({ message }): Promise => { + return { content: [{ type: 'text', text: message }] }; + }); + + transport = new HTTPServerTransport({ + sessionIdGenerator: undefined + }); + + await mcpServer.connect(transport); + }); + + afterEach(async () => { + await transport.close(); + }); + + it('should work without session management', async () => { + const request = createRequest('POST', TEST_MESSAGES.initialize); + const response = await transport.handleRequest(request); + + expect(response.status).toBe(200); + expect(response.headers.get('mcp-session-id')).toBeNull(); + }); + + it('should not require session ID on subsequent requests', async () => { + // Initialize + const initRequest = createRequest('POST', TEST_MESSAGES.initialize); + await transport.handleRequest(initRequest); + + // Subsequent request without session ID should work + const request = createRequest('POST', TEST_MESSAGES.toolsList); + const response = await transport.handleRequest(request); + + expect(response.status).toBe(200); + }); + }); + + describe('HTTPServerTransport - JSON Response Mode', () => { + let transport: HTTPServerTransport; + let mcpServer: McpServer; + let sessionId: string; + + beforeEach(async () => { + mcpServer = new McpServer({ name: 'test-server', version: '1.0.0' }, { capabilities: { logging: {} } }); + + mcpServer.tool('greet', 'Greeting tool', { name: z.string() }, async ({ name }): Promise => { + return { content: [{ type: 'text', text: `Hello, ${name}!` }] }; + }); + + transport = new HTTPServerTransport({ + sessionIdGenerator: () => randomUUID(), + enableJsonResponse: true + }); + + await mcpServer.connect(transport); + }); + + afterEach(async () => { + await transport.close(); + }); + + async function initializeServer(): Promise { + const request = createRequest('POST', TEST_MESSAGES.initialize); + const response = await transport.handleRequest(request); + + expect(response.status).toBe(200); + const newSessionId = response.headers.get('mcp-session-id'); + expect(newSessionId).toBeDefined(); + return newSessionId as string; + } + + it('should return JSON response instead of SSE', async () => { + sessionId = await initializeServer(); + + const request = createRequest('POST', TEST_MESSAGES.toolsList, { sessionId }); + const response = await transport.handleRequest(request); + + expect(response.status).toBe(200); + expect(response.headers.get('content-type')).toBe('application/json'); + + const data = await response.json(); + expect(data).toMatchObject({ + jsonrpc: '2.0', + result: expect.objectContaining({ + tools: expect.any(Array) + }), + id: 'tools-1' + }); + }); + + it('should handle tool calls in JSON response mode', async () => { + sessionId = await initializeServer(); + + const toolCallMessage: JSONRPCMessage = { + jsonrpc: '2.0', + method: 'tools/call', + params: { + name: 'greet', + arguments: { name: 'World' } + }, + id: 'call-1' + }; + + const request = createRequest('POST', toolCallMessage, { sessionId }); + const response = await transport.handleRequest(request); + + expect(response.status).toBe(200); + expect(response.headers.get('content-type')).toBe('application/json'); + + const data = await response.json(); + expect(data).toMatchObject({ + jsonrpc: '2.0', + result: { + content: [{ type: 'text', text: 'Hello, World!' }] + }, + id: 'call-1' + }); + }); + }); + + describe('HTTPServerTransport - Session Callbacks', () => { + it('should call onsessioninitialized callback', async () => { + const onInitialized = vi.fn(); + + const mcpServer = new McpServer({ name: 'test-server', version: '1.0.0' }, { capabilities: {} }); + const transport = new HTTPServerTransport({ + sessionIdGenerator: () => 'test-session-123', + onsessioninitialized: onInitialized + }); + + await mcpServer.connect(transport); + + const request = createRequest('POST', TEST_MESSAGES.initialize); + await transport.handleRequest(request); + + expect(onInitialized).toHaveBeenCalledWith('test-session-123'); + + await transport.close(); + }); + + it('should call onsessionclosed callback on DELETE', async () => { + const onClosed = vi.fn(); + + const mcpServer = new McpServer({ name: 'test-server', version: '1.0.0' }, { capabilities: {} }); + const transport = new HTTPServerTransport({ + sessionIdGenerator: () => 'test-session-456', + onsessionclosed: onClosed + }); + + await mcpServer.connect(transport); + + // Initialize first + const initRequest = createRequest('POST', TEST_MESSAGES.initialize); + await transport.handleRequest(initRequest); + + // Then delete + const deleteRequest = createRequest('DELETE', undefined, { sessionId: 'test-session-456' }); + await transport.handleRequest(deleteRequest); + + expect(onClosed).toHaveBeenCalledWith('test-session-456'); + }); + }); + + describe('HTTPServerTransport - Event Store (Resumability)', () => { + let transport: HTTPServerTransport; + let mcpServer: McpServer; + let eventStore: EventStore; + let storedEvents: Map; + let sessionId: string; + + beforeEach(async () => { + storedEvents = new Map(); + + eventStore = { + async storeEvent(streamId: StreamId, message: JSONRPCMessage): Promise { + const eventId = `${streamId}_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`; + storedEvents.set(eventId, { streamId, message }); + return eventId; + }, + async getStreamIdForEventId(eventId: EventId): Promise { + const event = storedEvents.get(eventId); + return event?.streamId; + }, + async replayEventsAfter( + lastEventId: EventId, + { send }: { send: (eventId: EventId, message: JSONRPCMessage) => Promise } + ): Promise { + const lastEvent = storedEvents.get(lastEventId); + if (!lastEvent) { + throw new Error('Event not found'); + } + + // Replay events after lastEventId for the same stream + const streamId = lastEvent.streamId; + const entries = Array.from(storedEvents.entries()); + let foundLast = false; + + for (const [eventId, event] of entries) { + if (eventId === lastEventId) { + foundLast = true; + continue; + } + if (foundLast && event.streamId === streamId) { + await send(eventId, event.message); + } + } + + return streamId; + } + }; + + mcpServer = new McpServer({ name: 'test-server', version: '1.0.0' }, { capabilities: { logging: {} } }); + + mcpServer.tool('greet', 'Greeting tool', { name: z.string() }, async ({ name }): Promise => { + return { content: [{ type: 'text', text: `Hello, ${name}!` }] }; + }); + + transport = new HTTPServerTransport({ + sessionIdGenerator: () => randomUUID(), + eventStore + }); + + await mcpServer.connect(transport); + }); + + afterEach(async () => { + await transport.close(); + }); + + async function initializeServer(): Promise { + const request = createRequest('POST', TEST_MESSAGES.initialize); + const response = await transport.handleRequest(request); + + expect(response.status).toBe(200); + const newSessionId = response.headers.get('mcp-session-id'); + expect(newSessionId).toBeDefined(); + return newSessionId as string; + } + + it('should store events when event store is configured', async () => { + sessionId = await initializeServer(); + + const request = createRequest('POST', TEST_MESSAGES.toolsList, { sessionId }); + await transport.handleRequest(request); + + // Events should have been stored (priming event + response) + expect(storedEvents.size).toBeGreaterThan(0); + }); + + it('should include event ID in SSE events', async () => { + sessionId = await initializeServer(); + + const request = createRequest('POST', TEST_MESSAGES.toolsList, { sessionId }); + const response = await transport.handleRequest(request); + + const text = await readSSEEvent(response); + + // Should have id: field in the SSE event + expect(text).toContain('id:'); + }); + }); + + describe('HTTPServerTransport - Protocol Version Validation', () => { + let transport: HTTPServerTransport; + let mcpServer: McpServer; + let sessionId: string; + + beforeEach(async () => { + mcpServer = new McpServer({ name: 'test-server', version: '1.0.0' }, { capabilities: {} }); + + transport = new HTTPServerTransport({ + sessionIdGenerator: () => randomUUID() + }); + + await mcpServer.connect(transport); + }); + + afterEach(async () => { + await transport.close(); + }); + + async function initializeServer(): Promise { + const request = createRequest('POST', TEST_MESSAGES.initialize); + const response = await transport.handleRequest(request); + return response.headers.get('mcp-session-id') as string; + } + + it('should reject unsupported protocol version in header', async () => { + sessionId = await initializeServer(); + + const request = new Request('http://localhost/mcp', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json, text/event-stream', + 'mcp-session-id': sessionId, + 'mcp-protocol-version': 'unsupported-version' + }, + body: JSON.stringify(TEST_MESSAGES.toolsList) + }); + + const response = await transport.handleRequest(request); + + expect(response.status).toBe(400); + const errorData = await response.json(); + expectErrorResponse(errorData, -32000, /Unsupported protocol version/); + }); + }); + + describe('HTTPServerTransport - start() method', () => { + it('should throw error when started twice', async () => { + const transport = new HTTPServerTransport({ + sessionIdGenerator: () => randomUUID() + }); + + await transport.start(); + + await expect(transport.start()).rejects.toThrow('Transport already started'); + }); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 92dbf8253..75db0d4cb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -302,6 +302,12 @@ importers: '@modelcontextprotocol/examples-shared': specifier: workspace:^ version: link:../shared + '@modelcontextprotocol/express': + specifier: workspace:^ + version: link:../../packages/middleware/express + '@modelcontextprotocol/node': + specifier: workspace:^ + version: link:../../packages/middleware/node '@modelcontextprotocol/server': specifier: workspace:^ version: link:../../packages/server @@ -339,6 +345,9 @@ importers: examples/shared: dependencies: + '@modelcontextprotocol/express': + specifier: workspace:^ + version: link:../../packages/middleware/express '@modelcontextprotocol/server': specifier: workspace:^ version: link:../../packages/server @@ -553,14 +562,8 @@ importers: specifier: catalog:devTools version: 4.0.9(@types/node@24.10.3)(tsx@4.20.6) - packages/server: + packages/middleware/express: dependencies: - '@hono/node-server': - specifier: catalog:runtimeServerOnly - version: 1.19.7(hono@4.11.1) - content-type: - specifier: catalog:runtimeServerOnly - version: 1.0.5 cors: specifier: catalog:runtimeServerOnly version: 2.8.5 @@ -570,15 +573,158 @@ importers: express-rate-limit: specifier: catalog:runtimeServerOnly version: 7.5.1(express@5.1.0) - hono: - specifier: catalog:runtimeServerOnly - version: 4.11.1 pkce-challenge: specifier: catalog:runtimeShared version: 5.0.0 + zod: + specifier: catalog:runtimeShared + version: 3.25.76 + devDependencies: + '@eslint/js': + specifier: catalog:devTools + version: 9.39.1 + '@modelcontextprotocol/core': + specifier: workspace:^ + version: link:../../core + '@modelcontextprotocol/eslint-config': + specifier: workspace:^ + version: link:../../../common/eslint-config + '@modelcontextprotocol/node': + specifier: workspace:^ + version: link:../node + '@modelcontextprotocol/server': + specifier: workspace:^ + version: link:../../server + '@modelcontextprotocol/test-helpers': + specifier: workspace:^ + version: link:../../../test/helpers + '@modelcontextprotocol/tsconfig': + specifier: workspace:^ + version: link:../../../common/tsconfig + '@modelcontextprotocol/vitest-config': + specifier: workspace:^ + version: link:../../../common/vitest-config + '@types/cors': + specifier: catalog:devTools + version: 2.8.19 + '@types/express': + specifier: catalog:devTools + version: 5.0.5 + '@types/express-serve-static-core': + specifier: catalog:devTools + version: 5.1.0 + '@types/supertest': + specifier: catalog:devTools + version: 6.0.3 + '@typescript/native-preview': + specifier: catalog:devTools + version: 7.0.0-dev.20251218.3 + eslint: + specifier: catalog:devTools + version: 9.39.1 + eslint-config-prettier: + specifier: catalog:devTools + version: 10.1.8(eslint@9.39.1) + eslint-plugin-n: + specifier: catalog:devTools + version: 17.23.1(eslint@9.39.1)(typescript@5.9.3) + prettier: + specifier: catalog:devTools + version: 3.6.2 + supertest: + specifier: catalog:devTools + version: 7.1.4 + tsdown: + specifier: catalog:devTools + version: 0.18.0(@typescript/native-preview@7.0.0-dev.20251218.3)(typescript@5.9.3) + tsx: + specifier: catalog:devTools + version: 4.20.6 + typescript: + specifier: catalog:devTools + version: 5.9.3 + typescript-eslint: + specifier: catalog:devTools + version: 8.49.0(eslint@9.39.1)(typescript@5.9.3) + vitest: + specifier: catalog:devTools + version: 4.0.9(@types/node@24.10.3)(tsx@4.20.6) + + packages/middleware/node: + dependencies: + '@hono/node-server': + specifier: catalog:runtimeServerOnly + version: 1.19.7(hono@4.11.1) + '@modelcontextprotocol/core': + specifier: workspace:^ + version: link:../../core + content-type: + specifier: catalog:runtimeServerOnly + version: 1.0.5 raw-body: specifier: catalog:runtimeServerOnly version: 3.0.1 + devDependencies: + '@eslint/js': + specifier: catalog:devTools + version: 9.39.1 + '@modelcontextprotocol/eslint-config': + specifier: workspace:^ + version: link:../../../common/eslint-config + '@modelcontextprotocol/server': + specifier: workspace:^ + version: link:../../server + '@modelcontextprotocol/test-helpers': + specifier: workspace:^ + version: link:../../../test/helpers + '@modelcontextprotocol/tsconfig': + specifier: workspace:^ + version: link:../../../common/tsconfig + '@modelcontextprotocol/vitest-config': + specifier: workspace:^ + version: link:../../../common/vitest-config + '@types/content-type': + specifier: catalog:devTools + version: 1.1.9 + '@types/supertest': + specifier: catalog:devTools + version: 6.0.3 + '@typescript/native-preview': + specifier: catalog:devTools + version: 7.0.0-dev.20251218.3 + eslint: + specifier: catalog:devTools + version: 9.39.1 + eslint-config-prettier: + specifier: catalog:devTools + version: 10.1.8(eslint@9.39.1) + eslint-plugin-n: + specifier: catalog:devTools + version: 17.23.1(eslint@9.39.1)(typescript@5.9.3) + prettier: + specifier: catalog:devTools + version: 3.6.2 + supertest: + specifier: catalog:devTools + version: 7.1.4 + tsdown: + specifier: catalog:devTools + version: 0.18.0(@typescript/native-preview@7.0.0-dev.20251218.3)(typescript@5.9.3) + tsx: + specifier: catalog:devTools + version: 4.20.6 + typescript: + specifier: catalog:devTools + version: 5.9.3 + typescript-eslint: + specifier: catalog:devTools + version: 8.49.0(eslint@9.39.1)(typescript@5.9.3) + vitest: + specifier: catalog:devTools + version: 4.0.9(@types/node@24.10.3)(tsx@4.20.6) + + packages/server: + dependencies: zod: specifier: catalog:runtimeShared version: 3.25.76 @@ -607,24 +753,12 @@ importers: '@modelcontextprotocol/vitest-config': specifier: workspace:^ version: link:../../common/vitest-config - '@types/content-type': - specifier: catalog:devTools - version: 1.1.9 - '@types/cors': - specifier: catalog:devTools - version: 2.8.19 '@types/cross-spawn': specifier: catalog:devTools version: 6.0.6 '@types/eventsource': specifier: catalog:devTools version: 1.1.15 - '@types/express': - specifier: catalog:devTools - version: 5.0.5 - '@types/express-serve-static-core': - specifier: catalog:devTools - version: 5.1.0 '@types/supertest': specifier: catalog:devTools version: 6.0.3 @@ -700,6 +834,12 @@ importers: '@modelcontextprotocol/eslint-config': specifier: workspace:^ version: link:../../common/eslint-config + '@modelcontextprotocol/express': + specifier: workspace:^ + version: link:../../packages/middleware/express + '@modelcontextprotocol/node': + specifier: workspace:^ + version: link:../../packages/middleware/node '@modelcontextprotocol/server': specifier: workspace:^ version: link:../../packages/server @@ -2480,10 +2620,6 @@ packages: resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==} hasBin: true - js-yaml@4.1.0: - resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} - hasBin: true - js-yaml@4.1.1: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true @@ -3663,7 +3799,7 @@ snapshots: globals: 14.0.0 ignore: 5.3.2 import-fresh: 3.3.1 - js-yaml: 4.1.0 + js-yaml: 4.1.1 minimatch: 3.1.2 strip-json-comments: 3.1.1 transitivePeerDependencies: @@ -5225,10 +5361,6 @@ snapshots: argparse: 1.0.10 esprima: 4.0.1 - js-yaml@4.1.0: - dependencies: - argparse: 2.0.1 - js-yaml@4.1.1: dependencies: argparse: 2.0.1 diff --git a/test/integration/package.json b/test/integration/package.json index e709e431a..ed2c99953 100644 --- a/test/integration/package.json +++ b/test/integration/package.json @@ -35,6 +35,8 @@ "@modelcontextprotocol/core": "workspace:^", "@modelcontextprotocol/client": "workspace:^", "@modelcontextprotocol/server": "workspace:^", + "@modelcontextprotocol/node": "workspace:^", + "@modelcontextprotocol/express": "workspace:^", "zod": "catalog:runtimeShared", "vitest": "catalog:devTools", "supertest": "catalog:devTools", diff --git a/test/integration/test/processCleanup.test.ts b/test/integration/test/processCleanup.test.ts index aa991501e..e3fe19c82 100644 --- a/test/integration/test/processCleanup.test.ts +++ b/test/integration/test/processCleanup.test.ts @@ -2,7 +2,8 @@ import path from 'node:path'; import { Readable, Writable } from 'node:stream'; import { Client, StdioClientTransport } from '@modelcontextprotocol/client'; -import { LoggingMessageNotificationSchema, Server, StdioServerTransport } from '@modelcontextprotocol/server'; +import { LoggingMessageNotificationSchema } from '@modelcontextprotocol/core'; +import { Server, StdioServerTransport } from '@modelcontextprotocol/server'; // Use the local fixtures directory alongside this test file const FIXTURES_DIR = path.resolve(__dirname, './__fixtures__'); diff --git a/test/integration/test/server.test.ts b/test/integration/test/server.test.ts index fcac6cc45..b2e41b439 100644 --- a/test/integration/test/server.test.ts +++ b/test/integration/test/server.test.ts @@ -18,6 +18,8 @@ import { ElicitRequestSchema, ElicitResultSchema, ErrorCode, + InMemoryTaskMessageQueue, + InMemoryTaskStore, InMemoryTransport, LATEST_PROTOCOL_VERSION, ListPromptsRequestSchema, @@ -30,7 +32,8 @@ import { SetLevelRequestSchema, SUPPORTED_PROTOCOL_VERSIONS } from '@modelcontextprotocol/core'; -import { createMcpExpressApp, InMemoryTaskMessageQueue, InMemoryTaskStore, McpServer, Server } from '@modelcontextprotocol/server'; +import { createMcpExpressApp } from '@modelcontextprotocol/express'; +import { McpServer, Server } from '@modelcontextprotocol/server'; import supertest from 'supertest'; import * as z3 from 'zod/v3'; import * as z4 from 'zod/v4'; diff --git a/test/integration/test/server/mcp.test.ts b/test/integration/test/server/mcp.test.ts index f7bcececc..417326f50 100644 --- a/test/integration/test/server/mcp.test.ts +++ b/test/integration/test/server/mcp.test.ts @@ -17,10 +17,8 @@ import { type TextContent, UrlElicitationRequiredError } from '@modelcontextprotocol/core'; - -import { completable } from '../../../../packages/server/src/server/completable.js'; -import { McpServer, ResourceTemplate } from '../../../../packages/server/src/server/mcp.js'; -import { type ZodMatrixEntry, zodTestMatrix } from '../../../../packages/server/test/server/__fixtures__/zodTestMatrix.js'; +import { completable, McpServer, ResourceTemplate } from '@modelcontextprotocol/server'; +import { type ZodMatrixEntry, zodTestMatrix } from '@modelcontextprotocol/test-helpers'; function createLatch() { let latch = false; diff --git a/test/integration/test/stateManagementStreamableHttp.test.ts b/test/integration/test/stateManagementStreamableHttp.test.ts index c33100efa..32a32627b 100644 --- a/test/integration/test/stateManagementStreamableHttp.test.ts +++ b/test/integration/test/stateManagementStreamableHttp.test.ts @@ -7,10 +7,10 @@ import { LATEST_PROTOCOL_VERSION, ListPromptsResultSchema, ListResourcesResultSchema, - ListToolsResultSchema, - McpServer, - StreamableHTTPServerTransport -} from '@modelcontextprotocol/server'; + ListToolsResultSchema +} from '@modelcontextprotocol/core'; +import { StreamableHTTPServerTransport } from '@modelcontextprotocol/node'; +import { McpServer } from '@modelcontextprotocol/server'; import { listenOnRandomPort } from '@modelcontextprotocol/test-helpers'; import { type ZodMatrixEntry, zodTestMatrix } from '@modelcontextprotocol/test-helpers'; diff --git a/test/integration/test/taskLifecycle.test.ts b/test/integration/test/taskLifecycle.test.ts index 216479e93..3cc5e44b8 100644 --- a/test/integration/test/taskLifecycle.test.ts +++ b/test/integration/test/taskLifecycle.test.ts @@ -2,7 +2,6 @@ import { randomUUID } from 'node:crypto'; import { createServer, type Server } from 'node:http'; import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client'; -import type { TaskRequestOptions } from '@modelcontextprotocol/server'; import { CallToolResultSchema, CreateTaskResultSchema, @@ -12,11 +11,12 @@ import { InMemoryTaskMessageQueue, InMemoryTaskStore, McpError, - McpServer, RELATED_TASK_META_KEY, - StreamableHTTPServerTransport, + type TaskRequestOptions, TaskSchema -} from '@modelcontextprotocol/server'; +} from '@modelcontextprotocol/core'; +import { StreamableHTTPServerTransport } from '@modelcontextprotocol/node'; +import { McpServer } from '@modelcontextprotocol/server'; import { listenOnRandomPort, waitForTaskStatus } from '@modelcontextprotocol/test-helpers'; import { z } from 'zod'; diff --git a/test/integration/test/taskResumability.test.ts b/test/integration/test/taskResumability.test.ts index 1e4d8a0fd..1666e685d 100644 --- a/test/integration/test/taskResumability.test.ts +++ b/test/integration/test/taskResumability.test.ts @@ -2,13 +2,9 @@ import { randomUUID } from 'node:crypto'; import { createServer, type Server } from 'node:http'; import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client'; -import { - CallToolResultSchema, - LoggingMessageNotificationSchema, - McpServer, - StreamableHTTPServerTransport -} from '@modelcontextprotocol/server'; -import type { EventStore, JSONRPCMessage } from '@modelcontextprotocol/server'; +import type { JSONRPCMessage } from '@modelcontextprotocol/core'; +import { StreamableHTTPServerTransport } from '@modelcontextprotocol/node'; +import { CallToolResultSchema, type EventStore, LoggingMessageNotificationSchema, McpServer } from '@modelcontextprotocol/server'; import type { ZodMatrixEntry } from '@modelcontextprotocol/test-helpers'; import { listenOnRandomPort, zodTestMatrix } from '@modelcontextprotocol/test-helpers'; diff --git a/test/integration/tsconfig.json b/test/integration/tsconfig.json index f69a602fd..22229a75c 100644 --- a/test/integration/tsconfig.json +++ b/test/integration/tsconfig.json @@ -8,6 +8,8 @@ "@modelcontextprotocol/core": ["./node_modules/@modelcontextprotocol/core/src/index.ts"], "@modelcontextprotocol/client": ["./node_modules/@modelcontextprotocol/client/src/index.ts"], "@modelcontextprotocol/server": ["./node_modules/@modelcontextprotocol/server/src/index.ts"], + "@modelcontextprotocol/node": ["./node_modules/@modelcontextprotocol/node/src/index.ts"], + "@modelcontextprotocol/express": ["./node_modules/@modelcontextprotocol/express/src/index.ts"], "@modelcontextprotocol/vitest-config": ["./node_modules/@modelcontextprotocol/vitest-config/tsconfig.json"], "@modelcontextprotocol/test-helpers": ["./node_modules/@modelcontextprotocol/test-helpers/src/index.ts"] }