Skip to content

Commit 5a07c08

Browse files
kvzclaude
andauthored
mcp-server: return 200 for bare GET health probes instead of 406 (#347)
Directory crawlers like Glama probe MCP endpoints with a plain GET (no Accept: text/event-stream header). The MCP SDK rejects these with 406, but crawlers interpret that as "server unhealthy". Intercept non-MCP GETs in the request handler and return a friendly JSON status. Real MCP clients always send the SSE Accept header and are unaffected. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 5e98458 commit 5a07c08

File tree

3 files changed

+61
-0
lines changed

3 files changed

+61
-0
lines changed

.changeset/mcp-health-probe.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@transloadit/mcp-server': patch
3+
---
4+
5+
Return friendly 200 JSON for bare GET health probes instead of 406
6+
7+
Directory crawlers (Glama, uptime monitors) probe MCP endpoints with a plain GET without the required `Accept: text/event-stream` header. Previously this reached the MCP SDK transport which returned an opaque 406 "Not Acceptable". Now the HTTP handler intercepts these non-MCP GETs and returns a `{"name":"Transloadit MCP Server","status":"ok","docs":"..."}` response. Real MCP clients always include the SSE Accept header and are unaffected.

packages/mcp-server/src/http-request-handler.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,24 @@ export const createMcpRequestHandler = (
5151
return
5252
}
5353

54+
// Bare GETs without the SSE Accept header are not valid MCP requests (the
55+
// Streamable HTTP spec requires Accept: text/event-stream for GET). Return
56+
// a friendly JSON status so directory health-probes (Glama, uptime monitors)
57+
// see a 200 instead of the SDK's opaque 406.
58+
const accept = req.headers.accept ?? ''
59+
if (req.method === 'GET' && !accept.includes('text/event-stream')) {
60+
res.statusCode = 200
61+
res.setHeader('Content-Type', 'application/json')
62+
res.end(
63+
JSON.stringify({
64+
name: 'Transloadit MCP Server',
65+
status: 'ok',
66+
docs: 'https://transloadit.com/docs/sdks/mcp-server/',
67+
}),
68+
)
69+
return
70+
}
71+
5472
try {
5573
const parsedBody = (req as { body?: unknown }).body
5674
await transport.handleRequest(req, res, parsedBody)

packages/mcp-server/test/e2e/streamable-http-auth.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,42 @@ test('streamable http: allows authenticated client', async () => {
4545
}
4646
})
4747

48+
test('streamable http: returns friendly JSON for bare GET health probes', async () => {
49+
const server = await startHttpServer()
50+
51+
try {
52+
// Bare GET without Accept: text/event-stream (like Glama, uptime monitors)
53+
const response = await fetch(server.url, { method: 'GET' })
54+
expect(response.status).toBe(200)
55+
expect(response.headers.get('content-type')).toContain('application/json')
56+
57+
const body = await response.json()
58+
expect(body).toMatchObject({
59+
name: 'Transloadit MCP Server',
60+
status: 'ok',
61+
})
62+
expect(body.docs).toContain('transloadit.com')
63+
} finally {
64+
await server.close()
65+
}
66+
})
67+
68+
test('streamable http: passes through GET with SSE Accept header', async () => {
69+
const server = await startHttpServer()
70+
71+
try {
72+
// GET with Accept: text/event-stream should reach the MCP transport
73+
// (which will reject it for missing session-id, proving it passed through)
74+
const response = await fetch(server.url, {
75+
method: 'GET',
76+
headers: { Accept: 'text/event-stream' },
77+
})
78+
expect(response.status).not.toBe(200)
79+
} finally {
80+
await server.close()
81+
}
82+
})
83+
4884
test('streamable http: rejects disallowed origins', async () => {
4985
const server = await startHttpServer({
5086
allowedOrigins: ['https://allowed.example'],

0 commit comments

Comments
 (0)