@wener/mcps 1.0.2 → 1.0.4

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 (59) hide show
  1. package/README.md +144 -0
  2. package/dist/index.mjs +213076 -1
  3. package/dist/mcps-cli.mjs +102547 -59344
  4. package/lib/chat/handler.js +2 -2
  5. package/lib/chat/handler.js.map +1 -1
  6. package/lib/cli-start.js +36 -0
  7. package/lib/cli-start.js.map +1 -0
  8. package/lib/cli.js +19 -0
  9. package/lib/cli.js.map +1 -0
  10. package/lib/dev.server.js +7 -1
  11. package/lib/dev.server.js.map +1 -1
  12. package/lib/index.js +21 -3
  13. package/lib/index.js.map +1 -1
  14. package/lib/mcps-cli.js +6 -35
  15. package/lib/mcps-cli.js.map +1 -1
  16. package/lib/providers/feishu/def.js +35 -0
  17. package/lib/providers/feishu/def.js.map +1 -0
  18. package/lib/providers/findMcpServerDef.js +1 -0
  19. package/lib/providers/findMcpServerDef.js.map +1 -1
  20. package/lib/scripts/bundle.js +7 -1
  21. package/lib/scripts/bundle.js.map +1 -1
  22. package/lib/server/api-routes.js +7 -8
  23. package/lib/server/api-routes.js.map +1 -1
  24. package/lib/server/audit-db.js +64 -0
  25. package/lib/server/audit-db.js.map +1 -0
  26. package/lib/server/{audit.js → audit-plugin.js} +72 -126
  27. package/lib/server/audit-plugin.js.map +1 -0
  28. package/lib/server/events.js +13 -0
  29. package/lib/server/events.js.map +1 -0
  30. package/lib/server/mcp-routes.js +31 -60
  31. package/lib/server/mcp-routes.js.map +1 -1
  32. package/lib/server/mcps-router.js +19 -24
  33. package/lib/server/mcps-router.js.map +1 -1
  34. package/lib/server/schema.js +22 -2
  35. package/lib/server/schema.js.map +1 -1
  36. package/lib/server/server.js +142 -87
  37. package/lib/server/server.js.map +1 -1
  38. package/package.json +33 -6
  39. package/src/chat/handler.ts +2 -2
  40. package/src/cli-start.ts +43 -0
  41. package/src/cli.ts +45 -0
  42. package/src/dev.server.ts +8 -1
  43. package/src/index.ts +47 -1
  44. package/src/mcps-cli.ts +6 -48
  45. package/src/providers/feishu/def.ts +37 -0
  46. package/src/providers/findMcpServerDef.ts +1 -0
  47. package/src/scripts/bundle.ts +12 -1
  48. package/src/server/api-routes.ts +11 -8
  49. package/src/server/audit-db.ts +65 -0
  50. package/src/server/{audit.ts → audit-plugin.ts} +69 -142
  51. package/src/server/events.ts +29 -0
  52. package/src/server/mcp-routes.ts +30 -58
  53. package/src/server/mcps-router.ts +21 -29
  54. package/src/server/schema.ts +23 -2
  55. package/src/server/server.ts +149 -81
  56. package/lib/server/audit.js.map +0 -1
  57. package/lib/server/db.js +0 -97
  58. package/lib/server/db.js.map +0 -1
  59. package/src/server/db.ts +0 -115
package/src/mcps-cli.ts CHANGED
@@ -1,56 +1,14 @@
1
1
  #!/usr/bin/env node
2
- /**
3
- * MCPS - MCP Proxy Server CLI
4
- *
5
- * A unified MCP server that supports:
6
- * - Tencent CLS (Cloud Log Service)
7
- * - SQL (MySQL, PostgreSQL, SQLite)
8
- * - Prometheus
9
- * - Relay (proxy to other MCP servers)
10
- */
11
- import { serve } from '@hono/node-server';
12
- import { Command } from 'commander';
13
2
  import consola from 'consola';
14
- import { createServer } from './server/server';
3
+ import { createProgram } from './cli';
15
4
 
16
5
  const log = consola.withTag('mcps');
17
6
 
18
- const program = new Command();
19
-
20
- program
21
- .name('mcps')
22
- .description('MCP Proxy Server - Unified MCP service with relay, SQL, CLS, and Prometheus support')
23
- .version('0.1.0')
24
- .option('-p, --port <port>', 'Port to listen on', '8036')
25
- .option('-c, --cwd <path>', 'Working directory for config files', process.cwd())
26
- .option('--discovery-config', 'Enable server config discovery endpoints', false)
27
- .action(async (options) => {
28
- const port = Number.parseInt(options.port, 10);
29
- const { app } = createServer({
30
- cwd: options.cwd,
31
- port,
32
- discoveryConfig: options.discoveryConfig,
33
- });
34
-
35
- log.info(`Starting MCPS server on port ${port}`);
36
-
37
- serve({
38
- fetch: app.fetch,
39
- port,
40
- hostname: '0.0.0.0',
41
- });
42
-
43
- log.success(`MCPS server running at http://localhost:${port}`);
44
- });
45
-
46
- // Handle graceful shutdown
47
- process.on('SIGINT', () => {
48
- log.info('Shutting down...');
49
- process.exit(130);
50
- });
51
- process.on('SIGTERM', () => {
52
- log.info('Shutting down...');
53
- process.exit(143);
7
+ const program = createProgram({
8
+ setup: async (ctx) => {
9
+ const { setupAudit } = await import('./server/audit-plugin.js');
10
+ setupAudit(ctx);
11
+ },
54
12
  });
55
13
 
56
14
  program.parseAsync(process.argv).catch((error) => {
@@ -0,0 +1,37 @@
1
+ import { FeishuMcpServerDef, type CreateFeishuMcpServerOptions } from '@wener/ai/mcp/feishu';
2
+ import { HeaderNames, type FeishuConfig } from '../../server/schema';
3
+ import { defineMcpServerHandler, registerMcpServerHandler } from '../McpServerHandlerDef';
4
+
5
+ export const FeishuMcpServerHandlerDef = defineMcpServerHandler<CreateFeishuMcpServerOptions, FeishuConfig>(
6
+ FeishuMcpServerDef,
7
+ {
8
+ headerMappings: [
9
+ { header: HeaderNames.FEISHU_APP_ID, property: 'appId', required: true },
10
+ { header: HeaderNames.FEISHU_APP_SECRET, property: 'appSecret', required: true },
11
+ { header: HeaderNames.FEISHU_DOMAIN, property: 'domain' },
12
+ ],
13
+
14
+ resolveConfig(config, headers) {
15
+ const appId =
16
+ config.appId ||
17
+ headers?.get(HeaderNames.FEISHU_APP_ID) ||
18
+ config.headers?.[HeaderNames.FEISHU_APP_ID];
19
+ const appSecret =
20
+ config.appSecret ||
21
+ headers?.get(HeaderNames.FEISHU_APP_SECRET) ||
22
+ config.headers?.[HeaderNames.FEISHU_APP_SECRET];
23
+
24
+ if (!appId || !appSecret) return null;
25
+
26
+ const domain =
27
+ config.domain ||
28
+ headers?.get(HeaderNames.FEISHU_DOMAIN) ||
29
+ config.headers?.[HeaderNames.FEISHU_DOMAIN] ||
30
+ 'feishu';
31
+
32
+ return { appId, appSecret, domain };
33
+ },
34
+ },
35
+ );
36
+
37
+ registerMcpServerHandler(FeishuMcpServerHandlerDef);
@@ -4,6 +4,7 @@ import './prometheus/def';
4
4
  import './tencent-cls/def';
5
5
  import './sql/def';
6
6
  import './relay/def';
7
+ import './feishu/def';
7
8
 
8
9
  /**
9
10
  * Find MCP server definitions matching a predicate
@@ -39,7 +39,18 @@ const commonOptions: esbuild.BuildOptions = {
39
39
  sourcemap: false,
40
40
  legalComments: 'none',
41
41
  // External native modules
42
- external: ['better-sqlite3', 'oracledb', 'mariadb/callback', 'mysql'],
42
+ external: [
43
+ 'better-sqlite3',
44
+ 'bun:sqlite',
45
+ 'kysely-bun-sqlite',
46
+ 'oracledb',
47
+ 'mariadb/callback',
48
+ 'mysql',
49
+ '@nestjs/websockets',
50
+ '@nestjs/microservices',
51
+ '@nestjs/platform-express',
52
+ '@larksuiteoapi/node-sdk',
53
+ ],
43
54
  };
44
55
 
45
56
  // Ensure dist directory exists
@@ -6,26 +6,29 @@ import { CORSPlugin } from '@orpc/server/plugins';
6
6
  import { ZodToJsonSchemaConverter } from '@orpc/zod/zod4';
7
7
  import type { Hono } from 'hono';
8
8
  import { html } from 'hono/html';
9
- import { AuditRouter } from './audit';
10
9
  import { createMcpsRouter } from './mcps-router';
11
10
  import type { McpsConfig } from './schema';
11
+ import type { StatsProvider } from './server';
12
12
 
13
13
  export interface RegisterApiRoutesOptions {
14
14
  app: Hono;
15
15
  config: McpsConfig;
16
+ /** Additional oRPC routers registered by plugins (e.g. audit) */
17
+ apiRouters?: Record<string, any>;
18
+ /** Optional stats provider from audit plugin */
19
+ statsProvider?: StatsProvider;
16
20
  }
17
21
 
18
22
  /**
19
- * Register oRPC API routes for audit and MCPS
23
+ * Register oRPC API routes for MCPS.
24
+ * Audit router is not included by default - use setupAudit() plugin to add it.
20
25
  */
21
- export function registerApiRoutes({ app, config }: RegisterApiRoutesOptions) {
22
- // Create MCPS router with config context
23
- const McpsRouter = createMcpsRouter({ config });
26
+ export function registerApiRoutes({ app, config, apiRouters, statsProvider }: RegisterApiRoutesOptions) {
27
+ const McpsRouter = createMcpsRouter({ config, statsProvider });
24
28
 
25
- // Combined router for all APIs
26
- const combinedRouter = {
27
- audit: AuditRouter,
29
+ const combinedRouter: Record<string, any> = {
28
30
  mcps: McpsRouter,
31
+ ...apiRouters,
29
32
  };
30
33
 
31
34
  const handleByRpc = new RPCHandler(combinedRouter);
@@ -0,0 +1,65 @@
1
+ import { MikroORM, type Options } from '@mikro-orm/core';
2
+ import { SqliteDriver } from '@mikro-orm/sql';
3
+ import { createSqliteDialect } from '@wener/server/mikro-orm';
4
+ import { ChatRequestEntity } from '../entities/ChatRequestEntity';
5
+ import { McpRequestEntity } from '../entities/McpRequestEntity';
6
+ import { RequestLogEntity } from '../entities/RequestLogEntity';
7
+ import { ResponseEntity } from '../entities/ResponseEntity';
8
+ import type { DbConfig } from './schema';
9
+
10
+ export { RequestLogEntity };
11
+
12
+ let orm: MikroORM<SqliteDriver> | null = null;
13
+ let initPromise: Promise<MikroORM<SqliteDriver>> | null = null;
14
+
15
+ async function getOrmConfig(dbConfig?: DbConfig): Promise<Options<SqliteDriver>> {
16
+ const dbPath = dbConfig?.path || '.mcps.db';
17
+ return {
18
+ driver: SqliteDriver,
19
+ dbName: dbPath,
20
+ entities: [ChatRequestEntity, McpRequestEntity, RequestLogEntity, ResponseEntity],
21
+ driverOptions: await createSqliteDialect(dbPath),
22
+ debug: process.env.NODE_ENV === 'development',
23
+ allowGlobalContext: true,
24
+ };
25
+ }
26
+
27
+ export async function ensureDbInitialized(dbConfig?: DbConfig): Promise<MikroORM<SqliteDriver>> {
28
+ if (orm) return orm;
29
+ if (initPromise) return initPromise;
30
+
31
+ initPromise = (async () => {
32
+ const config = await getOrmConfig(dbConfig);
33
+ orm = await MikroORM.init(config);
34
+ await orm.schema.update();
35
+ return orm;
36
+ })();
37
+
38
+ try {
39
+ return await initPromise;
40
+ } catch (e) {
41
+ initPromise = null;
42
+ throw e;
43
+ }
44
+ }
45
+
46
+ export function getOrm(): MikroORM<SqliteDriver> {
47
+ if (!orm) throw new Error('Database not initialized');
48
+ return orm;
49
+ }
50
+
51
+ export function getEntityManager() {
52
+ return getOrm().em;
53
+ }
54
+
55
+ export async function closeDb(): Promise<void> {
56
+ if (orm) {
57
+ await orm.close();
58
+ orm = null;
59
+ initPromise = null;
60
+ }
61
+ }
62
+
63
+ export function isDbInitialized(): boolean {
64
+ return orm !== null;
65
+ }
@@ -1,14 +1,10 @@
1
1
  import { implement } from '@orpc/server';
2
- import type { Context, Next } from 'hono';
3
2
  import { LRUCache } from 'lru-cache';
4
3
  import { AuditContract, type AuditEvent } from '../contracts';
5
- import { RequestLogEntity } from '../entities';
6
- import { ensureDbInitialized, configureDb } from './db';
4
+ import { McpsEventType, type McpsEmitter } from './events';
7
5
  import type { AuditConfig, DbConfig } from './schema';
6
+ import type { McpsServerContext } from './server';
8
7
 
9
- /**
10
- * Convert Headers to a plain record
11
- */
12
8
  function headersToRecord(headers: Headers): Record<string, string> {
13
9
  const record: Record<string, string> = {};
14
10
  headers.forEach((value, key) => {
@@ -17,56 +13,22 @@ function headersToRecord(headers: Headers): Record<string, string> {
17
13
  return record;
18
14
  }
19
15
 
20
- // In-memory audit store using LRU cache
21
16
  const auditStore = new LRUCache<string, AuditEvent>({
22
- max: 10000, // Keep last 10k events
23
- ttl: 1000 * 60 * 60 * 24, // 24 hours
17
+ max: 10000,
18
+ ttl: 1000 * 60 * 60 * 24,
24
19
  });
25
20
 
26
- // Counter for IDs
27
21
  let eventCounter = 0;
28
-
29
- // Audit configuration state
30
- let auditEnabled = true; // default to enabled
31
22
  let dbConfigured = false;
23
+ let storedAuditConfig: AuditConfig | undefined;
24
+ let storedDbConfig: DbConfig | undefined;
32
25
 
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
26
  async function persistToDb(event: AuditEvent, id: string): Promise<void> {
63
- if (!auditEnabled || !dbConfigured) {
64
- return;
65
- }
27
+ if (!dbConfigured) return;
66
28
 
67
29
  try {
68
- // Lazy initialize DB on first persist
69
- const orm = await ensureDbInitialized();
30
+ const { ensureDbInitialized, RequestLogEntity } = await import('./audit-db.js');
31
+ const orm = await ensureDbInitialized(storedDbConfig);
70
32
  const em = orm.em.fork();
71
33
 
72
34
  const logEntry = new RequestLogEntity();
@@ -80,7 +42,6 @@ async function persistToDb(event: AuditEvent, id: string): Promise<void> {
80
42
  logEntry.durationMs = event.durationMs ?? undefined;
81
43
  logEntry.error = event.error ?? undefined;
82
44
  logEntry.requestHeaders = event.requestHeaders ?? undefined;
83
- // Determine request type
84
45
  if (event.path.startsWith('/mcp/')) {
85
46
  logEntry.requestType = 'mcp';
86
47
  } else if (event.path.startsWith('/v1/')) {
@@ -91,30 +52,20 @@ async function persistToDb(event: AuditEvent, id: string): Promise<void> {
91
52
  em.persist(logEntry);
92
53
  await em.flush();
93
54
  } catch (e) {
94
- // Log persistence errors but don't throw - in-memory store is the primary
95
55
  console.error('Failed to persist audit log:', e);
96
56
  }
97
57
  }
98
58
 
99
- /**
100
- * Add an audit event
101
- */
102
59
  export function addAuditEvent(event: Omit<AuditEvent, 'id'>): AuditEvent {
103
60
  const id = `${Date.now()}-${++eventCounter}`;
104
61
  const fullEvent: AuditEvent = { ...event, id };
105
62
  auditStore.set(id, fullEvent);
106
63
 
107
- // Persist to database asynchronously (lazy init)
108
- persistToDb(fullEvent, id).catch(() => {
109
- // Already logged in persistToDb
110
- });
64
+ persistToDb(fullEvent, id).catch(() => {});
111
65
 
112
66
  return fullEvent;
113
67
  }
114
68
 
115
- /**
116
- * Query audit events
117
- */
118
69
  export function queryAuditEvents(options: {
119
70
  limit?: number;
120
71
  offset?: number;
@@ -126,25 +77,16 @@ export function queryAuditEvents(options: {
126
77
  }): { events: AuditEvent[]; total: number } {
127
78
  const { limit = 50, offset = 0, serverName, serverType, method, from, to } = options;
128
79
 
129
- // Get all events as array
130
80
  let events: AuditEvent[] = [];
131
81
  for (const [, event] of auditStore.entries()) {
132
82
  events.push(event);
133
83
  }
134
84
 
135
- // Sort by timestamp desc
136
85
  events.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
137
86
 
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
- }
87
+ if (serverName) events = events.filter((e) => e.serverName === serverName);
88
+ if (serverType) events = events.filter((e) => e.serverType === serverType);
89
+ if (method) events = events.filter((e) => e.method === method);
148
90
  if (from) {
149
91
  const fromTime = new Date(from).getTime();
150
92
  events = events.filter((e) => new Date(e.timestamp).getTime() >= fromTime);
@@ -155,23 +97,17 @@ export function queryAuditEvents(options: {
155
97
  }
156
98
 
157
99
  const total = events.length;
158
-
159
- // Paginate
160
100
  events = events.slice(offset, offset + limit);
161
101
 
162
102
  return { events, total };
163
103
  }
164
104
 
165
- /**
166
- * Get audit statistics
167
- */
168
105
  export function getAuditStats(options: { from?: string | null; to?: string | null }) {
169
106
  let events: AuditEvent[] = [];
170
107
  for (const [, event] of auditStore.entries()) {
171
108
  events.push(event);
172
109
  }
173
110
 
174
- // Apply time filters
175
111
  if (options.from) {
176
112
  const fromTime = new Date(options.from).getTime();
177
113
  events = events.filter((e) => new Date(e.timestamp).getTime() >= fromTime);
@@ -181,14 +117,12 @@ export function getAuditStats(options: { from?: string | null; to?: string | nul
181
117
  events = events.filter((e) => new Date(e.timestamp).getTime() <= toTime);
182
118
  }
183
119
 
184
- // Calculate stats
185
120
  const totalRequests = events.length;
186
121
  const totalErrors = events.filter((e) => e.error || (e.status && e.status >= 400)).length;
187
122
 
188
123
  const durations = events.map((e) => e.durationMs).filter((d): d is number => d != null);
189
124
  const avgDurationMs = durations.length > 0 ? durations.reduce((a, b) => a + b, 0) / durations.length : 0;
190
125
 
191
- // Group by server
192
126
  const serverCounts = new Map<string, number>();
193
127
  for (const event of events) {
194
128
  const name = event.serverName || 'unknown';
@@ -198,11 +132,10 @@ export function getAuditStats(options: { from?: string | null; to?: string | nul
198
132
  .map(([name, count]) => ({ name, count }))
199
133
  .sort((a, b) => b.count - a.count);
200
134
 
201
- // Group by method
202
135
  const methodCounts = new Map<string, number>();
203
136
  for (const event of events) {
204
- const method = event.method || 'unknown';
205
- methodCounts.set(method, (methodCounts.get(method) || 0) + 1);
137
+ const m = event.method || 'unknown';
138
+ methodCounts.set(m, (methodCounts.get(m) || 0) + 1);
206
139
  }
207
140
  const byMethod = Array.from(methodCounts.entries())
208
141
  .map(([method, count]) => ({ method, count }))
@@ -211,9 +144,6 @@ export function getAuditStats(options: { from?: string | null; to?: string | nul
211
144
  return { totalRequests, totalErrors, avgDurationMs, byServer, byMethod };
212
145
  }
213
146
 
214
- /**
215
- * Clear audit events before a timestamp
216
- */
217
147
  export function clearAuditEvents(before: string): number {
218
148
  const beforeTime = new Date(before).getTime();
219
149
  let deleted = 0;
@@ -228,22 +158,16 @@ export function clearAuditEvents(before: string): number {
228
158
  return deleted;
229
159
  }
230
160
 
231
- /**
232
- * Audit Router implementation
233
- */
234
161
  export const AuditRouter = implement(AuditContract).router({
235
162
  list: implement(AuditContract.list).handler(async ({ input }) => {
236
163
  return queryAuditEvents(input);
237
164
  }),
238
-
239
165
  get: implement(AuditContract.get).handler(async ({ input }) => {
240
166
  return auditStore.get(input.id) ?? null;
241
167
  }),
242
-
243
168
  stats: implement(AuditContract.stats).handler(async ({ input }) => {
244
169
  return getAuditStats(input);
245
170
  }),
246
-
247
171
  clear: implement(AuditContract.clear).handler(async ({ input }) => {
248
172
  const deleted = clearAuditEvents(input.before);
249
173
  return { deleted };
@@ -251,60 +175,63 @@ export const AuditRouter = implement(AuditContract).router({
251
175
  });
252
176
 
253
177
  /**
254
- * Hono middleware for audit logging
178
+ * Set up audit by subscribing to the server emitter.
179
+ * Call this from the `setup` callback of `createServer` to opt in to audit.
180
+ *
181
+ * @example
182
+ * ```ts
183
+ * createServer({
184
+ * setup: (ctx) => {
185
+ * setupAudit(ctx);
186
+ * },
187
+ * });
188
+ * ```
255
189
  */
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
- }
190
+ export function setupAudit(ctx: McpsServerContext, options?: { auditConfig?: AuditConfig; dbConfig?: DbConfig }) {
191
+ const auditConfig = options?.auditConfig ?? ctx.config.audit;
192
+ const dbConfig = options?.dbConfig ?? ctx.config.db;
275
193
 
276
- // Extract model info from chat requests
277
- if (path.startsWith('/v1/')) {
278
- serverType = 'chat';
279
- }
194
+ const enabled = auditConfig?.enabled !== false;
195
+ if (!enabled) return;
280
196
 
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
- }
197
+ const auditDbConfig = auditConfig?.db ?? dbConfig;
198
+ if (auditDbConfig) {
199
+ storedDbConfig = auditDbConfig;
200
+ dbConfigured = true;
201
+ }
202
+ storedAuditConfig = auditConfig;
203
+
204
+ // Subscribe to request events
205
+ ctx.emitter.on(McpsEventType.Request, (event) => {
206
+ const shouldAudit =
207
+ event.path.startsWith('/mcp/') ||
208
+ event.path.startsWith('/v1/') ||
209
+ (event.path.startsWith('/api/') && event.method !== 'GET');
210
+
211
+ if (shouldAudit) {
212
+ addAuditEvent({
213
+ timestamp: event.timestamp,
214
+ method: event.method,
215
+ path: event.path,
216
+ serverName: event.serverName,
217
+ serverType: event.serverType,
218
+ status: event.status,
219
+ durationMs: event.durationMs,
220
+ error: event.error,
221
+ requestHeaders: event.requestHeaders,
222
+ });
308
223
  }
224
+ });
225
+
226
+ // Register audit API router
227
+ ctx.apiRouters.audit = AuditRouter;
228
+
229
+ // Register stats provider so mcps-router can access stats
230
+ ctx.statsProvider = {
231
+ getStats: getAuditStats,
232
+ queryEvents: (opts) => {
233
+ const result = queryAuditEvents(opts);
234
+ return { events: result.events.map((e) => ({ path: e.path })), total: result.total };
235
+ },
309
236
  };
310
237
  }
@@ -0,0 +1,29 @@
1
+ import Emittery from 'emittery';
2
+
3
+ export const McpsEventType = {
4
+ Request: 'Mcps:Request',
5
+ } as const;
6
+
7
+ export type McpsRequestEvent = {
8
+ timestamp: string;
9
+ method: string;
10
+ path: string;
11
+ serverName?: string;
12
+ serverType?: string;
13
+ status?: number;
14
+ durationMs?: number;
15
+ error?: string;
16
+ requestHeaders?: Record<string, string>;
17
+ };
18
+
19
+ export type McpsEventData = {
20
+ [McpsEventType.Request]: McpsRequestEvent;
21
+ };
22
+
23
+ export type McpsEmitter = Emittery<McpsEventData>;
24
+
25
+ export function createMcpsEmitter(): McpsEmitter {
26
+ return new Emittery<McpsEventData>({
27
+ debug: { name: 'McpsEmitter' },
28
+ });
29
+ }