Skip to content

Commit 470360d

Browse files
fix(web): return 405 for GET /api/mcp instead of hanging connection (#1064)
* fix(web): return 405 for GET /api/mcp instead of hanging connection The GET SSE stream is only used for server-initiated messages, which Sourcebot does not send. Per the MCP Streamable HTTP spec, servers that do not offer a GET SSE stream MUST return 405 Method Not Allowed. Previously the handler opened an SSE stream that never flushed its HTTP headers, causing clients to hang until timeout. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: update CHANGELOG for #1064 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent f8e21d6 commit 470360d

File tree

2 files changed

+12
-34
lines changed

2 files changed

+12
-34
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Added
1111
- Added `GET /api/diff` endpoint for retrieving structured diffs between two git refs ([#1063](https://github.com/sourcebot-dev/sourcebot/pull/1063))
1212

13+
### Fixed
14+
- Fixed `GET /api/mcp` hanging with zero bytes by returning `405 Method Not Allowed` per the MCP Streamable HTTP spec ([#1064](https://github.com/sourcebot-dev/sourcebot/pull/1064))
15+
1316
## [4.16.3] - 2026-03-27
1417

1518
### Added

packages/web/src/app/api/(server)/mcp/route.ts

Lines changed: 9 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -129,38 +129,13 @@ export const DELETE = apiHandler(async (request: NextRequest) => {
129129
return result;
130130
});
131131

132-
export const GET = apiHandler(async (request: NextRequest) => {
133-
const result = await sew(() =>
134-
withOptionalAuthV2(async ({ user }) => {
135-
if (env.EXPERIMENT_ASK_GH_ENABLED === 'true' && !user) {
136-
return notAuthenticated();
137-
}
138-
const ownerId = user?.id ?? null;
139-
const sessionId = request.headers.get(MCP_SESSION_ID_HEADER);
140-
if (!sessionId || !sessions.has(sessionId)) {
141-
return {
142-
statusCode: StatusCodes.NOT_FOUND,
143-
errorCode: ErrorCode.NOT_FOUND,
144-
message: 'Session not found.',
145-
} satisfies ServiceError;
146-
}
147-
148-
const session = sessions.get(sessionId)!;
149-
if (session.ownerId !== ownerId) {
150-
return {
151-
statusCode: StatusCodes.FORBIDDEN,
152-
errorCode: ErrorCode.INSUFFICIENT_PERMISSIONS,
153-
message: 'Session does not belong to the authenticated user.',
154-
} satisfies ServiceError;
155-
}
156-
157-
return session.transport.handleRequest(request);
158-
})
159-
);
160-
161-
if (isServiceError(result)) {
162-
return mcpErrorResponse(result);
163-
}
164-
165-
return result;
132+
// Sourcebot does not send server-initiated messages, so the GET SSE stream is not
133+
// supported. Per the MCP Streamable HTTP spec, servers that do not offer a GET SSE
134+
// stream MUST return 405 Method Not Allowed.
135+
// @see: https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#listening-for-messages-from-the-server
136+
export const GET = apiHandler(async (_request: NextRequest) => {
137+
return new Response(null, {
138+
status: StatusCodes.METHOD_NOT_ALLOWED,
139+
headers: { Allow: 'POST, DELETE' },
140+
});
166141
});

0 commit comments

Comments
 (0)