@wener/mcps 1.0.2 → 1.0.5

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 (103) hide show
  1. package/README.md +144 -0
  2. package/dist/index.mjs +213076 -1
  3. package/dist/mcps-cli.mjs +102632 -59429
  4. package/lib/audit/AuditContract.js.map +1 -0
  5. package/lib/{chat/audit.js → audit/chat.js} +1 -1
  6. package/lib/audit/chat.js.map +1 -0
  7. package/lib/audit/entities/ChatRequestEntity.js.map +1 -0
  8. package/lib/audit/entities/McpRequestEntity.js.map +1 -0
  9. package/lib/audit/entities/RequestLogEntity.js.map +1 -0
  10. package/lib/audit/entities/ResponseEntity.js.map +1 -0
  11. package/lib/audit/entities/index.js +6 -0
  12. package/lib/audit/entities/index.js.map +1 -0
  13. package/lib/audit/server/db.js +64 -0
  14. package/lib/audit/server/db.js.map +1 -0
  15. package/lib/audit/server/index.js +2 -0
  16. package/lib/audit/server/index.js.map +1 -0
  17. package/lib/{server/audit.js → audit/server/plugin.js} +73 -127
  18. package/lib/audit/server/plugin.js.map +1 -0
  19. package/lib/audit/types.js.map +1 -0
  20. package/lib/chat/handler.js +5 -5
  21. package/lib/chat/handler.js.map +1 -1
  22. package/lib/chat/index.js +1 -1
  23. package/lib/chat/index.js.map +1 -1
  24. package/lib/cli-start.js +36 -0
  25. package/lib/cli-start.js.map +1 -0
  26. package/lib/cli.js +19 -0
  27. package/lib/cli.js.map +1 -0
  28. package/lib/contracts/index.js +1 -1
  29. package/lib/contracts/index.js.map +1 -1
  30. package/lib/dev.server.js +7 -1
  31. package/lib/dev.server.js.map +1 -1
  32. package/lib/entities/index.js +2 -10
  33. package/lib/entities/index.js.map +1 -1
  34. package/lib/index.js +21 -3
  35. package/lib/index.js.map +1 -1
  36. package/lib/mcps-cli.js +6 -35
  37. package/lib/mcps-cli.js.map +1 -1
  38. package/lib/providers/feishu/def.js +35 -0
  39. package/lib/providers/feishu/def.js.map +1 -0
  40. package/lib/providers/findMcpServerDef.js +1 -0
  41. package/lib/providers/findMcpServerDef.js.map +1 -1
  42. package/lib/scripts/bundle.js +7 -1
  43. package/lib/scripts/bundle.js.map +1 -1
  44. package/lib/server/api-routes.js +7 -8
  45. package/lib/server/api-routes.js.map +1 -1
  46. package/lib/server/events.js +13 -0
  47. package/lib/server/events.js.map +1 -0
  48. package/lib/server/mcp-routes.js +31 -60
  49. package/lib/server/mcp-routes.js.map +1 -1
  50. package/lib/server/mcps-router.js +19 -24
  51. package/lib/server/mcps-router.js.map +1 -1
  52. package/lib/server/schema.js +22 -2
  53. package/lib/server/schema.js.map +1 -1
  54. package/lib/server/server.js +142 -87
  55. package/lib/server/server.js.map +1 -1
  56. package/package.json +145 -85
  57. package/src/{chat/audit.ts → audit/chat.ts} +3 -3
  58. package/src/audit/entities/index.ts +6 -0
  59. package/src/audit/server/db.ts +65 -0
  60. package/src/audit/server/index.ts +8 -0
  61. package/src/{server/audit.ts → audit/server/plugin.ts} +71 -144
  62. package/src/chat/handler.ts +5 -5
  63. package/src/chat/index.ts +1 -1
  64. package/src/cli-start.ts +43 -0
  65. package/src/cli.ts +45 -0
  66. package/src/contracts/index.ts +1 -1
  67. package/src/dev.server.ts +8 -1
  68. package/src/entities/index.ts +2 -12
  69. package/src/index.ts +47 -1
  70. package/src/mcps-cli.ts +6 -48
  71. package/src/providers/feishu/def.ts +37 -0
  72. package/src/providers/findMcpServerDef.ts +1 -0
  73. package/src/scripts/bundle.ts +12 -1
  74. package/src/server/api-routes.ts +11 -8
  75. package/src/server/events.ts +29 -0
  76. package/src/server/mcp-routes.ts +30 -58
  77. package/src/server/mcps-router.ts +21 -29
  78. package/src/server/schema.ts +23 -2
  79. package/src/server/server.ts +149 -81
  80. package/LICENSE +0 -21
  81. package/lib/chat/audit.js.map +0 -1
  82. package/lib/contracts/AuditContract.js.map +0 -1
  83. package/lib/entities/ChatRequestEntity.js.map +0 -1
  84. package/lib/entities/McpRequestEntity.js.map +0 -1
  85. package/lib/entities/RequestLogEntity.js.map +0 -1
  86. package/lib/entities/ResponseEntity.js.map +0 -1
  87. package/lib/entities/types.js.map +0 -1
  88. package/lib/server/audit.js.map +0 -1
  89. package/lib/server/db.js +0 -97
  90. package/lib/server/db.js.map +0 -1
  91. package/src/server/db.ts +0 -115
  92. /package/lib/{contracts → audit}/AuditContract.js +0 -0
  93. /package/lib/{entities → audit/entities}/ChatRequestEntity.js +0 -0
  94. /package/lib/{entities → audit/entities}/McpRequestEntity.js +0 -0
  95. /package/lib/{entities → audit/entities}/RequestLogEntity.js +0 -0
  96. /package/lib/{entities → audit/entities}/ResponseEntity.js +0 -0
  97. /package/lib/{entities → audit}/types.js +0 -0
  98. /package/src/{contracts → audit}/AuditContract.ts +0 -0
  99. /package/src/{entities → audit/entities}/ChatRequestEntity.ts +0 -0
  100. /package/src/{entities → audit/entities}/McpRequestEntity.ts +0 -0
  101. /package/src/{entities → audit/entities}/RequestLogEntity.ts +0 -0
  102. /package/src/{entities → audit/entities}/ResponseEntity.ts +0 -0
  103. /package/src/{entities → audit}/types.ts +0 -0
@@ -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,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
+ }
@@ -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
  /**