@wener/mcps 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/LICENSE +21 -0
  2. package/dist/index.mjs +15 -0
  3. package/dist/mcps-cli.mjs +174727 -0
  4. package/lib/chat/agent.js +187 -0
  5. package/lib/chat/agent.js.map +1 -0
  6. package/lib/chat/audit.js +238 -0
  7. package/lib/chat/audit.js.map +1 -0
  8. package/lib/chat/converters.js +467 -0
  9. package/lib/chat/converters.js.map +1 -0
  10. package/lib/chat/handler.js +1068 -0
  11. package/lib/chat/handler.js.map +1 -0
  12. package/lib/chat/index.js +12 -0
  13. package/lib/chat/index.js.map +1 -0
  14. package/lib/chat/types.js +35 -0
  15. package/lib/chat/types.js.map +1 -0
  16. package/lib/contracts/AuditContract.js +85 -0
  17. package/lib/contracts/AuditContract.js.map +1 -0
  18. package/lib/contracts/McpsContract.js +113 -0
  19. package/lib/contracts/McpsContract.js.map +1 -0
  20. package/lib/contracts/index.js +3 -0
  21. package/lib/contracts/index.js.map +1 -0
  22. package/lib/dev.server.js +7 -0
  23. package/lib/dev.server.js.map +1 -0
  24. package/lib/entities/ChatRequestEntity.js +318 -0
  25. package/lib/entities/ChatRequestEntity.js.map +1 -0
  26. package/lib/entities/McpRequestEntity.js +271 -0
  27. package/lib/entities/McpRequestEntity.js.map +1 -0
  28. package/lib/entities/RequestLogEntity.js +177 -0
  29. package/lib/entities/RequestLogEntity.js.map +1 -0
  30. package/lib/entities/ResponseEntity.js +150 -0
  31. package/lib/entities/ResponseEntity.js.map +1 -0
  32. package/lib/entities/index.js +11 -0
  33. package/lib/entities/index.js.map +1 -0
  34. package/lib/entities/types.js +11 -0
  35. package/lib/entities/types.js.map +1 -0
  36. package/lib/index.js +3 -0
  37. package/lib/index.js.map +1 -0
  38. package/lib/mcps-cli.js +44 -0
  39. package/lib/mcps-cli.js.map +1 -0
  40. package/lib/providers/McpServerHandlerDef.js +40 -0
  41. package/lib/providers/McpServerHandlerDef.js.map +1 -0
  42. package/lib/providers/findMcpServerDef.js +26 -0
  43. package/lib/providers/findMcpServerDef.js.map +1 -0
  44. package/lib/providers/prometheus/def.js +24 -0
  45. package/lib/providers/prometheus/def.js.map +1 -0
  46. package/lib/providers/prometheus/index.js +2 -0
  47. package/lib/providers/prometheus/index.js.map +1 -0
  48. package/lib/providers/relay/def.js +32 -0
  49. package/lib/providers/relay/def.js.map +1 -0
  50. package/lib/providers/relay/index.js +2 -0
  51. package/lib/providers/relay/index.js.map +1 -0
  52. package/lib/providers/sql/def.js +31 -0
  53. package/lib/providers/sql/def.js.map +1 -0
  54. package/lib/providers/sql/index.js +2 -0
  55. package/lib/providers/sql/index.js.map +1 -0
  56. package/lib/providers/tencent-cls/def.js +44 -0
  57. package/lib/providers/tencent-cls/def.js.map +1 -0
  58. package/lib/providers/tencent-cls/index.js +2 -0
  59. package/lib/providers/tencent-cls/index.js.map +1 -0
  60. package/lib/scripts/bundle.js +90 -0
  61. package/lib/scripts/bundle.js.map +1 -0
  62. package/lib/server/api-routes.js +96 -0
  63. package/lib/server/api-routes.js.map +1 -0
  64. package/lib/server/audit.js +274 -0
  65. package/lib/server/audit.js.map +1 -0
  66. package/lib/server/chat-routes.js +82 -0
  67. package/lib/server/chat-routes.js.map +1 -0
  68. package/lib/server/config.js +223 -0
  69. package/lib/server/config.js.map +1 -0
  70. package/lib/server/db.js +97 -0
  71. package/lib/server/db.js.map +1 -0
  72. package/lib/server/index.js +2 -0
  73. package/lib/server/index.js.map +1 -0
  74. package/lib/server/mcp-handler.js +167 -0
  75. package/lib/server/mcp-handler.js.map +1 -0
  76. package/lib/server/mcp-routes.js +112 -0
  77. package/lib/server/mcp-routes.js.map +1 -0
  78. package/lib/server/mcps-router.js +119 -0
  79. package/lib/server/mcps-router.js.map +1 -0
  80. package/lib/server/schema.js +129 -0
  81. package/lib/server/schema.js.map +1 -0
  82. package/lib/server/server.js +166 -0
  83. package/lib/server/server.js.map +1 -0
  84. package/lib/web/ChatPage.js +827 -0
  85. package/lib/web/ChatPage.js.map +1 -0
  86. package/lib/web/McpInspectorPage.js +214 -0
  87. package/lib/web/McpInspectorPage.js.map +1 -0
  88. package/lib/web/ServersPage.js +93 -0
  89. package/lib/web/ServersPage.js.map +1 -0
  90. package/lib/web/main.js +541 -0
  91. package/lib/web/main.js.map +1 -0
  92. package/package.json +83 -0
  93. package/src/chat/agent.ts +240 -0
  94. package/src/chat/audit.ts +377 -0
  95. package/src/chat/converters.test.ts +325 -0
  96. package/src/chat/converters.ts +459 -0
  97. package/src/chat/handler.test.ts +137 -0
  98. package/src/chat/handler.ts +1233 -0
  99. package/src/chat/index.ts +16 -0
  100. package/src/chat/types.ts +72 -0
  101. package/src/contracts/AuditContract.ts +93 -0
  102. package/src/contracts/McpsContract.ts +141 -0
  103. package/src/contracts/index.ts +18 -0
  104. package/src/dev.server.ts +7 -0
  105. package/src/entities/ChatRequestEntity.ts +157 -0
  106. package/src/entities/McpRequestEntity.ts +149 -0
  107. package/src/entities/RequestLogEntity.ts +78 -0
  108. package/src/entities/ResponseEntity.ts +75 -0
  109. package/src/entities/index.ts +12 -0
  110. package/src/entities/types.ts +188 -0
  111. package/src/index.ts +1 -0
  112. package/src/mcps-cli.ts +59 -0
  113. package/src/providers/McpServerHandlerDef.ts +105 -0
  114. package/src/providers/findMcpServerDef.ts +31 -0
  115. package/src/providers/prometheus/def.ts +21 -0
  116. package/src/providers/prometheus/index.ts +1 -0
  117. package/src/providers/relay/def.ts +31 -0
  118. package/src/providers/relay/index.ts +1 -0
  119. package/src/providers/relay/relay.test.ts +47 -0
  120. package/src/providers/sql/def.ts +33 -0
  121. package/src/providers/sql/index.ts +1 -0
  122. package/src/providers/tencent-cls/def.ts +38 -0
  123. package/src/providers/tencent-cls/index.ts +1 -0
  124. package/src/scripts/bundle.ts +82 -0
  125. package/src/server/api-routes.ts +98 -0
  126. package/src/server/audit.ts +310 -0
  127. package/src/server/chat-routes.ts +95 -0
  128. package/src/server/config.test.ts +162 -0
  129. package/src/server/config.ts +198 -0
  130. package/src/server/db.ts +115 -0
  131. package/src/server/index.ts +1 -0
  132. package/src/server/mcp-handler.ts +209 -0
  133. package/src/server/mcp-routes.ts +133 -0
  134. package/src/server/mcps-router.ts +133 -0
  135. package/src/server/schema.ts +175 -0
  136. package/src/server/server.ts +163 -0
  137. package/src/web/ChatPage.tsx +1005 -0
  138. package/src/web/McpInspectorPage.tsx +254 -0
  139. package/src/web/ServersPage.tsx +139 -0
  140. package/src/web/main.tsx +600 -0
  141. package/src/web/styles.css +15 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/server/mcp-handler.ts"],"sourcesContent":["import type { StreamableHTTPTransport } from '@hono/mcp';\nimport consola from 'consola';\nimport { HeaderNames } from './schema';\n\nconst mcpLog = consola.withTag('mcp');\n\n/**\n * Tool annotation filter options\n */\nexport interface ToolFilterOptions {\n\t/** Only include tools with readOnlyHint: true */\n\treadonlyOnly?: boolean;\n\t/** Include patterns (glob) */\n\tincludePatterns?: string[];\n\t/** Exclude patterns (glob) */\n\texcludePatterns?: string[];\n}\n\n/**\n * Check if a pattern matches a string (simple glob support)\n */\nfunction matchPattern(pattern: string, value: string): boolean {\n\t// Convert glob to regex\n\tconst regex = new RegExp(`^${pattern.replace(/\\*/g, '.*').replace(/\\?/g, '.')}$`, 'i');\n\treturn regex.test(value);\n}\n\n/**\n * Filter tools based on filter options\n */\nexport function filterTools(tools: any[], options: ToolFilterOptions): any[] {\n\tlet filtered = tools;\n\n\t// Filter by readonly annotation\n\tif (options.readonlyOnly) {\n\t\tfiltered = filtered.filter((tool) => {\n\t\t\tconst readOnlyHint = tool.annotations?.readOnlyHint;\n\t\t\treturn readOnlyHint === true;\n\t\t});\n\t}\n\n\t// Filter by include patterns\n\tif (options.includePatterns && options.includePatterns.length > 0) {\n\t\tfiltered = filtered.filter((tool) => options.includePatterns?.some((pattern) => matchPattern(pattern, tool.name)));\n\t}\n\n\t// Filter by exclude patterns\n\tif (options.excludePatterns && options.excludePatterns.length > 0) {\n\t\tfiltered = filtered.filter((tool) => !options.excludePatterns?.some((pattern) => matchPattern(pattern, tool.name)));\n\t}\n\n\treturn filtered;\n}\n\n/**\n * Create a logging and filtering wrapper for MCP transport\n * - Logs tool calls and their results with duration\n * - Filters tools/list response based on X-MCP-Readonly, X-MCP-Include, X-MCP-Exclude headers\n */\nexport function createMcpLoggingHandler(transport: StreamableHTTPTransport, serverName: string) {\n\tconst originalHandleRequest = transport.handleRequest.bind(transport);\n\n\treturn async (c: Parameters<typeof transport.handleRequest>[0]) => {\n\t\tconst startTime = Date.now();\n\n\t\t// Get filter headers\n\t\tconst readonlyHeader = c.req.header(HeaderNames.MCP_READONLY);\n\t\tconst includeHeader = c.req.header(HeaderNames.MCP_INCLUDE);\n\t\tconst excludeHeader = c.req.header(HeaderNames.MCP_EXCLUDE);\n\n\t\tconst filterOptions: ToolFilterOptions = {};\n\t\tif (readonlyHeader?.toLowerCase() === 'true') {\n\t\t\tfilterOptions.readonlyOnly = true;\n\t\t}\n\t\tif (includeHeader) {\n\t\t\tfilterOptions.includePatterns = includeHeader.split(',').map((p) => p.trim());\n\t\t}\n\t\tif (excludeHeader) {\n\t\t\tfilterOptions.excludePatterns = excludeHeader.split(',').map((p) => p.trim());\n\t\t}\n\n\t\tconst needsFiltering = filterOptions.readonlyOnly || filterOptions.includePatterns || filterOptions.excludePatterns;\n\n\t\t// Log incoming request (clone body to avoid consuming it)\n\t\tconst contentType = c.req.header('content-type');\n\t\tconst isPost = c.req.method === 'POST';\n\t\tlet isToolsList = false;\n\t\tlet isToolsCall = false;\n\t\tlet toolName = '';\n\t\tlet parsedBody: any;\n\n\t\tif (isPost && contentType?.includes('application/json')) {\n\t\t\ttry {\n\t\t\t\t// Clone the request to read body without consuming it\n\t\t\t\tconst clonedReq = c.req.raw.clone();\n\t\t\t\tparsedBody = await clonedReq.json();\n\t\t\t\t// JSON-RPC request\n\t\t\t\tif (parsedBody.method === 'tools/call' && parsedBody.params) {\n\t\t\t\t\tconst { name, arguments: args } = parsedBody.params;\n\t\t\t\t\ttoolName = name;\n\t\t\t\t\tisToolsCall = true;\n\t\t\t\t\tmcpLog.info(`→ [${serverName}] tools/call: ${name}`, args ? JSON.stringify(args).slice(0, 200) : '');\n\t\t\t\t} else if (parsedBody.method === 'tools/list') {\n\t\t\t\t\tmcpLog.debug(`→ [${serverName}] tools/list`);\n\t\t\t\t\tisToolsList = true;\n\t\t\t\t} else if (parsedBody.method) {\n\t\t\t\t\tmcpLog.debug(`→ [${serverName}] ${parsedBody.method}`);\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Ignore parse errors, let the transport handle them\n\t\t\t}\n\t\t}\n\n\t\t// Call original handler with error handling\n\t\tlet response: Response | undefined;\n\t\ttry {\n\t\t\tresponse = await originalHandleRequest(c);\n\t\t} catch (e) {\n\t\t\tconst duration = Date.now() - startTime;\n\t\t\tmcpLog.error(`✗ [${serverName}] handler error (${duration}ms):`, e);\n\t\t\tthrow e;\n\t\t}\n\n\t\t// For tools/call, we need to wait for the SSE stream to complete to get accurate duration\n\t\t// Clone the response to read it without consuming the original\n\t\tif (isToolsCall && response instanceof Response) {\n\t\t\tconst clonedResponse = response.clone();\n\t\t\t// Read the cloned response in the background to measure actual duration\n\t\t\tclonedResponse\n\t\t\t\t.text()\n\t\t\t\t.then(() => {\n\t\t\t\t\tconst duration = Date.now() - startTime;\n\t\t\t\t\tmcpLog.info(`← [${serverName}] tools/call: ${toolName} (${duration}ms)`);\n\t\t\t\t})\n\t\t\t\t.catch(() => {\n\t\t\t\t\t// Ignore read errors\n\t\t\t\t});\n\t\t}\n\n\t\t// If this is a tools/list response and we need filtering, intercept and modify\n\t\tif (isToolsList && needsFiltering && response instanceof Response) {\n\t\t\ttry {\n\t\t\t\tconst responseText = await response.clone().text();\n\t\t\t\tconst contentTypeHeader = response.headers.get('content-type') || '';\n\n\t\t\t\t// Handle SSE format (event: message\\ndata: {...})\n\t\t\t\tif (contentTypeHeader.includes('text/event-stream')) {\n\t\t\t\t\t// Parse SSE data - find the data line\n\t\t\t\t\tconst lines = responseText.split('\\n');\n\t\t\t\t\tlet jsonData = '';\n\t\t\t\t\tfor (const line of lines) {\n\t\t\t\t\t\tif (line.startsWith('data: ')) {\n\t\t\t\t\t\t\tjsonData = line.slice(6);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tif (jsonData) {\n\t\t\t\t\t\tconst responseData = JSON.parse(jsonData);\n\n\t\t\t\t\t\tif (responseData.result?.tools && Array.isArray(responseData.result.tools)) {\n\t\t\t\t\t\t\tconst originalCount = responseData.result.tools.length;\n\t\t\t\t\t\t\tresponseData.result.tools = filterTools(responseData.result.tools, filterOptions);\n\t\t\t\t\t\t\tconst filteredCount = responseData.result.tools.length;\n\n\t\t\t\t\t\t\tif (filteredCount !== originalCount) {\n\t\t\t\t\t\t\t\tmcpLog.info(\n\t\t\t\t\t\t\t\t\t`← [${serverName}] tools/list: filtered ${originalCount} → ${filteredCount} tools (readonly=${filterOptions.readonlyOnly || false})`,\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// Return new SSE response with filtered tools\n\t\t\t\t\t\t\tconst newSseData = `event: message\\ndata: ${JSON.stringify(responseData)}\\n\\n`;\n\t\t\t\t\t\t\treturn new Response(newSseData, {\n\t\t\t\t\t\t\t\tstatus: response.status,\n\t\t\t\t\t\t\t\theaders: response.headers,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// Handle plain JSON response\n\t\t\t\t\tconst responseData = JSON.parse(responseText);\n\n\t\t\t\t\tif (responseData.result?.tools && Array.isArray(responseData.result.tools)) {\n\t\t\t\t\t\tconst originalCount = responseData.result.tools.length;\n\t\t\t\t\t\tresponseData.result.tools = filterTools(responseData.result.tools, filterOptions);\n\t\t\t\t\t\tconst filteredCount = responseData.result.tools.length;\n\n\t\t\t\t\t\tif (filteredCount !== originalCount) {\n\t\t\t\t\t\t\tmcpLog.info(\n\t\t\t\t\t\t\t\t`← [${serverName}] tools/list: filtered ${originalCount} → ${filteredCount} tools (readonly=${filterOptions.readonlyOnly || false})`,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\treturn new Response(JSON.stringify(responseData), {\n\t\t\t\t\t\t\tstatus: response.status,\n\t\t\t\t\t\t\theaders: response.headers,\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} catch (e) {\n\t\t\t\t// If we can't parse/modify, return original\n\t\t\t\tmcpLog.warn(`[${serverName}] Failed to filter tools/list response: ${e instanceof Error ? e.message : e}`);\n\t\t\t}\n\t\t}\n\n\t\treturn response;\n\t};\n}\n"],"names":["consola","HeaderNames","mcpLog","withTag","matchPattern","pattern","value","regex","RegExp","replace","test","filterTools","tools","options","filtered","readonlyOnly","filter","tool","readOnlyHint","annotations","includePatterns","length","some","name","excludePatterns","createMcpLoggingHandler","transport","serverName","originalHandleRequest","handleRequest","bind","c","startTime","Date","now","readonlyHeader","req","header","MCP_READONLY","includeHeader","MCP_INCLUDE","excludeHeader","MCP_EXCLUDE","filterOptions","toLowerCase","split","map","p","trim","needsFiltering","contentType","isPost","method","isToolsList","isToolsCall","toolName","parsedBody","includes","clonedReq","raw","clone","json","params","arguments","args","info","JSON","stringify","slice","debug","response","e","duration","error","Response","clonedResponse","text","then","catch","responseText","contentTypeHeader","headers","get","lines","jsonData","line","startsWith","responseData","parse","result","Array","isArray","originalCount","filteredCount","newSseData","status","warn","Error","message"],"mappings":"AACA,OAAOA,aAAa,UAAU;AAC9B,SAASC,WAAW,QAAQ,WAAW;AAEvC,MAAMC,SAASF,QAAQG,OAAO,CAAC;AAc/B;;CAEC,GACD,SAASC,aAAaC,OAAe,EAAEC,KAAa;IACnD,wBAAwB;IACxB,MAAMC,QAAQ,IAAIC,OAAO,CAAC,CAAC,EAAEH,QAAQI,OAAO,CAAC,OAAO,MAAMA,OAAO,CAAC,OAAO,KAAK,CAAC,CAAC,EAAE;IAClF,OAAOF,MAAMG,IAAI,CAACJ;AACnB;AAEA;;CAEC,GACD,OAAO,SAASK,YAAYC,KAAY,EAAEC,OAA0B;IACnE,IAAIC,WAAWF;IAEf,gCAAgC;IAChC,IAAIC,QAAQE,YAAY,EAAE;QACzBD,WAAWA,SAASE,MAAM,CAAC,CAACC;YAC3B,MAAMC,eAAeD,KAAKE,WAAW,EAAED;YACvC,OAAOA,iBAAiB;QACzB;IACD;IAEA,6BAA6B;IAC7B,IAAIL,QAAQO,eAAe,IAAIP,QAAQO,eAAe,CAACC,MAAM,GAAG,GAAG;QAClEP,WAAWA,SAASE,MAAM,CAAC,CAACC,OAASJ,QAAQO,eAAe,EAAEE,KAAK,CAACjB,UAAYD,aAAaC,SAASY,KAAKM,IAAI;IAChH;IAEA,6BAA6B;IAC7B,IAAIV,QAAQW,eAAe,IAAIX,QAAQW,eAAe,CAACH,MAAM,GAAG,GAAG;QAClEP,WAAWA,SAASE,MAAM,CAAC,CAACC,OAAS,CAACJ,QAAQW,eAAe,EAAEF,KAAK,CAACjB,UAAYD,aAAaC,SAASY,KAAKM,IAAI;IACjH;IAEA,OAAOT;AACR;AAEA;;;;CAIC,GACD,OAAO,SAASW,wBAAwBC,SAAkC,EAAEC,UAAkB;IAC7F,MAAMC,wBAAwBF,UAAUG,aAAa,CAACC,IAAI,CAACJ;IAE3D,OAAO,OAAOK;QACb,MAAMC,YAAYC,KAAKC,GAAG;QAE1B,qBAAqB;QACrB,MAAMC,iBAAiBJ,EAAEK,GAAG,CAACC,MAAM,CAACpC,YAAYqC,YAAY;QAC5D,MAAMC,gBAAgBR,EAAEK,GAAG,CAACC,MAAM,CAACpC,YAAYuC,WAAW;QAC1D,MAAMC,gBAAgBV,EAAEK,GAAG,CAACC,MAAM,CAACpC,YAAYyC,WAAW;QAE1D,MAAMC,gBAAmC,CAAC;QAC1C,IAAIR,gBAAgBS,kBAAkB,QAAQ;YAC7CD,cAAc5B,YAAY,GAAG;QAC9B;QACA,IAAIwB,eAAe;YAClBI,cAAcvB,eAAe,GAAGmB,cAAcM,KAAK,CAAC,KAAKC,GAAG,CAAC,CAACC,IAAMA,EAAEC,IAAI;QAC3E;QACA,IAAIP,eAAe;YAClBE,cAAcnB,eAAe,GAAGiB,cAAcI,KAAK,CAAC,KAAKC,GAAG,CAAC,CAACC,IAAMA,EAAEC,IAAI;QAC3E;QAEA,MAAMC,iBAAiBN,cAAc5B,YAAY,IAAI4B,cAAcvB,eAAe,IAAIuB,cAAcnB,eAAe;QAEnH,0DAA0D;QAC1D,MAAM0B,cAAcnB,EAAEK,GAAG,CAACC,MAAM,CAAC;QACjC,MAAMc,SAASpB,EAAEK,GAAG,CAACgB,MAAM,KAAK;QAChC,IAAIC,cAAc;QAClB,IAAIC,cAAc;QAClB,IAAIC,WAAW;QACf,IAAIC;QAEJ,IAAIL,UAAUD,aAAaO,SAAS,qBAAqB;YACxD,IAAI;gBACH,sDAAsD;gBACtD,MAAMC,YAAY3B,EAAEK,GAAG,CAACuB,GAAG,CAACC,KAAK;gBACjCJ,aAAa,MAAME,UAAUG,IAAI;gBACjC,mBAAmB;gBACnB,IAAIL,WAAWJ,MAAM,KAAK,gBAAgBI,WAAWM,MAAM,EAAE;oBAC5D,MAAM,EAAEvC,IAAI,EAAEwC,WAAWC,IAAI,EAAE,GAAGR,WAAWM,MAAM;oBACnDP,WAAWhC;oBACX+B,cAAc;oBACdpD,OAAO+D,IAAI,CAAC,CAAC,GAAG,EAAEtC,WAAW,cAAc,EAAEJ,MAAM,EAAEyC,OAAOE,KAAKC,SAAS,CAACH,MAAMI,KAAK,CAAC,GAAG,OAAO;gBAClG,OAAO,IAAIZ,WAAWJ,MAAM,KAAK,cAAc;oBAC9ClD,OAAOmE,KAAK,CAAC,CAAC,GAAG,EAAE1C,WAAW,YAAY,CAAC;oBAC3C0B,cAAc;gBACf,OAAO,IAAIG,WAAWJ,MAAM,EAAE;oBAC7BlD,OAAOmE,KAAK,CAAC,CAAC,GAAG,EAAE1C,WAAW,EAAE,EAAE6B,WAAWJ,MAAM,EAAE;gBACtD;YACD,EAAE,OAAM;YACP,qDAAqD;YACtD;QACD;QAEA,4CAA4C;QAC5C,IAAIkB;QACJ,IAAI;YACHA,WAAW,MAAM1C,sBAAsBG;QACxC,EAAE,OAAOwC,GAAG;YACX,MAAMC,WAAWvC,KAAKC,GAAG,KAAKF;YAC9B9B,OAAOuE,KAAK,CAAC,CAAC,GAAG,EAAE9C,WAAW,iBAAiB,EAAE6C,SAAS,IAAI,CAAC,EAAED;YACjE,MAAMA;QACP;QAEA,0FAA0F;QAC1F,+DAA+D;QAC/D,IAAIjB,eAAegB,oBAAoBI,UAAU;YAChD,MAAMC,iBAAiBL,SAASV,KAAK;YACrC,wEAAwE;YACxEe,eACEC,IAAI,GACJC,IAAI,CAAC;gBACL,MAAML,WAAWvC,KAAKC,GAAG,KAAKF;gBAC9B9B,OAAO+D,IAAI,CAAC,CAAC,GAAG,EAAEtC,WAAW,cAAc,EAAE4B,SAAS,EAAE,EAAEiB,SAAS,GAAG,CAAC;YACxE,GACCM,KAAK,CAAC;YACN,qBAAqB;YACtB;QACF;QAEA,+EAA+E;QAC/E,IAAIzB,eAAeJ,kBAAkBqB,oBAAoBI,UAAU;YAClE,IAAI;gBACH,MAAMK,eAAe,MAAMT,SAASV,KAAK,GAAGgB,IAAI;gBAChD,MAAMI,oBAAoBV,SAASW,OAAO,CAACC,GAAG,CAAC,mBAAmB;gBAElE,kDAAkD;gBAClD,IAAIF,kBAAkBvB,QAAQ,CAAC,sBAAsB;oBACpD,sCAAsC;oBACtC,MAAM0B,QAAQJ,aAAalC,KAAK,CAAC;oBACjC,IAAIuC,WAAW;oBACf,KAAK,MAAMC,QAAQF,MAAO;wBACzB,IAAIE,KAAKC,UAAU,CAAC,WAAW;4BAC9BF,WAAWC,KAAKjB,KAAK,CAAC;4BACtB;wBACD;oBACD;oBAEA,IAAIgB,UAAU;wBACb,MAAMG,eAAerB,KAAKsB,KAAK,CAACJ;wBAEhC,IAAIG,aAAaE,MAAM,EAAE7E,SAAS8E,MAAMC,OAAO,CAACJ,aAAaE,MAAM,CAAC7E,KAAK,GAAG;4BAC3E,MAAMgF,gBAAgBL,aAAaE,MAAM,CAAC7E,KAAK,CAACS,MAAM;4BACtDkE,aAAaE,MAAM,CAAC7E,KAAK,GAAGD,YAAY4E,aAAaE,MAAM,CAAC7E,KAAK,EAAE+B;4BACnE,MAAMkD,gBAAgBN,aAAaE,MAAM,CAAC7E,KAAK,CAACS,MAAM;4BAEtD,IAAIwE,kBAAkBD,eAAe;gCACpC1F,OAAO+D,IAAI,CACV,CAAC,GAAG,EAAEtC,WAAW,uBAAuB,EAAEiE,cAAc,GAAG,EAAEC,cAAc,iBAAiB,EAAElD,cAAc5B,YAAY,IAAI,MAAM,CAAC,CAAC;4BAEtI;4BAEA,8CAA8C;4BAC9C,MAAM+E,aAAa,CAAC,sBAAsB,EAAE5B,KAAKC,SAAS,CAACoB,cAAc,IAAI,CAAC;4BAC9E,OAAO,IAAIb,SAASoB,YAAY;gCAC/BC,QAAQzB,SAASyB,MAAM;gCACvBd,SAASX,SAASW,OAAO;4BAC1B;wBACD;oBACD;gBACD,OAAO;oBACN,6BAA6B;oBAC7B,MAAMM,eAAerB,KAAKsB,KAAK,CAACT;oBAEhC,IAAIQ,aAAaE,MAAM,EAAE7E,SAAS8E,MAAMC,OAAO,CAACJ,aAAaE,MAAM,CAAC7E,KAAK,GAAG;wBAC3E,MAAMgF,gBAAgBL,aAAaE,MAAM,CAAC7E,KAAK,CAACS,MAAM;wBACtDkE,aAAaE,MAAM,CAAC7E,KAAK,GAAGD,YAAY4E,aAAaE,MAAM,CAAC7E,KAAK,EAAE+B;wBACnE,MAAMkD,gBAAgBN,aAAaE,MAAM,CAAC7E,KAAK,CAACS,MAAM;wBAEtD,IAAIwE,kBAAkBD,eAAe;4BACpC1F,OAAO+D,IAAI,CACV,CAAC,GAAG,EAAEtC,WAAW,uBAAuB,EAAEiE,cAAc,GAAG,EAAEC,cAAc,iBAAiB,EAAElD,cAAc5B,YAAY,IAAI,MAAM,CAAC,CAAC;wBAEtI;wBAEA,OAAO,IAAI2D,SAASR,KAAKC,SAAS,CAACoB,eAAe;4BACjDQ,QAAQzB,SAASyB,MAAM;4BACvBd,SAASX,SAASW,OAAO;wBAC1B;oBACD;gBACD;YACD,EAAE,OAAOV,GAAG;gBACX,4CAA4C;gBAC5CrE,OAAO8F,IAAI,CAAC,CAAC,CAAC,EAAErE,WAAW,wCAAwC,EAAE4C,aAAa0B,QAAQ1B,EAAE2B,OAAO,GAAG3B,GAAG;YAC1G;QACD;QAEA,OAAOD;IACR;AACD"}
@@ -0,0 +1,112 @@
1
+ import { StreamableHTTPTransport } from "@hono/mcp";
2
+ import consola from "consola";
3
+ import { getMcpServerHandlerDef } from "../providers/McpServerHandlerDef.js";
4
+ import { findMcpServerDef } from "../providers/findMcpServerDef.js";
5
+ import { createMcpLoggingHandler } from "./mcp-handler.js";
6
+ const log = consola.withTag("mcps");
7
+ /**
8
+ * Register MCP routes for both pre-configured and dynamic endpoints
9
+ */ export function registerMcpRoutes({ app, config, serverCache }) {
10
+ const serverDefs = findMcpServerDef();
11
+ // Register pre-configured servers from config
12
+ // These are named endpoints like /mcp/my-sql that use config from file
13
+ for (const [name, serverConfig] of Object.entries(config.servers)) {
14
+ const def = getMcpServerHandlerDef(serverConfig.type);
15
+ if (!def) {
16
+ log.warn(`Unknown server type for ${name}: ${serverConfig.type}`);
17
+ continue;
18
+ }
19
+ // Resolve config using def (config comes from file, not headers)
20
+ const options = def.resolveConfig(serverConfig);
21
+ if (!options) {
22
+ log.warn(`Failed to resolve config for ${name}`);
23
+ continue;
24
+ }
25
+ const validation = def.validateOptions(options);
26
+ if (!validation.valid) {
27
+ log.warn(`Invalid config for ${name}: ${validation.error}`);
28
+ continue;
29
+ }
30
+ // Create and cache the server instance at startup
31
+ const cacheKey = `config::${name}`;
32
+ const item = def.create(options);
33
+ if (!item) {
34
+ log.warn(`Failed to create server: ${name}`);
35
+ continue;
36
+ }
37
+ serverCache.set(cacheKey, item);
38
+ const path = `/mcp/${name}`;
39
+ log.info(`Registered MCP server: ${path} (${serverConfig.type})`);
40
+ app.all(path, async (c) => {
41
+ const serverItem = serverCache.get(cacheKey);
42
+ if (!serverItem) {
43
+ return c.text("Server not found", 404);
44
+ }
45
+ // Create a new transport for each request to avoid "Transport already started" error
46
+ const transport = new StreamableHTTPTransport();
47
+ try {
48
+ await serverItem.server.connect(transport);
49
+ const handleRequest = createMcpLoggingHandler(transport, name);
50
+ return await handleRequest(c);
51
+ }
52
+ catch (e) {
53
+ log.error(`[${name}] Request error:`, e);
54
+ return c.text(`Internal server error: ${e instanceof Error ? e.message : "Unknown error"}`, 500);
55
+ }
56
+ });
57
+ }
58
+ // Register dynamic endpoints for all server types
59
+ // These endpoints accept config via HTTP headers
60
+ for (const def of serverDefs) {
61
+ const path = `/mcp/${def.name}`;
62
+ log.debug(`Registering dynamic endpoint: ${path}`);
63
+ app.all(path, async (c) => {
64
+ // Use def.resolveConfig to parse config from headers
65
+ const options = def.resolveConfig({
66
+ type: def.name
67
+ }, c.req.raw.headers);
68
+ if (!options) {
69
+ // Build error message from headerMappings
70
+ const requiredHeaders = def.headerMappings?.filter((m) => m.required).map((m) => m.header).join(", ") || "required headers";
71
+ return c.text(`Missing ${requiredHeaders}`, 400);
72
+ }
73
+ // Validate options
74
+ const validation = def.validateOptions(options);
75
+ if (!validation.valid) {
76
+ return c.text(validation.error || `Invalid configuration for ${def.name}`, 400);
77
+ }
78
+ // Get or create cached server instance
79
+ const key = def.getCacheKey(options);
80
+ let item = serverCache.get(key);
81
+ if (!item) {
82
+ log.info(`Creating new ${def.title} server: ${key}`);
83
+ try {
84
+ const newItem = def.create(options);
85
+ if (!newItem)
86
+ return c.text(`Failed to create ${def.name} server`, 500);
87
+ item = newItem;
88
+ serverCache.set(key, item);
89
+ }
90
+ catch (e) {
91
+ log.error(`Failed to create ${def.name} server:`, e);
92
+ return c.text(`Failed to create ${def.name} server`, 500);
93
+ }
94
+ }
95
+ // Create a new transport for each request
96
+ const transport = new StreamableHTTPTransport();
97
+ try {
98
+ await item.server.connect(transport);
99
+ const handleRequest = createMcpLoggingHandler(transport, def.name);
100
+ return await handleRequest(c);
101
+ }
102
+ catch (e) {
103
+ log.error(`[${def.name}] Request error:`, e);
104
+ return c.text(`Internal server error: ${e instanceof Error ? e.message : "Unknown error"}`, 500);
105
+ }
106
+ });
107
+ }
108
+ return {
109
+ serverDefs
110
+ };
111
+ }
112
+ //# sourceMappingURL=mcp-routes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/server/mcp-routes.ts"],"sourcesContent":["import { StreamableHTTPTransport } from '@hono/mcp';\nimport consola from 'consola';\nimport type { Hono } from 'hono';\nimport type { LRUCache } from 'lru-cache';\nimport type { McpServerInstance } from '@wener/ai/mcp';\nimport { getMcpServerHandlerDef, type McpServerHandlerDef } from '../providers/McpServerHandlerDef';\nimport { findMcpServerDef } from '../providers/findMcpServerDef';\nimport { createMcpLoggingHandler } from './mcp-handler';\nimport type { McpsConfig, ServerConfig } from './schema';\n\nconst log = consola.withTag('mcps');\n\nexport interface RegisterMcpRoutesOptions {\n\tapp: Hono;\n\tconfig: McpsConfig;\n\tserverCache: LRUCache<string, McpServerInstance>;\n}\n\n/**\n * Register MCP routes for both pre-configured and dynamic endpoints\n */\nexport function registerMcpRoutes({ app, config, serverCache }: RegisterMcpRoutesOptions) {\n\tconst serverDefs = findMcpServerDef();\n\n\t// Register pre-configured servers from config\n\t// These are named endpoints like /mcp/my-sql that use config from file\n\tfor (const [name, serverConfig] of Object.entries(config.servers)) {\n\t\tconst def = getMcpServerHandlerDef(serverConfig.type);\n\t\tif (!def) {\n\t\t\tlog.warn(`Unknown server type for ${name}: ${serverConfig.type}`);\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Resolve config using def (config comes from file, not headers)\n\t\tconst options = def.resolveConfig(serverConfig);\n\t\tif (!options) {\n\t\t\tlog.warn(`Failed to resolve config for ${name}`);\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst validation = def.validateOptions(options);\n\t\tif (!validation.valid) {\n\t\t\tlog.warn(`Invalid config for ${name}: ${validation.error}`);\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Create and cache the server instance at startup\n\t\tconst cacheKey = `config::${name}`;\n\t\tconst item = def.create(options);\n\t\tif (!item) {\n\t\t\tlog.warn(`Failed to create server: ${name}`);\n\t\t\tcontinue;\n\t\t}\n\t\tserverCache.set(cacheKey, item);\n\n\t\tconst path = `/mcp/${name}`;\n\t\tlog.info(`Registered MCP server: ${path} (${serverConfig.type})`);\n\n\t\tapp.all(path, async (c) => {\n\t\t\tconst serverItem = serverCache.get(cacheKey);\n\t\t\tif (!serverItem) {\n\t\t\t\treturn c.text('Server not found', 404);\n\t\t\t}\n\t\t\t// Create a new transport for each request to avoid \"Transport already started\" error\n\t\t\tconst transport = new StreamableHTTPTransport();\n\t\t\ttry {\n\t\t\t\tawait serverItem.server.connect(transport);\n\t\t\t\tconst handleRequest = createMcpLoggingHandler(transport, name);\n\t\t\t\treturn await handleRequest(c);\n\t\t\t} catch (e) {\n\t\t\t\tlog.error(`[${name}] Request error:`, e);\n\t\t\t\treturn c.text(`Internal server error: ${e instanceof Error ? e.message : 'Unknown error'}`, 500);\n\t\t\t}\n\t\t});\n\t}\n\n\t// Register dynamic endpoints for all server types\n\t// These endpoints accept config via HTTP headers\n\tfor (const def of serverDefs) {\n\t\tconst path = `/mcp/${def.name}`;\n\t\tlog.debug(`Registering dynamic endpoint: ${path}`);\n\n\t\tapp.all(path, async (c) => {\n\t\t\t// Use def.resolveConfig to parse config from headers\n\t\t\tconst options = def.resolveConfig({ type: def.name } as any, c.req.raw.headers);\n\n\t\t\tif (!options) {\n\t\t\t\t// Build error message from headerMappings\n\t\t\t\tconst requiredHeaders =\n\t\t\t\t\tdef.headerMappings\n\t\t\t\t\t\t?.filter((m) => m.required)\n\t\t\t\t\t\t.map((m) => m.header)\n\t\t\t\t\t\t.join(', ') || 'required headers';\n\t\t\t\treturn c.text(`Missing ${requiredHeaders}`, 400);\n\t\t\t}\n\n\t\t\t// Validate options\n\t\t\tconst validation = def.validateOptions(options);\n\t\t\tif (!validation.valid) {\n\t\t\t\treturn c.text(validation.error || `Invalid configuration for ${def.name}`, 400);\n\t\t\t}\n\n\t\t\t// Get or create cached server instance\n\t\t\tconst key = def.getCacheKey(options);\n\t\t\tlet item = serverCache.get(key);\n\t\t\tif (!item) {\n\t\t\t\tlog.info(`Creating new ${def.title} server: ${key}`);\n\t\t\t\ttry {\n\t\t\t\t\tconst newItem = def.create(options);\n\t\t\t\t\tif (!newItem) return c.text(`Failed to create ${def.name} server`, 500);\n\t\t\t\t\titem = newItem;\n\t\t\t\t\tserverCache.set(key, item);\n\t\t\t\t} catch (e) {\n\t\t\t\t\tlog.error(`Failed to create ${def.name} server:`, e);\n\t\t\t\t\treturn c.text(`Failed to create ${def.name} server`, 500);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Create a new transport for each request\n\t\t\tconst transport = new StreamableHTTPTransport();\n\t\t\ttry {\n\t\t\t\tawait item.server.connect(transport);\n\t\t\t\tconst handleRequest = createMcpLoggingHandler(transport, def.name);\n\t\t\t\treturn await handleRequest(c);\n\t\t\t} catch (e) {\n\t\t\t\tlog.error(`[${def.name}] Request error:`, e);\n\t\t\t\treturn c.text(`Internal server error: ${e instanceof Error ? e.message : 'Unknown error'}`, 500);\n\t\t\t}\n\t\t});\n\t}\n\n\treturn { serverDefs };\n}\n"],"names":["StreamableHTTPTransport","consola","getMcpServerHandlerDef","findMcpServerDef","createMcpLoggingHandler","log","withTag","registerMcpRoutes","app","config","serverCache","serverDefs","name","serverConfig","Object","entries","servers","def","type","warn","options","resolveConfig","validation","validateOptions","valid","error","cacheKey","item","create","set","path","info","all","c","serverItem","get","text","transport","server","connect","handleRequest","e","Error","message","debug","req","raw","headers","requiredHeaders","headerMappings","filter","m","required","map","header","join","key","getCacheKey","title","newItem"],"mappings":"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"}
@@ -0,0 +1,119 @@
1
+ import { implement } from "@orpc/server";
2
+ import { McpsContract } from "../contracts/index.js";
3
+ import { findMcpServerDef } from "../providers/findMcpServerDef.js";
4
+ import { getAuditStats, queryAuditEvents } from "./audit.js";
5
+ // Simple glob pattern matching
6
+ function matchGlob(pattern, text) {
7
+ const regexPattern = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&") // Escape regex special chars except * and ?
8
+ .replace(/\*/g, ".*").replace(/\?/g, ".");
9
+ return new RegExp(`^${regexPattern}$`, "i").test(text);
10
+ }
11
+ // Build server types from registry
12
+ function getServerTypes() {
13
+ return findMcpServerDef().map((def) => ({
14
+ type: def.name,
15
+ description: def.description,
16
+ dynamicEndpoint: `/mcp/${def.name}`
17
+ }));
18
+ }
19
+ // Build endpoints from server types
20
+ function getEndpoints() {
21
+ const mcpEndpoints = findMcpServerDef().map((def) => `/mcp/${def.name}`);
22
+ const chatEndpoints = [
23
+ "/v1/chat/completions",
24
+ "/v1/messages",
25
+ "/v1/models",
26
+ "/v1/responses",
27
+ "/v1/models/:model:generateContent",
28
+ "/v1/models/:model:streamGenerateContent"
29
+ ];
30
+ return [
31
+ ...mcpEndpoints,
32
+ ...chatEndpoints
33
+ ];
34
+ }
35
+ /**
36
+ * Create MCPS Router with config context
37
+ */ export function createMcpsRouter(ctx) {
38
+ const { config } = ctx;
39
+ // Build server info list
40
+ const servers = Object.entries(config.servers).map(([name, serverConfig]) => ({
41
+ name,
42
+ type: serverConfig.type,
43
+ disabled: serverConfig.disabled
44
+ }));
45
+ // Build model info list
46
+ const models = (config.models ?? []).map((model) => ({
47
+ name: model.name,
48
+ adapter: model.adapter,
49
+ baseUrl: model.baseUrl,
50
+ contextWindow: model.contextWindow,
51
+ maxInputTokens: model.maxInputTokens,
52
+ maxOutputTokens: model.maxOutputTokens
53
+ }));
54
+ return implement(McpsContract).router({
55
+ overview: implement(McpsContract.overview).handler(async () => {
56
+ return {
57
+ name: "@wener/mcps",
58
+ version: "0.1.0",
59
+ servers,
60
+ serverTypes: getServerTypes(),
61
+ models,
62
+ endpoints: getEndpoints()
63
+ };
64
+ }),
65
+ stats: implement(McpsContract.stats).handler(async ({ input }) => {
66
+ const auditStats = getAuditStats(input);
67
+ // Build endpoint stats from audit events
68
+ const { events } = queryAuditEvents({
69
+ limit: 10000
70
+ });
71
+ const endpointCounts = new Map();
72
+ for (const event of events) {
73
+ const endpoint = event.path || "unknown";
74
+ endpointCounts.set(endpoint, (endpointCounts.get(endpoint) || 0) + 1);
75
+ }
76
+ const byEndpoint = Array.from(endpointCounts.entries()).map(([endpoint, count]) => ({
77
+ endpoint,
78
+ count
79
+ })).sort((a, b) => b.count - a.count).slice(0, 20); // Top 20 endpoints
80
+ return {
81
+ ...auditStats,
82
+ byEndpoint
83
+ };
84
+ }),
85
+ servers: implement(McpsContract.servers).handler(async () => {
86
+ return {
87
+ servers
88
+ };
89
+ }),
90
+ models: implement(McpsContract.models).handler(async () => {
91
+ return {
92
+ models
93
+ };
94
+ }),
95
+ tools: implement(McpsContract.tools).handler(async ({ input }) => {
96
+ // TODO: This is a placeholder that returns empty tools list.
97
+ // Full implementation would require connecting to each MCP server
98
+ // and calling tools/list. Consider caching tool lists and
99
+ // refreshing periodically or on demand.
100
+ const tools = [];
101
+ // For now, return an empty list.
102
+ // When MCP servers support persistent connections or when we
103
+ // implement a tool registry, this will be populated.
104
+ // Apply filters if provided
105
+ let filteredTools = tools;
106
+ if (input.server) {
107
+ filteredTools = filteredTools.filter((t) => t.serverName === input.server);
108
+ }
109
+ if (input.filter) {
110
+ const patterns = input.filter.split(",").map((p) => p.trim());
111
+ filteredTools = filteredTools.filter((t) => patterns.some((pattern) => matchGlob(pattern, t.name)));
112
+ }
113
+ return {
114
+ tools: filteredTools
115
+ };
116
+ })
117
+ });
118
+ }
119
+ //# sourceMappingURL=mcps-router.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/server/mcps-router.ts"],"sourcesContent":["import { implement } from '@orpc/server';\nimport { McpsContract, type ModelInfo, type ServerInfo, type ServerTypeInfo, type ToolInfo } from '../contracts';\nimport { findMcpServerDef } from '../providers/findMcpServerDef';\nimport { getAuditStats, queryAuditEvents } from './audit';\nimport type { McpsConfig } from './schema';\n\n// Simple glob pattern matching\nfunction matchGlob(pattern: string, text: string): boolean {\n\tconst regexPattern = pattern\n\t\t.replace(/[.+^${}()|[\\]\\\\]/g, '\\\\$&') // Escape regex special chars except * and ?\n\t\t.replace(/\\*/g, '.*')\n\t\t.replace(/\\?/g, '.');\n\treturn new RegExp(`^${regexPattern}$`, 'i').test(text);\n}\n\n// Build server types from registry\nfunction getServerTypes(): ServerTypeInfo[] {\n\treturn findMcpServerDef().map((def) => ({\n\t\ttype: def.name,\n\t\tdescription: def.description,\n\t\tdynamicEndpoint: `/mcp/${def.name}`,\n\t}));\n}\n\n// Build endpoints from server types\nfunction getEndpoints(): string[] {\n\tconst mcpEndpoints = findMcpServerDef().map((def) => `/mcp/${def.name}`);\n\tconst chatEndpoints = [\n\t\t'/v1/chat/completions',\n\t\t'/v1/messages',\n\t\t'/v1/models',\n\t\t'/v1/responses',\n\t\t'/v1/models/:model:generateContent',\n\t\t'/v1/models/:model:streamGenerateContent',\n\t];\n\treturn [...mcpEndpoints, ...chatEndpoints];\n}\n\nexport interface McpsRouterContext {\n\tconfig: McpsConfig;\n}\n\n/**\n * Create MCPS Router with config context\n */\nexport function createMcpsRouter(ctx: McpsRouterContext) {\n\tconst { config } = ctx;\n\n\t// Build server info list\n\tconst servers: ServerInfo[] = Object.entries(config.servers).map(([name, serverConfig]) => ({\n\t\tname,\n\t\ttype: serverConfig.type,\n\t\tdisabled: serverConfig.disabled,\n\t}));\n\n\t// Build model info list\n\tconst models: ModelInfo[] = (config.models ?? []).map((model) => ({\n\t\tname: model.name,\n\t\tadapter: model.adapter,\n\t\tbaseUrl: model.baseUrl,\n\t\tcontextWindow: model.contextWindow,\n\t\tmaxInputTokens: model.maxInputTokens,\n\t\tmaxOutputTokens: model.maxOutputTokens,\n\t}));\n\n\treturn implement(McpsContract).router({\n\t\toverview: implement(McpsContract.overview).handler(async () => {\n\t\t\treturn {\n\t\t\t\tname: '@wener/mcps',\n\t\t\t\tversion: '0.1.0',\n\t\t\t\tservers,\n\t\t\t\tserverTypes: getServerTypes(),\n\t\t\t\tmodels,\n\t\t\t\tendpoints: getEndpoints(),\n\t\t\t};\n\t\t}),\n\n\t\tstats: implement(McpsContract.stats).handler(async ({ input }) => {\n\t\t\tconst auditStats = getAuditStats(input);\n\n\t\t\t// Build endpoint stats from audit events\n\t\t\tconst { events } = queryAuditEvents({ limit: 10000 });\n\t\t\tconst endpointCounts = new Map<string, number>();\n\t\t\tfor (const event of events) {\n\t\t\t\tconst endpoint = event.path || 'unknown';\n\t\t\t\tendpointCounts.set(endpoint, (endpointCounts.get(endpoint) || 0) + 1);\n\t\t\t}\n\t\t\tconst byEndpoint = Array.from(endpointCounts.entries())\n\t\t\t\t.map(([endpoint, count]) => ({ endpoint, count }))\n\t\t\t\t.sort((a, b) => b.count - a.count)\n\t\t\t\t.slice(0, 20); // Top 20 endpoints\n\n\t\t\treturn {\n\t\t\t\t...auditStats,\n\t\t\t\tbyEndpoint,\n\t\t\t};\n\t\t}),\n\n\t\tservers: implement(McpsContract.servers).handler(async () => {\n\t\t\treturn { servers };\n\t\t}),\n\n\t\tmodels: implement(McpsContract.models).handler(async () => {\n\t\t\treturn { models };\n\t\t}),\n\n\t\ttools: implement(McpsContract.tools).handler(async ({ input }) => {\n\t\t\t// TODO: This is a placeholder that returns empty tools list.\n\t\t\t// Full implementation would require connecting to each MCP server\n\t\t\t// and calling tools/list. Consider caching tool lists and\n\t\t\t// refreshing periodically or on demand.\n\t\t\tconst tools: ToolInfo[] = [];\n\n\t\t\t// For now, return an empty list.\n\t\t\t// When MCP servers support persistent connections or when we\n\t\t\t// implement a tool registry, this will be populated.\n\n\t\t\t// Apply filters if provided\n\t\t\tlet filteredTools = tools;\n\n\t\t\tif (input.server) {\n\t\t\t\tfilteredTools = filteredTools.filter((t) => t.serverName === input.server);\n\t\t\t}\n\n\t\t\tif (input.filter) {\n\t\t\t\tconst patterns = input.filter.split(',').map((p) => p.trim());\n\t\t\t\tfilteredTools = filteredTools.filter((t) => patterns.some((pattern) => matchGlob(pattern, t.name)));\n\t\t\t}\n\n\t\t\treturn { tools: filteredTools };\n\t\t}),\n\t});\n}\n"],"names":["implement","McpsContract","findMcpServerDef","getAuditStats","queryAuditEvents","matchGlob","pattern","text","regexPattern","replace","RegExp","test","getServerTypes","map","def","type","name","description","dynamicEndpoint","getEndpoints","mcpEndpoints","chatEndpoints","createMcpsRouter","ctx","config","servers","Object","entries","serverConfig","disabled","models","model","adapter","baseUrl","contextWindow","maxInputTokens","maxOutputTokens","router","overview","handler","version","serverTypes","endpoints","stats","input","auditStats","events","limit","endpointCounts","Map","event","endpoint","path","set","get","byEndpoint","Array","from","count","sort","a","b","slice","tools","filteredTools","server","filter","t","serverName","patterns","split","p","trim","some"],"mappings":"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"}
@@ -0,0 +1,129 @@
1
+ import { z } from 'zod';
2
+ // Header name constants for config parsing
3
+ export const HeaderNames = {
4
+ CLS_SECRET_ID: 'X-CLS-SECRET-ID',
5
+ CLS_SECRET_KEY: 'X-CLS-SECRET-KEY',
6
+ CLS_REGION: 'X-CLS-REGION',
7
+ CLS_ENDPOINT: 'X-CLS-ENDPOINT',
8
+ DB_URL: 'X-DB-URL',
9
+ DB_READ_URL: 'X-DB-READ-URL',
10
+ DB_WRITE_URL: 'X-DB-WRITE-URL',
11
+ SERVICE_URL: 'X-SERVICE-URL',
12
+ TOKEN: 'X-TOKEN',
13
+ MCP_URL: 'X-MCP-URL',
14
+ MCP_TYPE: 'X-MCP-TYPE',
15
+ MCP_COMMAND: 'X-MCP-COMMAND',
16
+ // Tool filtering headers
17
+ MCP_READONLY: 'X-MCP-Readonly',
18
+ MCP_INCLUDE: 'X-MCP-Include',
19
+ MCP_EXCLUDE: 'X-MCP-Exclude'
20
+ };
21
+ // Base server config with common fields
22
+ export const BaseServerConfigSchema = z.object({
23
+ disabled: z.boolean().optional(),
24
+ // Headers can be used as alternative to direct config properties
25
+ headers: z.record(z.string(), z.string()).optional()
26
+ });
27
+ // Tencent CLS config - supports both direct config and headers
28
+ export const TencentClsConfigSchema = BaseServerConfigSchema.extend({
29
+ type: z.literal('tencent-cls'),
30
+ clientId: z.string().optional().describe('Tencent Cloud Secret ID'),
31
+ clientSecret: z.string().optional().describe('Tencent Cloud Secret Key'),
32
+ region: z.string().optional().describe('CLS region'),
33
+ endpoint: z.string().optional().describe('CLS endpoint')
34
+ });
35
+ // SQL config with read/write separation
36
+ export const SqlConfigSchema = BaseServerConfigSchema.extend({
37
+ type: z.literal('sql'),
38
+ dbUrl: z.string().optional().describe('Database URL (for both read and write)'),
39
+ dbReadUrl: z.string().optional().describe('Database URL for read operations'),
40
+ dbWriteUrl: z.string().optional().describe('Database URL for write operations')
41
+ });
42
+ // Prometheus config
43
+ export const PrometheusConfigSchema = BaseServerConfigSchema.extend({
44
+ type: z.literal('prometheus'),
45
+ url: z.string().optional().describe('Prometheus server URL')
46
+ });
47
+ // Relay config for proxying to other MCP servers
48
+ export const RelayConfigSchema = BaseServerConfigSchema.extend({
49
+ type: z.literal('relay'),
50
+ url: z.string().optional().describe('Target MCP server URL'),
51
+ transport: z.enum([
52
+ 'http',
53
+ 'sse'
54
+ ]).optional().describe('MCP transport type'),
55
+ command: z.string().optional().describe('Command to run (stdio transport)'),
56
+ args: z.array(z.string()).optional().describe('Command arguments')
57
+ });
58
+ // Union of all server configs
59
+ export const ServerConfigSchema = z.discriminatedUnion('type', [
60
+ TencentClsConfigSchema,
61
+ SqlConfigSchema,
62
+ PrometheusConfigSchema,
63
+ RelayConfigSchema
64
+ ]);
65
+ /**
66
+ * Resolve config from headers - merge headers into config properties
67
+ * @deprecated Use McpServerDef.resolveConfig instead. This function is kept for backward compatibility.
68
+ */ export function resolveServerConfig(config) {
69
+ // This function is now a pass-through - actual resolution is done in McpServerDef.resolveConfig
70
+ // Keeping it for backward compatibility with any external code that might use it
71
+ return config;
72
+ }
73
+ // ============================================================================
74
+ // Chat/LLM Gateway Configuration
75
+ // ============================================================================
76
+ /**
77
+ * Adapter types for protocol conversion
78
+ */ export const AdapterType = z.enum([
79
+ 'openai',
80
+ 'anthropic',
81
+ 'gemini'
82
+ ]);
83
+ /**
84
+ * Adapter endpoint configuration for chat models
85
+ */ export const AdapterEndpointConfigSchema = z.object({
86
+ baseUrl: z.string().optional(),
87
+ headers: z.record(z.string(), z.string()).optional(),
88
+ processors: z.array(z.string()).optional()
89
+ });
90
+ /**
91
+ * Model configuration for chat gateway
92
+ */ export const ModelConfigSchema = z.object({
93
+ /** Model name or pattern (supports wildcards like "gpt-*") */ name: z.string(),
94
+ /** Base URL for the API */ baseUrl: z.string().optional(),
95
+ /** API key (uses Authorization: Bearer header) */ apiKey: z.string().optional(),
96
+ /** Additional headers */ headers: z.record(z.string(), z.string()).optional(),
97
+ /** Default adapter to use for protocol conversion */ adapter: AdapterType.optional(),
98
+ /** Adapter-specific endpoint configurations */ adapters: z.object({
99
+ openai: AdapterEndpointConfigSchema.optional(),
100
+ anthropic: AdapterEndpointConfigSchema.optional(),
101
+ gemini: AdapterEndpointConfigSchema.optional()
102
+ }).optional(),
103
+ /** Processor chain */ processors: z.array(z.string()).optional(),
104
+ /** Context window size (max total tokens) */ contextWindow: z.number().optional(),
105
+ /** Max input tokens */ maxInputTokens: z.number().optional(),
106
+ /** Max output tokens */ maxOutputTokens: z.number().optional(),
107
+ /** Whether to fetch models from upstream on /v1/models */ fetchUpstreamModels: z.boolean().optional()
108
+ });
109
+ /**
110
+ * Database configuration for audit storage
111
+ */ export const DbConfigSchema = z.object({
112
+ /** Path to SQLite database file (default: .mcps.db) */ path: z.string().optional()
113
+ });
114
+ /**
115
+ * Audit configuration
116
+ */ export const AuditConfigSchema = z.object({
117
+ /** Enable audit logging (default: true) */ enabled: z.boolean().optional(),
118
+ /** Database configuration for audit storage (overrides root db config) */ db: DbConfigSchema.optional()
119
+ });
120
+ // Main config file schema
121
+ export const McpsConfigSchema = z.object({
122
+ servers: z.record(z.string(), ServerConfigSchema).default({}),
123
+ /** Model configurations for chat gateway (array format) */ models: z.array(ModelConfigSchema).optional(),
124
+ /** Whether to expose server config discovery endpoints (default: false) */ discoveryConfig: z.boolean().optional(),
125
+ /** Database configuration (shared, used as fallback for audit.db) */ db: DbConfigSchema.optional(),
126
+ /** Audit configuration */ audit: AuditConfigSchema.optional()
127
+ });
128
+
129
+ //# sourceMappingURL=schema.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/server/schema.ts"],"sourcesContent":["import { z } from 'zod';\n\n// Header name constants for config parsing\nexport const HeaderNames = {\n\tCLS_SECRET_ID: 'X-CLS-SECRET-ID',\n\tCLS_SECRET_KEY: 'X-CLS-SECRET-KEY',\n\tCLS_REGION: 'X-CLS-REGION',\n\tCLS_ENDPOINT: 'X-CLS-ENDPOINT',\n\tDB_URL: 'X-DB-URL',\n\tDB_READ_URL: 'X-DB-READ-URL',\n\tDB_WRITE_URL: 'X-DB-WRITE-URL',\n\tSERVICE_URL: 'X-SERVICE-URL',\n\tTOKEN: 'X-TOKEN',\n\tMCP_URL: 'X-MCP-URL',\n\tMCP_TYPE: 'X-MCP-TYPE',\n\tMCP_COMMAND: 'X-MCP-COMMAND',\n\t// Tool filtering headers\n\tMCP_READONLY: 'X-MCP-Readonly',\n\tMCP_INCLUDE: 'X-MCP-Include',\n\tMCP_EXCLUDE: 'X-MCP-Exclude',\n} as const;\n\n// Base server config with common fields\nexport const BaseServerConfigSchema = z.object({\n\tdisabled: z.boolean().optional(),\n\t// Headers can be used as alternative to direct config properties\n\theaders: z.record(z.string(), z.string()).optional(),\n});\n\n// Tencent CLS config - supports both direct config and headers\nexport const TencentClsConfigSchema = BaseServerConfigSchema.extend({\n\ttype: z.literal('tencent-cls'),\n\tclientId: z.string().optional().describe('Tencent Cloud Secret ID'),\n\tclientSecret: z.string().optional().describe('Tencent Cloud Secret Key'),\n\tregion: z.string().optional().describe('CLS region'),\n\tendpoint: z.string().optional().describe('CLS endpoint'),\n});\nexport type TencentClsConfig = z.infer<typeof TencentClsConfigSchema>;\n\n// SQL config with read/write separation\nexport const SqlConfigSchema = BaseServerConfigSchema.extend({\n\ttype: z.literal('sql'),\n\tdbUrl: z.string().optional().describe('Database URL (for both read and write)'),\n\tdbReadUrl: z.string().optional().describe('Database URL for read operations'),\n\tdbWriteUrl: z.string().optional().describe('Database URL for write operations'),\n});\nexport type SqlConfig = z.infer<typeof SqlConfigSchema>;\n\n// Prometheus config\nexport const PrometheusConfigSchema = BaseServerConfigSchema.extend({\n\ttype: z.literal('prometheus'),\n\turl: z.string().optional().describe('Prometheus server URL'),\n});\nexport type PrometheusConfig = z.infer<typeof PrometheusConfigSchema>;\n\n// Relay config for proxying to other MCP servers\nexport const RelayConfigSchema = BaseServerConfigSchema.extend({\n\ttype: z.literal('relay'),\n\turl: z.string().optional().describe('Target MCP server URL'),\n\ttransport: z.enum(['http', 'sse']).optional().describe('MCP transport type'),\n\tcommand: z.string().optional().describe('Command to run (stdio transport)'),\n\targs: z.array(z.string()).optional().describe('Command arguments'),\n});\nexport type RelayConfig = z.infer<typeof RelayConfigSchema>;\n\n// Union of all server configs\nexport const ServerConfigSchema = z.discriminatedUnion('type', [\n\tTencentClsConfigSchema,\n\tSqlConfigSchema,\n\tPrometheusConfigSchema,\n\tRelayConfigSchema,\n]);\nexport type ServerConfig = z.infer<typeof ServerConfigSchema>;\n\n/**\n * Resolve config from headers - merge headers into config properties\n * @deprecated Use McpServerDef.resolveConfig instead. This function is kept for backward compatibility.\n */\nexport function resolveServerConfig<T extends ServerConfig>(config: T): T {\n\t// This function is now a pass-through - actual resolution is done in McpServerDef.resolveConfig\n\t// Keeping it for backward compatibility with any external code that might use it\n\treturn config;\n}\n\n// ============================================================================\n// Chat/LLM Gateway Configuration\n// ============================================================================\n\n/**\n * Adapter types for protocol conversion\n */\nexport const AdapterType = z.enum(['openai', 'anthropic', 'gemini']);\nexport type AdapterType = z.infer<typeof AdapterType>;\n\n/**\n * Adapter endpoint configuration for chat models\n */\nexport const AdapterEndpointConfigSchema = z.object({\n\tbaseUrl: z.string().optional(),\n\theaders: z.record(z.string(), z.string()).optional(),\n\tprocessors: z.array(z.string()).optional(),\n});\nexport type AdapterEndpointConfig = z.infer<typeof AdapterEndpointConfigSchema>;\n\n/**\n * Model configuration for chat gateway\n */\nexport const ModelConfigSchema = z.object({\n\t/** Model name or pattern (supports wildcards like \"gpt-*\") */\n\tname: z.string(),\n\t/** Base URL for the API */\n\tbaseUrl: z.string().optional(),\n\t/** API key (uses Authorization: Bearer header) */\n\tapiKey: z.string().optional(),\n\t/** Additional headers */\n\theaders: z.record(z.string(), z.string()).optional(),\n\t/** Default adapter to use for protocol conversion */\n\tadapter: AdapterType.optional(),\n\t/** Adapter-specific endpoint configurations */\n\tadapters: z\n\t\t.object({\n\t\t\topenai: AdapterEndpointConfigSchema.optional(),\n\t\t\tanthropic: AdapterEndpointConfigSchema.optional(),\n\t\t\tgemini: AdapterEndpointConfigSchema.optional(),\n\t\t})\n\t\t.optional(),\n\t/** Processor chain */\n\tprocessors: z.array(z.string()).optional(),\n\t/** Context window size (max total tokens) */\n\tcontextWindow: z.number().optional(),\n\t/** Max input tokens */\n\tmaxInputTokens: z.number().optional(),\n\t/** Max output tokens */\n\tmaxOutputTokens: z.number().optional(),\n\t/** Whether to fetch models from upstream on /v1/models */\n\tfetchUpstreamModels: z.boolean().optional(),\n});\nexport type ModelConfig = z.infer<typeof ModelConfigSchema>;\n\n/**\n * Database configuration for audit storage\n */\nexport const DbConfigSchema = z.object({\n\t/** Path to SQLite database file (default: .mcps.db) */\n\tpath: z.string().optional(),\n});\nexport type DbConfig = z.infer<typeof DbConfigSchema>;\n\n/**\n * Audit configuration\n */\nexport const AuditConfigSchema = z.object({\n\t/** Enable audit logging (default: true) */\n\tenabled: z.boolean().optional(),\n\t/** Database configuration for audit storage (overrides root db config) */\n\tdb: DbConfigSchema.optional(),\n});\nexport type AuditConfig = z.infer<typeof AuditConfigSchema>;\n\n// Main config file schema\nexport const McpsConfigSchema = z.object({\n\tservers: z.record(z.string(), ServerConfigSchema).default({}),\n\t/** Model configurations for chat gateway (array format) */\n\tmodels: z.array(ModelConfigSchema).optional(),\n\t/** Whether to expose server config discovery endpoints (default: false) */\n\tdiscoveryConfig: z.boolean().optional(),\n\t/** Database configuration (shared, used as fallback for audit.db) */\n\tdb: DbConfigSchema.optional(),\n\t/** Audit configuration */\n\taudit: AuditConfigSchema.optional(),\n});\nexport type McpsConfig = z.infer<typeof McpsConfigSchema>;\n\n// Legacy ChatConfig type alias for backwards compatibility\nexport type ChatConfig = { models?: ModelConfig[] };\n"],"names":["z","HeaderNames","CLS_SECRET_ID","CLS_SECRET_KEY","CLS_REGION","CLS_ENDPOINT","DB_URL","DB_READ_URL","DB_WRITE_URL","SERVICE_URL","TOKEN","MCP_URL","MCP_TYPE","MCP_COMMAND","MCP_READONLY","MCP_INCLUDE","MCP_EXCLUDE","BaseServerConfigSchema","object","disabled","boolean","optional","headers","record","string","TencentClsConfigSchema","extend","type","literal","clientId","describe","clientSecret","region","endpoint","SqlConfigSchema","dbUrl","dbReadUrl","dbWriteUrl","PrometheusConfigSchema","url","RelayConfigSchema","transport","enum","command","args","array","ServerConfigSchema","discriminatedUnion","resolveServerConfig","config","AdapterType","AdapterEndpointConfigSchema","baseUrl","processors","ModelConfigSchema","name","apiKey","adapter","adapters","openai","anthropic","gemini","contextWindow","number","maxInputTokens","maxOutputTokens","fetchUpstreamModels","DbConfigSchema","path","AuditConfigSchema","enabled","db","McpsConfigSchema","servers","default","models","discoveryConfig","audit"],"mappings":"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"}
@@ -0,0 +1,166 @@
1
+ import consola from "consola";
2
+ import { Hono } from "hono";
3
+ import { logger } from "hono/logger";
4
+ import { LRUCache } from "lru-cache";
5
+ import { isDevelopment } from "std-env";
6
+ import { findMcpServerDef } from "../providers/findMcpServerDef.js";
7
+ import { registerApiRoutes } from "./api-routes.js";
8
+ import { auditMiddleware, configureAudit } from "./audit.js";
9
+ import { registerChatRoutes } from "./chat-routes.js";
10
+ import { loadConfig, loadEnvFiles, substituteEnvVars } from "./config.js";
11
+ import { registerMcpRoutes } from "./mcp-routes.js";
12
+ const log = consola.withTag("mcps");
13
+ export function createServer(options = {}) {
14
+ const { cwd = process.cwd(), discoveryConfig: discoveryConfigOption } = options;
15
+ const app = new Hono();
16
+ // Request logging
17
+ app.use(logger((v) => {
18
+ log.debug(v);
19
+ }));
20
+ // Audit middleware
21
+ app.use(auditMiddleware());
22
+ // Load .env files first
23
+ loadEnvFiles(cwd);
24
+ // Load config (with env var substitution)
25
+ const config = substituteEnvVars(loadConfig(cwd));
26
+ // discoveryConfig: CLI option overrides config file, defaults to false
27
+ const discoveryConfig = discoveryConfigOption ?? config.discoveryConfig ?? false;
28
+ // Log available server types from registry
29
+ const serverDefs = findMcpServerDef();
30
+ log.info(`Available server types: ${serverDefs.map((d) => d.name).join(", ")}`);
31
+ log.info(`Loaded ${Object.keys(config.servers).length} servers from config (discoveryConfig: ${discoveryConfig})`);
32
+ // Configure audit with lazy DB initialization
33
+ // DB will only be initialized when the first audit event needs persistence
34
+ configureAudit(config.audit, config.db);
35
+ const auditEnabled = config.audit?.enabled !== false;
36
+ const auditDbPath = config.audit?.db?.path ?? config.db?.path ?? ".mcps.db";
37
+ log.info(`Audit configured: enabled=${auditEnabled}, db=${auditDbPath} (lazy init)`);
38
+ // Unified cache for all MCP servers (both pre-configured and dynamic)
39
+ // Keyed by cache key from def.getCacheKey() or `config::${name}` for pre-configured
40
+ const serverCache = new LRUCache({
41
+ max: 100,
42
+ dispose: (value, key) => {
43
+ log.info(`Closing expired MCP server: ${key}`);
44
+ value.close?.().catch((e) => log.error("Failed to close server", e));
45
+ }
46
+ });
47
+ // =========================================================================
48
+ // Register MCP routes (pre-configured and dynamic endpoints)
49
+ // =========================================================================
50
+ registerMcpRoutes({
51
+ app,
52
+ config,
53
+ serverCache
54
+ });
55
+ // =========================================================================
56
+ // Register Chat/LLM Gateway routes
57
+ // =========================================================================
58
+ registerChatRoutes({
59
+ app,
60
+ config
61
+ });
62
+ // =========================================================================
63
+ // Health and info endpoints
64
+ // =========================================================================
65
+ app.get("/health", (c) => c.json({
66
+ status: "ok"
67
+ }));
68
+ app.get("/", (c) => {
69
+ return c.json({
70
+ message: "hello"
71
+ });
72
+ });
73
+ // Server info - only available in dev mode or when discoveryConfig is enabled
74
+ if (isDevelopment || discoveryConfig) {
75
+ app.get("/info", (c) => {
76
+ // Dynamically get routes from Hono app
77
+ const routes = app.routes.map((r) => ({
78
+ method: r.method,
79
+ path: r.path
80
+ })).filter((r) => r.method !== "ALL" || r.path.startsWith("/mcp/")).sort((a, b) => a.path.localeCompare(b.path) || a.method.localeCompare(b.method));
81
+ // Group routes by category
82
+ const mcpRoutes = routes.filter((r) => r.path.startsWith("/mcp/")).map((r) => r.path);
83
+ const apiRoutes = routes.filter((r) => r.path.startsWith("/api/")).map((r) => `${r.method} ${r.path}`);
84
+ const chatRoutes = routes.filter((r) => r.path.startsWith("/v1/")).map((r) => `${r.method} ${r.path}`);
85
+ // Get unique MCP paths
86
+ const uniqueMcpPaths = [
87
+ ...new Set(mcpRoutes)
88
+ ];
89
+ return c.json({
90
+ name: "@wener/mcps",
91
+ version: "0.1.0",
92
+ servers: Object.keys(config.servers),
93
+ serverTypes: findMcpServerDef().map((d) => d.name),
94
+ models: config.models ? config.models.map((m) => m.name) : [],
95
+ endpoints: {
96
+ mcp: uniqueMcpPaths,
97
+ api: [
98
+ ...new Set(apiRoutes)
99
+ ],
100
+ chat: [
101
+ ...new Set(chatRoutes)
102
+ ]
103
+ }
104
+ });
105
+ });
106
+ }
107
+ // =========================================================================
108
+ // Register oRPC API routes
109
+ // =========================================================================
110
+ registerApiRoutes({
111
+ app,
112
+ config
113
+ });
114
+ /**
115
+ * Print available endpoints grouped by category
116
+ */ const printEndpoints = () => {
117
+ const routes = app.routes;
118
+ const mcpPaths = new Set();
119
+ const apiPaths = new Set();
120
+ const chatPaths = new Set();
121
+ const otherPaths = new Set();
122
+ for (const route of routes) {
123
+ const path = route.path;
124
+ if (path.startsWith("/mcp/")) {
125
+ mcpPaths.add(path);
126
+ }
127
+ else if (path.startsWith("/api/")) {
128
+ apiPaths.add(`${route.method} ${path}`);
129
+ }
130
+ else if (path.startsWith("/v1/")) {
131
+ chatPaths.add(`${route.method} ${path}`);
132
+ }
133
+ else if (route.method !== "ALL") {
134
+ otherPaths.add(`${route.method} ${path}`);
135
+ }
136
+ }
137
+ log.info("Available endpoints:");
138
+ if (mcpPaths.size > 0) {
139
+ log.info(` MCP: ${[
140
+ ...mcpPaths
141
+ ].sort().join(", ")}`);
142
+ }
143
+ if (chatPaths.size > 0) {
144
+ log.info(` Chat: ${[
145
+ ...chatPaths
146
+ ].sort().join(", ")}`);
147
+ }
148
+ if (apiPaths.size > 0) {
149
+ log.info(` API: ${[
150
+ ...apiPaths
151
+ ].sort().join(", ")}`);
152
+ }
153
+ if (otherPaths.size > 0) {
154
+ log.info(` Other: ${[
155
+ ...otherPaths
156
+ ].sort().join(", ")}`);
157
+ }
158
+ };
159
+ return {
160
+ app,
161
+ config,
162
+ serverCache,
163
+ printEndpoints
164
+ };
165
+ }
166
+ //# sourceMappingURL=server.js.map