11import { UnauthorizedError } from '@modelcontextprotocol/sdk/client/auth.js'
22import { createLogger } from '@sim/logger'
3+ import { getErrorMessage } from '@sim/utils/errors'
34import type { NextRequest } from 'next/server'
45import { mcpToolDiscoveryQuerySchema , refreshMcpToolsBodySchema } from '@/lib/api/contracts/mcp'
56import { validationErrorResponse } from '@/lib/api/server'
67import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
7- import { getParsedBody , withMcpAuth } from '@/lib/mcp/middleware'
8+ import {
9+ mcpBodyReadErrorResponse ,
10+ readMcpJsonBodyWithLimit ,
11+ withMcpAuth ,
12+ } from '@/lib/mcp/middleware'
813import { mcpService } from '@/lib/mcp/service'
914import { McpOauthAuthorizationRequiredError , type McpToolDiscoveryResponse } from '@/lib/mcp/types'
1015import { categorizeError , createMcpErrorResponse , createMcpSuccessResponse } from '@/lib/mcp/utils'
1116
1217const logger = createLogger ( 'McpToolDiscoveryAPI' )
18+ const MCP_REFRESH_DISCOVERY_CONCURRENCY = 5
1319
1420export const dynamic = 'force-dynamic'
1521
22+ async function settleWithConcurrency < T , R > (
23+ items : T [ ] ,
24+ concurrency : number ,
25+ task : ( item : T ) => Promise < R >
26+ ) : Promise < Array < PromiseSettledResult < R > > > {
27+ const results : Array < PromiseSettledResult < R > | undefined > = new Array ( items . length )
28+ let nextIndex = 0
29+
30+ const workers = Array . from ( { length : Math . min ( concurrency , items . length ) } , async ( ) => {
31+ while ( nextIndex < items . length ) {
32+ const index = nextIndex
33+ nextIndex += 1
34+ try {
35+ results [ index ] = { status : 'fulfilled' , value : await task ( items [ index ] ) }
36+ } catch ( reason ) {
37+ results [ index ] = { status : 'rejected' , reason }
38+ }
39+ }
40+ } )
41+
42+ await Promise . all ( workers )
43+
44+ return results . map (
45+ ( result ) =>
46+ result ?? {
47+ status : 'rejected' ,
48+ reason : new Error ( 'MCP refresh discovery task did not run' ) ,
49+ }
50+ )
51+ }
52+
1653export const GET = withRouteHandler (
1754 withMcpAuth ( 'read' ) ( async ( request : NextRequest , { userId, workspaceId, requestId } ) => {
1855 try {
@@ -63,7 +100,7 @@ export const GET = withRouteHandler(
63100export const POST = withRouteHandler (
64101 withMcpAuth ( 'read' ) ( async ( request : NextRequest , { userId, workspaceId, requestId } ) => {
65102 try {
66- const rawBody = getParsedBody ( request ) ?? ( await request . json ( ) )
103+ const rawBody = await readMcpJsonBodyWithLimit ( request )
67104 const parsedBody = refreshMcpToolsBodySchema . safeParse ( rawBody )
68105
69106 if ( ! parsedBody . success ) {
@@ -74,11 +111,13 @@ export const POST = withRouteHandler(
74111
75112 logger . info ( `[${ requestId } ] Refreshing tools for ${ serverIds . length } servers` )
76113
77- const results = await Promise . allSettled (
78- serverIds . map ( async ( serverId : string ) => {
114+ const results = await settleWithConcurrency (
115+ serverIds ,
116+ MCP_REFRESH_DISCOVERY_CONCURRENCY ,
117+ async ( serverId : string ) => {
79118 const tools = await mcpService . discoverServerTools ( userId , serverId , workspaceId , true )
80119 return { serverId, toolCount : tools . length }
81- } )
120+ }
82121 )
83122
84123 const successes : Array < { serverId : string ; toolCount : number } > = [ ]
@@ -91,7 +130,7 @@ export const POST = withRouteHandler(
91130 } else {
92131 failures . push ( {
93132 serverId,
94- error : result . reason instanceof Error ? result . reason . message : 'Unknown error' ,
133+ error : getErrorMessage ( result . reason , 'Unknown error' ) ,
95134 } )
96135 }
97136 } )
@@ -107,6 +146,8 @@ export const POST = withRouteHandler(
107146 } ,
108147 } )
109148 } catch ( error ) {
149+ const bodyErrorResponse = mcpBodyReadErrorResponse ( error , request )
150+ if ( bodyErrorResponse ) return bodyErrorResponse
110151 if (
111152 error instanceof McpOauthAuthorizationRequiredError ||
112153 error instanceof UnauthorizedError
0 commit comments