Skip to content

Commit cfaaa5d

Browse files
committed
fix: implement ChatGPT SSE+HTTP hybrid protocol
ChatGPT expects SSE transport with HTTP message endpoint: 1. SSE endpoint sends 'endpoint' event with POST URL 2. ChatGPT POSTs requests to /api/mcp/message 3. Responses returned as JSON (not SSE events) Changes: - SSE endpoint now sends 'endpoint' event immediately - Added /api/mcp/message POST endpoint for ChatGPT requests - Removed immediate tool list/initialization from SSE - SSE stream stays open for keepalive only - Keepalive interval reduced to 15s This matches the HTTP/SSE transport spec that ChatGPT expects.
1 parent 31e87a4 commit cfaaa5d

File tree

1 file changed

+48
-50
lines changed

1 file changed

+48
-50
lines changed

src/api/controllers/mcp.controller.ts

Lines changed: 48 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -34,66 +34,28 @@ export class McpController {
3434
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
3535
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
3636

37-
// Send immediate keepalive to prevent timeout
38-
res.write(': keepalive\n\n');
39-
40-
// Store connection state
41-
const connectionState = {
42-
token: null as string | null,
43-
userId: null as string | null,
44-
};
45-
4637
try {
47-
// Send initialization
48-
this.sendSSEMessage(res, 'message', {
49-
jsonrpc: '2.0',
50-
result: this.mcpService.getInitializeResponse(),
51-
});
38+
// Send immediate message endpoint event (ChatGPT protocol)
39+
// ChatGPT expects an "endpoint" event telling it where to POST requests
40+
const messageEndpoint = `${req.protocol}://${req.get('host')}/api/mcp/message`;
41+
this.sendSSEMessage(res, 'endpoint', messageEndpoint);
5242

53-
// Send available tools (including login)
54-
const tools = this.mcpService.listTools();
55-
this.sendSSEMessage(res, 'message', {
56-
jsonrpc: '2.0',
57-
result: { tools },
58-
});
59-
60-
// Send ready signal
61-
this.sendSSEMessage(res, 'message', {
62-
jsonrpc: '2.0',
63-
result: {
64-
status: 'ready',
65-
toolCount: tools.length,
66-
message: 'Use the login tool to authenticate',
67-
},
68-
});
69-
70-
// Handle incoming MCP requests
71-
req.on('data', async (chunk) => {
72-
try {
73-
const request = JSON.parse(chunk.toString()) as McpRequest;
74-
const response = await this.processMcpRequest(request, connectionState);
75-
this.sendSSEMessage(res, 'message', response);
76-
} catch (error) {
77-
this.sendSSEMessage(res, 'error', {
78-
jsonrpc: '2.0',
79-
error: {
80-
code: MCP_ERROR_CODES.PARSE_ERROR,
81-
message: error.message || 'Failed to parse request',
82-
},
83-
});
84-
}
85-
});
43+
// Send immediate keepalive
44+
res.write(': keepalive\n\n');
8645

8746
// Keep connection alive
8847
const keepAlive = setInterval(() => {
89-
res.write(': keep-alive\n\n');
90-
}, 30000);
48+
res.write(': keepalive\n\n');
49+
}, 15000); // Every 15 seconds
9150

9251
// Cleanup on disconnect
9352
req.on('close', () => {
9453
clearInterval(keepAlive);
9554
res.end();
9655
});
56+
57+
// Keep the connection open indefinitely
58+
// ChatGPT will POST to /api/mcp/message for actual requests
9759
} catch (error) {
9860
this.sendSSEMessage(res, 'error', {
9961
error: error.message || 'Stream error',
@@ -103,7 +65,43 @@ export class McpController {
10365
}
10466

10567
/**
106-
* MCP JSON-RPC Handler - Alternative POST endpoint
68+
* MCP Message Endpoint - ChatGPT posts requests here
69+
* POST /api/mcp/message
70+
*/
71+
@Post('message')
72+
@ApiOperation({
73+
summary: 'MCP Message Handler',
74+
description:
75+
'Handle MCP requests from ChatGPT connector. This is where ChatGPT POSTs tool calls.',
76+
})
77+
@ApiResponse({
78+
status: 200,
79+
description: 'MCP response',
80+
})
81+
async handleMcpMessage(@Body() request: McpRequest, @Res() res: Response): Promise<void> {
82+
// Store connection state (stateless for ChatGPT - each request is independent)
83+
const connectionState = {
84+
token: null as string | null,
85+
userId: null as string | null,
86+
};
87+
88+
try {
89+
const response = await this.processMcpRequest(request, connectionState);
90+
res.json(response);
91+
} catch (error) {
92+
res.json({
93+
jsonrpc: '2.0',
94+
id: request.id,
95+
error: {
96+
code: MCP_ERROR_CODES.INTERNAL_ERROR,
97+
message: error.message || 'Internal server error',
98+
},
99+
});
100+
}
101+
}
102+
103+
/**
104+
* MCP JSON-RPC Handler - Alternative POST endpoint (for non-ChatGPT clients)
107105
* POST /api/mcp
108106
*/
109107
@Post()

0 commit comments

Comments
 (0)