@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
@@ -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
- import { AuditContract, type AuditEvent } from '../contracts';
5
- import { RequestLogEntity } from '../entities';
6
- import { ensureDbInitialized, configureDb } from './db';
7
- import type { AuditConfig, DbConfig } from './schema';
3
+ import { McpsEventType } from '../../server/events';
4
+ import type { AuditConfig, DbConfig } from '../../server/schema';
5
+ import type { McpsServerContext } from '../../server/server';
6
+ import { AuditContract, type AuditEvent } from '../AuditContract';
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('./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
  }
@@ -6,7 +6,7 @@ import consola from 'consola';
6
6
  import { Hono } from 'hono';
7
7
  import { streamSSE } from 'hono/streaming';
8
8
  import type { ChatConfig, ModelConfig } from '../server/schema';
9
- import { ChatProtocol, createAuditContext, extractClientIp } from './audit';
9
+ import { ChatProtocol, createAuditContext, extractClientIp } from '../audit/chat';
10
10
  import {
11
11
  openaiToAnthropicRequest,
12
12
  anthropicToOpenaiResponse,
@@ -747,8 +747,8 @@ export function createChatHandler(options: ChatHandlerOptions = {}) {
747
747
  let previousContext: { input: unknown; output: unknown[] } | null = null;
748
748
  if (request.previous_response_id) {
749
749
  try {
750
- const { isDbInitialized, getEntityManager } = await import('../server/db');
751
- const { ResponseEntity } = await import('../entities');
750
+ const { isDbInitialized, getEntityManager } = await import('../audit/server/db');
751
+ const { ResponseEntity } = await import('../audit/entities');
752
752
  if (isDbInitialized()) {
753
753
  const em = getEntityManager().fork();
754
754
  const prevResponse = await em.findOne(ResponseEntity, { responseId: request.previous_response_id });
@@ -840,8 +840,8 @@ export function createChatHandler(options: ChatHandlerOptions = {}) {
840
840
 
841
841
  // Store response for future previous_response_id lookups
842
842
  try {
843
- const { isDbInitialized, getEntityManager } = await import('../server/db');
844
- const { ResponseEntity } = await import('../entities');
843
+ const { isDbInitialized, getEntityManager } = await import('../audit/server/db');
844
+ const { ResponseEntity } = await import('../audit/entities');
845
845
  if (isDbInitialized()) {
846
846
  const em = getEntityManager().fork();
847
847
  const responseEntity = new ResponseEntity();
package/src/chat/index.ts CHANGED
@@ -10,7 +10,7 @@ export * from './types';
10
10
  export * from './converters';
11
11
 
12
12
  // Audit
13
- export * from './audit';
13
+ export * from '../audit/chat';
14
14
 
15
15
  // Handler
16
16
  export { createChatHandler, type ChatHandlerOptions } from './handler';
@@ -0,0 +1,43 @@
1
+ import { serve } from '@hono/node-server';
2
+ import consola from 'consola';
3
+ import { createServer, type CreateServerOptions } from './server/server';
4
+
5
+ const log = consola.withTag('mcps');
6
+
7
+ let serverHandle: ReturnType<typeof serve> | undefined;
8
+
9
+ function shutdown() {
10
+ log.info('Shutting down...');
11
+ if (serverHandle) {
12
+ serverHandle.close(() => process.exit(0));
13
+ setTimeout(() => process.exit(1), 3000);
14
+ } else {
15
+ process.exit(0);
16
+ }
17
+ }
18
+
19
+ process.on('SIGINT', shutdown);
20
+ process.on('SIGTERM', shutdown);
21
+
22
+ export async function startServer(options: { port: string; cwd: string; discoveryConfig: boolean; setup?: CreateServerOptions['setup'] }) {
23
+ const port = Number.parseInt(options.port, 10);
24
+ const { app, printEndpoints, finalize } = createServer({
25
+ cwd: options.cwd,
26
+ port,
27
+ discoveryConfig: options.discoveryConfig,
28
+ setup: options.setup,
29
+ });
30
+
31
+ await finalize();
32
+
33
+ log.info(`Starting MCPS server on port ${port}`);
34
+
35
+ serverHandle = serve({
36
+ fetch: app.fetch,
37
+ port,
38
+ hostname: '0.0.0.0',
39
+ });
40
+
41
+ log.success(`MCPS server running at http://localhost:${port}`);
42
+ printEndpoints();
43
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,45 @@
1
+ import { Command } from 'commander';
2
+ import type { CreateServerOptions } from './server/server';
3
+
4
+ export interface CreateProgramOptions {
5
+ name?: string;
6
+ description?: string;
7
+ version?: string;
8
+ defaultPort?: string;
9
+ /** Called before the server starts, allows registering additional providers via side-effect imports */
10
+ beforeStart?: () => void | Promise<void>;
11
+ /** Server setup callback, passed to createServer. Use to register plugins like audit. */
12
+ setup?: CreateServerOptions['setup'];
13
+ }
14
+
15
+ /**
16
+ * Create a reusable Commander program for MCPS server.
17
+ * Consumers can register providers before calling `program.parseAsync()`.
18
+ */
19
+ export function createProgram(options: CreateProgramOptions = {}): Command {
20
+ const {
21
+ name = 'mcps',
22
+ description = 'MCP Proxy Server - Unified MCP service with relay, SQL, CLS, and Prometheus support',
23
+ version = '0.1.0',
24
+ defaultPort = '8036',
25
+ beforeStart,
26
+ setup,
27
+ } = options;
28
+
29
+ const program = new Command();
30
+
31
+ program
32
+ .name(name)
33
+ .description(description)
34
+ .version(version)
35
+ .option('-p, --port <port>', 'Port to listen on', defaultPort)
36
+ .option('-c, --cwd <path>', 'Working directory for config files', process.cwd())
37
+ .option('--discovery-config', 'Enable server config discovery endpoints', false)
38
+ .action(async (opts) => {
39
+ await beforeStart?.();
40
+ const { startServer } = await import('./cli-start.js');
41
+ await startServer({ ...opts, setup });
42
+ });
43
+
44
+ return program;
45
+ }
@@ -1,4 +1,4 @@
1
- export { AuditContract, AuditEventSchema, AuditQuerySchema, AuditStatsSchema, type AuditEvent } from './AuditContract';
1
+ export { AuditContract, AuditEventSchema, AuditQuerySchema, AuditStatsSchema, type AuditEvent } from '../audit/AuditContract';
2
2
  export {
3
3
  McpsContract,
4
4
  ServiceOverviewSchema,
package/src/dev.server.ts CHANGED
@@ -1,6 +1,13 @@
1
+ import { setupAudit } from '#/audit/server';
1
2
  import { createServer } from '#/server/server';
2
3
 
3
- const { app } = createServer({});
4
+ const { app, finalize } = createServer({
5
+ setup: (ctx) => {
6
+ setupAudit(ctx);
7
+ },
8
+ });
9
+
10
+ await finalize();
4
11
 
5
12
  export default {
6
13
  fetch: app.fetch,
@@ -1,12 +1,2 @@
1
- /**
2
- * Entity type definitions
3
- * These are pure TypeScript types without decorators
4
- * Can be used with any ORM or database layer
5
- */
6
- export * from './types';
7
-
8
- // MikroORM Entities
9
- export { ChatRequestEntity, ChatProtocolType, RequestStatus } from './ChatRequestEntity';
10
- export { McpRequestEntity, McpServerType, McpRequestType } from './McpRequestEntity';
11
- export { RequestLogEntity } from './RequestLogEntity';
12
- export { ResponseEntity } from './ResponseEntity';
1
+ // Re-export from new location for backward compatibility
2
+ export * from '../audit/entities/index';
package/src/index.ts CHANGED
@@ -1 +1,47 @@
1
- export {};
1
+ // Core server
2
+ export { createServer, type CreateServerOptions, type McpsServerContext, type StatsProvider } from './server/server';
3
+
4
+ // Events (emittery-based decoupling)
5
+ export { McpsEventType, createMcpsEmitter, type McpsEmitter, type McpsEventData, type McpsRequestEvent } from './server/events';
6
+
7
+ // Configuration
8
+ export { loadConfig, substituteEnvVars, loadEnvFiles } from './server/config';
9
+ export {
10
+ type McpsConfig,
11
+ type ServerConfig,
12
+ type ModelConfig,
13
+ type AuditConfig,
14
+ type DbConfig,
15
+ McpsConfigSchema,
16
+ ServerConfigSchema,
17
+ ModelConfigSchema,
18
+ AuditConfigSchema,
19
+ DbConfigSchema,
20
+ HeaderNames,
21
+ } from './server/schema';
22
+
23
+ // MCP routes and handler
24
+ export { registerMcpRoutes } from './server/mcp-routes';
25
+ export { registerChatRoutes } from './server/chat-routes';
26
+ export { registerApiRoutes } from './server/api-routes';
27
+ export { createMcpLoggingHandler } from './server/mcp-handler';
28
+ export { createMcpsRouter } from './server/mcps-router';
29
+
30
+ // Providers
31
+ export {
32
+ type McpServerHandlerDef,
33
+ type McpServerHandlerDef as McpServerDef,
34
+ type HeaderMapping,
35
+ type DefineMcpServerHandlerOptions,
36
+ defineMcpServerHandler,
37
+ registerMcpServerHandler,
38
+ getAllMcpServerHandlerDefs,
39
+ getMcpServerHandlerDef,
40
+ } from './providers/McpServerHandlerDef';
41
+ export { findMcpServerDef, resolveMcpServerDef, getMcpServerDefCount } from './providers/findMcpServerDef';
42
+
43
+ // Contracts
44
+ export * from './contracts';
45
+
46
+ // Chat
47
+ export { createChatHandler, type ChatHandlerOptions } from './chat';
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('./audit/server/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