@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.
- package/README.md +144 -0
- package/dist/index.mjs +213076 -1
- package/dist/mcps-cli.mjs +102632 -59429
- package/lib/audit/AuditContract.js.map +1 -0
- package/lib/{chat/audit.js → audit/chat.js} +1 -1
- package/lib/audit/chat.js.map +1 -0
- package/lib/audit/entities/ChatRequestEntity.js.map +1 -0
- package/lib/audit/entities/McpRequestEntity.js.map +1 -0
- package/lib/audit/entities/RequestLogEntity.js.map +1 -0
- package/lib/audit/entities/ResponseEntity.js.map +1 -0
- package/lib/audit/entities/index.js +6 -0
- package/lib/audit/entities/index.js.map +1 -0
- package/lib/audit/server/db.js +64 -0
- package/lib/audit/server/db.js.map +1 -0
- package/lib/audit/server/index.js +2 -0
- package/lib/audit/server/index.js.map +1 -0
- package/lib/{server/audit.js → audit/server/plugin.js} +73 -127
- package/lib/audit/server/plugin.js.map +1 -0
- package/lib/audit/types.js.map +1 -0
- package/lib/chat/handler.js +5 -5
- package/lib/chat/handler.js.map +1 -1
- package/lib/chat/index.js +1 -1
- package/lib/chat/index.js.map +1 -1
- package/lib/cli-start.js +36 -0
- package/lib/cli-start.js.map +1 -0
- package/lib/cli.js +19 -0
- package/lib/cli.js.map +1 -0
- package/lib/contracts/index.js +1 -1
- package/lib/contracts/index.js.map +1 -1
- package/lib/dev.server.js +7 -1
- package/lib/dev.server.js.map +1 -1
- package/lib/entities/index.js +2 -10
- package/lib/entities/index.js.map +1 -1
- package/lib/index.js +21 -3
- package/lib/index.js.map +1 -1
- package/lib/mcps-cli.js +6 -35
- package/lib/mcps-cli.js.map +1 -1
- package/lib/providers/feishu/def.js +35 -0
- package/lib/providers/feishu/def.js.map +1 -0
- package/lib/providers/findMcpServerDef.js +1 -0
- package/lib/providers/findMcpServerDef.js.map +1 -1
- package/lib/scripts/bundle.js +7 -1
- package/lib/scripts/bundle.js.map +1 -1
- package/lib/server/api-routes.js +7 -8
- package/lib/server/api-routes.js.map +1 -1
- package/lib/server/events.js +13 -0
- package/lib/server/events.js.map +1 -0
- package/lib/server/mcp-routes.js +31 -60
- package/lib/server/mcp-routes.js.map +1 -1
- package/lib/server/mcps-router.js +19 -24
- package/lib/server/mcps-router.js.map +1 -1
- package/lib/server/schema.js +22 -2
- package/lib/server/schema.js.map +1 -1
- package/lib/server/server.js +142 -87
- package/lib/server/server.js.map +1 -1
- package/package.json +145 -85
- package/src/{chat/audit.ts → audit/chat.ts} +3 -3
- package/src/audit/entities/index.ts +6 -0
- package/src/audit/server/db.ts +65 -0
- package/src/audit/server/index.ts +8 -0
- package/src/{server/audit.ts → audit/server/plugin.ts} +71 -144
- package/src/chat/handler.ts +5 -5
- package/src/chat/index.ts +1 -1
- package/src/cli-start.ts +43 -0
- package/src/cli.ts +45 -0
- package/src/contracts/index.ts +1 -1
- package/src/dev.server.ts +8 -1
- package/src/entities/index.ts +2 -12
- package/src/index.ts +47 -1
- package/src/mcps-cli.ts +6 -48
- package/src/providers/feishu/def.ts +37 -0
- package/src/providers/findMcpServerDef.ts +1 -0
- package/src/scripts/bundle.ts +12 -1
- package/src/server/api-routes.ts +11 -8
- package/src/server/events.ts +29 -0
- package/src/server/mcp-routes.ts +30 -58
- package/src/server/mcps-router.ts +21 -29
- package/src/server/schema.ts +23 -2
- package/src/server/server.ts +149 -81
- package/LICENSE +0 -21
- package/lib/chat/audit.js.map +0 -1
- package/lib/contracts/AuditContract.js.map +0 -1
- package/lib/entities/ChatRequestEntity.js.map +0 -1
- package/lib/entities/McpRequestEntity.js.map +0 -1
- package/lib/entities/RequestLogEntity.js.map +0 -1
- package/lib/entities/ResponseEntity.js.map +0 -1
- package/lib/entities/types.js.map +0 -1
- package/lib/server/audit.js.map +0 -1
- package/lib/server/db.js +0 -97
- package/lib/server/db.js.map +0 -1
- package/src/server/db.ts +0 -115
- /package/lib/{contracts → audit}/AuditContract.js +0 -0
- /package/lib/{entities → audit/entities}/ChatRequestEntity.js +0 -0
- /package/lib/{entities → audit/entities}/McpRequestEntity.js +0 -0
- /package/lib/{entities → audit/entities}/RequestLogEntity.js +0 -0
- /package/lib/{entities → audit/entities}/ResponseEntity.js +0 -0
- /package/lib/{entities → audit}/types.js +0 -0
- /package/src/{contracts → audit}/AuditContract.ts +0 -0
- /package/src/{entities → audit/entities}/ChatRequestEntity.ts +0 -0
- /package/src/{entities → audit/entities}/McpRequestEntity.ts +0 -0
- /package/src/{entities → audit/entities}/RequestLogEntity.ts +0 -0
- /package/src/{entities → audit/entities}/ResponseEntity.ts +0 -0
- /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 {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import
|
|
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,
|
|
23
|
-
ttl: 1000 * 60 * 60 * 24,
|
|
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 (!
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
27
|
+
if (!dbConfigured) return;
|
|
66
28
|
|
|
67
29
|
try {
|
|
68
|
-
|
|
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
|
-
|
|
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
|
-
|
|
139
|
-
if (
|
|
140
|
-
|
|
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
|
|
205
|
-
methodCounts.set(
|
|
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
|
-
*
|
|
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
|
|
257
|
-
|
|
258
|
-
|
|
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
|
-
|
|
277
|
-
|
|
278
|
-
serverType = 'chat';
|
|
279
|
-
}
|
|
194
|
+
const enabled = auditConfig?.enabled !== false;
|
|
195
|
+
if (!enabled) return;
|
|
280
196
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
}
|
package/src/chat/handler.ts
CHANGED
|
@@ -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 '
|
|
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
package/src/cli-start.ts
ADDED
|
@@ -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
|
+
}
|
package/src/contracts/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { AuditContract, AuditEventSchema, AuditQuerySchema, AuditStatsSchema, type AuditEvent } from '
|
|
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,
|
package/src/entities/index.ts
CHANGED
|
@@ -1,12 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
*
|
|
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
|
-
|
|
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 {
|
|
3
|
+
import { createProgram } from './cli';
|
|
15
4
|
|
|
16
5
|
const log = consola.withTag('mcps');
|
|
17
6
|
|
|
18
|
-
const program =
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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);
|