Merge branch 'main' into update-protocol-version

This commit is contained in:
Cliff Hall 2025-04-28 17:59:36 -04:00 • committed by GitHub
commit f2f030e479
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 60 additions and 17 deletions

View file

@ -321,13 +321,13 @@ app.post('/mcp', async (req: Request, res: Response) => {
const transport: StreamableHTTPServerTransport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
res.on('close', () => {
console.log('Request closed');
transport.close();
server.close();
});
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
} catch (error) {
console.error('Error handling MCP request:', error);
if (!res.headersSent) {

View file

@ -1,12 +1,24 @@
import { StreamableHTTPClientTransport, StreamableHTTPReconnectionOptions } from "./streamableHttp.js";
import { OAuthClientProvider, UnauthorizedError } from "./auth.js";
import { JSONRPCMessage } from "../types.js";
describe("StreamableHTTPClientTransport", () => {
let transport: StreamableHTTPClientTransport;
let mockAuthProvider: jest.Mocked<OAuthClientProvider>;
beforeEach(() => {
transport = new StreamableHTTPClientTransport(new URL("http://localhost:1234/mcp"));
mockAuthProvider = {
get redirectUrl() { return "http://localhost/callback"; },
get clientMetadata() { return { redirect_uris: ["http://localhost/callback"] }; },
clientInformation: jest.fn(() => ({ client_id: "test-client-id", client_secret: "test-client-secret" })),
tokens: jest.fn(),
saveTokens: jest.fn(),
redirectToAuthorization: jest.fn(),
saveCodeVerifier: jest.fn(),
codeVerifier: jest.fn(),
};
transport = new StreamableHTTPClientTransport(new URL("http://localhost:1234/mcp"), { authProvider: mockAuthProvider });
jest.spyOn(global, "fetch");
});
@ -497,4 +509,27 @@ describe("StreamableHTTPClientTransport", () => {
expect(getDelay(10)).toBe(5000);
});
it("attempts auth flow on 401 during POST request", async () => {
const message: JSONRPCMessage = {
jsonrpc: "2.0",
method: "test",
params: {},
id: "test-id"
};
(global.fetch as jest.Mock)
.mockResolvedValueOnce({
ok: false,
status: 401,
statusText: "Unauthorized",
headers: new Headers()
})
.mockResolvedValue({
ok: false,
status: 404
});
await expect(transport.send(message)).rejects.toThrow(UnauthorizedError);
expect(mockAuthProvider.redirectToAuthorization.mock.calls).toHaveLength(1);
});
});

View file

@ -93,7 +93,7 @@ export interface StreamableHTTPServerTransportOptions {
* - State is maintained in-memory (connections, message history)
*
* In stateless mode:
* - Session ID is only included in initialization responses
* - No Session ID is included in any responses
* - No session validation is performed
*/
export class StreamableHTTPServerTransport implements Transport {
@ -166,7 +166,7 @@ export class StreamableHTTPServerTransport implements Transport {
}
// If an Mcp-Session-Id is returned by the server during initialization,
// clients using the Streamable HTTP transport MUST include it
// clients using the Streamable HTTP transport MUST include it
// in the Mcp-Session-Id header on all of their subsequent HTTP requests.
if (!this.validateSession(req, res)) {
return;
@ -180,7 +180,7 @@ export class StreamableHTTPServerTransport implements Transport {
}
}
// The server MUST either return Content-Type: text/event-stream in response to this HTTP GET,
// The server MUST either return Content-Type: text/event-stream in response to this HTTP GET,
// or else return HTTP 405 Method Not Allowed
const headers: Record<string, string> = {
"Content-Type": "text/event-stream",
@ -587,7 +587,7 @@ export class StreamableHTTPServerTransport implements Transport {
}
}
if (isJSONRPCResponse(message)) {
if (isJSONRPCResponse(message) || isJSONRPCError(message)) {
this._requestResponseMap.set(requestId, message);
const relatedIds = Array.from(this._requestToStreamMapping.entries())
.filter(([_, streamId]) => this._streamMapping.get(streamId) === response)

View file

@ -21,6 +21,7 @@ import {
RequestId,
Result,
ServerCapabilities,
RequestMeta,
} from "../types.js";
import { Transport, TransportSendOptions } from "./transport.js";
import { AuthInfo } from "../server/auth/types.js";
@ -115,6 +116,11 @@ export type RequestHandlerExtra<SendRequestT extends Request,
*/
sessionId?: string;
/**
* Metadata from the original request.
*/
_meta?: RequestMeta;
/**
* The JSON-RPC ID of the request being handled.
* This can be useful for tracking or logging purposes.
@ -361,6 +367,7 @@ export abstract class Protocol<
const fullExtra: RequestHandlerExtra<SendRequestT, SendNotificationT> = {
signal: abortController.signal,
sessionId: this._transport?.sessionId,
_meta: request.params?._meta,
sendNotification:
(notification) =>
this.notification(notification, { relatedRequestId: request.id }),

View file

@ -20,18 +20,18 @@ export const ProgressTokenSchema = z.union([z.string(), z.number().int()]);
*/
export const CursorSchema = z.string();
const RequestMetaSchema = z
.object({
/**
* If specified, the caller is requesting out-of-band progress notifications for this request (as represented by notifications/progress). The value of this parameter is an opaque token that will be attached to any subsequent notifications. The receiver is not obligated to provide these notifications.
*/
progressToken: z.optional(ProgressTokenSchema),
})
.passthrough();
const BaseRequestParamsSchema = z
.object({
_meta: z.optional(
z
.object({
/**
* If specified, the caller is requesting out-of-band progress notifications for this request (as represented by notifications/progress). The value of this parameter is an opaque token that will be attached to any subsequent notifications. The receiver is not obligated to provide these notifications.
*/
progressToken: z.optional(ProgressTokenSchema),
})
.passthrough(),
),
_meta: z.optional(RequestMetaSchema),
})
.passthrough();
@ -1241,6 +1241,7 @@ type Infer<Schema extends ZodTypeAny> = Flatten<z.infer<Schema>>;
export type ProgressToken = Infer<typeof ProgressTokenSchema>;
export type Cursor = Infer<typeof CursorSchema>;
export type Request = Infer<typeof RequestSchema>;
export type RequestMeta = Infer<typeof RequestMetaSchema>;
export type Notification = Infer<typeof NotificationSchema>;
export type Result = Infer<typeof ResultSchema>;
export type RequestId = Infer<typeof RequestIdSchema>;