@wener/mcps 1.0.1
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/LICENSE +21 -0
- package/dist/index.mjs +15 -0
- package/dist/mcps-cli.mjs +174727 -0
- package/lib/chat/agent.js +187 -0
- package/lib/chat/agent.js.map +1 -0
- package/lib/chat/audit.js +238 -0
- package/lib/chat/audit.js.map +1 -0
- package/lib/chat/converters.js +467 -0
- package/lib/chat/converters.js.map +1 -0
- package/lib/chat/handler.js +1068 -0
- package/lib/chat/handler.js.map +1 -0
- package/lib/chat/index.js +12 -0
- package/lib/chat/index.js.map +1 -0
- package/lib/chat/types.js +35 -0
- package/lib/chat/types.js.map +1 -0
- package/lib/contracts/AuditContract.js +85 -0
- package/lib/contracts/AuditContract.js.map +1 -0
- package/lib/contracts/McpsContract.js +113 -0
- package/lib/contracts/McpsContract.js.map +1 -0
- package/lib/contracts/index.js +3 -0
- package/lib/contracts/index.js.map +1 -0
- package/lib/dev.server.js +7 -0
- package/lib/dev.server.js.map +1 -0
- package/lib/entities/ChatRequestEntity.js +318 -0
- package/lib/entities/ChatRequestEntity.js.map +1 -0
- package/lib/entities/McpRequestEntity.js +271 -0
- package/lib/entities/McpRequestEntity.js.map +1 -0
- package/lib/entities/RequestLogEntity.js +177 -0
- package/lib/entities/RequestLogEntity.js.map +1 -0
- package/lib/entities/ResponseEntity.js +150 -0
- package/lib/entities/ResponseEntity.js.map +1 -0
- package/lib/entities/index.js +11 -0
- package/lib/entities/index.js.map +1 -0
- package/lib/entities/types.js +11 -0
- package/lib/entities/types.js.map +1 -0
- package/lib/index.js +3 -0
- package/lib/index.js.map +1 -0
- package/lib/mcps-cli.js +44 -0
- package/lib/mcps-cli.js.map +1 -0
- package/lib/providers/McpServerHandlerDef.js +40 -0
- package/lib/providers/McpServerHandlerDef.js.map +1 -0
- package/lib/providers/findMcpServerDef.js +26 -0
- package/lib/providers/findMcpServerDef.js.map +1 -0
- package/lib/providers/prometheus/def.js +24 -0
- package/lib/providers/prometheus/def.js.map +1 -0
- package/lib/providers/prometheus/index.js +2 -0
- package/lib/providers/prometheus/index.js.map +1 -0
- package/lib/providers/relay/def.js +32 -0
- package/lib/providers/relay/def.js.map +1 -0
- package/lib/providers/relay/index.js +2 -0
- package/lib/providers/relay/index.js.map +1 -0
- package/lib/providers/sql/def.js +31 -0
- package/lib/providers/sql/def.js.map +1 -0
- package/lib/providers/sql/index.js +2 -0
- package/lib/providers/sql/index.js.map +1 -0
- package/lib/providers/tencent-cls/def.js +44 -0
- package/lib/providers/tencent-cls/def.js.map +1 -0
- package/lib/providers/tencent-cls/index.js +2 -0
- package/lib/providers/tencent-cls/index.js.map +1 -0
- package/lib/scripts/bundle.js +90 -0
- package/lib/scripts/bundle.js.map +1 -0
- package/lib/server/api-routes.js +96 -0
- package/lib/server/api-routes.js.map +1 -0
- package/lib/server/audit.js +274 -0
- package/lib/server/audit.js.map +1 -0
- package/lib/server/chat-routes.js +82 -0
- package/lib/server/chat-routes.js.map +1 -0
- package/lib/server/config.js +223 -0
- package/lib/server/config.js.map +1 -0
- package/lib/server/db.js +97 -0
- package/lib/server/db.js.map +1 -0
- package/lib/server/index.js +2 -0
- package/lib/server/index.js.map +1 -0
- package/lib/server/mcp-handler.js +167 -0
- package/lib/server/mcp-handler.js.map +1 -0
- package/lib/server/mcp-routes.js +112 -0
- package/lib/server/mcp-routes.js.map +1 -0
- package/lib/server/mcps-router.js +119 -0
- package/lib/server/mcps-router.js.map +1 -0
- package/lib/server/schema.js +129 -0
- package/lib/server/schema.js.map +1 -0
- package/lib/server/server.js +166 -0
- package/lib/server/server.js.map +1 -0
- package/lib/web/ChatPage.js +827 -0
- package/lib/web/ChatPage.js.map +1 -0
- package/lib/web/McpInspectorPage.js +214 -0
- package/lib/web/McpInspectorPage.js.map +1 -0
- package/lib/web/ServersPage.js +93 -0
- package/lib/web/ServersPage.js.map +1 -0
- package/lib/web/main.js +541 -0
- package/lib/web/main.js.map +1 -0
- package/package.json +83 -0
- package/src/chat/agent.ts +240 -0
- package/src/chat/audit.ts +377 -0
- package/src/chat/converters.test.ts +325 -0
- package/src/chat/converters.ts +459 -0
- package/src/chat/handler.test.ts +137 -0
- package/src/chat/handler.ts +1233 -0
- package/src/chat/index.ts +16 -0
- package/src/chat/types.ts +72 -0
- package/src/contracts/AuditContract.ts +93 -0
- package/src/contracts/McpsContract.ts +141 -0
- package/src/contracts/index.ts +18 -0
- package/src/dev.server.ts +7 -0
- package/src/entities/ChatRequestEntity.ts +157 -0
- package/src/entities/McpRequestEntity.ts +149 -0
- package/src/entities/RequestLogEntity.ts +78 -0
- package/src/entities/ResponseEntity.ts +75 -0
- package/src/entities/index.ts +12 -0
- package/src/entities/types.ts +188 -0
- package/src/index.ts +1 -0
- package/src/mcps-cli.ts +59 -0
- package/src/providers/McpServerHandlerDef.ts +105 -0
- package/src/providers/findMcpServerDef.ts +31 -0
- package/src/providers/prometheus/def.ts +21 -0
- package/src/providers/prometheus/index.ts +1 -0
- package/src/providers/relay/def.ts +31 -0
- package/src/providers/relay/index.ts +1 -0
- package/src/providers/relay/relay.test.ts +47 -0
- package/src/providers/sql/def.ts +33 -0
- package/src/providers/sql/index.ts +1 -0
- package/src/providers/tencent-cls/def.ts +38 -0
- package/src/providers/tencent-cls/index.ts +1 -0
- package/src/scripts/bundle.ts +82 -0
- package/src/server/api-routes.ts +98 -0
- package/src/server/audit.ts +310 -0
- package/src/server/chat-routes.ts +95 -0
- package/src/server/config.test.ts +162 -0
- package/src/server/config.ts +198 -0
- package/src/server/db.ts +115 -0
- package/src/server/index.ts +1 -0
- package/src/server/mcp-handler.ts +209 -0
- package/src/server/mcp-routes.ts +133 -0
- package/src/server/mcps-router.ts +133 -0
- package/src/server/schema.ts +175 -0
- package/src/server/server.ts +163 -0
- package/src/web/ChatPage.tsx +1005 -0
- package/src/web/McpInspectorPage.tsx +254 -0
- package/src/web/ServersPage.tsx +139 -0
- package/src/web/main.tsx +600 -0
- package/src/web/styles.css +15 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { SqlMcpServerDef, SqlMcpServerHandlerDef } from './def';
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { TencentClsMcpServerDef, type CreateTencentClsMcpServerOptions } from '@wener/ai/mcp/tencent-cls';
|
|
2
|
+
import { HeaderNames, type TencentClsConfig } from '../../server/schema';
|
|
3
|
+
import { defineMcpServerHandler, registerMcpServerHandler } from '../McpServerHandlerDef';
|
|
4
|
+
|
|
5
|
+
export const TencentClsMcpServerHandlerDef = defineMcpServerHandler<
|
|
6
|
+
CreateTencentClsMcpServerOptions,
|
|
7
|
+
TencentClsConfig
|
|
8
|
+
>(TencentClsMcpServerDef, {
|
|
9
|
+
headerMappings: [
|
|
10
|
+
{ header: HeaderNames.CLS_SECRET_ID, property: 'clientId', required: true },
|
|
11
|
+
{ header: HeaderNames.CLS_SECRET_KEY, property: 'clientSecret', required: true },
|
|
12
|
+
{ header: HeaderNames.CLS_REGION, property: 'region', default: 'ap-shanghai' },
|
|
13
|
+
{ header: HeaderNames.CLS_ENDPOINT, property: 'endpoint' },
|
|
14
|
+
],
|
|
15
|
+
|
|
16
|
+
resolveConfig(config, headers) {
|
|
17
|
+
const clientId =
|
|
18
|
+
config.clientId || headers?.get(HeaderNames.CLS_SECRET_ID) || config.headers?.[HeaderNames.CLS_SECRET_ID];
|
|
19
|
+
const clientSecret =
|
|
20
|
+
config.clientSecret || headers?.get(HeaderNames.CLS_SECRET_KEY) || config.headers?.[HeaderNames.CLS_SECRET_KEY];
|
|
21
|
+
const region =
|
|
22
|
+
config.region ||
|
|
23
|
+
headers?.get(HeaderNames.CLS_REGION) ||
|
|
24
|
+
config.headers?.[HeaderNames.CLS_REGION] ||
|
|
25
|
+
'ap-shanghai';
|
|
26
|
+
const endpoint =
|
|
27
|
+
config.endpoint || headers?.get(HeaderNames.CLS_ENDPOINT) || config.headers?.[HeaderNames.CLS_ENDPOINT];
|
|
28
|
+
|
|
29
|
+
if (!clientId || !clientSecret) return null;
|
|
30
|
+
|
|
31
|
+
return { clientId, clientSecret, region, endpoint };
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
registerMcpServerHandler(TencentClsMcpServerHandlerDef);
|
|
36
|
+
|
|
37
|
+
// backward compatibility
|
|
38
|
+
export { TencentClsMcpServerHandlerDef as TencentClsMcpServerDef };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { TencentClsMcpServerDef, TencentClsMcpServerHandlerDef } from './def';
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import * as esbuild from 'esbuild';
|
|
4
|
+
|
|
5
|
+
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf-8'));
|
|
6
|
+
|
|
7
|
+
const banner = `// Bundled with esbuild
|
|
8
|
+
// ${pkg.name}@${pkg.version}
|
|
9
|
+
|
|
10
|
+
var require,__filename,__dirname;
|
|
11
|
+
{
|
|
12
|
+
const {createRequire} = await import('node:module');
|
|
13
|
+
require ||= createRequire(import.meta.url);
|
|
14
|
+
}
|
|
15
|
+
{
|
|
16
|
+
const {fileURLToPath} = await import('node:url');
|
|
17
|
+
const {dirname} = await import('node:path');
|
|
18
|
+
__filename ||= fileURLToPath(import.meta.url);
|
|
19
|
+
__dirname ||= dirname(__filename)
|
|
20
|
+
};
|
|
21
|
+
`;
|
|
22
|
+
|
|
23
|
+
const commonOptions: esbuild.BuildOptions = {
|
|
24
|
+
bundle: true,
|
|
25
|
+
logLevel: 'info',
|
|
26
|
+
banner: { js: banner },
|
|
27
|
+
define: {
|
|
28
|
+
NODE_ENV: JSON.stringify('production'),
|
|
29
|
+
__DEV__: JSON.stringify(false),
|
|
30
|
+
'process.env.NODE_ENV': JSON.stringify('production'),
|
|
31
|
+
},
|
|
32
|
+
keepNames: true,
|
|
33
|
+
treeShaking: true,
|
|
34
|
+
minifySyntax: true,
|
|
35
|
+
format: 'esm',
|
|
36
|
+
platform: 'node',
|
|
37
|
+
charset: 'utf8',
|
|
38
|
+
target: 'node18',
|
|
39
|
+
sourcemap: false,
|
|
40
|
+
legalComments: 'none',
|
|
41
|
+
// External native modules
|
|
42
|
+
external: ['better-sqlite3', 'oracledb', 'mariadb/callback', 'mysql'],
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// Ensure dist directory exists
|
|
46
|
+
fs.mkdirSync('dist', { recursive: true });
|
|
47
|
+
|
|
48
|
+
console.log('Building MCPS...');
|
|
49
|
+
|
|
50
|
+
// Build library entry (index.ts)
|
|
51
|
+
const libResult = await esbuild.build({
|
|
52
|
+
...commonOptions,
|
|
53
|
+
entryPoints: ['src/index.ts'],
|
|
54
|
+
outfile: 'dist/index.mjs',
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Build CLI entry (mcps-cli.ts)
|
|
58
|
+
const cliResult = await esbuild.build({
|
|
59
|
+
...commonOptions,
|
|
60
|
+
entryPoints: ['src/mcps-cli.ts'],
|
|
61
|
+
outfile: 'dist/mcps-cli.mjs',
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
if (libResult.errors.length === 0 && cliResult.errors.length === 0) {
|
|
65
|
+
// Process CLI output - add shebang
|
|
66
|
+
const cliOutfile = 'dist/mcps-cli.mjs';
|
|
67
|
+
let content = fs.readFileSync(cliOutfile, 'utf-8');
|
|
68
|
+
content = content.replace(/^#!.*\n/gm, '');
|
|
69
|
+
fs.writeFileSync(cliOutfile, `#!/usr/bin/env node\n${content}`);
|
|
70
|
+
fs.chmodSync(cliOutfile, 0o755);
|
|
71
|
+
|
|
72
|
+
const libStats = fs.statSync('dist/index.mjs');
|
|
73
|
+
const cliStats = fs.statSync(cliOutfile);
|
|
74
|
+
|
|
75
|
+
console.log(`✅ Build successful!`);
|
|
76
|
+
console.log(`📦 Library: dist/index.mjs (${(libStats.size / 1024).toFixed(1)}KB)`);
|
|
77
|
+
console.log(`📦 CLI: ${cliOutfile} (${(cliStats.size / 1024).toFixed(1)}KB)`);
|
|
78
|
+
console.log(`🚀 Ready for npm publish and npx usage`);
|
|
79
|
+
} else {
|
|
80
|
+
console.error('❌ Build failed:', [...libResult.errors, ...cliResult.errors]);
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { SmartCoercionPlugin } from '@orpc/json-schema';
|
|
2
|
+
import { OpenAPIGenerator } from '@orpc/openapi';
|
|
3
|
+
import { OpenAPIHandler } from '@orpc/openapi/fetch';
|
|
4
|
+
import { RPCHandler } from '@orpc/server/fetch';
|
|
5
|
+
import { CORSPlugin } from '@orpc/server/plugins';
|
|
6
|
+
import { ZodToJsonSchemaConverter } from '@orpc/zod/zod4';
|
|
7
|
+
import type { Hono } from 'hono';
|
|
8
|
+
import { html } from 'hono/html';
|
|
9
|
+
import { AuditRouter } from './audit';
|
|
10
|
+
import { createMcpsRouter } from './mcps-router';
|
|
11
|
+
import type { McpsConfig } from './schema';
|
|
12
|
+
|
|
13
|
+
export interface RegisterApiRoutesOptions {
|
|
14
|
+
app: Hono;
|
|
15
|
+
config: McpsConfig;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Register oRPC API routes for audit and MCPS
|
|
20
|
+
*/
|
|
21
|
+
export function registerApiRoutes({ app, config }: RegisterApiRoutesOptions) {
|
|
22
|
+
// Create MCPS router with config context
|
|
23
|
+
const McpsRouter = createMcpsRouter({ config });
|
|
24
|
+
|
|
25
|
+
// Combined router for all APIs
|
|
26
|
+
const combinedRouter = {
|
|
27
|
+
audit: AuditRouter,
|
|
28
|
+
mcps: McpsRouter,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const handleByRpc = new RPCHandler(combinedRouter);
|
|
32
|
+
const handleByOpenAPI = new OpenAPIHandler(combinedRouter, {
|
|
33
|
+
plugins: [
|
|
34
|
+
new SmartCoercionPlugin({
|
|
35
|
+
schemaConverters: [new ZodToJsonSchemaConverter()],
|
|
36
|
+
}),
|
|
37
|
+
new CORSPlugin({
|
|
38
|
+
exposeHeaders: ['Content-Disposition'],
|
|
39
|
+
}),
|
|
40
|
+
],
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
app.use('/api/rpc/*', async (c, next) => {
|
|
44
|
+
const { matched, response } = await handleByRpc.handle(c.req.raw, {
|
|
45
|
+
prefix: '/api/rpc',
|
|
46
|
+
context: {},
|
|
47
|
+
});
|
|
48
|
+
if (matched) {
|
|
49
|
+
return c.newResponse(response.body, response);
|
|
50
|
+
}
|
|
51
|
+
return next();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
app.use('/api/*', async (c, next) => {
|
|
55
|
+
const { matched, response } = await handleByOpenAPI.handle(c.req.raw, {
|
|
56
|
+
prefix: '/api',
|
|
57
|
+
context: {},
|
|
58
|
+
});
|
|
59
|
+
if (matched) {
|
|
60
|
+
return c.newResponse(response.body, response);
|
|
61
|
+
}
|
|
62
|
+
return next();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// OpenAPI spec
|
|
66
|
+
const openAPIGenerator = new OpenAPIGenerator({
|
|
67
|
+
schemaConverters: [new ZodToJsonSchemaConverter()],
|
|
68
|
+
});
|
|
69
|
+
let specCache: unknown;
|
|
70
|
+
|
|
71
|
+
app.get('/api/spec.json', async (c) => {
|
|
72
|
+
if (!specCache) {
|
|
73
|
+
specCache = await openAPIGenerator.generate(combinedRouter, {
|
|
74
|
+
info: { title: 'MCPS API', version: '1.0.0' },
|
|
75
|
+
servers: [{ url: '/api' }],
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
return c.json(specCache);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Swagger UI
|
|
82
|
+
app.get('/api/docs', (c) => {
|
|
83
|
+
return c.html(
|
|
84
|
+
html`<!doctype html>
|
|
85
|
+
<html lang="en">
|
|
86
|
+
<head>
|
|
87
|
+
<title>MCPS API</title>
|
|
88
|
+
<meta charset="utf-8" />
|
|
89
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
90
|
+
</head>
|
|
91
|
+
<body>
|
|
92
|
+
<script id="api-reference" data-url="/api/spec.json"></script>
|
|
93
|
+
<script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
|
|
94
|
+
</body>
|
|
95
|
+
</html>`,
|
|
96
|
+
);
|
|
97
|
+
});
|
|
98
|
+
}
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import { implement } from '@orpc/server';
|
|
2
|
+
import type { Context, Next } from 'hono';
|
|
3
|
+
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';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Convert Headers to a plain record
|
|
11
|
+
*/
|
|
12
|
+
function headersToRecord(headers: Headers): Record<string, string> {
|
|
13
|
+
const record: Record<string, string> = {};
|
|
14
|
+
headers.forEach((value, key) => {
|
|
15
|
+
record[key] = value;
|
|
16
|
+
});
|
|
17
|
+
return record;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// In-memory audit store using LRU cache
|
|
21
|
+
const auditStore = new LRUCache<string, AuditEvent>({
|
|
22
|
+
max: 10000, // Keep last 10k events
|
|
23
|
+
ttl: 1000 * 60 * 60 * 24, // 24 hours
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// Counter for IDs
|
|
27
|
+
let eventCounter = 0;
|
|
28
|
+
|
|
29
|
+
// Audit configuration state
|
|
30
|
+
let auditEnabled = true; // default to enabled
|
|
31
|
+
let dbConfigured = false;
|
|
32
|
+
|
|
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
|
+
async function persistToDb(event: AuditEvent, id: string): Promise<void> {
|
|
63
|
+
if (!auditEnabled || !dbConfigured) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
// Lazy initialize DB on first persist
|
|
69
|
+
const orm = await ensureDbInitialized();
|
|
70
|
+
const em = orm.em.fork();
|
|
71
|
+
|
|
72
|
+
const logEntry = new RequestLogEntity();
|
|
73
|
+
logEntry.requestId = id;
|
|
74
|
+
logEntry.timestamp = new Date(event.timestamp);
|
|
75
|
+
logEntry.method = event.method;
|
|
76
|
+
logEntry.path = event.path;
|
|
77
|
+
logEntry.serverName = event.serverName ?? undefined;
|
|
78
|
+
logEntry.serverType = event.serverType ?? undefined;
|
|
79
|
+
logEntry.status = event.status ?? undefined;
|
|
80
|
+
logEntry.durationMs = event.durationMs ?? undefined;
|
|
81
|
+
logEntry.error = event.error ?? undefined;
|
|
82
|
+
logEntry.requestHeaders = event.requestHeaders ?? undefined;
|
|
83
|
+
// Determine request type
|
|
84
|
+
if (event.path.startsWith('/mcp/')) {
|
|
85
|
+
logEntry.requestType = 'mcp';
|
|
86
|
+
} else if (event.path.startsWith('/v1/')) {
|
|
87
|
+
logEntry.requestType = 'chat';
|
|
88
|
+
} else {
|
|
89
|
+
logEntry.requestType = 'api';
|
|
90
|
+
}
|
|
91
|
+
em.persist(logEntry);
|
|
92
|
+
await em.flush();
|
|
93
|
+
} catch (e) {
|
|
94
|
+
// Log persistence errors but don't throw - in-memory store is the primary
|
|
95
|
+
console.error('Failed to persist audit log:', e);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Add an audit event
|
|
101
|
+
*/
|
|
102
|
+
export function addAuditEvent(event: Omit<AuditEvent, 'id'>): AuditEvent {
|
|
103
|
+
const id = `${Date.now()}-${++eventCounter}`;
|
|
104
|
+
const fullEvent: AuditEvent = { ...event, id };
|
|
105
|
+
auditStore.set(id, fullEvent);
|
|
106
|
+
|
|
107
|
+
// Persist to database asynchronously (lazy init)
|
|
108
|
+
persistToDb(fullEvent, id).catch(() => {
|
|
109
|
+
// Already logged in persistToDb
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
return fullEvent;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Query audit events
|
|
117
|
+
*/
|
|
118
|
+
export function queryAuditEvents(options: {
|
|
119
|
+
limit?: number;
|
|
120
|
+
offset?: number;
|
|
121
|
+
serverName?: string | null;
|
|
122
|
+
serverType?: string | null;
|
|
123
|
+
method?: string | null;
|
|
124
|
+
from?: string | null;
|
|
125
|
+
to?: string | null;
|
|
126
|
+
}): { events: AuditEvent[]; total: number } {
|
|
127
|
+
const { limit = 50, offset = 0, serverName, serverType, method, from, to } = options;
|
|
128
|
+
|
|
129
|
+
// Get all events as array
|
|
130
|
+
let events: AuditEvent[] = [];
|
|
131
|
+
for (const [, event] of auditStore.entries()) {
|
|
132
|
+
events.push(event);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Sort by timestamp desc
|
|
136
|
+
events.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
|
137
|
+
|
|
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
|
+
}
|
|
148
|
+
if (from) {
|
|
149
|
+
const fromTime = new Date(from).getTime();
|
|
150
|
+
events = events.filter((e) => new Date(e.timestamp).getTime() >= fromTime);
|
|
151
|
+
}
|
|
152
|
+
if (to) {
|
|
153
|
+
const toTime = new Date(to).getTime();
|
|
154
|
+
events = events.filter((e) => new Date(e.timestamp).getTime() <= toTime);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const total = events.length;
|
|
158
|
+
|
|
159
|
+
// Paginate
|
|
160
|
+
events = events.slice(offset, offset + limit);
|
|
161
|
+
|
|
162
|
+
return { events, total };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Get audit statistics
|
|
167
|
+
*/
|
|
168
|
+
export function getAuditStats(options: { from?: string | null; to?: string | null }) {
|
|
169
|
+
let events: AuditEvent[] = [];
|
|
170
|
+
for (const [, event] of auditStore.entries()) {
|
|
171
|
+
events.push(event);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Apply time filters
|
|
175
|
+
if (options.from) {
|
|
176
|
+
const fromTime = new Date(options.from).getTime();
|
|
177
|
+
events = events.filter((e) => new Date(e.timestamp).getTime() >= fromTime);
|
|
178
|
+
}
|
|
179
|
+
if (options.to) {
|
|
180
|
+
const toTime = new Date(options.to).getTime();
|
|
181
|
+
events = events.filter((e) => new Date(e.timestamp).getTime() <= toTime);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Calculate stats
|
|
185
|
+
const totalRequests = events.length;
|
|
186
|
+
const totalErrors = events.filter((e) => e.error || (e.status && e.status >= 400)).length;
|
|
187
|
+
|
|
188
|
+
const durations = events.map((e) => e.durationMs).filter((d): d is number => d != null);
|
|
189
|
+
const avgDurationMs = durations.length > 0 ? durations.reduce((a, b) => a + b, 0) / durations.length : 0;
|
|
190
|
+
|
|
191
|
+
// Group by server
|
|
192
|
+
const serverCounts = new Map<string, number>();
|
|
193
|
+
for (const event of events) {
|
|
194
|
+
const name = event.serverName || 'unknown';
|
|
195
|
+
serverCounts.set(name, (serverCounts.get(name) || 0) + 1);
|
|
196
|
+
}
|
|
197
|
+
const byServer = Array.from(serverCounts.entries())
|
|
198
|
+
.map(([name, count]) => ({ name, count }))
|
|
199
|
+
.sort((a, b) => b.count - a.count);
|
|
200
|
+
|
|
201
|
+
// Group by method
|
|
202
|
+
const methodCounts = new Map<string, number>();
|
|
203
|
+
for (const event of events) {
|
|
204
|
+
const method = event.method || 'unknown';
|
|
205
|
+
methodCounts.set(method, (methodCounts.get(method) || 0) + 1);
|
|
206
|
+
}
|
|
207
|
+
const byMethod = Array.from(methodCounts.entries())
|
|
208
|
+
.map(([method, count]) => ({ method, count }))
|
|
209
|
+
.sort((a, b) => b.count - a.count);
|
|
210
|
+
|
|
211
|
+
return { totalRequests, totalErrors, avgDurationMs, byServer, byMethod };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Clear audit events before a timestamp
|
|
216
|
+
*/
|
|
217
|
+
export function clearAuditEvents(before: string): number {
|
|
218
|
+
const beforeTime = new Date(before).getTime();
|
|
219
|
+
let deleted = 0;
|
|
220
|
+
|
|
221
|
+
for (const [id, event] of auditStore.entries()) {
|
|
222
|
+
if (new Date(event.timestamp).getTime() < beforeTime) {
|
|
223
|
+
auditStore.delete(id);
|
|
224
|
+
deleted++;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return deleted;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Audit Router implementation
|
|
233
|
+
*/
|
|
234
|
+
export const AuditRouter = implement(AuditContract).router({
|
|
235
|
+
list: implement(AuditContract.list).handler(async ({ input }) => {
|
|
236
|
+
return queryAuditEvents(input);
|
|
237
|
+
}),
|
|
238
|
+
|
|
239
|
+
get: implement(AuditContract.get).handler(async ({ input }) => {
|
|
240
|
+
return auditStore.get(input.id) ?? null;
|
|
241
|
+
}),
|
|
242
|
+
|
|
243
|
+
stats: implement(AuditContract.stats).handler(async ({ input }) => {
|
|
244
|
+
return getAuditStats(input);
|
|
245
|
+
}),
|
|
246
|
+
|
|
247
|
+
clear: implement(AuditContract.clear).handler(async ({ input }) => {
|
|
248
|
+
const deleted = clearAuditEvents(input.before);
|
|
249
|
+
return { deleted };
|
|
250
|
+
}),
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Hono middleware for audit logging
|
|
255
|
+
*/
|
|
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
|
+
}
|
|
275
|
+
|
|
276
|
+
// Extract model info from chat requests
|
|
277
|
+
if (path.startsWith('/v1/')) {
|
|
278
|
+
serverType = 'chat';
|
|
279
|
+
}
|
|
280
|
+
|
|
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
|
+
}
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import consola from 'consola';
|
|
2
|
+
import type { Hono } from 'hono';
|
|
3
|
+
import { createChatHandler } from '../chat';
|
|
4
|
+
import { registerAgentRoutes } from '../chat/agent';
|
|
5
|
+
import type { McpsConfig, ModelConfig } from './schema';
|
|
6
|
+
|
|
7
|
+
const log = consola.withTag('mcps');
|
|
8
|
+
|
|
9
|
+
export interface RegisterChatRoutesOptions {
|
|
10
|
+
app: Hono;
|
|
11
|
+
config: McpsConfig;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Register Chat/LLM Gateway routes
|
|
16
|
+
*/
|
|
17
|
+
export function registerChatRoutes({ app, config }: RegisterChatRoutesOptions) {
|
|
18
|
+
if (!config.models || config.models.length === 0) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
log.info(`Registering chat gateway with ${config.models.length} models`);
|
|
23
|
+
const chatHandler = createChatHandler({ config: { models: config.models } });
|
|
24
|
+
app.route('/', chatHandler);
|
|
25
|
+
|
|
26
|
+
// Helper function to resolve model config
|
|
27
|
+
function resolveModelConfig(modelName: string): ModelConfig | null {
|
|
28
|
+
const models = config.models || [];
|
|
29
|
+
// Exact match first
|
|
30
|
+
for (const modelConfig of models) {
|
|
31
|
+
if (modelConfig.name === modelName) return modelConfig;
|
|
32
|
+
}
|
|
33
|
+
// Wildcard match
|
|
34
|
+
for (const modelConfig of models) {
|
|
35
|
+
const pattern = modelConfig.name;
|
|
36
|
+
if (pattern.includes('*')) {
|
|
37
|
+
const regex = new RegExp(`^${pattern.replace(/\*/g, '.*')}$`);
|
|
38
|
+
if (regex.test(modelName)) return modelConfig;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Register agent routes with tool loop support
|
|
45
|
+
registerAgentRoutes(app, {
|
|
46
|
+
resolveModelConfig,
|
|
47
|
+
// MCP tools will be loaded on-demand from configured servers
|
|
48
|
+
getMcpTools: async (serverNames) => {
|
|
49
|
+
if (!serverNames || serverNames.length === 0) {
|
|
50
|
+
return {};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const allTools: Record<string, any> = {};
|
|
54
|
+
const { createMCPClient } = await import('@ai-sdk/mcp');
|
|
55
|
+
|
|
56
|
+
for (const serverName of serverNames) {
|
|
57
|
+
const serverConfig = config.servers[serverName];
|
|
58
|
+
if (!serverConfig) {
|
|
59
|
+
log.warn(`MCP server not found: ${serverName}`);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
// Get the server URL - for configured servers, connect via HTTP
|
|
65
|
+
// The server exposes MCP at /mcp/{serverName}
|
|
66
|
+
const port = process.env.PORT || '3001';
|
|
67
|
+
const serverUrl = `http://127.0.0.1:${port}/mcp/${serverName}`;
|
|
68
|
+
|
|
69
|
+
// Create MCP client using SSE transport for HTTP-based MCP
|
|
70
|
+
const { SSEClientTransport } = await import('@modelcontextprotocol/sdk/client/sse.js');
|
|
71
|
+
const client = await createMCPClient({
|
|
72
|
+
transport: new SSEClientTransport(new URL(serverUrl)),
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Get tools from this server
|
|
76
|
+
const serverTools = await client.tools();
|
|
77
|
+
for (const [toolName, tool] of Object.entries(serverTools)) {
|
|
78
|
+
allTools[`${serverName}_${toolName}`] = tool;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
log.info(`Loaded ${Object.keys(serverTools).length} tools from ${serverName}`);
|
|
82
|
+
|
|
83
|
+
// Close the client after getting tools
|
|
84
|
+
await client.close();
|
|
85
|
+
} catch (err) {
|
|
86
|
+
log.error(`Failed to load tools from ${serverName}:`, err);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
log.info(`Agent loaded ${Object.keys(allTools).length} total tools from servers: ${serverNames.join(', ')}`);
|
|
91
|
+
return allTools;
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
log.info('Registered agent routes at /v1/agent/*');
|
|
95
|
+
}
|