Skip to content

Commit 69dc2f0

Browse files
fix(mcp): Use SDK web-standard transport for copilot mcp (#4320)
* fix(api): return 499 on copilot mcp user aborts * fix(mcp): fix copilot mcp response
1 parent 3afcad2 commit 69dc2f0

1 file changed

Lines changed: 11 additions & 210 deletions

File tree

apps/sim/app/api/mcp/copilot/route.ts

Lines changed: 11 additions & 210 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
2-
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'
2+
import { WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js'
33
import {
44
CallToolRequestSchema,
55
type CallToolResult,
@@ -166,16 +166,6 @@ function createError(id: RequestId, code: ErrorCode | number, message: string):
166166
}
167167
}
168168

169-
function normalizeRequestHeaders(request: NextRequest): HeaderMap {
170-
const headers: HeaderMap = {}
171-
172-
request.headers.forEach((value, key) => {
173-
headers[key.toLowerCase()] = value
174-
})
175-
176-
return headers
177-
}
178-
179169
function readHeader(headers: HeaderMap | undefined, name: string): string | undefined {
180170
if (!headers) return undefined
181171
const value = headers[name.toLowerCase()]
@@ -185,190 +175,6 @@ function readHeader(headers: HeaderMap | undefined, name: string): string | unde
185175
return value
186176
}
187177

188-
class NextResponseCapture {
189-
private _status = 200
190-
private _headers = new Headers()
191-
private _controller: ReadableStreamDefaultController<Uint8Array> | null = null
192-
private _pendingChunks: Uint8Array[] = []
193-
private _closeHandlers: Array<() => void> = []
194-
private _errorHandlers: Array<(error: Error) => void> = []
195-
private _headersWritten = false
196-
private _ended = false
197-
private _headersPromise: Promise<void>
198-
private _resolveHeaders: (() => void) | null = null
199-
private _endedPromise: Promise<void>
200-
private _resolveEnded: (() => void) | null = null
201-
readonly readable: ReadableStream<Uint8Array>
202-
203-
constructor() {
204-
this._headersPromise = new Promise<void>((resolve) => {
205-
this._resolveHeaders = resolve
206-
})
207-
208-
this._endedPromise = new Promise<void>((resolve) => {
209-
this._resolveEnded = resolve
210-
})
211-
212-
this.readable = new ReadableStream<Uint8Array>({
213-
start: (controller) => {
214-
this._controller = controller
215-
if (this._pendingChunks.length > 0) {
216-
for (const chunk of this._pendingChunks) {
217-
controller.enqueue(chunk)
218-
}
219-
this._pendingChunks = []
220-
}
221-
},
222-
cancel: () => {
223-
this._ended = true
224-
this._resolveEnded?.()
225-
this.triggerCloseHandlers()
226-
},
227-
})
228-
}
229-
230-
private markHeadersWritten(): void {
231-
if (this._headersWritten) return
232-
this._headersWritten = true
233-
this._resolveHeaders?.()
234-
}
235-
236-
private triggerCloseHandlers(): void {
237-
for (const handler of this._closeHandlers) {
238-
try {
239-
handler()
240-
} catch (error) {
241-
this.triggerErrorHandlers(toError(error))
242-
}
243-
}
244-
}
245-
246-
private triggerErrorHandlers(error: Error): void {
247-
for (const errorHandler of this._errorHandlers) {
248-
errorHandler(error)
249-
}
250-
}
251-
252-
private normalizeChunk(chunk: unknown): Uint8Array | null {
253-
if (typeof chunk === 'string') {
254-
return new TextEncoder().encode(chunk)
255-
}
256-
257-
if (chunk instanceof Uint8Array) {
258-
return chunk
259-
}
260-
261-
if (chunk === undefined || chunk === null) {
262-
return null
263-
}
264-
265-
return new TextEncoder().encode(String(chunk))
266-
}
267-
268-
writeHead(status: number, headers?: Record<string, string | number | string[]>): this {
269-
this._status = status
270-
271-
if (headers) {
272-
Object.entries(headers).forEach(([key, value]) => {
273-
if (Array.isArray(value)) {
274-
this._headers.set(key, value.join(', '))
275-
} else {
276-
this._headers.set(key, String(value))
277-
}
278-
})
279-
}
280-
281-
this.markHeadersWritten()
282-
return this
283-
}
284-
285-
flushHeaders(): this {
286-
this.markHeadersWritten()
287-
return this
288-
}
289-
290-
write(chunk: unknown): boolean {
291-
const normalized = this.normalizeChunk(chunk)
292-
if (!normalized) return true
293-
294-
this.markHeadersWritten()
295-
296-
if (this._controller) {
297-
try {
298-
this._controller.enqueue(normalized)
299-
} catch (error) {
300-
this.triggerErrorHandlers(toError(error))
301-
}
302-
} else {
303-
this._pendingChunks.push(normalized)
304-
}
305-
306-
return true
307-
}
308-
309-
end(chunk?: unknown): this {
310-
if (chunk !== undefined) this.write(chunk)
311-
this.markHeadersWritten()
312-
if (this._ended) return this
313-
314-
this._ended = true
315-
this._resolveEnded?.()
316-
317-
if (this._controller) {
318-
try {
319-
this._controller.close()
320-
} catch (error) {
321-
this.triggerErrorHandlers(toError(error))
322-
}
323-
}
324-
325-
this.triggerCloseHandlers()
326-
327-
return this
328-
}
329-
330-
async waitForHeaders(timeoutMs = 30000): Promise<void> {
331-
if (this._headersWritten) return
332-
333-
await Promise.race([
334-
this._headersPromise,
335-
new Promise<void>((resolve) => {
336-
setTimeout(resolve, timeoutMs)
337-
}),
338-
])
339-
}
340-
341-
async waitForEnd(timeoutMs = 30000): Promise<void> {
342-
if (this._ended) return
343-
344-
await Promise.race([
345-
this._endedPromise,
346-
new Promise<void>((resolve) => {
347-
setTimeout(resolve, timeoutMs)
348-
}),
349-
])
350-
}
351-
352-
on(event: 'close' | 'error', handler: (() => void) | ((error: Error) => void)): this {
353-
if (event === 'close') {
354-
this._closeHandlers.push(handler as () => void)
355-
}
356-
357-
if (event === 'error') {
358-
this._errorHandlers.push(handler as (error: Error) => void)
359-
}
360-
361-
return this
362-
}
363-
364-
toNextResponse(): NextResponse {
365-
return new NextResponse(this.readable, {
366-
status: this._status,
367-
headers: this._headers,
368-
})
369-
}
370-
}
371-
372178
function buildMcpServer(abortSignal?: AbortSignal): Server {
373179
const server = new Server(
374180
{
@@ -503,29 +309,17 @@ function buildMcpServer(abortSignal?: AbortSignal): Server {
503309
async function handleMcpRequestWithSdk(
504310
request: NextRequest,
505311
parsedBody: unknown
506-
): Promise<NextResponse> {
312+
): Promise<Response> {
507313
const server = buildMcpServer(request.signal)
508-
const transport = new StreamableHTTPServerTransport({
314+
const transport = new WebStandardStreamableHTTPServerTransport({
509315
sessionIdGenerator: undefined,
510316
enableJsonResponse: true,
511317
})
512318

513-
const responseCapture = new NextResponseCapture()
514-
const requestAdapter = {
515-
method: request.method,
516-
headers: normalizeRequestHeaders(request),
517-
}
518-
519319
await server.connect(transport)
520320

521321
try {
522-
await transport.handleRequest(requestAdapter as any, responseCapture as any, parsedBody)
523-
await responseCapture.waitForHeaders()
524-
// Must exceed the longest possible tool execution.
525-
// Using ORCHESTRATION_TIMEOUT_MS + 60 s buffer so the orchestrator can
526-
// finish or time-out on its own before the transport is torn down.
527-
await responseCapture.waitForEnd(ORCHESTRATION_TIMEOUT_MS + 60_000)
528-
return responseCapture.toNextResponse()
322+
return await transport.handleRequest(request, { parsedBody })
529323
} finally {
530324
await server.close().catch(() => {})
531325
await transport.close().catch(() => {})
@@ -567,6 +361,13 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
567361

568362
return await handleMcpRequestWithSdk(request, parsedBody)
569363
} catch (error) {
364+
if (request.signal.aborted || (error as Error)?.name === 'AbortError') {
365+
return NextResponse.json(
366+
createError(0, ErrorCode.ConnectionClosed, 'Client cancelled request'),
367+
{ status: 499 }
368+
)
369+
}
370+
570371
logger.error('Error handling MCP request', { error })
571372
return NextResponse.json(createError(0, ErrorCode.InternalError, 'Internal error'), {
572373
status: 500,

0 commit comments

Comments
 (0)