@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
@@ -17,13 +17,15 @@ export interface RegisterMcpRoutesOptions {
17
17
  }
18
18
 
19
19
  /**
20
- * Register MCP routes for both pre-configured and dynamic endpoints
20
+ * Register MCP routes for both pre-configured and dynamic endpoints.
21
+ *
22
+ * McpServer only supports one transport connection at a time, so we create
23
+ * a fresh server instance per request instead of caching stateful servers.
21
24
  */
22
- export function registerMcpRoutes({ app, config, serverCache }: RegisterMcpRoutesOptions) {
25
+ export function registerMcpRoutes({ app, config, serverCache: _serverCache }: RegisterMcpRoutesOptions) {
23
26
  const serverDefs = findMcpServerDef();
24
27
 
25
28
  // Register pre-configured servers from config
26
- // These are named endpoints like /mcp/my-sql that use config from file
27
29
  for (const [name, serverConfig] of Object.entries(config.servers)) {
28
30
  const def = getMcpServerHandlerDef(serverConfig.type);
29
31
  if (!def) {
@@ -31,7 +33,6 @@ export function registerMcpRoutes({ app, config, serverCache }: RegisterMcpRoute
31
33
  continue;
32
34
  }
33
35
 
34
- // Resolve config using def (config comes from file, not headers)
35
36
  const options = def.resolveConfig(serverConfig);
36
37
  if (!options) {
37
38
  log.warn(`Failed to resolve config for ${name}`);
@@ -44,48 +45,23 @@ export function registerMcpRoutes({ app, config, serverCache }: RegisterMcpRoute
44
45
  continue;
45
46
  }
46
47
 
47
- // Create and cache the server instance at startup
48
- const cacheKey = `config::${name}`;
49
- const item = def.create(options);
50
- if (!item) {
51
- log.warn(`Failed to create server: ${name}`);
52
- continue;
53
- }
54
- serverCache.set(cacheKey, item);
55
-
56
48
  const path = `/mcp/${name}`;
57
49
  log.info(`Registered MCP server: ${path} (${serverConfig.type})`);
58
50
 
59
51
  app.all(path, async (c) => {
60
- const serverItem = serverCache.get(cacheKey);
61
- if (!serverItem) {
62
- return c.text('Server not found', 404);
63
- }
64
- // Create a new transport for each request to avoid "Transport already started" error
65
- const transport = new StreamableHTTPTransport();
66
- try {
67
- await serverItem.server.connect(transport);
68
- const handleRequest = createMcpLoggingHandler(transport, name);
69
- return await handleRequest(c);
70
- } catch (e) {
71
- log.error(`[${name}] Request error:`, e);
72
- return c.text(`Internal server error: ${e instanceof Error ? e.message : 'Unknown error'}`, 500);
73
- }
52
+ return handleMcpRequest(c, def, options, name);
74
53
  });
75
54
  }
76
55
 
77
- // Register dynamic endpoints for all server types
78
- // These endpoints accept config via HTTP headers
56
+ // Register dynamic endpoints for all server types (header-based config)
79
57
  for (const def of serverDefs) {
80
58
  const path = `/mcp/${def.name}`;
81
59
  log.debug(`Registering dynamic endpoint: ${path}`);
82
60
 
83
61
  app.all(path, async (c) => {
84
- // Use def.resolveConfig to parse config from headers
85
62
  const options = def.resolveConfig({ type: def.name } as any, c.req.raw.headers);
86
63
 
87
64
  if (!options) {
88
- // Build error message from headerMappings
89
65
  const requiredHeaders =
90
66
  def.headerMappings
91
67
  ?.filter((m) => m.required)
@@ -94,40 +70,36 @@ export function registerMcpRoutes({ app, config, serverCache }: RegisterMcpRoute
94
70
  return c.text(`Missing ${requiredHeaders}`, 400);
95
71
  }
96
72
 
97
- // Validate options
98
73
  const validation = def.validateOptions(options);
99
74
  if (!validation.valid) {
100
75
  return c.text(validation.error || `Invalid configuration for ${def.name}`, 400);
101
76
  }
102
77
 
103
- // Get or create cached server instance
104
- const key = def.getCacheKey(options);
105
- let item = serverCache.get(key);
106
- if (!item) {
107
- log.info(`Creating new ${def.title} server: ${key}`);
108
- try {
109
- const newItem = def.create(options);
110
- if (!newItem) return c.text(`Failed to create ${def.name} server`, 500);
111
- item = newItem;
112
- serverCache.set(key, item);
113
- } catch (e) {
114
- log.error(`Failed to create ${def.name} server:`, e);
115
- return c.text(`Failed to create ${def.name} server`, 500);
116
- }
117
- }
118
-
119
- // Create a new transport for each request
120
- const transport = new StreamableHTTPTransport();
121
- try {
122
- await item.server.connect(transport);
123
- const handleRequest = createMcpLoggingHandler(transport, def.name);
124
- return await handleRequest(c);
125
- } catch (e) {
126
- log.error(`[${def.name}] Request error:`, e);
127
- return c.text(`Internal server error: ${e instanceof Error ? e.message : 'Unknown error'}`, 500);
128
- }
78
+ return handleMcpRequest(c, def, options, def.name);
129
79
  });
130
80
  }
131
81
 
132
82
  return { serverDefs };
133
83
  }
84
+
85
+ async function handleMcpRequest(c: any, def: McpServerHandlerDef, options: any, name: string) {
86
+ let item: McpServerInstance | undefined;
87
+ try {
88
+ item = def.create(options);
89
+ if (!item) return c.text(`Failed to create ${name} server`, 500);
90
+ } catch (e) {
91
+ log.error(`Failed to create ${name} server:`, e);
92
+ return c.text(`Failed to create ${name} server`, 500);
93
+ }
94
+
95
+ const transport = new StreamableHTTPTransport();
96
+ try {
97
+ await item.server.connect(transport);
98
+ const handleRequest = createMcpLoggingHandler(transport, name);
99
+ return await handleRequest(c);
100
+ } catch (e) {
101
+ log.error(`[${name}] Request error:`, e);
102
+ item.close?.().catch(() => {});
103
+ return c.text(`Internal server error: ${e instanceof Error ? e.message : 'Unknown error'}`, 500);
104
+ }
105
+ }
@@ -1,19 +1,17 @@
1
1
  import { implement } from '@orpc/server';
2
2
  import { McpsContract, type ModelInfo, type ServerInfo, type ServerTypeInfo, type ToolInfo } from '../contracts';
3
3
  import { findMcpServerDef } from '../providers/findMcpServerDef';
4
- import { getAuditStats, queryAuditEvents } from './audit';
5
4
  import type { McpsConfig } from './schema';
5
+ import type { StatsProvider } from './server';
6
6
 
7
- // Simple glob pattern matching
8
7
  function matchGlob(pattern: string, text: string): boolean {
9
8
  const regexPattern = pattern
10
- .replace(/[.+^${}()|[\]\\]/g, '\\$&') // Escape regex special chars except * and ?
9
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&')
11
10
  .replace(/\*/g, '.*')
12
11
  .replace(/\?/g, '.');
13
12
  return new RegExp(`^${regexPattern}$`, 'i').test(text);
14
13
  }
15
14
 
16
- // Build server types from registry
17
15
  function getServerTypes(): ServerTypeInfo[] {
18
16
  return findMcpServerDef().map((def) => ({
19
17
  type: def.name,
@@ -22,7 +20,6 @@ function getServerTypes(): ServerTypeInfo[] {
22
20
  }));
23
21
  }
24
22
 
25
- // Build endpoints from server types
26
23
  function getEndpoints(): string[] {
27
24
  const mcpEndpoints = findMcpServerDef().map((def) => `/mcp/${def.name}`);
28
25
  const chatEndpoints = [
@@ -38,22 +35,27 @@ function getEndpoints(): string[] {
38
35
 
39
36
  export interface McpsRouterContext {
40
37
  config: McpsConfig;
38
+ /** Optional stats provider (e.g. from audit plugin) */
39
+ statsProvider?: StatsProvider;
41
40
  }
42
41
 
43
- /**
44
- * Create MCPS Router with config context
45
- */
42
+ const emptyStats = {
43
+ totalRequests: 0,
44
+ totalErrors: 0,
45
+ avgDurationMs: 0,
46
+ byServer: [] as Array<{ name: string; count: number }>,
47
+ byMethod: [] as Array<{ method: string; count: number }>,
48
+ };
49
+
46
50
  export function createMcpsRouter(ctx: McpsRouterContext) {
47
- const { config } = ctx;
51
+ const { config, statsProvider } = ctx;
48
52
 
49
- // Build server info list
50
53
  const servers: ServerInfo[] = Object.entries(config.servers).map(([name, serverConfig]) => ({
51
54
  name,
52
55
  type: serverConfig.type,
53
56
  disabled: serverConfig.disabled,
54
57
  }));
55
58
 
56
- // Build model info list
57
59
  const models: ModelInfo[] = (config.models ?? []).map((model) => ({
58
60
  name: model.name,
59
61
  adapter: model.adapter,
@@ -76,10 +78,13 @@ export function createMcpsRouter(ctx: McpsRouterContext) {
76
78
  }),
77
79
 
78
80
  stats: implement(McpsContract.stats).handler(async ({ input }) => {
79
- const auditStats = getAuditStats(input);
81
+ if (!statsProvider) {
82
+ return { ...emptyStats, byEndpoint: [] };
83
+ }
84
+
85
+ const auditStats = statsProvider.getStats(input);
86
+ const { events } = statsProvider.queryEvents({ limit: 10000 });
80
87
 
81
- // Build endpoint stats from audit events
82
- const { events } = queryAuditEvents({ limit: 10000 });
83
88
  const endpointCounts = new Map<string, number>();
84
89
  for (const event of events) {
85
90
  const endpoint = event.path || 'unknown';
@@ -88,12 +93,9 @@ export function createMcpsRouter(ctx: McpsRouterContext) {
88
93
  const byEndpoint = Array.from(endpointCounts.entries())
89
94
  .map(([endpoint, count]) => ({ endpoint, count }))
90
95
  .sort((a, b) => b.count - a.count)
91
- .slice(0, 20); // Top 20 endpoints
96
+ .slice(0, 20);
92
97
 
93
- return {
94
- ...auditStats,
95
- byEndpoint,
96
- };
98
+ return { ...auditStats, byEndpoint };
97
99
  }),
98
100
 
99
101
  servers: implement(McpsContract.servers).handler(async () => {
@@ -105,17 +107,7 @@ export function createMcpsRouter(ctx: McpsRouterContext) {
105
107
  }),
106
108
 
107
109
  tools: implement(McpsContract.tools).handler(async ({ input }) => {
108
- // TODO: This is a placeholder that returns empty tools list.
109
- // Full implementation would require connecting to each MCP server
110
- // and calling tools/list. Consider caching tool lists and
111
- // refreshing periodically or on demand.
112
110
  const tools: ToolInfo[] = [];
113
-
114
- // For now, return an empty list.
115
- // When MCP servers support persistent connections or when we
116
- // implement a tool registry, this will be populated.
117
-
118
- // Apply filters if provided
119
111
  let filteredTools = tools;
120
112
 
121
113
  if (input.server) {
@@ -14,6 +14,9 @@ export const HeaderNames = {
14
14
  MCP_URL: 'X-MCP-URL',
15
15
  MCP_TYPE: 'X-MCP-TYPE',
16
16
  MCP_COMMAND: 'X-MCP-COMMAND',
17
+ FEISHU_APP_ID: 'X-FEISHU-APP-ID',
18
+ FEISHU_APP_SECRET: 'X-FEISHU-APP-SECRET',
19
+ FEISHU_DOMAIN: 'X-FEISHU-DOMAIN',
17
20
  // Tool filtering headers
18
21
  MCP_READONLY: 'X-MCP-Readonly',
19
22
  MCP_INCLUDE: 'X-MCP-Include',
@@ -53,6 +56,15 @@ export const PrometheusConfigSchema = BaseServerConfigSchema.extend({
53
56
  });
54
57
  export type PrometheusConfig = z.infer<typeof PrometheusConfigSchema>;
55
58
 
59
+ // Feishu/Lark config
60
+ export const FeishuConfigSchema = BaseServerConfigSchema.extend({
61
+ type: z.literal('feishu'),
62
+ appId: z.string().optional().describe('Feishu App ID'),
63
+ appSecret: z.string().optional().describe('Feishu App Secret'),
64
+ domain: z.string().optional().describe('feishu (China) or lark (International)'),
65
+ });
66
+ export type FeishuConfig = z.infer<typeof FeishuConfigSchema>;
67
+
56
68
  // Relay config for proxying to other MCP servers
57
69
  export const RelayConfigSchema = BaseServerConfigSchema.extend({
58
70
  type: z.literal('relay'),
@@ -63,13 +75,22 @@ export const RelayConfigSchema = BaseServerConfigSchema.extend({
63
75
  });
64
76
  export type RelayConfig = z.infer<typeof RelayConfigSchema>;
65
77
 
66
- // Union of all server configs
67
- export const ServerConfigSchema = z.discriminatedUnion('type', [
78
+ // Known server config schemas
79
+ const KnownServerConfigSchema = z.discriminatedUnion('type', [
68
80
  TencentClsConfigSchema,
69
81
  SqlConfigSchema,
70
82
  PrometheusConfigSchema,
83
+ FeishuConfigSchema,
71
84
  RelayConfigSchema,
72
85
  ]);
86
+
87
+ // Catch-all for custom/extension server types (e.g. platform-admin, fusionops-admin)
88
+ const GenericServerConfigSchema = BaseServerConfigSchema.extend({
89
+ type: z.string(),
90
+ }).passthrough();
91
+
92
+ // Union of known types with generic fallback for extensibility
93
+ export const ServerConfigSchema = z.union([KnownServerConfigSchema, GenericServerConfigSchema]);
73
94
  export type ServerConfig = z.infer<typeof ServerConfigSchema>;
74
95
 
75
96
  /**
@@ -1,4 +1,5 @@
1
1
  import consola from 'consola';
2
+ import type { Context, Next } from 'hono';
2
3
  import { Hono } from 'hono';
3
4
  import { logger } from 'hono/logger';
4
5
  import { LRUCache } from 'lru-cache';
@@ -6,24 +7,53 @@ import { isDevelopment } from 'std-env';
6
7
  import type { McpServerInstance } from '@wener/ai/mcp';
7
8
  import { findMcpServerDef } from '../providers/findMcpServerDef';
8
9
  import { registerApiRoutes } from './api-routes';
9
- import { auditMiddleware, configureAudit } from './audit';
10
10
  import { registerChatRoutes } from './chat-routes';
11
11
  import { loadConfig, loadEnvFiles, substituteEnvVars } from './config';
12
+ import { createMcpsEmitter, McpsEventType, type McpsEmitter } from './events';
12
13
  import { registerMcpRoutes } from './mcp-routes';
13
14
 
14
15
  const log = consola.withTag('mcps');
15
16
 
17
+ export interface McpsServerContext {
18
+ app: Hono;
19
+ config: import('./schema').McpsConfig;
20
+ emitter: McpsEmitter;
21
+ serverCache: LRUCache<string, McpServerInstance>;
22
+ /** Plugins can register additional oRPC routers here */
23
+ apiRouters: Record<string, any>;
24
+ /** Plugins can register stats providers here */
25
+ statsProvider?: StatsProvider;
26
+ }
27
+
28
+ export interface StatsProvider {
29
+ getStats(options: { from?: string | null; to?: string | null }): {
30
+ totalRequests: number;
31
+ totalErrors: number;
32
+ avgDurationMs: number;
33
+ byServer: Array<{ name: string; count: number }>;
34
+ byMethod: Array<{ method: string; count: number }>;
35
+ };
36
+ queryEvents(options: { limit?: number }): { events: Array<{ path: string }>; total: number };
37
+ }
38
+
16
39
  export interface CreateServerOptions {
17
40
  cwd?: string;
18
41
  port?: number;
19
42
  /** Enable server config discovery endpoints (default: false) */
20
43
  discoveryConfig?: boolean;
44
+ /**
45
+ * Called after core server is created but before routes are registered.
46
+ * Use this to set up optional plugins like audit via the emitter.
47
+ */
48
+ setup?: (ctx: McpsServerContext) => void | Promise<void>;
21
49
  }
22
50
 
23
51
  export function createServer(options: CreateServerOptions = {}) {
24
- const { cwd = process.cwd(), discoveryConfig: discoveryConfigOption } = options;
52
+ const { cwd = process.cwd(), discoveryConfig: discoveryConfigOption, setup } = options;
25
53
 
26
54
  const app = new Hono();
55
+ const emitter = createMcpsEmitter();
56
+ const apiRouters: Record<string, any> = {};
27
57
 
28
58
  // Request logging
29
59
  app.use(
@@ -32,15 +62,14 @@ export function createServer(options: CreateServerOptions = {}) {
32
62
  }),
33
63
  );
34
64
 
35
- // Audit middleware
36
- app.use(auditMiddleware());
65
+ // Request event middleware - emits events for subscribers (audit, monitoring, etc.)
66
+ app.use(requestEventMiddleware(emitter));
37
67
 
38
68
  // Load .env files first
39
69
  loadEnvFiles(cwd);
40
70
 
41
71
  // Load config (with env var substitution)
42
72
  const config = substituteEnvVars(loadConfig(cwd));
43
- // discoveryConfig: CLI option overrides config file, defaults to false
44
73
  const discoveryConfig = discoveryConfigOption ?? config.discoveryConfig ?? false;
45
74
 
46
75
  // Log available server types from registry
@@ -48,15 +77,7 @@ export function createServer(options: CreateServerOptions = {}) {
48
77
  log.info(`Available server types: ${serverDefs.map((d) => d.name).join(', ')}`);
49
78
  log.info(`Loaded ${Object.keys(config.servers).length} servers from config (discoveryConfig: ${discoveryConfig})`);
50
79
 
51
- // Configure audit with lazy DB initialization
52
- // DB will only be initialized when the first audit event needs persistence
53
- configureAudit(config.audit, config.db);
54
- const auditEnabled = config.audit?.enabled !== false;
55
- const auditDbPath = config.audit?.db?.path ?? config.db?.path ?? '.mcps.db';
56
- log.info(`Audit configured: enabled=${auditEnabled}, db=${auditDbPath} (lazy init)`);
57
-
58
- // Unified cache for all MCP servers (both pre-configured and dynamic)
59
- // Keyed by cache key from def.getCacheKey() or `config::${name}` for pre-configured
80
+ // Unified cache for all MCP servers
60
81
  const serverCache = new LRUCache<string, McpServerInstance>({
61
82
  max: 100,
62
83
  dispose: (value, key) => {
@@ -65,65 +86,61 @@ export function createServer(options: CreateServerOptions = {}) {
65
86
  },
66
87
  });
67
88
 
68
- // =========================================================================
69
- // Register MCP routes (pre-configured and dynamic endpoints)
70
- // =========================================================================
71
- registerMcpRoutes({ app, config, serverCache });
72
-
73
- // =========================================================================
74
- // Register Chat/LLM Gateway routes
75
- // =========================================================================
76
- registerChatRoutes({ app, config });
77
-
78
- // =========================================================================
79
- // Health and info endpoints
80
- // =========================================================================
81
- app.get('/health', (c) => c.json({ status: 'ok' }));
82
-
83
- app.get('/', (c) => {
84
- return c.json({ message: 'hello' });
85
- });
86
-
87
- // Server info - only available in dev mode or when discoveryConfig is enabled
88
- if (isDevelopment || discoveryConfig) {
89
- app.get('/info', (c) => {
90
- // Dynamically get routes from Hono app
91
- const routes = app.routes
92
- .map((r) => ({ method: r.method, path: r.path }))
93
- .filter((r) => r.method !== 'ALL' || r.path.startsWith('/mcp/'))
94
- .sort((a, b) => a.path.localeCompare(b.path) || a.method.localeCompare(b.method));
95
-
96
- // Group routes by category
97
- const mcpRoutes = routes.filter((r) => r.path.startsWith('/mcp/')).map((r) => r.path);
98
- const apiRoutes = routes.filter((r) => r.path.startsWith('/api/')).map((r) => `${r.method} ${r.path}`);
99
- const chatRoutes = routes.filter((r) => r.path.startsWith('/v1/')).map((r) => `${r.method} ${r.path}`);
100
-
101
- // Get unique MCP paths
102
- const uniqueMcpPaths = [...new Set(mcpRoutes)];
103
-
104
- return c.json({
105
- name: '@wener/mcps',
106
- version: '0.1.0',
107
- servers: Object.keys(config.servers),
108
- serverTypes: findMcpServerDef().map((d) => d.name),
109
- models: config.models ? config.models.map((m) => m.name) : [],
110
- endpoints: {
111
- mcp: uniqueMcpPaths,
112
- api: [...new Set(apiRoutes)],
113
- chat: [...new Set(chatRoutes)],
114
- },
89
+ const ctx: McpsServerContext = { app, config, emitter, serverCache, apiRouters };
90
+
91
+ const finalize = async () => {
92
+ // Allow plugins to set up before routes are registered
93
+ await setup?.(ctx);
94
+
95
+ // =========================================================================
96
+ // Register MCP routes (pre-configured and dynamic endpoints)
97
+ // =========================================================================
98
+ registerMcpRoutes({ app, config, serverCache });
99
+
100
+ // =========================================================================
101
+ // Register Chat/LLM Gateway routes
102
+ // =========================================================================
103
+ registerChatRoutes({ app, config });
104
+
105
+ // =========================================================================
106
+ // Health and info endpoints
107
+ // =========================================================================
108
+ app.get('/health', (c) => c.json({ status: 'ok' }));
109
+ app.get('/', (c) => c.json({ message: 'hello' }));
110
+
111
+ if (isDevelopment || discoveryConfig) {
112
+ app.get('/info', (c) => {
113
+ const routes = app.routes
114
+ .map((r) => ({ method: r.method, path: r.path }))
115
+ .filter((r) => r.method !== 'ALL' || r.path.startsWith('/mcp/'))
116
+ .sort((a, b) => a.path.localeCompare(b.path) || a.method.localeCompare(b.method));
117
+
118
+ const mcpRoutes = routes.filter((r) => r.path.startsWith('/mcp/')).map((r) => r.path);
119
+ const apiRoutes = routes.filter((r) => r.path.startsWith('/api/')).map((r) => `${r.method} ${r.path}`);
120
+ const chatRoutes = routes.filter((r) => r.path.startsWith('/v1/')).map((r) => `${r.method} ${r.path}`);
121
+ const uniqueMcpPaths = [...new Set(mcpRoutes)];
122
+
123
+ return c.json({
124
+ name: '@wener/mcps',
125
+ version: '0.1.0',
126
+ servers: Object.keys(config.servers),
127
+ serverTypes: findMcpServerDef().map((d) => d.name),
128
+ models: config.models ? config.models.map((m) => m.name) : [],
129
+ endpoints: {
130
+ mcp: uniqueMcpPaths,
131
+ api: [...new Set(apiRoutes)],
132
+ chat: [...new Set(chatRoutes)],
133
+ },
134
+ });
115
135
  });
116
- });
117
- }
136
+ }
118
137
 
119
- // =========================================================================
120
- // Register oRPC API routes
121
- // =========================================================================
122
- registerApiRoutes({ app, config });
138
+ // =========================================================================
139
+ // Register oRPC API routes (with any plugin-provided routers)
140
+ // =========================================================================
141
+ registerApiRoutes({ app, config, apiRouters, statsProvider: ctx.statsProvider });
142
+ };
123
143
 
124
- /**
125
- * Print available endpoints grouped by category
126
- */
127
144
  const printEndpoints = () => {
128
145
  const routes = app.routes;
129
146
  const mcpPaths = new Set<string>();
@@ -145,19 +162,70 @@ export function createServer(options: CreateServerOptions = {}) {
145
162
  }
146
163
 
147
164
  log.info('Available endpoints:');
148
- if (mcpPaths.size > 0) {
149
- log.info(` MCP: ${[...mcpPaths].sort().join(', ')}`);
150
- }
151
- if (chatPaths.size > 0) {
152
- log.info(` Chat: ${[...chatPaths].sort().join(', ')}`);
165
+ if (mcpPaths.size > 0) log.info(` MCP: ${[...mcpPaths].sort().join(', ')}`);
166
+ if (chatPaths.size > 0) log.info(` Chat: ${[...chatPaths].sort().join(', ')}`);
167
+ if (apiPaths.size > 0) log.info(` API: ${[...apiPaths].sort().join(', ')}`);
168
+ if (otherPaths.size > 0) log.info(` Other: ${[...otherPaths].sort().join(', ')}`);
169
+ };
170
+
171
+ return { app, config, emitter, serverCache, printEndpoints, finalize };
172
+ }
173
+
174
+ function headersToRecord(headers: Headers): Record<string, string> {
175
+ const record: Record<string, string> = {};
176
+ headers.forEach((value, key) => {
177
+ record[key] = value;
178
+ });
179
+ return record;
180
+ }
181
+
182
+ /**
183
+ * Middleware that emits request events via the emitter.
184
+ * Subscribers (like audit plugin) can listen and handle these events.
185
+ */
186
+ function requestEventMiddleware(emitter: McpsEmitter) {
187
+ return async (c: Context, next: Next) => {
188
+ const startTime = Date.now();
189
+ const path = c.req.path;
190
+
191
+ let serverName: string | undefined;
192
+ let serverType: string | undefined;
193
+
194
+ const mcpMatch = path.match(/^\/mcp\/([^/]+)/);
195
+ if (mcpMatch) {
196
+ serverName = mcpMatch[1];
197
+ if (['tencent-cls', 'sql', 'prometheus', 'relay'].includes(serverName)) {
198
+ serverType = serverName;
199
+ } else {
200
+ serverType = 'custom';
201
+ }
153
202
  }
154
- if (apiPaths.size > 0) {
155
- log.info(` API: ${[...apiPaths].sort().join(', ')}`);
203
+
204
+ if (path.startsWith('/v1/')) {
205
+ serverType = 'chat';
156
206
  }
157
- if (otherPaths.size > 0) {
158
- log.info(` Other: ${[...otherPaths].sort().join(', ')}`);
207
+
208
+ let error: string | undefined;
209
+
210
+ try {
211
+ await next();
212
+ } catch (e) {
213
+ error = e instanceof Error ? e.message : String(e);
214
+ throw e;
215
+ } finally {
216
+ const durationMs = Date.now() - startTime;
217
+
218
+ emitter.emit(McpsEventType.Request, {
219
+ timestamp: new Date().toISOString(),
220
+ method: c.req.method,
221
+ path,
222
+ serverName,
223
+ serverType,
224
+ status: c.res.status,
225
+ durationMs,
226
+ error,
227
+ requestHeaders: headersToRecord(c.req.raw.headers),
228
+ });
159
229
  }
160
230
  };
161
-
162
- return { app, config, serverCache, printEndpoints };
163
231
  }
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../../src/server/audit.ts"],"sourcesContent":["import { implement } from '@orpc/server';\nimport type { Context, Next } from 'hono';\nimport { LRUCache } from 'lru-cache';\nimport { AuditContract, type AuditEvent } from '../contracts';\nimport { RequestLogEntity } from '../entities';\nimport { ensureDbInitialized, configureDb } from './db';\nimport type { AuditConfig, DbConfig } from './schema';\n\n/**\n * Convert Headers to a plain record\n */\nfunction headersToRecord(headers: Headers): Record<string, string> {\n\tconst record: Record<string, string> = {};\n\theaders.forEach((value, key) => {\n\t\trecord[key] = value;\n\t});\n\treturn record;\n}\n\n// In-memory audit store using LRU cache\nconst auditStore = new LRUCache<string, AuditEvent>({\n\tmax: 10000, // Keep last 10k events\n\tttl: 1000 * 60 * 60 * 24, // 24 hours\n});\n\n// Counter for IDs\nlet eventCounter = 0;\n\n// Audit configuration state\nlet auditEnabled = true; // default to enabled\nlet dbConfigured = false;\n\n/**\n * Configure audit module with settings\n * Call this before using audit features\n *\n * @param auditConfig - Audit config section\n * @param fallbackDbConfig - Fallback db config from root config\n */\nexport function configureAudit(auditConfig?: AuditConfig, fallbackDbConfig?: DbConfig): void {\n\t// Determine if audit is enabled (default: true)\n\tauditEnabled = auditConfig?.enabled !== false;\n\n\tif (auditEnabled) {\n\t\t// Use audit.db config if present, otherwise fallback to root db config\n\t\tconst dbConfig = auditConfig?.db ?? fallbackDbConfig;\n\t\tconfigureDb(dbConfig);\n\t\tdbConfigured = true;\n\t}\n}\n\n/**\n * Check if audit is enabled\n */\nexport function isAuditEnabled(): boolean {\n\treturn auditEnabled;\n}\n\n/**\n * Persist audit event to database (lazy init)\n */\nasync function persistToDb(event: AuditEvent, id: string): Promise<void> {\n\tif (!auditEnabled || !dbConfigured) {\n\t\treturn;\n\t}\n\n\ttry {\n\t\t// Lazy initialize DB on first persist\n\t\tconst orm = await ensureDbInitialized();\n\t\tconst em = orm.em.fork();\n\n\t\tconst logEntry = new RequestLogEntity();\n\t\tlogEntry.requestId = id;\n\t\tlogEntry.timestamp = new Date(event.timestamp);\n\t\tlogEntry.method = event.method;\n\t\tlogEntry.path = event.path;\n\t\tlogEntry.serverName = event.serverName ?? undefined;\n\t\tlogEntry.serverType = event.serverType ?? undefined;\n\t\tlogEntry.status = event.status ?? undefined;\n\t\tlogEntry.durationMs = event.durationMs ?? undefined;\n\t\tlogEntry.error = event.error ?? undefined;\n\t\tlogEntry.requestHeaders = event.requestHeaders ?? undefined;\n\t\t// Determine request type\n\t\tif (event.path.startsWith('/mcp/')) {\n\t\t\tlogEntry.requestType = 'mcp';\n\t\t} else if (event.path.startsWith('/v1/')) {\n\t\t\tlogEntry.requestType = 'chat';\n\t\t} else {\n\t\t\tlogEntry.requestType = 'api';\n\t\t}\n\t\tem.persist(logEntry);\n\t\tawait em.flush();\n\t} catch (e) {\n\t\t// Log persistence errors but don't throw - in-memory store is the primary\n\t\tconsole.error('Failed to persist audit log:', e);\n\t}\n}\n\n/**\n * Add an audit event\n */\nexport function addAuditEvent(event: Omit<AuditEvent, 'id'>): AuditEvent {\n\tconst id = `${Date.now()}-${++eventCounter}`;\n\tconst fullEvent: AuditEvent = { ...event, id };\n\tauditStore.set(id, fullEvent);\n\n\t// Persist to database asynchronously (lazy init)\n\tpersistToDb(fullEvent, id).catch(() => {\n\t\t// Already logged in persistToDb\n\t});\n\n\treturn fullEvent;\n}\n\n/**\n * Query audit events\n */\nexport function queryAuditEvents(options: {\n\tlimit?: number;\n\toffset?: number;\n\tserverName?: string | null;\n\tserverType?: string | null;\n\tmethod?: string | null;\n\tfrom?: string | null;\n\tto?: string | null;\n}): { events: AuditEvent[]; total: number } {\n\tconst { limit = 50, offset = 0, serverName, serverType, method, from, to } = options;\n\n\t// Get all events as array\n\tlet events: AuditEvent[] = [];\n\tfor (const [, event] of auditStore.entries()) {\n\t\tevents.push(event);\n\t}\n\n\t// Sort by timestamp desc\n\tevents.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());\n\n\t// Apply filters\n\tif (serverName) {\n\t\tevents = events.filter((e) => e.serverName === serverName);\n\t}\n\tif (serverType) {\n\t\tevents = events.filter((e) => e.serverType === serverType);\n\t}\n\tif (method) {\n\t\tevents = events.filter((e) => e.method === method);\n\t}\n\tif (from) {\n\t\tconst fromTime = new Date(from).getTime();\n\t\tevents = events.filter((e) => new Date(e.timestamp).getTime() >= fromTime);\n\t}\n\tif (to) {\n\t\tconst toTime = new Date(to).getTime();\n\t\tevents = events.filter((e) => new Date(e.timestamp).getTime() <= toTime);\n\t}\n\n\tconst total = events.length;\n\n\t// Paginate\n\tevents = events.slice(offset, offset + limit);\n\n\treturn { events, total };\n}\n\n/**\n * Get audit statistics\n */\nexport function getAuditStats(options: { from?: string | null; to?: string | null }) {\n\tlet events: AuditEvent[] = [];\n\tfor (const [, event] of auditStore.entries()) {\n\t\tevents.push(event);\n\t}\n\n\t// Apply time filters\n\tif (options.from) {\n\t\tconst fromTime = new Date(options.from).getTime();\n\t\tevents = events.filter((e) => new Date(e.timestamp).getTime() >= fromTime);\n\t}\n\tif (options.to) {\n\t\tconst toTime = new Date(options.to).getTime();\n\t\tevents = events.filter((e) => new Date(e.timestamp).getTime() <= toTime);\n\t}\n\n\t// Calculate stats\n\tconst totalRequests = events.length;\n\tconst totalErrors = events.filter((e) => e.error || (e.status && e.status >= 400)).length;\n\n\tconst durations = events.map((e) => e.durationMs).filter((d): d is number => d != null);\n\tconst avgDurationMs = durations.length > 0 ? durations.reduce((a, b) => a + b, 0) / durations.length : 0;\n\n\t// Group by server\n\tconst serverCounts = new Map<string, number>();\n\tfor (const event of events) {\n\t\tconst name = event.serverName || 'unknown';\n\t\tserverCounts.set(name, (serverCounts.get(name) || 0) + 1);\n\t}\n\tconst byServer = Array.from(serverCounts.entries())\n\t\t.map(([name, count]) => ({ name, count }))\n\t\t.sort((a, b) => b.count - a.count);\n\n\t// Group by method\n\tconst methodCounts = new Map<string, number>();\n\tfor (const event of events) {\n\t\tconst method = event.method || 'unknown';\n\t\tmethodCounts.set(method, (methodCounts.get(method) || 0) + 1);\n\t}\n\tconst byMethod = Array.from(methodCounts.entries())\n\t\t.map(([method, count]) => ({ method, count }))\n\t\t.sort((a, b) => b.count - a.count);\n\n\treturn { totalRequests, totalErrors, avgDurationMs, byServer, byMethod };\n}\n\n/**\n * Clear audit events before a timestamp\n */\nexport function clearAuditEvents(before: string): number {\n\tconst beforeTime = new Date(before).getTime();\n\tlet deleted = 0;\n\n\tfor (const [id, event] of auditStore.entries()) {\n\t\tif (new Date(event.timestamp).getTime() < beforeTime) {\n\t\t\tauditStore.delete(id);\n\t\t\tdeleted++;\n\t\t}\n\t}\n\n\treturn deleted;\n}\n\n/**\n * Audit Router implementation\n */\nexport const AuditRouter = implement(AuditContract).router({\n\tlist: implement(AuditContract.list).handler(async ({ input }) => {\n\t\treturn queryAuditEvents(input);\n\t}),\n\n\tget: implement(AuditContract.get).handler(async ({ input }) => {\n\t\treturn auditStore.get(input.id) ?? null;\n\t}),\n\n\tstats: implement(AuditContract.stats).handler(async ({ input }) => {\n\t\treturn getAuditStats(input);\n\t}),\n\n\tclear: implement(AuditContract.clear).handler(async ({ input }) => {\n\t\tconst deleted = clearAuditEvents(input.before);\n\t\treturn { deleted };\n\t}),\n});\n\n/**\n * Hono middleware for audit logging\n */\nexport function auditMiddleware() {\n\treturn async (c: Context, next: Next) => {\n\t\tconst startTime = Date.now();\n\t\tconst path = c.req.path;\n\n\t\t// Extract server info from path\n\t\tlet serverName: string | undefined;\n\t\tlet serverType: string | undefined;\n\n\t\tconst mcpMatch = path.match(/^\\/mcp\\/([^/]+)/);\n\t\tif (mcpMatch) {\n\t\t\tserverName = mcpMatch[1];\n\t\t\t// Infer type from well-known paths\n\t\t\tif (serverName === 'tencent-cls') serverType = 'tencent-cls';\n\t\t\telse if (serverName === 'sql') serverType = 'sql';\n\t\t\telse if (serverName === 'prometheus') serverType = 'prometheus';\n\t\t\telse if (serverName === 'relay') serverType = 'relay';\n\t\t\telse serverType = 'custom';\n\t\t}\n\n\t\t// Extract model info from chat requests\n\t\tif (path.startsWith('/v1/')) {\n\t\t\tserverType = 'chat';\n\t\t}\n\n\t\tlet error: string | undefined;\n\n\t\ttry {\n\t\t\tawait next();\n\t\t} catch (e) {\n\t\t\terror = e instanceof Error ? e.message : String(e);\n\t\t\tthrow e;\n\t\t} finally {\n\t\t\tconst durationMs = Date.now() - startTime;\n\n\t\t\t// Audit MCP requests, Chat API requests, and other API requests\n\t\t\tconst shouldAudit =\n\t\t\t\tpath.startsWith('/mcp/') || path.startsWith('/v1/') || (path.startsWith('/api/') && c.req.method !== 'GET');\n\n\t\t\tif (shouldAudit) {\n\t\t\t\taddAuditEvent({\n\t\t\t\t\ttimestamp: new Date().toISOString(),\n\t\t\t\t\tmethod: c.req.method,\n\t\t\t\t\tpath,\n\t\t\t\t\tserverName,\n\t\t\t\t\tserverType,\n\t\t\t\t\tstatus: c.res.status,\n\t\t\t\t\tdurationMs,\n\t\t\t\t\terror,\n\t\t\t\t\trequestHeaders: headersToRecord(c.req.raw.headers),\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t};\n}\n"],"names":["implement","LRUCache","AuditContract","RequestLogEntity","ensureDbInitialized","configureDb","headersToRecord","headers","record","forEach","value","key","auditStore","max","ttl","eventCounter","auditEnabled","dbConfigured","configureAudit","auditConfig","fallbackDbConfig","enabled","dbConfig","db","isAuditEnabled","persistToDb","event","id","orm","em","fork","logEntry","requestId","timestamp","Date","method","path","serverName","undefined","serverType","status","durationMs","error","requestHeaders","startsWith","requestType","persist","flush","e","console","addAuditEvent","now","fullEvent","set","catch","queryAuditEvents","options","limit","offset","from","to","events","entries","push","sort","a","b","getTime","filter","fromTime","toTime","total","length","slice","getAuditStats","totalRequests","totalErrors","durations","map","d","avgDurationMs","reduce","serverCounts","Map","name","get","byServer","Array","count","methodCounts","byMethod","clearAuditEvents","before","beforeTime","deleted","delete","AuditRouter","router","list","handler","input","stats","clear","auditMiddleware","c","next","startTime","req","mcpMatch","match","Error","message","String","shouldAudit","toISOString","res","raw"],"mappings":"AAAA,SAASA,SAAS,QAAQ,eAAe;AAEzC,SAASC,QAAQ,QAAQ,YAAY;AACrC,SAASC,aAAa,QAAyB,eAAe;AAC9D,SAASC,gBAAgB,QAAQ,cAAc;AAC/C,SAASC,mBAAmB,EAAEC,WAAW,QAAQ,OAAO;AAGxD;;CAEC,GACD,SAASC,gBAAgBC,OAAgB;IACxC,MAAMC,SAAiC,CAAC;IACxCD,QAAQE,OAAO,CAAC,CAACC,OAAOC;QACvBH,MAAM,CAACG,IAAI,GAAGD;IACf;IACA,OAAOF;AACR;AAEA,wCAAwC;AACxC,MAAMI,aAAa,IAAIX,SAA6B;IACnDY,KAAK;IACLC,KAAK,OAAO,KAAK,KAAK;AACvB;AAEA,kBAAkB;AAClB,IAAIC,eAAe;AAEnB,4BAA4B;AAC5B,IAAIC,eAAe,MAAM,qBAAqB;AAC9C,IAAIC,eAAe;AAEnB;;;;;;CAMC,GACD,OAAO,SAASC,eAAeC,WAAyB,EAAEC,gBAA2B;IACpF,gDAAgD;IAChDJ,eAAeG,aAAaE,YAAY;IAExC,IAAIL,cAAc;QACjB,uEAAuE;QACvE,MAAMM,WAAWH,aAAaI,MAAMH;QACpCf,YAAYiB;QACZL,eAAe;IAChB;AACD;AAEA;;CAEC,GACD,OAAO,SAASO;IACf,OAAOR;AACR;AAEA;;CAEC,GACD,eAAeS,YAAYC,KAAiB,EAAEC,EAAU;IACvD,IAAI,CAACX,gBAAgB,CAACC,cAAc;QACnC;IACD;IAEA,IAAI;QACH,sCAAsC;QACtC,MAAMW,MAAM,MAAMxB;QAClB,MAAMyB,KAAKD,IAAIC,EAAE,CAACC,IAAI;QAEtB,MAAMC,WAAW,IAAI5B;QACrB4B,SAASC,SAAS,GAAGL;QACrBI,SAASE,SAAS,GAAG,IAAIC,KAAKR,MAAMO,SAAS;QAC7CF,SAASI,MAAM,GAAGT,MAAMS,MAAM;QAC9BJ,SAASK,IAAI,GAAGV,MAAMU,IAAI;QAC1BL,SAASM,UAAU,GAAGX,MAAMW,UAAU,IAAIC;QAC1CP,SAASQ,UAAU,GAAGb,MAAMa,UAAU,IAAID;QAC1CP,SAASS,MAAM,GAAGd,MAAMc,MAAM,IAAIF;QAClCP,SAASU,UAAU,GAAGf,MAAMe,UAAU,IAAIH;QAC1CP,SAASW,KAAK,GAAGhB,MAAMgB,KAAK,IAAIJ;QAChCP,SAASY,cAAc,GAAGjB,MAAMiB,cAAc,IAAIL;QAClD,yBAAyB;QACzB,IAAIZ,MAAMU,IAAI,CAACQ,UAAU,CAAC,UAAU;YACnCb,SAASc,WAAW,GAAG;QACxB,OAAO,IAAInB,MAAMU,IAAI,CAACQ,UAAU,CAAC,SAAS;YACzCb,SAASc,WAAW,GAAG;QACxB,OAAO;YACNd,SAASc,WAAW,GAAG;QACxB;QACAhB,GAAGiB,OAAO,CAACf;QACX,MAAMF,GAAGkB,KAAK;IACf,EAAE,OAAOC,GAAG;QACX,0EAA0E;QAC1EC,QAAQP,KAAK,CAAC,gCAAgCM;IAC/C;AACD;AAEA;;CAEC,GACD,OAAO,SAASE,cAAcxB,KAA6B;IAC1D,MAAMC,KAAK,GAAGO,KAAKiB,GAAG,GAAG,CAAC,EAAE,EAAEpC,cAAc;IAC5C,MAAMqC,YAAwB;QAAE,GAAG1B,KAAK;QAAEC;IAAG;IAC7Cf,WAAWyC,GAAG,CAAC1B,IAAIyB;IAEnB,iDAAiD;IACjD3B,YAAY2B,WAAWzB,IAAI2B,KAAK,CAAC;IAChC,gCAAgC;IACjC;IAEA,OAAOF;AACR;AAEA;;CAEC,GACD,OAAO,SAASG,iBAAiBC,OAQhC;IACA,MAAM,EAAEC,QAAQ,EAAE,EAAEC,SAAS,CAAC,EAAErB,UAAU,EAAEE,UAAU,EAAEJ,MAAM,EAAEwB,IAAI,EAAEC,EAAE,EAAE,GAAGJ;IAE7E,0BAA0B;IAC1B,IAAIK,SAAuB,EAAE;IAC7B,KAAK,MAAM,GAAGnC,MAAM,IAAId,WAAWkD,OAAO,GAAI;QAC7CD,OAAOE,IAAI,CAACrC;IACb;IAEA,yBAAyB;IACzBmC,OAAOG,IAAI,CAAC,CAACC,GAAGC,IAAM,IAAIhC,KAAKgC,EAAEjC,SAAS,EAAEkC,OAAO,KAAK,IAAIjC,KAAK+B,EAAEhC,SAAS,EAAEkC,OAAO;IAErF,gBAAgB;IAChB,IAAI9B,YAAY;QACfwB,SAASA,OAAOO,MAAM,CAAC,CAACpB,IAAMA,EAAEX,UAAU,KAAKA;IAChD;IACA,IAAIE,YAAY;QACfsB,SAASA,OAAOO,MAAM,CAAC,CAACpB,IAAMA,EAAET,UAAU,KAAKA;IAChD;IACA,IAAIJ,QAAQ;QACX0B,SAASA,OAAOO,MAAM,CAAC,CAACpB,IAAMA,EAAEb,MAAM,KAAKA;IAC5C;IACA,IAAIwB,MAAM;QACT,MAAMU,WAAW,IAAInC,KAAKyB,MAAMQ,OAAO;QACvCN,SAASA,OAAOO,MAAM,CAAC,CAACpB,IAAM,IAAId,KAAKc,EAAEf,SAAS,EAAEkC,OAAO,MAAME;IAClE;IACA,IAAIT,IAAI;QACP,MAAMU,SAAS,IAAIpC,KAAK0B,IAAIO,OAAO;QACnCN,SAASA,OAAOO,MAAM,CAAC,CAACpB,IAAM,IAAId,KAAKc,EAAEf,SAAS,EAAEkC,OAAO,MAAMG;IAClE;IAEA,MAAMC,QAAQV,OAAOW,MAAM;IAE3B,WAAW;IACXX,SAASA,OAAOY,KAAK,CAACf,QAAQA,SAASD;IAEvC,OAAO;QAAEI;QAAQU;IAAM;AACxB;AAEA;;CAEC,GACD,OAAO,SAASG,cAAclB,OAAqD;IAClF,IAAIK,SAAuB,EAAE;IAC7B,KAAK,MAAM,GAAGnC,MAAM,IAAId,WAAWkD,OAAO,GAAI;QAC7CD,OAAOE,IAAI,CAACrC;IACb;IAEA,qBAAqB;IACrB,IAAI8B,QAAQG,IAAI,EAAE;QACjB,MAAMU,WAAW,IAAInC,KAAKsB,QAAQG,IAAI,EAAEQ,OAAO;QAC/CN,SAASA,OAAOO,MAAM,CAAC,CAACpB,IAAM,IAAId,KAAKc,EAAEf,SAAS,EAAEkC,OAAO,MAAME;IAClE;IACA,IAAIb,QAAQI,EAAE,EAAE;QACf,MAAMU,SAAS,IAAIpC,KAAKsB,QAAQI,EAAE,EAAEO,OAAO;QAC3CN,SAASA,OAAOO,MAAM,CAAC,CAACpB,IAAM,IAAId,KAAKc,EAAEf,SAAS,EAAEkC,OAAO,MAAMG;IAClE;IAEA,kBAAkB;IAClB,MAAMK,gBAAgBd,OAAOW,MAAM;IACnC,MAAMI,cAAcf,OAAOO,MAAM,CAAC,CAACpB,IAAMA,EAAEN,KAAK,IAAKM,EAAER,MAAM,IAAIQ,EAAER,MAAM,IAAI,KAAMgC,MAAM;IAEzF,MAAMK,YAAYhB,OAAOiB,GAAG,CAAC,CAAC9B,IAAMA,EAAEP,UAAU,EAAE2B,MAAM,CAAC,CAACW,IAAmBA,KAAK;IAClF,MAAMC,gBAAgBH,UAAUL,MAAM,GAAG,IAAIK,UAAUI,MAAM,CAAC,CAAChB,GAAGC,IAAMD,IAAIC,GAAG,KAAKW,UAAUL,MAAM,GAAG;IAEvG,kBAAkB;IAClB,MAAMU,eAAe,IAAIC;IACzB,KAAK,MAAMzD,SAASmC,OAAQ;QAC3B,MAAMuB,OAAO1D,MAAMW,UAAU,IAAI;QACjC6C,aAAa7B,GAAG,CAAC+B,MAAM,AAACF,CAAAA,aAAaG,GAAG,CAACD,SAAS,CAAA,IAAK;IACxD;IACA,MAAME,WAAWC,MAAM5B,IAAI,CAACuB,aAAapB,OAAO,IAC9CgB,GAAG,CAAC,CAAC,CAACM,MAAMI,MAAM,GAAM,CAAA;YAAEJ;YAAMI;QAAM,CAAA,GACtCxB,IAAI,CAAC,CAACC,GAAGC,IAAMA,EAAEsB,KAAK,GAAGvB,EAAEuB,KAAK;IAElC,kBAAkB;IAClB,MAAMC,eAAe,IAAIN;IACzB,KAAK,MAAMzD,SAASmC,OAAQ;QAC3B,MAAM1B,SAAST,MAAMS,MAAM,IAAI;QAC/BsD,aAAapC,GAAG,CAAClB,QAAQ,AAACsD,CAAAA,aAAaJ,GAAG,CAAClD,WAAW,CAAA,IAAK;IAC5D;IACA,MAAMuD,WAAWH,MAAM5B,IAAI,CAAC8B,aAAa3B,OAAO,IAC9CgB,GAAG,CAAC,CAAC,CAAC3C,QAAQqD,MAAM,GAAM,CAAA;YAAErD;YAAQqD;QAAM,CAAA,GAC1CxB,IAAI,CAAC,CAACC,GAAGC,IAAMA,EAAEsB,KAAK,GAAGvB,EAAEuB,KAAK;IAElC,OAAO;QAAEb;QAAeC;QAAaI;QAAeM;QAAUI;IAAS;AACxE;AAEA;;CAEC,GACD,OAAO,SAASC,iBAAiBC,MAAc;IAC9C,MAAMC,aAAa,IAAI3D,KAAK0D,QAAQzB,OAAO;IAC3C,IAAI2B,UAAU;IAEd,KAAK,MAAM,CAACnE,IAAID,MAAM,IAAId,WAAWkD,OAAO,GAAI;QAC/C,IAAI,IAAI5B,KAAKR,MAAMO,SAAS,EAAEkC,OAAO,KAAK0B,YAAY;YACrDjF,WAAWmF,MAAM,CAACpE;YAClBmE;QACD;IACD;IAEA,OAAOA;AACR;AAEA;;CAEC,GACD,OAAO,MAAME,cAAchG,UAAUE,eAAe+F,MAAM,CAAC;IAC1DC,MAAMlG,UAAUE,cAAcgG,IAAI,EAAEC,OAAO,CAAC,OAAO,EAAEC,KAAK,EAAE;QAC3D,OAAO7C,iBAAiB6C;IACzB;IAEAf,KAAKrF,UAAUE,cAAcmF,GAAG,EAAEc,OAAO,CAAC,OAAO,EAAEC,KAAK,EAAE;QACzD,OAAOxF,WAAWyE,GAAG,CAACe,MAAMzE,EAAE,KAAK;IACpC;IAEA0E,OAAOrG,UAAUE,cAAcmG,KAAK,EAAEF,OAAO,CAAC,OAAO,EAAEC,KAAK,EAAE;QAC7D,OAAO1B,cAAc0B;IACtB;IAEAE,OAAOtG,UAAUE,cAAcoG,KAAK,EAAEH,OAAO,CAAC,OAAO,EAAEC,KAAK,EAAE;QAC7D,MAAMN,UAAUH,iBAAiBS,MAAMR,MAAM;QAC7C,OAAO;YAAEE;QAAQ;IAClB;AACD,GAAG;AAEH;;CAEC,GACD,OAAO,SAASS;IACf,OAAO,OAAOC,GAAYC;QACzB,MAAMC,YAAYxE,KAAKiB,GAAG;QAC1B,MAAMf,OAAOoE,EAAEG,GAAG,CAACvE,IAAI;QAEvB,gCAAgC;QAChC,IAAIC;QACJ,IAAIE;QAEJ,MAAMqE,WAAWxE,KAAKyE,KAAK,CAAC;QAC5B,IAAID,UAAU;YACbvE,aAAauE,QAAQ,CAAC,EAAE;YACxB,mCAAmC;YACnC,IAAIvE,eAAe,eAAeE,aAAa;iBAC1C,IAAIF,eAAe,OAAOE,aAAa;iBACvC,IAAIF,eAAe,cAAcE,aAAa;iBAC9C,IAAIF,eAAe,SAASE,aAAa;iBACzCA,aAAa;QACnB;QAEA,wCAAwC;QACxC,IAAIH,KAAKQ,UAAU,CAAC,SAAS;YAC5BL,aAAa;QACd;QAEA,IAAIG;QAEJ,IAAI;YACH,MAAM+D;QACP,EAAE,OAAOzD,GAAG;YACXN,QAAQM,aAAa8D,QAAQ9D,EAAE+D,OAAO,GAAGC,OAAOhE;YAChD,MAAMA;QACP,SAAU;YACT,MAAMP,aAAaP,KAAKiB,GAAG,KAAKuD;YAEhC,gEAAgE;YAChE,MAAMO,cACL7E,KAAKQ,UAAU,CAAC,YAAYR,KAAKQ,UAAU,CAAC,WAAYR,KAAKQ,UAAU,CAAC,YAAY4D,EAAEG,GAAG,CAACxE,MAAM,KAAK;YAEtG,IAAI8E,aAAa;gBAChB/D,cAAc;oBACbjB,WAAW,IAAIC,OAAOgF,WAAW;oBACjC/E,QAAQqE,EAAEG,GAAG,CAACxE,MAAM;oBACpBC;oBACAC;oBACAE;oBACAC,QAAQgE,EAAEW,GAAG,CAAC3E,MAAM;oBACpBC;oBACAC;oBACAC,gBAAgBrC,gBAAgBkG,EAAEG,GAAG,CAACS,GAAG,CAAC7G,OAAO;gBAClD;YACD;QACD;IACD;AACD"}