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:
Olivier Chafik 2026-04-27 16:35:26 +01:00
commit e83d4207a6
No known key found for this signature in database
2 changed files with 167 additions and 113 deletions

View file

@ -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:'));

View file

@ -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);