[v2] Decouple server from express and hono - http framework-agnostic MCP server (#1326)

This commit is contained in:
Konstantin Konstantinov 2026-01-16 12:13:25 +02:00 • committed by GitHub
commit f495077e08
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 8193 additions and 15757 deletions

View file

@ -9,7 +9,8 @@
import { Client } from '@modelcontextprotocol/client';
import { InMemoryTransport, ListPromptsResultSchema } from '@modelcontextprotocol/core';
import { McpServer } from '@modelcontextprotocol/server';
import { type ZodMatrixEntry, zodTestMatrix } from '@modelcontextprotocol/test-helpers';
import type { ZodMatrixEntry } from '@modelcontextprotocol/test-helpers';
import { zodTestMatrix } from '@modelcontextprotocol/test-helpers';
describe.each(zodTestMatrix)('Issue #1277: $zodVersionLabel', (entry: ZodMatrixEntry) => {
const { z } = entry;

View file

@ -30,7 +30,9 @@ import {
SetLevelRequestSchema,
SUPPORTED_PROTOCOL_VERSIONS
} from '@modelcontextprotocol/core';
import { createMcpExpressApp, InMemoryTaskMessageQueue, InMemoryTaskStore, McpServer, Server } from '@modelcontextprotocol/server';
import { createMcpExpressApp } from '@modelcontextprotocol/express';
import { InMemoryTaskStore, McpServer, Server } from '@modelcontextprotocol/server';
import type { Request, Response } from 'express';
import supertest from 'supertest';
import * as z3 from 'zod/v3';
import * as z4 from 'zod/v4';
@ -2066,7 +2068,7 @@ describe('createMcpExpressApp', () => {
test('should parse JSON bodies', async () => {
const app = createMcpExpressApp({ host: '0.0.0.0' }); // Disable host validation for this test
app.post('/test', (req, res) => {
app.post('/test', (req: Request, res: Response) => {
res.json({ received: req.body });
});
@ -2078,7 +2080,7 @@ describe('createMcpExpressApp', () => {
test('should reject requests with invalid Host header by default', async () => {
const app = createMcpExpressApp();
app.post('/test', (_req, res) => {
app.post('/test', (_req: Request, res: Response) => {
res.json({ success: true });
});
@ -2097,7 +2099,7 @@ describe('createMcpExpressApp', () => {
test('should allow requests with localhost Host header', async () => {
const app = createMcpExpressApp();
app.post('/test', (_req, res) => {
app.post('/test', (_req: Request, res: Response) => {
res.json({ success: true });
});
@ -2109,7 +2111,7 @@ describe('createMcpExpressApp', () => {
test('should allow requests with 127.0.0.1 Host header', async () => {
const app = createMcpExpressApp();
app.post('/test', (_req, res) => {
app.post('/test', (_req: Request, res: Response) => {
res.json({ success: true });
});
@ -2121,7 +2123,7 @@ describe('createMcpExpressApp', () => {
test('should not apply host validation when host is 0.0.0.0', async () => {
const app = createMcpExpressApp({ host: '0.0.0.0' });
app.post('/test', (_req, res) => {
app.post('/test', (_req: Request, res: Response) => {
res.json({ success: true });
});
@ -2134,7 +2136,7 @@ describe('createMcpExpressApp', () => {
test('should apply host validation when host is explicitly localhost', async () => {
const app = createMcpExpressApp({ host: 'localhost' });
app.post('/test', (_req, res) => {
app.post('/test', (_req: Request, res: Response) => {
res.json({ success: true });
});
@ -2146,7 +2148,7 @@ describe('createMcpExpressApp', () => {
test('should allow requests with IPv6 localhost Host header', async () => {
const app = createMcpExpressApp();
app.post('/test', (_req, res) => {
app.post('/test', (_req: Request, res: Response) => {
res.json({ success: true });
});
@ -2158,7 +2160,7 @@ describe('createMcpExpressApp', () => {
test('should apply host validation when host is ::1 (IPv6 localhost)', async () => {
const app = createMcpExpressApp({ host: '::1' });
app.post('/test', (_req, res) => {
app.post('/test', (_req: Request, res: Response) => {
res.json({ success: true });
});
@ -2185,7 +2187,7 @@ describe('createMcpExpressApp', () => {
test('should use custom allowedHosts when provided', async () => {
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
const app = createMcpExpressApp({ host: '0.0.0.0', allowedHosts: ['myapp.local', 'localhost'] });
app.post('/test', (_req, res) => {
app.post('/test', (_req: Request, res: Response) => {
res.json({ success: true });
});
@ -2205,7 +2207,7 @@ describe('createMcpExpressApp', () => {
test('should override default localhost validation when allowedHosts is provided', async () => {
// Even though host is localhost, we're using custom allowedHosts
const app = createMcpExpressApp({ host: 'localhost', allowedHosts: ['custom.local'] });
app.post('/test', (_req, res) => {
app.post('/test', (_req: Request, res: Response) => {
res.json({ success: true });
});

View file

@ -1,26 +1,27 @@
import { Client } from '@modelcontextprotocol/client';
import { getDisplayName, InMemoryTaskStore, InMemoryTransport, UriTemplate } from '@modelcontextprotocol/core';
import type { CallToolResult, Notification, TextContent } from '@modelcontextprotocol/core';
import {
type CallToolResult,
CallToolResultSchema,
CompleteResultSchema,
ElicitRequestSchema,
ErrorCode,
getDisplayName,
GetPromptResultSchema,
InMemoryTaskStore,
InMemoryTransport,
ListPromptsResultSchema,
ListResourcesResultSchema,
ListResourceTemplatesResultSchema,
ListToolsResultSchema,
LoggingMessageNotificationSchema,
type Notification,
ReadResourceResultSchema,
type TextContent,
UriTemplate,
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 } from '@modelcontextprotocol/test-helpers';
import { zodTestMatrix } from '@modelcontextprotocol/test-helpers';
import { afterEach, beforeEach, describe, expect, test } from 'vitest';
function createLatch() {
let latch = false;

View file

@ -1,18 +1,19 @@
import { randomUUID } from 'node:crypto';
import { createServer, type Server } from 'node:http';
import type { Server } from 'node:http';
import { createServer } from 'node:http';
import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client';
import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node';
import {
CallToolResultSchema,
LATEST_PROTOCOL_VERSION,
ListPromptsResultSchema,
ListResourcesResultSchema,
ListToolsResultSchema,
McpServer,
StreamableHTTPServerTransport
McpServer
} from '@modelcontextprotocol/server';
import { listenOnRandomPort } from '@modelcontextprotocol/test-helpers';
import { type ZodMatrixEntry, zodTestMatrix } from '@modelcontextprotocol/test-helpers';
import type { ZodMatrixEntry } from '@modelcontextprotocol/test-helpers';
import { listenOnRandomPort, zodTestMatrix } from '@modelcontextprotocol/test-helpers';
describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => {
const { z } = entry;
@ -68,7 +69,7 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => {
);
// Create transport with or without session management
const serverTransport = new StreamableHTTPServerTransport({
const serverTransport = new NodeStreamableHTTPServerTransport({
sessionIdGenerator: withSessionManagement
? () => randomUUID() // With session management, generate UUID
: undefined // Without session management, return undefined
@ -89,7 +90,7 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => {
describe('Stateless Mode', () => {
let server: Server;
let mcpServer: McpServer;
let serverTransport: StreamableHTTPServerTransport;
let serverTransport: NodeStreamableHTTPServerTransport;
let baseUrl: URL;
beforeEach(async () => {
@ -253,7 +254,7 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => {
describe('Stateful Mode', () => {
let server: Server;
let mcpServer: McpServer;
let serverTransport: StreamableHTTPServerTransport;
let serverTransport: NodeStreamableHTTPServerTransport;
let baseUrl: URL;
beforeEach(async () => {

View file

@ -1,7 +1,9 @@
import { randomUUID } from 'node:crypto';
import { createServer, type Server } from 'node:http';
import type { Server } from 'node:http';
import { createServer } from 'node:http';
import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client';
import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node';
import type { TaskRequestOptions } from '@modelcontextprotocol/server';
import {
CallToolResultSchema,
@ -14,7 +16,6 @@ import {
McpError,
McpServer,
RELATED_TASK_META_KEY,
StreamableHTTPServerTransport,
TaskSchema
} from '@modelcontextprotocol/server';
import { listenOnRandomPort, waitForTaskStatus } from '@modelcontextprotocol/test-helpers';
@ -23,7 +24,7 @@ import { z } from 'zod';
describe('Task Lifecycle Integration Tests', () => {
let server: Server;
let mcpServer: McpServer;
let serverTransport: StreamableHTTPServerTransport;
let serverTransport: NodeStreamableHTTPServerTransport;
let baseUrl: URL;
let taskStore: InMemoryTaskStore;
@ -188,7 +189,7 @@ describe('Task Lifecycle Integration Tests', () => {
);
// Create transport
serverTransport = new StreamableHTTPServerTransport({
serverTransport = new NodeStreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID()
});
@ -1040,7 +1041,7 @@ describe('Task Lifecycle Integration Tests', () => {
method: 'tasks/cancel',
params: { taskId }
},
z.object({ _meta: z.record(z.unknown()).optional() })
z.object({ _meta: z.record(z.string(), z.unknown()).optional() })
);
// Verify task is cancelled

View file

@ -1,14 +1,11 @@
import { randomUUID } from 'node:crypto';
import { createServer, type Server } from 'node:http';
import type { Server } from 'node:http';
import { createServer } from 'node:http';
import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client';
import {
CallToolResultSchema,
LoggingMessageNotificationSchema,
McpServer,
StreamableHTTPServerTransport
} from '@modelcontextprotocol/server';
import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node';
import type { EventStore, JSONRPCMessage } from '@modelcontextprotocol/server';
import { CallToolResultSchema, LoggingMessageNotificationSchema, McpServer } from '@modelcontextprotocol/server';
import type { ZodMatrixEntry } from '@modelcontextprotocol/test-helpers';
import { listenOnRandomPort, zodTestMatrix } from '@modelcontextprotocol/test-helpers';
@ -51,7 +48,7 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => {
describe('Transport resumability', () => {
let server: Server;
let mcpServer: McpServer;
let serverTransport: StreamableHTTPServerTransport;
let serverTransport: NodeStreamableHTTPServerTransport;
let baseUrl: URL;
let eventStore: InMemoryEventStore;
@ -117,7 +114,7 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => {
);
// Create a transport with the event store
serverTransport = new StreamableHTTPServerTransport({
serverTransport = new NodeStreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
eventStore
});

View file

@ -1,7 +1,8 @@
import { Client } from '@modelcontextprotocol/client';
import { InMemoryTransport } from '@modelcontextprotocol/core';
import { McpServer, ResourceTemplate, Server } from '@modelcontextprotocol/server';
import { type ZodMatrixEntry, zodTestMatrix } from '@modelcontextprotocol/test-helpers';
import type { ZodMatrixEntry } from '@modelcontextprotocol/test-helpers';
import { zodTestMatrix } from '@modelcontextprotocol/test-helpers';
describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => {
const { z } = entry;