@@ -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