Skip to content

Commit a293735

Browse files
committed
Add REST API endpoint for MCP server
Add POST /mcp/api/ask endpoint for tools like Redleader that can't use MCP protocol directly. Includes x-client-id header for analytics.
1 parent 49a76b8 commit a293735

File tree

1 file changed

+66
-0
lines changed

1 file changed

+66
-0
lines changed

netlify/functions/mcp.mjs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,72 @@ export default async (request, context) => {
321321
})
322322
}
323323

324+
// HTTP endpoint for tools such as Redleader that can't use the MCP protocol directly.
325+
// POST /mcp/api/ask { "question": "..." }
326+
// Optional header: x-client-id (for analytics filtering, such as "redleader")
327+
if (request.method === 'POST' && url.pathname.endsWith('/api/ask')) {
328+
const clientId = request.headers.get('x-client-id') || 'unknown'
329+
const start = Date.now()
330+
331+
try {
332+
const body = await request.json()
333+
const question = String(body?.question || '').trim()
334+
335+
if (!question) {
336+
return new Response(
337+
JSON.stringify({ error: 'missing_query', message: 'Provide a non-empty "question".' }),
338+
{ status: 400, headers: { 'content-type': 'application/json', 'cache-control': 'no-store' } }
339+
)
340+
}
341+
342+
console.log('REST API request', { client_id: clientId, question_length: question.length })
343+
344+
// Connect to Kapa
345+
await withTimeout(ensureKapaConnected(), CONNECT_TIMEOUT_MS, 'kapa_connect')
346+
347+
// Call the tool
348+
let result
349+
try {
350+
result = await withTimeout(callKapaSearch(question), CALL_TIMEOUT_MS, 'kapa_callTool')
351+
} catch (err) {
352+
const msg = err instanceof Error ? err.message : String(err)
353+
if (isTransientError(msg)) {
354+
resetKapaConnection()
355+
await withTimeout(ensureKapaConnected(), CONNECT_TIMEOUT_MS, 'kapa_reconnect')
356+
result = await withTimeout(callKapaSearch(question), CALL_TIMEOUT_MS, 'kapa_callTool_retry')
357+
} else {
358+
throw err
359+
}
360+
}
361+
362+
// Extract text content from MCP response
363+
const textContent = result?.content?.find((c) => c.type === 'text')?.text
364+
const responseData = textContent ? JSON.parse(textContent) : result
365+
366+
console.log('REST API success', { client_id: clientId, duration_ms: Date.now() - start })
367+
368+
return new Response(
369+
JSON.stringify({ success: true, data: responseData, client_id: clientId, duration_ms: Date.now() - start }),
370+
{ status: 200, headers: { 'content-type': 'application/json', 'cache-control': 'no-store' } }
371+
)
372+
} catch (err) {
373+
const msg = err instanceof Error ? err.message : String(err)
374+
console.error('REST API error', { client_id: clientId, error: msg, duration_ms: Date.now() - start })
375+
376+
const isTimeout = msg.includes('timeout')
377+
return new Response(
378+
JSON.stringify({
379+
error: isTimeout ? 'timeout' : 'upstream_error',
380+
message: 'Request failed.',
381+
detail: msg,
382+
client_id: clientId,
383+
duration_ms: Date.now() - start,
384+
}),
385+
{ status: isTimeout ? 504 : 502, headers: { 'content-type': 'application/json', 'cache-control': 'no-store' } }
386+
)
387+
}
388+
}
389+
324390
// Browser redirect
325391
const ua = request.headers.get('user-agent') || ''
326392
const accept = request.headers.get('accept') || ''

0 commit comments

Comments
 (0)