@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.
- package/LICENSE +21 -0
- package/dist/index.mjs +15 -0
- package/dist/mcps-cli.mjs +174727 -0
- package/lib/chat/agent.js +187 -0
- package/lib/chat/agent.js.map +1 -0
- package/lib/chat/audit.js +238 -0
- package/lib/chat/audit.js.map +1 -0
- package/lib/chat/converters.js +467 -0
- package/lib/chat/converters.js.map +1 -0
- package/lib/chat/handler.js +1068 -0
- package/lib/chat/handler.js.map +1 -0
- package/lib/chat/index.js +12 -0
- package/lib/chat/index.js.map +1 -0
- package/lib/chat/types.js +35 -0
- package/lib/chat/types.js.map +1 -0
- package/lib/contracts/AuditContract.js +85 -0
- package/lib/contracts/AuditContract.js.map +1 -0
- package/lib/contracts/McpsContract.js +113 -0
- package/lib/contracts/McpsContract.js.map +1 -0
- package/lib/contracts/index.js +3 -0
- package/lib/contracts/index.js.map +1 -0
- package/lib/dev.server.js +7 -0
- package/lib/dev.server.js.map +1 -0
- package/lib/entities/ChatRequestEntity.js +318 -0
- package/lib/entities/ChatRequestEntity.js.map +1 -0
- package/lib/entities/McpRequestEntity.js +271 -0
- package/lib/entities/McpRequestEntity.js.map +1 -0
- package/lib/entities/RequestLogEntity.js +177 -0
- package/lib/entities/RequestLogEntity.js.map +1 -0
- package/lib/entities/ResponseEntity.js +150 -0
- package/lib/entities/ResponseEntity.js.map +1 -0
- package/lib/entities/index.js +11 -0
- package/lib/entities/index.js.map +1 -0
- package/lib/entities/types.js +11 -0
- package/lib/entities/types.js.map +1 -0
- package/lib/index.js +3 -0
- package/lib/index.js.map +1 -0
- package/lib/mcps-cli.js +44 -0
- package/lib/mcps-cli.js.map +1 -0
- package/lib/providers/McpServerHandlerDef.js +40 -0
- package/lib/providers/McpServerHandlerDef.js.map +1 -0
- package/lib/providers/findMcpServerDef.js +26 -0
- package/lib/providers/findMcpServerDef.js.map +1 -0
- package/lib/providers/prometheus/def.js +24 -0
- package/lib/providers/prometheus/def.js.map +1 -0
- package/lib/providers/prometheus/index.js +2 -0
- package/lib/providers/prometheus/index.js.map +1 -0
- package/lib/providers/relay/def.js +32 -0
- package/lib/providers/relay/def.js.map +1 -0
- package/lib/providers/relay/index.js +2 -0
- package/lib/providers/relay/index.js.map +1 -0
- package/lib/providers/sql/def.js +31 -0
- package/lib/providers/sql/def.js.map +1 -0
- package/lib/providers/sql/index.js +2 -0
- package/lib/providers/sql/index.js.map +1 -0
- package/lib/providers/tencent-cls/def.js +44 -0
- package/lib/providers/tencent-cls/def.js.map +1 -0
- package/lib/providers/tencent-cls/index.js +2 -0
- package/lib/providers/tencent-cls/index.js.map +1 -0
- package/lib/scripts/bundle.js +90 -0
- package/lib/scripts/bundle.js.map +1 -0
- package/lib/server/api-routes.js +96 -0
- package/lib/server/api-routes.js.map +1 -0
- package/lib/server/audit.js +274 -0
- package/lib/server/audit.js.map +1 -0
- package/lib/server/chat-routes.js +82 -0
- package/lib/server/chat-routes.js.map +1 -0
- package/lib/server/config.js +223 -0
- package/lib/server/config.js.map +1 -0
- package/lib/server/db.js +97 -0
- package/lib/server/db.js.map +1 -0
- package/lib/server/index.js +2 -0
- package/lib/server/index.js.map +1 -0
- package/lib/server/mcp-handler.js +167 -0
- package/lib/server/mcp-handler.js.map +1 -0
- package/lib/server/mcp-routes.js +112 -0
- package/lib/server/mcp-routes.js.map +1 -0
- package/lib/server/mcps-router.js +119 -0
- package/lib/server/mcps-router.js.map +1 -0
- package/lib/server/schema.js +129 -0
- package/lib/server/schema.js.map +1 -0
- package/lib/server/server.js +166 -0
- package/lib/server/server.js.map +1 -0
- package/lib/web/ChatPage.js +827 -0
- package/lib/web/ChatPage.js.map +1 -0
- package/lib/web/McpInspectorPage.js +214 -0
- package/lib/web/McpInspectorPage.js.map +1 -0
- package/lib/web/ServersPage.js +93 -0
- package/lib/web/ServersPage.js.map +1 -0
- package/lib/web/main.js +541 -0
- package/lib/web/main.js.map +1 -0
- package/package.json +83 -0
- package/src/chat/agent.ts +240 -0
- package/src/chat/audit.ts +377 -0
- package/src/chat/converters.test.ts +325 -0
- package/src/chat/converters.ts +459 -0
- package/src/chat/handler.test.ts +137 -0
- package/src/chat/handler.ts +1233 -0
- package/src/chat/index.ts +16 -0
- package/src/chat/types.ts +72 -0
- package/src/contracts/AuditContract.ts +93 -0
- package/src/contracts/McpsContract.ts +141 -0
- package/src/contracts/index.ts +18 -0
- package/src/dev.server.ts +7 -0
- package/src/entities/ChatRequestEntity.ts +157 -0
- package/src/entities/McpRequestEntity.ts +149 -0
- package/src/entities/RequestLogEntity.ts +78 -0
- package/src/entities/ResponseEntity.ts +75 -0
- package/src/entities/index.ts +12 -0
- package/src/entities/types.ts +188 -0
- package/src/index.ts +1 -0
- package/src/mcps-cli.ts +59 -0
- package/src/providers/McpServerHandlerDef.ts +105 -0
- package/src/providers/findMcpServerDef.ts +31 -0
- package/src/providers/prometheus/def.ts +21 -0
- package/src/providers/prometheus/index.ts +1 -0
- package/src/providers/relay/def.ts +31 -0
- package/src/providers/relay/index.ts +1 -0
- package/src/providers/relay/relay.test.ts +47 -0
- package/src/providers/sql/def.ts +33 -0
- package/src/providers/sql/index.ts +1 -0
- package/src/providers/tencent-cls/def.ts +38 -0
- package/src/providers/tencent-cls/index.ts +1 -0
- package/src/scripts/bundle.ts +82 -0
- package/src/server/api-routes.ts +98 -0
- package/src/server/audit.ts +310 -0
- package/src/server/chat-routes.ts +95 -0
- package/src/server/config.test.ts +162 -0
- package/src/server/config.ts +198 -0
- package/src/server/db.ts +115 -0
- package/src/server/index.ts +1 -0
- package/src/server/mcp-handler.ts +209 -0
- package/src/server/mcp-routes.ts +133 -0
- package/src/server/mcps-router.ts +133 -0
- package/src/server/schema.ts +175 -0
- package/src/server/server.ts +163 -0
- package/src/web/ChatPage.tsx +1005 -0
- package/src/web/McpInspectorPage.tsx +254 -0
- package/src/web/ServersPage.tsx +139 -0
- package/src/web/main.tsx +600 -0
- 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":"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"}
|
|
@@ -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
|