- Notifications
You must be signed in to change notification settings - Fork 1.5k
Closed
Labels
enhancementRequest for a new feature that's not currently supportedRequest for a new feature that's not currently supportedv2Ideas, requests and plans for v2 of the SDK which will incorporate major changes and fixesIdeas, requests and plans for v2 of the SDK which will incorporate major changes and fixes
Description
Is your feature request related to a problem? Please describe.
I don't want to work with Node's http request/response APIs.
Describe the solution you'd like
I would like to work with web standard fetch request/response APIs. In my case, I'm integrating with React Router (framework mode), but I imagine this would be helpful to anyone using Hono.js, other modern frameworks, Cloudflare, Bun, Deno, etc.
Describe alternatives you've considered
I've built a transport myself that seems to work:
import{typeTransport}from'@modelcontextprotocol/sdk/shared/transport.js'import{typeJSONRPCMessage,JSONRPCMessageSchema,}from'@modelcontextprotocol/sdk/types.js'/** * Server transport for SSE using Standard Fetch: this will send messages over * an SSE connection and receive messages from HTTP POST requests. */exportclassFetchSSEServerTransportimplementsTransport{ #stream?: ReadableStreamDefaultController<string> #sessionId: string #endpoint: stringonclose?: ()=>voidonerror?: (error: Error)=>voidonmessage?: (message: JSONRPCMessage)=>void/** * Creates a new SSE server transport, which will direct the client to POST * messages to the relative or absolute URL identified by `endpoint`. */constructor(endpoint: string,sessionId?: string|null){this.#endpoint =endpointthis.#sessionId =sessionId??crypto.randomUUID()}/** * Starts processing messages on the transport. * This is called by the Server class and should not be called directly. */asyncstart(): Promise<void>{if(this.#stream){thrownewError('FetchSSEServerTransport already started! If using Server class, note that connect() calls start() automatically.',)}}/** * Handles the initial SSE connection request. * This should be called from your Remix loader to establish the SSE stream. */asynchandleSSERequest(request: Request): Promise<Response>{conststream=newReadableStream<string>({start: (controller)=>{this.#stream =controller// Send headerscontroller.enqueue(': ping\n\n')// Keep connection alive// Send the endpoint eventcontroller.enqueue(`event: endpoint\ndata: ${encodeURI(this.#endpoint,)}?sessionId=${this.#sessionId}\n\n`,)// Handle cleanup when the connection closesrequest.signal.addEventListener('abort',()=>{controller.close()this.#stream =undefinedthis.onclose?.()})},cancel: ()=>{this.#stream =undefinedthis.onclose?.()},})returnnewResponse(stream,{headers: {'Content-Type': 'text/event-stream','Cache-Control': 'no-cache',Connection: 'keep-alive','Mcp-Session-Id': this.#sessionId,},})}/** * Handles incoming POST messages. * This should be called from your Remix action to handle incoming messages. */asynchandlePostMessage(request: Request): Promise<Response>{if(!this.#stream){constmessage='SSE connection not established'returnnewResponse(message,{status: 500})}letbody: unknowntry{constcontentType=request.headers.get('content-type')if(contentType!=='application/json'){thrownewError(`Unsupported content-type: ${contentType}`)}body=awaitrequest.json()}catch(error){this.onerror?.(errorasError)returnnewResponse(String(error),{status: 400})}try{awaitthis.handleMessage(body)}catch(error){console.error(error)returnnewResponse(`Invalid message: ${body}`,{status: 400})}returnnewResponse('Accepted',{status: 202})}/** * Handle a client message, regardless of how it arrived. */asynchandleMessage(message: unknown): Promise<void>{letparsedMessage: JSONRPCMessagetry{parsedMessage=JSONRPCMessageSchema.parse(message)}catch(error){this.onerror?.(errorasError)throwerror}this.onmessage?.(parsedMessage)}asyncclose(): Promise<void>{this.#stream?.close()this.#stream =undefinedthis.onclose?.()}asyncsend(message: JSONRPCMessage): Promise<void>{if(!this.#stream){thrownewError('Not connected')}// Send the message through the event streamthis.#stream.enqueue(`event: message\ndata: ${JSON.stringify(message)}\n\n`)}/** * Returns the session ID for this transport. * This can be used to route incoming POST requests. */getsessionId(): string{returnthis.#sessionId }}Feel free to use this.
jlalmes, nakasyou, rogervila, lloydzhou, franklintarter and 9 morelloydzhou
Metadata
Metadata
Assignees
Labels
enhancementRequest for a new feature that's not currently supportedRequest for a new feature that's not currently supportedv2Ideas, requests and plans for v2 of the SDK which will incorporate major changes and fixesIdeas, requests and plans for v2 of the SDK which will incorporate major changes and fixes