Merge branch 'main' into update-protocol-version
This commit is contained in:
commit
f2f030e479
5 changed files with 60 additions and 17 deletions
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 }),
|
||||
|
|
|
|||
21
src/types.ts
21
src/types.ts
|
|
@ -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>;
|
||||
|
|
|
|||
Loading…
Reference in a new issue