@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
+ export { SqlMcpServerDef, SqlMcpServerHandlerDef } from './def';
@@ -0,0 +1,38 @@
1
+ import { TencentClsMcpServerDef, type CreateTencentClsMcpServerOptions } from '@wener/ai/mcp/tencent-cls';
2
+ import { HeaderNames, type TencentClsConfig } from '../../server/schema';
3
+ import { defineMcpServerHandler, registerMcpServerHandler } from '../McpServerHandlerDef';
4
+
5
+ export const TencentClsMcpServerHandlerDef = defineMcpServerHandler<
6
+ CreateTencentClsMcpServerOptions,
7
+ TencentClsConfig
8
+ >(TencentClsMcpServerDef, {
9
+ headerMappings: [
10
+ { header: HeaderNames.CLS_SECRET_ID, property: 'clientId', required: true },
11
+ { header: HeaderNames.CLS_SECRET_KEY, property: 'clientSecret', required: true },
12
+ { header: HeaderNames.CLS_REGION, property: 'region', default: 'ap-shanghai' },
13
+ { header: HeaderNames.CLS_ENDPOINT, property: 'endpoint' },
14
+ ],
15
+
16
+ resolveConfig(config, headers) {
17
+ const clientId =
18
+ config.clientId || headers?.get(HeaderNames.CLS_SECRET_ID) || config.headers?.[HeaderNames.CLS_SECRET_ID];
19
+ const clientSecret =
20
+ config.clientSecret || headers?.get(HeaderNames.CLS_SECRET_KEY) || config.headers?.[HeaderNames.CLS_SECRET_KEY];
21
+ const region =
22
+ config.region ||
23
+ headers?.get(HeaderNames.CLS_REGION) ||
24
+ config.headers?.[HeaderNames.CLS_REGION] ||
25
+ 'ap-shanghai';
26
+ const endpoint =
27
+ config.endpoint || headers?.get(HeaderNames.CLS_ENDPOINT) || config.headers?.[HeaderNames.CLS_ENDPOINT];
28
+
29
+ if (!clientId || !clientSecret) return null;
30
+
31
+ return { clientId, clientSecret, region, endpoint };
32
+ },
33
+ });
34
+
35
+ registerMcpServerHandler(TencentClsMcpServerHandlerDef);
36
+
37
+ // backward compatibility
38
+ export { TencentClsMcpServerHandlerDef as TencentClsMcpServerDef };
@@ -0,0 +1 @@
1
+ export { TencentClsMcpServerDef, TencentClsMcpServerHandlerDef } from './def';
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs';
3
+ import * as esbuild from 'esbuild';
4
+
5
+ const pkg = JSON.parse(fs.readFileSync('package.json', 'utf-8'));
6
+
7
+ const banner = `// Bundled with esbuild
8
+ // ${pkg.name}@${pkg.version}
9
+
10
+ var require,__filename,__dirname;
11
+ {
12
+ const {createRequire} = await import('node:module');
13
+ require ||= createRequire(import.meta.url);
14
+ }
15
+ {
16
+ const {fileURLToPath} = await import('node:url');
17
+ const {dirname} = await import('node:path');
18
+ __filename ||= fileURLToPath(import.meta.url);
19
+ __dirname ||= dirname(__filename)
20
+ };
21
+ `;
22
+
23
+ const commonOptions: esbuild.BuildOptions = {
24
+ bundle: true,
25
+ logLevel: 'info',
26
+ banner: { js: banner },
27
+ define: {
28
+ NODE_ENV: JSON.stringify('production'),
29
+ __DEV__: JSON.stringify(false),
30
+ 'process.env.NODE_ENV': JSON.stringify('production'),
31
+ },
32
+ keepNames: true,
33
+ treeShaking: true,
34
+ minifySyntax: true,
35
+ format: 'esm',
36
+ platform: 'node',
37
+ charset: 'utf8',
38
+ target: 'node18',
39
+ sourcemap: false,
40
+ legalComments: 'none',
41
+ // External native modules
42
+ external: ['better-sqlite3', 'oracledb', 'mariadb/callback', 'mysql'],
43
+ };
44
+
45
+ // Ensure dist directory exists
46
+ fs.mkdirSync('dist', { recursive: true });
47
+
48
+ console.log('Building MCPS...');
49
+
50
+ // Build library entry (index.ts)
51
+ const libResult = await esbuild.build({
52
+ ...commonOptions,
53
+ entryPoints: ['src/index.ts'],
54
+ outfile: 'dist/index.mjs',
55
+ });
56
+
57
+ // Build CLI entry (mcps-cli.ts)
58
+ const cliResult = await esbuild.build({
59
+ ...commonOptions,
60
+ entryPoints: ['src/mcps-cli.ts'],
61
+ outfile: 'dist/mcps-cli.mjs',
62
+ });
63
+
64
+ if (libResult.errors.length === 0 && cliResult.errors.length === 0) {
65
+ // Process CLI output - add shebang
66
+ const cliOutfile = 'dist/mcps-cli.mjs';
67
+ let content = fs.readFileSync(cliOutfile, 'utf-8');
68
+ content = content.replace(/^#!.*\n/gm, '');
69
+ fs.writeFileSync(cliOutfile, `#!/usr/bin/env node\n${content}`);
70
+ fs.chmodSync(cliOutfile, 0o755);
71
+
72
+ const libStats = fs.statSync('dist/index.mjs');
73
+ const cliStats = fs.statSync(cliOutfile);
74
+
75
+ console.log(`✅ Build successful!`);
76
+ console.log(`📦 Library: dist/index.mjs (${(libStats.size / 1024).toFixed(1)}KB)`);
77
+ console.log(`📦 CLI: ${cliOutfile} (${(cliStats.size / 1024).toFixed(1)}KB)`);
78
+ console.log(`🚀 Ready for npm publish and npx usage`);
79
+ } else {
80
+ console.error('❌ Build failed:', [...libResult.errors, ...cliResult.errors]);
81
+ process.exit(1);
82
+ }
@@ -0,0 +1,98 @@
1
+ import { SmartCoercionPlugin } from '@orpc/json-schema';
2
+ import { OpenAPIGenerator } from '@orpc/openapi';
3
+ import { OpenAPIHandler } from '@orpc/openapi/fetch';
4
+ import { RPCHandler } from '@orpc/server/fetch';
5
+ import { CORSPlugin } from '@orpc/server/plugins';
6
+ import { ZodToJsonSchemaConverter } from '@orpc/zod/zod4';
7
+ import type { Hono } from 'hono';
8
+ import { html } from 'hono/html';
9
+ import { AuditRouter } from './audit';
10
+ import { createMcpsRouter } from './mcps-router';
11
+ import type { McpsConfig } from './schema';
12
+
13
+ export interface RegisterApiRoutesOptions {
14
+ app: Hono;
15
+ config: McpsConfig;
16
+ }
17
+
18
+ /**
19
+ * Register oRPC API routes for audit and MCPS
20
+ */
21
+ export function registerApiRoutes({ app, config }: RegisterApiRoutesOptions) {
22
+ // Create MCPS router with config context
23
+ const McpsRouter = createMcpsRouter({ config });
24
+
25
+ // Combined router for all APIs
26
+ const combinedRouter = {
27
+ audit: AuditRouter,
28
+ mcps: McpsRouter,
29
+ };
30
+
31
+ const handleByRpc = new RPCHandler(combinedRouter);
32
+ const handleByOpenAPI = new OpenAPIHandler(combinedRouter, {
33
+ plugins: [
34
+ new SmartCoercionPlugin({
35
+ schemaConverters: [new ZodToJsonSchemaConverter()],
36
+ }),
37
+ new CORSPlugin({
38
+ exposeHeaders: ['Content-Disposition'],
39
+ }),
40
+ ],
41
+ });
42
+
43
+ app.use('/api/rpc/*', async (c, next) => {
44
+ const { matched, response } = await handleByRpc.handle(c.req.raw, {
45
+ prefix: '/api/rpc',
46
+ context: {},
47
+ });
48
+ if (matched) {
49
+ return c.newResponse(response.body, response);
50
+ }
51
+ return next();
52
+ });
53
+
54
+ app.use('/api/*', async (c, next) => {
55
+ const { matched, response } = await handleByOpenAPI.handle(c.req.raw, {
56
+ prefix: '/api',
57
+ context: {},
58
+ });
59
+ if (matched) {
60
+ return c.newResponse(response.body, response);
61
+ }
62
+ return next();
63
+ });
64
+
65
+ // OpenAPI spec
66
+ const openAPIGenerator = new OpenAPIGenerator({
67
+ schemaConverters: [new ZodToJsonSchemaConverter()],
68
+ });
69
+ let specCache: unknown;
70
+
71
+ app.get('/api/spec.json', async (c) => {
72
+ if (!specCache) {
73
+ specCache = await openAPIGenerator.generate(combinedRouter, {
74
+ info: { title: 'MCPS API', version: '1.0.0' },
75
+ servers: [{ url: '/api' }],
76
+ });
77
+ }
78
+ return c.json(specCache);
79
+ });
80
+
81
+ // Swagger UI
82
+ app.get('/api/docs', (c) => {
83
+ return c.html(
84
+ html`<!doctype html>
85
+ <html lang="en">
86
+ <head>
87
+ <title>MCPS API</title>
88
+ <meta charset="utf-8" />
89
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
90
+ </head>
91
+ <body>
92
+ <script id="api-reference" data-url="/api/spec.json"></script>
93
+ <script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
94
+ </body>
95
+ </html>`,
96
+ );
97
+ });
98
+ }
@@ -0,0 +1,310 @@
1
+ import { implement } from '@orpc/server';
2
+ import type { Context, Next } from 'hono';
3
+ import { LRUCache } from 'lru-cache';
4
+ import { AuditContract, type AuditEvent } from '../contracts';
5
+ import { RequestLogEntity } from '../entities';
6
+ import { ensureDbInitialized, configureDb } from './db';
7
+ import type { AuditConfig, DbConfig } from './schema';
8
+
9
+ /**
10
+ * Convert Headers to a plain record
11
+ */
12
+ function headersToRecord(headers: Headers): Record<string, string> {
13
+ const record: Record<string, string> = {};
14
+ headers.forEach((value, key) => {
15
+ record[key] = value;
16
+ });
17
+ return record;
18
+ }
19
+
20
+ // In-memory audit store using LRU cache
21
+ const auditStore = new LRUCache<string, AuditEvent>({
22
+ max: 10000, // Keep last 10k events
23
+ ttl: 1000 * 60 * 60 * 24, // 24 hours
24
+ });
25
+
26
+ // Counter for IDs
27
+ let eventCounter = 0;
28
+
29
+ // Audit configuration state
30
+ let auditEnabled = true; // default to enabled
31
+ let dbConfigured = false;
32
+
33
+ /**
34
+ * Configure audit module with settings
35
+ * Call this before using audit features
36
+ *
37
+ * @param auditConfig - Audit config section
38
+ * @param fallbackDbConfig - Fallback db config from root config
39
+ */
40
+ export function configureAudit(auditConfig?: AuditConfig, fallbackDbConfig?: DbConfig): void {
41
+ // Determine if audit is enabled (default: true)
42
+ auditEnabled = auditConfig?.enabled !== false;
43
+
44
+ if (auditEnabled) {
45
+ // Use audit.db config if present, otherwise fallback to root db config
46
+ const dbConfig = auditConfig?.db ?? fallbackDbConfig;
47
+ configureDb(dbConfig);
48
+ dbConfigured = true;
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Check if audit is enabled
54
+ */
55
+ export function isAuditEnabled(): boolean {
56
+ return auditEnabled;
57
+ }
58
+
59
+ /**
60
+ * Persist audit event to database (lazy init)
61
+ */
62
+ async function persistToDb(event: AuditEvent, id: string): Promise<void> {
63
+ if (!auditEnabled || !dbConfigured) {
64
+ return;
65
+ }
66
+
67
+ try {
68
+ // Lazy initialize DB on first persist
69
+ const orm = await ensureDbInitialized();
70
+ const em = orm.em.fork();
71
+
72
+ const logEntry = new RequestLogEntity();
73
+ logEntry.requestId = id;
74
+ logEntry.timestamp = new Date(event.timestamp);
75
+ logEntry.method = event.method;
76
+ logEntry.path = event.path;
77
+ logEntry.serverName = event.serverName ?? undefined;
78
+ logEntry.serverType = event.serverType ?? undefined;
79
+ logEntry.status = event.status ?? undefined;
80
+ logEntry.durationMs = event.durationMs ?? undefined;
81
+ logEntry.error = event.error ?? undefined;
82
+ logEntry.requestHeaders = event.requestHeaders ?? undefined;
83
+ // Determine request type
84
+ if (event.path.startsWith('/mcp/')) {
85
+ logEntry.requestType = 'mcp';
86
+ } else if (event.path.startsWith('/v1/')) {
87
+ logEntry.requestType = 'chat';
88
+ } else {
89
+ logEntry.requestType = 'api';
90
+ }
91
+ em.persist(logEntry);
92
+ await em.flush();
93
+ } catch (e) {
94
+ // Log persistence errors but don't throw - in-memory store is the primary
95
+ console.error('Failed to persist audit log:', e);
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Add an audit event
101
+ */
102
+ export function addAuditEvent(event: Omit<AuditEvent, 'id'>): AuditEvent {
103
+ const id = `${Date.now()}-${++eventCounter}`;
104
+ const fullEvent: AuditEvent = { ...event, id };
105
+ auditStore.set(id, fullEvent);
106
+
107
+ // Persist to database asynchronously (lazy init)
108
+ persistToDb(fullEvent, id).catch(() => {
109
+ // Already logged in persistToDb
110
+ });
111
+
112
+ return fullEvent;
113
+ }
114
+
115
+ /**
116
+ * Query audit events
117
+ */
118
+ export function queryAuditEvents(options: {
119
+ limit?: number;
120
+ offset?: number;
121
+ serverName?: string | null;
122
+ serverType?: string | null;
123
+ method?: string | null;
124
+ from?: string | null;
125
+ to?: string | null;
126
+ }): { events: AuditEvent[]; total: number } {
127
+ const { limit = 50, offset = 0, serverName, serverType, method, from, to } = options;
128
+
129
+ // Get all events as array
130
+ let events: AuditEvent[] = [];
131
+ for (const [, event] of auditStore.entries()) {
132
+ events.push(event);
133
+ }
134
+
135
+ // Sort by timestamp desc
136
+ events.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
137
+
138
+ // Apply filters
139
+ if (serverName) {
140
+ events = events.filter((e) => e.serverName === serverName);
141
+ }
142
+ if (serverType) {
143
+ events = events.filter((e) => e.serverType === serverType);
144
+ }
145
+ if (method) {
146
+ events = events.filter((e) => e.method === method);
147
+ }
148
+ if (from) {
149
+ const fromTime = new Date(from).getTime();
150
+ events = events.filter((e) => new Date(e.timestamp).getTime() >= fromTime);
151
+ }
152
+ if (to) {
153
+ const toTime = new Date(to).getTime();
154
+ events = events.filter((e) => new Date(e.timestamp).getTime() <= toTime);
155
+ }
156
+
157
+ const total = events.length;
158
+
159
+ // Paginate
160
+ events = events.slice(offset, offset + limit);
161
+
162
+ return { events, total };
163
+ }
164
+
165
+ /**
166
+ * Get audit statistics
167
+ */
168
+ export function getAuditStats(options: { from?: string | null; to?: string | null }) {
169
+ let events: AuditEvent[] = [];
170
+ for (const [, event] of auditStore.entries()) {
171
+ events.push(event);
172
+ }
173
+
174
+ // Apply time filters
175
+ if (options.from) {
176
+ const fromTime = new Date(options.from).getTime();
177
+ events = events.filter((e) => new Date(e.timestamp).getTime() >= fromTime);
178
+ }
179
+ if (options.to) {
180
+ const toTime = new Date(options.to).getTime();
181
+ events = events.filter((e) => new Date(e.timestamp).getTime() <= toTime);
182
+ }
183
+
184
+ // Calculate stats
185
+ const totalRequests = events.length;
186
+ const totalErrors = events.filter((e) => e.error || (e.status && e.status >= 400)).length;
187
+
188
+ const durations = events.map((e) => e.durationMs).filter((d): d is number => d != null);
189
+ const avgDurationMs = durations.length > 0 ? durations.reduce((a, b) => a + b, 0) / durations.length : 0;
190
+
191
+ // Group by server
192
+ const serverCounts = new Map<string, number>();
193
+ for (const event of events) {
194
+ const name = event.serverName || 'unknown';
195
+ serverCounts.set(name, (serverCounts.get(name) || 0) + 1);
196
+ }
197
+ const byServer = Array.from(serverCounts.entries())
198
+ .map(([name, count]) => ({ name, count }))
199
+ .sort((a, b) => b.count - a.count);
200
+
201
+ // Group by method
202
+ const methodCounts = new Map<string, number>();
203
+ for (const event of events) {
204
+ const method = event.method || 'unknown';
205
+ methodCounts.set(method, (methodCounts.get(method) || 0) + 1);
206
+ }
207
+ const byMethod = Array.from(methodCounts.entries())
208
+ .map(([method, count]) => ({ method, count }))
209
+ .sort((a, b) => b.count - a.count);
210
+
211
+ return { totalRequests, totalErrors, avgDurationMs, byServer, byMethod };
212
+ }
213
+
214
+ /**
215
+ * Clear audit events before a timestamp
216
+ */
217
+ export function clearAuditEvents(before: string): number {
218
+ const beforeTime = new Date(before).getTime();
219
+ let deleted = 0;
220
+
221
+ for (const [id, event] of auditStore.entries()) {
222
+ if (new Date(event.timestamp).getTime() < beforeTime) {
223
+ auditStore.delete(id);
224
+ deleted++;
225
+ }
226
+ }
227
+
228
+ return deleted;
229
+ }
230
+
231
+ /**
232
+ * Audit Router implementation
233
+ */
234
+ export const AuditRouter = implement(AuditContract).router({
235
+ list: implement(AuditContract.list).handler(async ({ input }) => {
236
+ return queryAuditEvents(input);
237
+ }),
238
+
239
+ get: implement(AuditContract.get).handler(async ({ input }) => {
240
+ return auditStore.get(input.id) ?? null;
241
+ }),
242
+
243
+ stats: implement(AuditContract.stats).handler(async ({ input }) => {
244
+ return getAuditStats(input);
245
+ }),
246
+
247
+ clear: implement(AuditContract.clear).handler(async ({ input }) => {
248
+ const deleted = clearAuditEvents(input.before);
249
+ return { deleted };
250
+ }),
251
+ });
252
+
253
+ /**
254
+ * Hono middleware for audit logging
255
+ */
256
+ export function auditMiddleware() {
257
+ return async (c: Context, next: Next) => {
258
+ const startTime = Date.now();
259
+ const path = c.req.path;
260
+
261
+ // Extract server info from path
262
+ let serverName: string | undefined;
263
+ let serverType: string | undefined;
264
+
265
+ const mcpMatch = path.match(/^\/mcp\/([^/]+)/);
266
+ if (mcpMatch) {
267
+ serverName = mcpMatch[1];
268
+ // Infer type from well-known paths
269
+ if (serverName === 'tencent-cls') serverType = 'tencent-cls';
270
+ else if (serverName === 'sql') serverType = 'sql';
271
+ else if (serverName === 'prometheus') serverType = 'prometheus';
272
+ else if (serverName === 'relay') serverType = 'relay';
273
+ else serverType = 'custom';
274
+ }
275
+
276
+ // Extract model info from chat requests
277
+ if (path.startsWith('/v1/')) {
278
+ serverType = 'chat';
279
+ }
280
+
281
+ let error: string | undefined;
282
+
283
+ try {
284
+ await next();
285
+ } catch (e) {
286
+ error = e instanceof Error ? e.message : String(e);
287
+ throw e;
288
+ } finally {
289
+ const durationMs = Date.now() - startTime;
290
+
291
+ // Audit MCP requests, Chat API requests, and other API requests
292
+ const shouldAudit =
293
+ path.startsWith('/mcp/') || path.startsWith('/v1/') || (path.startsWith('/api/') && c.req.method !== 'GET');
294
+
295
+ if (shouldAudit) {
296
+ addAuditEvent({
297
+ timestamp: new Date().toISOString(),
298
+ method: c.req.method,
299
+ path,
300
+ serverName,
301
+ serverType,
302
+ status: c.res.status,
303
+ durationMs,
304
+ error,
305
+ requestHeaders: headersToRecord(c.req.raw.headers),
306
+ });
307
+ }
308
+ }
309
+ };
310
+ }
@@ -0,0 +1,95 @@
1
+ import consola from 'consola';
2
+ import type { Hono } from 'hono';
3
+ import { createChatHandler } from '../chat';
4
+ import { registerAgentRoutes } from '../chat/agent';
5
+ import type { McpsConfig, ModelConfig } from './schema';
6
+
7
+ const log = consola.withTag('mcps');
8
+
9
+ export interface RegisterChatRoutesOptions {
10
+ app: Hono;
11
+ config: McpsConfig;
12
+ }
13
+
14
+ /**
15
+ * Register Chat/LLM Gateway routes
16
+ */
17
+ export function registerChatRoutes({ app, config }: RegisterChatRoutesOptions) {
18
+ if (!config.models || config.models.length === 0) {
19
+ return;
20
+ }
21
+
22
+ log.info(`Registering chat gateway with ${config.models.length} models`);
23
+ const chatHandler = createChatHandler({ config: { models: config.models } });
24
+ app.route('/', chatHandler);
25
+
26
+ // Helper function to resolve model config
27
+ function resolveModelConfig(modelName: string): ModelConfig | null {
28
+ const models = config.models || [];
29
+ // Exact match first
30
+ for (const modelConfig of models) {
31
+ if (modelConfig.name === modelName) return modelConfig;
32
+ }
33
+ // Wildcard match
34
+ for (const modelConfig of models) {
35
+ const pattern = modelConfig.name;
36
+ if (pattern.includes('*')) {
37
+ const regex = new RegExp(`^${pattern.replace(/\*/g, '.*')}$`);
38
+ if (regex.test(modelName)) return modelConfig;
39
+ }
40
+ }
41
+ return null;
42
+ }
43
+
44
+ // Register agent routes with tool loop support
45
+ registerAgentRoutes(app, {
46
+ resolveModelConfig,
47
+ // MCP tools will be loaded on-demand from configured servers
48
+ getMcpTools: async (serverNames) => {
49
+ if (!serverNames || serverNames.length === 0) {
50
+ return {};
51
+ }
52
+
53
+ const allTools: Record<string, any> = {};
54
+ const { createMCPClient } = await import('@ai-sdk/mcp');
55
+
56
+ for (const serverName of serverNames) {
57
+ const serverConfig = config.servers[serverName];
58
+ if (!serverConfig) {
59
+ log.warn(`MCP server not found: ${serverName}`);
60
+ continue;
61
+ }
62
+
63
+ try {
64
+ // Get the server URL - for configured servers, connect via HTTP
65
+ // The server exposes MCP at /mcp/{serverName}
66
+ const port = process.env.PORT || '3001';
67
+ const serverUrl = `http://127.0.0.1:${port}/mcp/${serverName}`;
68
+
69
+ // Create MCP client using SSE transport for HTTP-based MCP
70
+ const { SSEClientTransport } = await import('@modelcontextprotocol/sdk/client/sse.js');
71
+ const client = await createMCPClient({
72
+ transport: new SSEClientTransport(new URL(serverUrl)),
73
+ });
74
+
75
+ // Get tools from this server
76
+ const serverTools = await client.tools();
77
+ for (const [toolName, tool] of Object.entries(serverTools)) {
78
+ allTools[`${serverName}_${toolName}`] = tool;
79
+ }
80
+
81
+ log.info(`Loaded ${Object.keys(serverTools).length} tools from ${serverName}`);
82
+
83
+ // Close the client after getting tools
84
+ await client.close();
85
+ } catch (err) {
86
+ log.error(`Failed to load tools from ${serverName}:`, err);
87
+ }
88
+ }
89
+
90
+ log.info(`Agent loaded ${Object.keys(allTools).length} total tools from servers: ${serverNames.join(', ')}`);
91
+ return allTools;
92
+ },
93
+ });
94
+ log.info('Registered agent routes at /v1/agent/*');
95
+ }