chore: bump ext-apps + example servers to 1.7.1, sdk to 1.29.0
ext-apps@1.7.x peers on @modelcontextprotocol/sdk ^1.29.0, which required bumping the pinned sdk from 1.24.2 and adapting to its StreamableHTTPServerTransport refactor: - redisTransport: sessionId is now a readonly getter on the Node wrapper; inject it (and _initialized) on the underlying web-standard transport so resumed sessions validate. Also guard message.id which is now optional on JSONRPCMessage. - shttp.integration.test: handleRequest now routes through @hono/node-server, so the mock IncomingMessage needs url/host/ rawHeaders/on/off/resume, and the session-id header now arrives via writeHead instead of setHeader.
This commit is contained in:
parent
219bba3ab0
commit
e83d4207a6
2 changed files with 167 additions and 113 deletions
|
|
@ -48,16 +48,32 @@ describe('Streamable HTTP Handler Integration Tests', () => {
|
|||
},
|
||||
} as unknown as Partial<Response>;
|
||||
|
||||
// Create mock request
|
||||
// Create mock request. SDK 1.29 routes handleRequest through
|
||||
// @hono/node-server which expects a real IncomingMessage shape, so
|
||||
// the mock supplies the EventEmitter/stream surface hono touches.
|
||||
mockReq = {
|
||||
method: 'POST',
|
||||
url: '/',
|
||||
on: jest.fn(),
|
||||
off: jest.fn(),
|
||||
once: jest.fn(),
|
||||
removeListener: jest.fn(),
|
||||
resume: jest.fn(),
|
||||
errored: null,
|
||||
headers: {
|
||||
host: 'localhost',
|
||||
'content-type': 'application/json',
|
||||
'accept': 'application/json, text/event-stream',
|
||||
'mcp-protocol-version': '2024-11-05',
|
||||
},
|
||||
body: {},
|
||||
};
|
||||
} as unknown as Partial<Request>;
|
||||
// Derive rawHeaders lazily so tests that mutate/replace `headers` stay in sync.
|
||||
Object.defineProperty(mockReq, 'rawHeaders', {
|
||||
get() {
|
||||
return Object.entries(this.headers ?? {}).flatMap(([k, v]) => [k, String(v)]);
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// Helper function to trigger cleanup after handleStreamableHTTP calls
|
||||
|
|
@ -83,7 +99,16 @@ describe('Streamable HTTP Handler Integration Tests', () => {
|
|||
if (sessionIdHeader?.[1]) {
|
||||
return sessionIdHeader[1] as string;
|
||||
}
|
||||
|
||||
|
||||
// SDK 1.29 writes headers via writeHead(status, headers) instead of setHeader
|
||||
const writeHeadCalls = (mockRes.writeHead as jest.Mock).mock.calls;
|
||||
for (const [, headers] of writeHeadCalls) {
|
||||
if (headers && typeof headers === 'object') {
|
||||
const id = (headers as Record<string, string>)['mcp-session-id'] ?? (headers as Record<string, string>)['Mcp-Session-Id'];
|
||||
if (id) return id;
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to extracting from Redis channels
|
||||
const allChannels = Array.from(mockRedis.subscribers.keys());
|
||||
const serverChannel = allChannels.find(channel => channel.includes('mcp:shttp:toserver:'));
|
||||
|
|
|
|||
|
|
@ -144,7 +144,7 @@ export async function redisRelayToMcpServer(sessionId: string, transport: Transp
|
|||
const messagePromise = new Promise<JSONRPCMessage>((resolve) => {
|
||||
transport.onmessage = async (message, extra) => {
|
||||
// First, set up response subscription if needed
|
||||
if ("id" in message) {
|
||||
if ("id" in message && message.id !== undefined) {
|
||||
logger.debug('Setting up response subscription', {
|
||||
sessionId,
|
||||
messageId: message.id,
|
||||
|
|
@ -289,7 +289,7 @@ export class ServerRedisTransport implements Transport {
|
|||
}
|
||||
|
||||
async send(message: JSONRPCMessage, options?: TransportSendOptions): Promise<void> {
|
||||
const relatedRequestId = options?.relatedRequestId?.toString() ?? ("id" in message ? message.id?.toString() : notificationStreamId);
|
||||
const relatedRequestId = options?.relatedRequestId?.toString() ?? ("id" in message && message.id !== undefined ? message.id.toString() : notificationStreamId);
|
||||
const channel = getToClientChannel(this._sessionId, relatedRequestId)
|
||||
|
||||
logger.debug('Sending message to client', {
|
||||
|
|
@ -336,13 +336,18 @@ export async function getShttpTransport(sessionId: string, onsessionclosed: (ses
|
|||
isGetRequest
|
||||
});
|
||||
|
||||
// Giving undefined here and setting the sessionId means the
|
||||
// transport wont try to create a new session.
|
||||
// Inject the existing session ID so the transport resumes it instead of
|
||||
// creating a new one. SDK 1.29 made sessionId readonly on the wrapper, but
|
||||
// the underlying web-standard transport still exposes it as a writable field;
|
||||
// _initialized must also be set so validateSession accepts non-init requests.
|
||||
const shttpTransport = new StreamableHTTPServerTransport({
|
||||
sessionIdGenerator: undefined,
|
||||
sessionIdGenerator: () => sessionId,
|
||||
onsessionclosed,
|
||||
})
|
||||
shttpTransport.sessionId = sessionId;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const inner = (shttpTransport as any)['_webStandardTransport'];
|
||||
inner.sessionId = sessionId;
|
||||
inner._initialized = true;
|
||||
|
||||
// Use the new request-id based relay approach
|
||||
const cleanup = await redisRelayToMcpServer(sessionId, shttpTransport, isGetRequest);
|
||||
|
|
|
|||
Loading…
Reference in a new issue