Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/mcp-health-probe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@transloadit/mcp-server': patch
---

Return friendly 200 JSON for bare GET health probes instead of 406

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.
18 changes: 18 additions & 0 deletions packages/mcp-server/src/http-request-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,24 @@ export const createMcpRequestHandler = (
return
}

// Bare GETs without the SSE Accept header are not valid MCP requests (the
// Streamable HTTP spec requires Accept: text/event-stream for GET). Return
// a friendly JSON status so directory health-probes (Glama, uptime monitors)
// see a 200 instead of the SDK's opaque 406.
const accept = req.headers.accept ?? ''
if (req.method === 'GET' && !accept.includes('text/event-stream')) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Normalize Accept header before SSE detection

HTTP media types are case-insensitive, so valid MCP clients can legally send mixed-case values like Accept: Text/Event-Stream. This check is case-sensitive, which means those requests are misclassified as health probes and get a 200 JSON status body instead of reaching transport.handleRequest, breaking the Streamable HTTP handshake for that client behavior. Converting the header to lowercase (or using media-type parsing) before matching would preserve compatibility.

Useful? React with 👍 / 👎.

res.statusCode = 200
res.setHeader('Content-Type', 'application/json')
res.end(
JSON.stringify({
name: 'Transloadit MCP Server',
status: 'ok',
docs: 'https://transloadit.com/docs/sdks/mcp-server/',
}),
)
return
}

try {
const parsedBody = (req as { body?: unknown }).body
await transport.handleRequest(req, res, parsedBody)
Expand Down
36 changes: 36 additions & 0 deletions packages/mcp-server/test/e2e/streamable-http-auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,42 @@ test('streamable http: allows authenticated client', async () => {
}
})

test('streamable http: returns friendly JSON for bare GET health probes', async () => {
const server = await startHttpServer()

try {
// Bare GET without Accept: text/event-stream (like Glama, uptime monitors)
const response = await fetch(server.url, { method: 'GET' })
expect(response.status).toBe(200)
expect(response.headers.get('content-type')).toContain('application/json')

const body = await response.json()
expect(body).toMatchObject({
name: 'Transloadit MCP Server',
status: 'ok',
})
expect(body.docs).toContain('transloadit.com')
} finally {
await server.close()
}
})

test('streamable http: passes through GET with SSE Accept header', async () => {
const server = await startHttpServer()

try {
// GET with Accept: text/event-stream should reach the MCP transport
// (which will reject it for missing session-id, proving it passed through)
const response = await fetch(server.url, {
method: 'GET',
headers: { Accept: 'text/event-stream' },
})
expect(response.status).not.toBe(200)
} finally {
await server.close()
}
})

test('streamable http: rejects disallowed origins', async () => {
const server = await startHttpServer({
allowedOrigins: ['https://allowed.example'],
Expand Down