Skip to content

Commit 223fcb0

Browse files
committed
move back to hono/node-server for mapping incoming node request to web request
1 parent 5b79322 commit 223fcb0

File tree

15 files changed

+85
-269
lines changed

15 files changed

+85
-269
lines changed

.github/workflows/publish.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,6 @@ jobs:
3838
run: pnpm run build:all
3939

4040
- name: Publish preview packages
41-
run: pnpm dlx pkg-pr-new publish --packageManager=npm --pnpm './packages/server' './packages/client' './packages/server-express' './packages/server-hono'
41+
run:
42+
pnpm dlx pkg-pr-new publish --packageManager=npm --pnpm './packages/server' './packages/client'
43+
'./packages/server-express' './packages/server-hono'

README.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
# MCP TypeScript SDK
22

3-
> [!IMPORTANT]
4-
> **This is the `main` branch which contains v2 of the SDK (currently in development, pre-alpha).**
3+
> [!IMPORTANT] **This is the `main` branch which contains v2 of the SDK (currently in development, pre-alpha).**
54
>
65
> We anticipate a stable v2 release in Q1 2026. Until then, **v1.x remains the recommended version** for production use. v1.x will continue to receive bug fixes and security updates for at least 6 months after v2 ships to give people time to upgrade.
76
>

examples/server/src/simpleTaskInteractive.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,9 @@ import {
4040
InMemoryTaskStore,
4141
isTerminal,
4242
ListToolsRequestSchema,
43+
NodeStreamableHTTPServerTransport,
4344
RELATED_TASK_META_KEY,
44-
Server,
45-
NodeStreamableHTTPServerTransport
45+
Server
4646
} from '@modelcontextprotocol/server';
4747
import { createMcpExpressApp } from '@modelcontextprotocol/server-express';
4848
import type { Request, Response } from 'express';

examples/server/src/sseAndStreamableHttpCompatibleServer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { randomUUID } from 'node:crypto';
22

33
import type { CallToolResult } from '@modelcontextprotocol/server';
4-
import { isInitializeRequest, McpServer, SSEServerTransport, NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/server';
4+
import { isInitializeRequest, McpServer, NodeStreamableHTTPServerTransport, SSEServerTransport } from '@modelcontextprotocol/server';
55
import { createMcpExpressApp } from '@modelcontextprotocol/server-express';
66
import type { Request, Response } from 'express';
77
import * as z from 'zod/v4';

packages/core/src/shared/protocol.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -292,14 +292,14 @@ export type RequestHandlerExtra<SendRequestT extends Request, SendNotificationT
292292

293293
/**
294294
* Closes the SSE stream for this request, triggering client reconnection.
295-
* Only available when using NodeStreamableHTTPServerTransport with eventStore configured.
295+
* Only available when using a StreamableHTTPServerTransport with eventStore configured.
296296
* Use this to implement polling behavior during long-running operations.
297297
*/
298298
closeSSEStream?: () => void;
299299

300300
/**
301301
* Closes the standalone GET SSE stream, triggering client reconnection.
302-
* Only available when using NodeStreamableHTTPServerTransport with eventStore configured.
302+
* Only available when using aStreamableHTTPServerTransport with eventStore configured.
303303
* Use this to implement polling behavior for server-initiated notifications.
304304
*/
305305
closeStandaloneSSEStream?: () => void;

packages/server-express/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@
4545
"dependencies": {
4646
"@modelcontextprotocol/server": "workspace:^",
4747
"express": "catalog:runtimeServerOnly",
48-
"express-rate-limit": "catalog:runtimeServerOnly"
48+
"express-rate-limit": "catalog:runtimeServerOnly",
49+
"@remix-run/node-fetch-server": "catalog:runtimeServerOnly"
4950
},
5051
"devDependencies": {
5152
"@modelcontextprotocol/tsconfig": "workspace:^",

packages/server-express/src/auth/bearerAuth.ts

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import { URL } from 'node:url';
33
import type { AuthInfo } from '@modelcontextprotocol/core';
44
import type { BearerAuthMiddlewareOptions } from '@modelcontextprotocol/server';
55
import { requireBearerAuth as requireBearerAuthWeb } from '@modelcontextprotocol/server';
6-
import type { NextFunction, Request as ExpressRequest, RequestHandler, Response as ExpressResponse } from 'express';
6+
import { sendResponse } from '@remix-run/node-fetch-server';
7+
import type { NextFunction, Request as ExpressRequest, RequestHandler } from 'express';
78

89
declare module 'express-serve-static-core' {
910
interface Request {
@@ -21,15 +22,6 @@ function expressRequestUrl(req: ExpressRequest): URL {
2122
return new URL(path, `${protocol}://${host}`);
2223
}
2324

24-
async function writeWebResponse(res: ExpressResponse, webResponse: Response): Promise<void> {
25-
res.status(webResponse.status);
26-
for (const [k, v] of webResponse.headers.entries()) {
27-
res.setHeader(k, v);
28-
}
29-
const bodyText = await webResponse.text();
30-
res.send(bodyText);
31-
}
32-
3325
/**
3426
* Express middleware wrapper for the Web-standard `requireBearerAuth` helper.
3527
*
@@ -54,7 +46,7 @@ export function requireBearerAuth(options: BearerAuthMiddlewareOptions): Request
5446
return;
5547
}
5648

57-
await writeWebResponse(res, result.response);
49+
await sendResponse(res, result.response);
5850
} catch (err) {
5951
next(err);
6052
}

packages/server-express/src/auth/router.ts

Lines changed: 12 additions & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -1,126 +1,15 @@
1-
import type { IncomingMessage } from 'node:http';
2-
import { Readable } from 'node:stream';
3-
import { URL } from 'node:url';
4-
5-
import type { AuthMetadataOptions, AuthRouterOptions, WebHandlerContext } from '@modelcontextprotocol/server';
1+
import type { AuthMetadataOptions, AuthRouterOptions } from '@modelcontextprotocol/server';
62
import {
3+
getParsedBody,
74
mcpAuthMetadataRouter as createWebAuthMetadataRouter,
85
mcpAuthRouter as createWebAuthRouter,
96
TooManyRequestsError
107
} from '@modelcontextprotocol/server';
11-
import type { RequestHandler, Response as ExpressResponse } from 'express';
8+
import { createRequest, sendResponse } from '@remix-run/node-fetch-server';
9+
import type { RequestHandler } from 'express';
1210
import express from 'express';
1311
import { rateLimit } from 'express-rate-limit';
1412

15-
type ExpressRequestLike = IncomingMessage & {
16-
method: string;
17-
headers: Record<string, string | string[] | undefined>;
18-
originalUrl?: string;
19-
url?: string;
20-
protocol?: string;
21-
// express adds this when trust proxy is enabled
22-
ip?: string;
23-
body?: unknown;
24-
get?: (name: string) => string | undefined;
25-
};
26-
27-
function expressRequestUrl(req: ExpressRequestLike): URL {
28-
const host = req.get?.('host') ?? req.headers.host ?? 'localhost';
29-
const proto = req.protocol ?? 'http';
30-
const path = req.originalUrl ?? req.url ?? '/';
31-
return new URL(path, `${proto}://${host}`);
32-
}
33-
34-
function toHeaders(req: ExpressRequestLike): Headers {
35-
const headers = new Headers();
36-
for (const [key, value] of Object.entries(req.headers)) {
37-
if (value === undefined) continue;
38-
if (Array.isArray(value)) {
39-
headers.set(key, value.join(', '));
40-
} else {
41-
headers.set(key, value);
42-
}
43-
}
44-
return headers;
45-
}
46-
47-
async function readBody(req: IncomingMessage): Promise<Uint8Array> {
48-
const chunks: Buffer[] = [];
49-
for await (const chunk of req) {
50-
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
51-
}
52-
return Buffer.concat(chunks);
53-
}
54-
55-
async function expressToWebRequest(req: ExpressRequestLike, parsedBodyProvided: boolean): Promise<Request> {
56-
const url = expressRequestUrl(req);
57-
const headers = toHeaders(req);
58-
59-
// If upstream body parsing ran, the Node stream is likely consumed.
60-
if (parsedBodyProvided) {
61-
return new Request(url, { method: req.method, headers });
62-
}
63-
64-
if (req.method === 'GET' || req.method === 'HEAD') {
65-
return new Request(url, { method: req.method, headers });
66-
}
67-
68-
const body = await readBody(req);
69-
return new Request(url, { method: req.method, headers, body });
70-
}
71-
72-
async function writeWebResponse(res: ExpressResponse, webResponse: Response): Promise<void> {
73-
res.status(webResponse.status);
74-
75-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
76-
const getSetCookie = (webResponse.headers as any).getSetCookie as (() => string[]) | undefined;
77-
const setCookies = typeof getSetCookie === 'function' ? getSetCookie.call(webResponse.headers) : undefined;
78-
79-
for (const [key, value] of webResponse.headers.entries()) {
80-
if (key.toLowerCase() === 'set-cookie' && setCookies?.length) continue;
81-
res.setHeader(key, value);
82-
}
83-
84-
if (setCookies?.length) {
85-
res.setHeader('set-cookie', setCookies);
86-
}
87-
88-
res.flushHeaders?.();
89-
90-
if (!webResponse.body) {
91-
res.end();
92-
return;
93-
}
94-
95-
await new Promise<void>((resolve, reject) => {
96-
const readable = Readable.fromWeb(webResponse.body as unknown as ReadableStream);
97-
readable.on('error', err => {
98-
try {
99-
res.destroy(err as Error);
100-
} catch {
101-
// ignore
102-
}
103-
reject(err);
104-
});
105-
res.on('error', reject);
106-
res.on('close', () => {
107-
try {
108-
readable.destroy();
109-
} catch {
110-
// ignore
111-
}
112-
});
113-
readable.pipe(res);
114-
res.on('finish', () => resolve());
115-
});
116-
}
117-
118-
function toHandlerContext(req: ExpressRequestLike): WebHandlerContext {
119-
return {
120-
parsedBody: req.body
121-
};
122-
}
123-
12413
export type ExpressAuthRateLimitOptions =
12514
| false
12615
| {
@@ -172,10 +61,10 @@ export function mcpAuthRouter(options: AuthRouterOptions & { rateLimit?: Express
17261
}
17362
handlers.push(async (req, res, next) => {
17463
try {
175-
const parsedBodyProvided = (req as ExpressRequestLike).body !== undefined;
176-
const webReq = await expressToWebRequest(req as ExpressRequestLike, parsedBodyProvided);
177-
const webRes = await route.handler(webReq, toHandlerContext(req as ExpressRequestLike));
178-
await writeWebResponse(res, webRes);
64+
const webReq = createRequest(req, res);
65+
const parsedBody = req.body !== undefined ? req.body : await getParsedBody(webReq);
66+
const webRes = await route.handler(webReq, { parsedBody });
67+
await sendResponse(res, webRes);
17968
} catch (err) {
18069
next(err);
18170
}
@@ -198,10 +87,10 @@ export function mcpAuthMetadataRouter(options: AuthMetadataOptions): RequestHand
19887
for (const route of web.routes) {
19988
router.all(route.path, async (req, res, next) => {
20089
try {
201-
const parsedBodyProvided = (req as ExpressRequestLike).body !== undefined;
202-
const webReq = await expressToWebRequest(req as ExpressRequestLike, parsedBodyProvided);
203-
const webRes = await route.handler(webReq, toHandlerContext(req as ExpressRequestLike));
204-
await writeWebResponse(res, webRes);
90+
const webReq = createRequest(req, res);
91+
const parsedBody = req.body !== undefined ? req.body : await getParsedBody(webReq);
92+
const webRes = await route.handler(webReq, { parsedBody });
93+
await sendResponse(res, webRes);
20594
} catch (err) {
20695
next(err);
20796
}

packages/server/package.json

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,10 @@
4444
"client": "tsx scripts/cli.ts client"
4545
},
4646
"dependencies": {
47+
"@hono/node-server": "catalog:runtimeServerOnly",
4748
"content-type": "catalog:runtimeServerOnly",
48-
"raw-body": "catalog:runtimeServerOnly",
4949
"pkce-challenge": "catalog:runtimeShared",
50+
"raw-body": "catalog:runtimeServerOnly",
5051
"zod": "catalog:runtimeShared",
5152
"zod-to-json-schema": "catalog:runtimeShared"
5253
},
@@ -63,13 +64,13 @@
6364
}
6465
},
6566
"devDependencies": {
67+
"@cfworker/json-schema": "catalog:runtimeShared",
68+
"@eslint/js": "catalog:devTools",
6669
"@modelcontextprotocol/core": "workspace:^",
67-
"@modelcontextprotocol/tsconfig": "workspace:^",
68-
"@modelcontextprotocol/vitest-config": "workspace:^",
6970
"@modelcontextprotocol/eslint-config": "workspace:^",
7071
"@modelcontextprotocol/test-helpers": "workspace:^",
71-
"@cfworker/json-schema": "catalog:runtimeShared",
72-
"@eslint/js": "catalog:devTools",
72+
"@modelcontextprotocol/tsconfig": "workspace:^",
73+
"@modelcontextprotocol/vitest-config": "workspace:^",
7374
"@types/content-type": "catalog:devTools",
7475
"@types/cors": "catalog:devTools",
7576
"@types/cross-spawn": "catalog:devTools",

0 commit comments

Comments
 (0)