@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,133 @@
|
|
|
1
|
+
import { StreamableHTTPTransport } from '@hono/mcp';
|
|
2
|
+
import consola from 'consola';
|
|
3
|
+
import type { Hono } from 'hono';
|
|
4
|
+
import type { LRUCache } from 'lru-cache';
|
|
5
|
+
import type { McpServerInstance } from '@wener/ai/mcp';
|
|
6
|
+
import { getMcpServerHandlerDef, type McpServerHandlerDef } from '../providers/McpServerHandlerDef';
|
|
7
|
+
import { findMcpServerDef } from '../providers/findMcpServerDef';
|
|
8
|
+
import { createMcpLoggingHandler } from './mcp-handler';
|
|
9
|
+
import type { McpsConfig, ServerConfig } from './schema';
|
|
10
|
+
|
|
11
|
+
const log = consola.withTag('mcps');
|
|
12
|
+
|
|
13
|
+
export interface RegisterMcpRoutesOptions {
|
|
14
|
+
app: Hono;
|
|
15
|
+
config: McpsConfig;
|
|
16
|
+
serverCache: LRUCache<string, McpServerInstance>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Register MCP routes for both pre-configured and dynamic endpoints
|
|
21
|
+
*/
|
|
22
|
+
export function registerMcpRoutes({ app, config, serverCache }: RegisterMcpRoutesOptions) {
|
|
23
|
+
const serverDefs = findMcpServerDef();
|
|
24
|
+
|
|
25
|
+
// Register pre-configured servers from config
|
|
26
|
+
// These are named endpoints like /mcp/my-sql that use config from file
|
|
27
|
+
for (const [name, serverConfig] of Object.entries(config.servers)) {
|
|
28
|
+
const def = getMcpServerHandlerDef(serverConfig.type);
|
|
29
|
+
if (!def) {
|
|
30
|
+
log.warn(`Unknown server type for ${name}: ${serverConfig.type}`);
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Resolve config using def (config comes from file, not headers)
|
|
35
|
+
const options = def.resolveConfig(serverConfig);
|
|
36
|
+
if (!options) {
|
|
37
|
+
log.warn(`Failed to resolve config for ${name}`);
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const validation = def.validateOptions(options);
|
|
42
|
+
if (!validation.valid) {
|
|
43
|
+
log.warn(`Invalid config for ${name}: ${validation.error}`);
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
|
|
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
|
+
const path = `/mcp/${name}`;
|
|
57
|
+
log.info(`Registered MCP server: ${path} (${serverConfig.type})`);
|
|
58
|
+
|
|
59
|
+
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
|
+
}
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Register dynamic endpoints for all server types
|
|
78
|
+
// These endpoints accept config via HTTP headers
|
|
79
|
+
for (const def of serverDefs) {
|
|
80
|
+
const path = `/mcp/${def.name}`;
|
|
81
|
+
log.debug(`Registering dynamic endpoint: ${path}`);
|
|
82
|
+
|
|
83
|
+
app.all(path, async (c) => {
|
|
84
|
+
// Use def.resolveConfig to parse config from headers
|
|
85
|
+
const options = def.resolveConfig({ type: def.name } as any, c.req.raw.headers);
|
|
86
|
+
|
|
87
|
+
if (!options) {
|
|
88
|
+
// Build error message from headerMappings
|
|
89
|
+
const requiredHeaders =
|
|
90
|
+
def.headerMappings
|
|
91
|
+
?.filter((m) => m.required)
|
|
92
|
+
.map((m) => m.header)
|
|
93
|
+
.join(', ') || 'required headers';
|
|
94
|
+
return c.text(`Missing ${requiredHeaders}`, 400);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Validate options
|
|
98
|
+
const validation = def.validateOptions(options);
|
|
99
|
+
if (!validation.valid) {
|
|
100
|
+
return c.text(validation.error || `Invalid configuration for ${def.name}`, 400);
|
|
101
|
+
}
|
|
102
|
+
|
|
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
|
+
}
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return { serverDefs };
|
|
133
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { implement } from '@orpc/server';
|
|
2
|
+
import { McpsContract, type ModelInfo, type ServerInfo, type ServerTypeInfo, type ToolInfo } from '../contracts';
|
|
3
|
+
import { findMcpServerDef } from '../providers/findMcpServerDef';
|
|
4
|
+
import { getAuditStats, queryAuditEvents } from './audit';
|
|
5
|
+
import type { McpsConfig } from './schema';
|
|
6
|
+
|
|
7
|
+
// Simple glob pattern matching
|
|
8
|
+
function matchGlob(pattern: string, text: string): boolean {
|
|
9
|
+
const regexPattern = pattern
|
|
10
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&') // Escape regex special chars except * and ?
|
|
11
|
+
.replace(/\*/g, '.*')
|
|
12
|
+
.replace(/\?/g, '.');
|
|
13
|
+
return new RegExp(`^${regexPattern}$`, 'i').test(text);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Build server types from registry
|
|
17
|
+
function getServerTypes(): ServerTypeInfo[] {
|
|
18
|
+
return findMcpServerDef().map((def) => ({
|
|
19
|
+
type: def.name,
|
|
20
|
+
description: def.description,
|
|
21
|
+
dynamicEndpoint: `/mcp/${def.name}`,
|
|
22
|
+
}));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Build endpoints from server types
|
|
26
|
+
function getEndpoints(): string[] {
|
|
27
|
+
const mcpEndpoints = findMcpServerDef().map((def) => `/mcp/${def.name}`);
|
|
28
|
+
const chatEndpoints = [
|
|
29
|
+
'/v1/chat/completions',
|
|
30
|
+
'/v1/messages',
|
|
31
|
+
'/v1/models',
|
|
32
|
+
'/v1/responses',
|
|
33
|
+
'/v1/models/:model:generateContent',
|
|
34
|
+
'/v1/models/:model:streamGenerateContent',
|
|
35
|
+
];
|
|
36
|
+
return [...mcpEndpoints, ...chatEndpoints];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface McpsRouterContext {
|
|
40
|
+
config: McpsConfig;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Create MCPS Router with config context
|
|
45
|
+
*/
|
|
46
|
+
export function createMcpsRouter(ctx: McpsRouterContext) {
|
|
47
|
+
const { config } = ctx;
|
|
48
|
+
|
|
49
|
+
// Build server info list
|
|
50
|
+
const servers: ServerInfo[] = Object.entries(config.servers).map(([name, serverConfig]) => ({
|
|
51
|
+
name,
|
|
52
|
+
type: serverConfig.type,
|
|
53
|
+
disabled: serverConfig.disabled,
|
|
54
|
+
}));
|
|
55
|
+
|
|
56
|
+
// Build model info list
|
|
57
|
+
const models: ModelInfo[] = (config.models ?? []).map((model) => ({
|
|
58
|
+
name: model.name,
|
|
59
|
+
adapter: model.adapter,
|
|
60
|
+
baseUrl: model.baseUrl,
|
|
61
|
+
contextWindow: model.contextWindow,
|
|
62
|
+
maxInputTokens: model.maxInputTokens,
|
|
63
|
+
maxOutputTokens: model.maxOutputTokens,
|
|
64
|
+
}));
|
|
65
|
+
|
|
66
|
+
return implement(McpsContract).router({
|
|
67
|
+
overview: implement(McpsContract.overview).handler(async () => {
|
|
68
|
+
return {
|
|
69
|
+
name: '@wener/mcps',
|
|
70
|
+
version: '0.1.0',
|
|
71
|
+
servers,
|
|
72
|
+
serverTypes: getServerTypes(),
|
|
73
|
+
models,
|
|
74
|
+
endpoints: getEndpoints(),
|
|
75
|
+
};
|
|
76
|
+
}),
|
|
77
|
+
|
|
78
|
+
stats: implement(McpsContract.stats).handler(async ({ input }) => {
|
|
79
|
+
const auditStats = getAuditStats(input);
|
|
80
|
+
|
|
81
|
+
// Build endpoint stats from audit events
|
|
82
|
+
const { events } = queryAuditEvents({ limit: 10000 });
|
|
83
|
+
const endpointCounts = new Map<string, number>();
|
|
84
|
+
for (const event of events) {
|
|
85
|
+
const endpoint = event.path || 'unknown';
|
|
86
|
+
endpointCounts.set(endpoint, (endpointCounts.get(endpoint) || 0) + 1);
|
|
87
|
+
}
|
|
88
|
+
const byEndpoint = Array.from(endpointCounts.entries())
|
|
89
|
+
.map(([endpoint, count]) => ({ endpoint, count }))
|
|
90
|
+
.sort((a, b) => b.count - a.count)
|
|
91
|
+
.slice(0, 20); // Top 20 endpoints
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
...auditStats,
|
|
95
|
+
byEndpoint,
|
|
96
|
+
};
|
|
97
|
+
}),
|
|
98
|
+
|
|
99
|
+
servers: implement(McpsContract.servers).handler(async () => {
|
|
100
|
+
return { servers };
|
|
101
|
+
}),
|
|
102
|
+
|
|
103
|
+
models: implement(McpsContract.models).handler(async () => {
|
|
104
|
+
return { models };
|
|
105
|
+
}),
|
|
106
|
+
|
|
107
|
+
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
|
+
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
|
+
let filteredTools = tools;
|
|
120
|
+
|
|
121
|
+
if (input.server) {
|
|
122
|
+
filteredTools = filteredTools.filter((t) => t.serverName === input.server);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (input.filter) {
|
|
126
|
+
const patterns = input.filter.split(',').map((p) => p.trim());
|
|
127
|
+
filteredTools = filteredTools.filter((t) => patterns.some((pattern) => matchGlob(pattern, t.name)));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return { tools: filteredTools };
|
|
131
|
+
}),
|
|
132
|
+
});
|
|
133
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
// Header name constants for config parsing
|
|
4
|
+
export const HeaderNames = {
|
|
5
|
+
CLS_SECRET_ID: 'X-CLS-SECRET-ID',
|
|
6
|
+
CLS_SECRET_KEY: 'X-CLS-SECRET-KEY',
|
|
7
|
+
CLS_REGION: 'X-CLS-REGION',
|
|
8
|
+
CLS_ENDPOINT: 'X-CLS-ENDPOINT',
|
|
9
|
+
DB_URL: 'X-DB-URL',
|
|
10
|
+
DB_READ_URL: 'X-DB-READ-URL',
|
|
11
|
+
DB_WRITE_URL: 'X-DB-WRITE-URL',
|
|
12
|
+
SERVICE_URL: 'X-SERVICE-URL',
|
|
13
|
+
TOKEN: 'X-TOKEN',
|
|
14
|
+
MCP_URL: 'X-MCP-URL',
|
|
15
|
+
MCP_TYPE: 'X-MCP-TYPE',
|
|
16
|
+
MCP_COMMAND: 'X-MCP-COMMAND',
|
|
17
|
+
// Tool filtering headers
|
|
18
|
+
MCP_READONLY: 'X-MCP-Readonly',
|
|
19
|
+
MCP_INCLUDE: 'X-MCP-Include',
|
|
20
|
+
MCP_EXCLUDE: 'X-MCP-Exclude',
|
|
21
|
+
} as const;
|
|
22
|
+
|
|
23
|
+
// Base server config with common fields
|
|
24
|
+
export const BaseServerConfigSchema = z.object({
|
|
25
|
+
disabled: z.boolean().optional(),
|
|
26
|
+
// Headers can be used as alternative to direct config properties
|
|
27
|
+
headers: z.record(z.string(), z.string()).optional(),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// Tencent CLS config - supports both direct config and headers
|
|
31
|
+
export const TencentClsConfigSchema = BaseServerConfigSchema.extend({
|
|
32
|
+
type: z.literal('tencent-cls'),
|
|
33
|
+
clientId: z.string().optional().describe('Tencent Cloud Secret ID'),
|
|
34
|
+
clientSecret: z.string().optional().describe('Tencent Cloud Secret Key'),
|
|
35
|
+
region: z.string().optional().describe('CLS region'),
|
|
36
|
+
endpoint: z.string().optional().describe('CLS endpoint'),
|
|
37
|
+
});
|
|
38
|
+
export type TencentClsConfig = z.infer<typeof TencentClsConfigSchema>;
|
|
39
|
+
|
|
40
|
+
// SQL config with read/write separation
|
|
41
|
+
export const SqlConfigSchema = BaseServerConfigSchema.extend({
|
|
42
|
+
type: z.literal('sql'),
|
|
43
|
+
dbUrl: z.string().optional().describe('Database URL (for both read and write)'),
|
|
44
|
+
dbReadUrl: z.string().optional().describe('Database URL for read operations'),
|
|
45
|
+
dbWriteUrl: z.string().optional().describe('Database URL for write operations'),
|
|
46
|
+
});
|
|
47
|
+
export type SqlConfig = z.infer<typeof SqlConfigSchema>;
|
|
48
|
+
|
|
49
|
+
// Prometheus config
|
|
50
|
+
export const PrometheusConfigSchema = BaseServerConfigSchema.extend({
|
|
51
|
+
type: z.literal('prometheus'),
|
|
52
|
+
url: z.string().optional().describe('Prometheus server URL'),
|
|
53
|
+
});
|
|
54
|
+
export type PrometheusConfig = z.infer<typeof PrometheusConfigSchema>;
|
|
55
|
+
|
|
56
|
+
// Relay config for proxying to other MCP servers
|
|
57
|
+
export const RelayConfigSchema = BaseServerConfigSchema.extend({
|
|
58
|
+
type: z.literal('relay'),
|
|
59
|
+
url: z.string().optional().describe('Target MCP server URL'),
|
|
60
|
+
transport: z.enum(['http', 'sse']).optional().describe('MCP transport type'),
|
|
61
|
+
command: z.string().optional().describe('Command to run (stdio transport)'),
|
|
62
|
+
args: z.array(z.string()).optional().describe('Command arguments'),
|
|
63
|
+
});
|
|
64
|
+
export type RelayConfig = z.infer<typeof RelayConfigSchema>;
|
|
65
|
+
|
|
66
|
+
// Union of all server configs
|
|
67
|
+
export const ServerConfigSchema = z.discriminatedUnion('type', [
|
|
68
|
+
TencentClsConfigSchema,
|
|
69
|
+
SqlConfigSchema,
|
|
70
|
+
PrometheusConfigSchema,
|
|
71
|
+
RelayConfigSchema,
|
|
72
|
+
]);
|
|
73
|
+
export type ServerConfig = z.infer<typeof ServerConfigSchema>;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Resolve config from headers - merge headers into config properties
|
|
77
|
+
* @deprecated Use McpServerDef.resolveConfig instead. This function is kept for backward compatibility.
|
|
78
|
+
*/
|
|
79
|
+
export function resolveServerConfig<T extends ServerConfig>(config: T): T {
|
|
80
|
+
// This function is now a pass-through - actual resolution is done in McpServerDef.resolveConfig
|
|
81
|
+
// Keeping it for backward compatibility with any external code that might use it
|
|
82
|
+
return config;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ============================================================================
|
|
86
|
+
// Chat/LLM Gateway Configuration
|
|
87
|
+
// ============================================================================
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Adapter types for protocol conversion
|
|
91
|
+
*/
|
|
92
|
+
export const AdapterType = z.enum(['openai', 'anthropic', 'gemini']);
|
|
93
|
+
export type AdapterType = z.infer<typeof AdapterType>;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Adapter endpoint configuration for chat models
|
|
97
|
+
*/
|
|
98
|
+
export const AdapterEndpointConfigSchema = z.object({
|
|
99
|
+
baseUrl: z.string().optional(),
|
|
100
|
+
headers: z.record(z.string(), z.string()).optional(),
|
|
101
|
+
processors: z.array(z.string()).optional(),
|
|
102
|
+
});
|
|
103
|
+
export type AdapterEndpointConfig = z.infer<typeof AdapterEndpointConfigSchema>;
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Model configuration for chat gateway
|
|
107
|
+
*/
|
|
108
|
+
export const ModelConfigSchema = z.object({
|
|
109
|
+
/** Model name or pattern (supports wildcards like "gpt-*") */
|
|
110
|
+
name: z.string(),
|
|
111
|
+
/** Base URL for the API */
|
|
112
|
+
baseUrl: z.string().optional(),
|
|
113
|
+
/** API key (uses Authorization: Bearer header) */
|
|
114
|
+
apiKey: z.string().optional(),
|
|
115
|
+
/** Additional headers */
|
|
116
|
+
headers: z.record(z.string(), z.string()).optional(),
|
|
117
|
+
/** Default adapter to use for protocol conversion */
|
|
118
|
+
adapter: AdapterType.optional(),
|
|
119
|
+
/** Adapter-specific endpoint configurations */
|
|
120
|
+
adapters: z
|
|
121
|
+
.object({
|
|
122
|
+
openai: AdapterEndpointConfigSchema.optional(),
|
|
123
|
+
anthropic: AdapterEndpointConfigSchema.optional(),
|
|
124
|
+
gemini: AdapterEndpointConfigSchema.optional(),
|
|
125
|
+
})
|
|
126
|
+
.optional(),
|
|
127
|
+
/** Processor chain */
|
|
128
|
+
processors: z.array(z.string()).optional(),
|
|
129
|
+
/** Context window size (max total tokens) */
|
|
130
|
+
contextWindow: z.number().optional(),
|
|
131
|
+
/** Max input tokens */
|
|
132
|
+
maxInputTokens: z.number().optional(),
|
|
133
|
+
/** Max output tokens */
|
|
134
|
+
maxOutputTokens: z.number().optional(),
|
|
135
|
+
/** Whether to fetch models from upstream on /v1/models */
|
|
136
|
+
fetchUpstreamModels: z.boolean().optional(),
|
|
137
|
+
});
|
|
138
|
+
export type ModelConfig = z.infer<typeof ModelConfigSchema>;
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Database configuration for audit storage
|
|
142
|
+
*/
|
|
143
|
+
export const DbConfigSchema = z.object({
|
|
144
|
+
/** Path to SQLite database file (default: .mcps.db) */
|
|
145
|
+
path: z.string().optional(),
|
|
146
|
+
});
|
|
147
|
+
export type DbConfig = z.infer<typeof DbConfigSchema>;
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Audit configuration
|
|
151
|
+
*/
|
|
152
|
+
export const AuditConfigSchema = z.object({
|
|
153
|
+
/** Enable audit logging (default: true) */
|
|
154
|
+
enabled: z.boolean().optional(),
|
|
155
|
+
/** Database configuration for audit storage (overrides root db config) */
|
|
156
|
+
db: DbConfigSchema.optional(),
|
|
157
|
+
});
|
|
158
|
+
export type AuditConfig = z.infer<typeof AuditConfigSchema>;
|
|
159
|
+
|
|
160
|
+
// Main config file schema
|
|
161
|
+
export const McpsConfigSchema = z.object({
|
|
162
|
+
servers: z.record(z.string(), ServerConfigSchema).default({}),
|
|
163
|
+
/** Model configurations for chat gateway (array format) */
|
|
164
|
+
models: z.array(ModelConfigSchema).optional(),
|
|
165
|
+
/** Whether to expose server config discovery endpoints (default: false) */
|
|
166
|
+
discoveryConfig: z.boolean().optional(),
|
|
167
|
+
/** Database configuration (shared, used as fallback for audit.db) */
|
|
168
|
+
db: DbConfigSchema.optional(),
|
|
169
|
+
/** Audit configuration */
|
|
170
|
+
audit: AuditConfigSchema.optional(),
|
|
171
|
+
});
|
|
172
|
+
export type McpsConfig = z.infer<typeof McpsConfigSchema>;
|
|
173
|
+
|
|
174
|
+
// Legacy ChatConfig type alias for backwards compatibility
|
|
175
|
+
export type ChatConfig = { models?: ModelConfig[] };
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import consola from 'consola';
|
|
2
|
+
import { Hono } from 'hono';
|
|
3
|
+
import { logger } from 'hono/logger';
|
|
4
|
+
import { LRUCache } from 'lru-cache';
|
|
5
|
+
import { isDevelopment } from 'std-env';
|
|
6
|
+
import type { McpServerInstance } from '@wener/ai/mcp';
|
|
7
|
+
import { findMcpServerDef } from '../providers/findMcpServerDef';
|
|
8
|
+
import { registerApiRoutes } from './api-routes';
|
|
9
|
+
import { auditMiddleware, configureAudit } from './audit';
|
|
10
|
+
import { registerChatRoutes } from './chat-routes';
|
|
11
|
+
import { loadConfig, loadEnvFiles, substituteEnvVars } from './config';
|
|
12
|
+
import { registerMcpRoutes } from './mcp-routes';
|
|
13
|
+
|
|
14
|
+
const log = consola.withTag('mcps');
|
|
15
|
+
|
|
16
|
+
export interface CreateServerOptions {
|
|
17
|
+
cwd?: string;
|
|
18
|
+
port?: number;
|
|
19
|
+
/** Enable server config discovery endpoints (default: false) */
|
|
20
|
+
discoveryConfig?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function createServer(options: CreateServerOptions = {}) {
|
|
24
|
+
const { cwd = process.cwd(), discoveryConfig: discoveryConfigOption } = options;
|
|
25
|
+
|
|
26
|
+
const app = new Hono();
|
|
27
|
+
|
|
28
|
+
// Request logging
|
|
29
|
+
app.use(
|
|
30
|
+
logger((v) => {
|
|
31
|
+
log.debug(v);
|
|
32
|
+
}),
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
// Audit middleware
|
|
36
|
+
app.use(auditMiddleware());
|
|
37
|
+
|
|
38
|
+
// Load .env files first
|
|
39
|
+
loadEnvFiles(cwd);
|
|
40
|
+
|
|
41
|
+
// Load config (with env var substitution)
|
|
42
|
+
const config = substituteEnvVars(loadConfig(cwd));
|
|
43
|
+
// discoveryConfig: CLI option overrides config file, defaults to false
|
|
44
|
+
const discoveryConfig = discoveryConfigOption ?? config.discoveryConfig ?? false;
|
|
45
|
+
|
|
46
|
+
// Log available server types from registry
|
|
47
|
+
const serverDefs = findMcpServerDef();
|
|
48
|
+
log.info(`Available server types: ${serverDefs.map((d) => d.name).join(', ')}`);
|
|
49
|
+
log.info(`Loaded ${Object.keys(config.servers).length} servers from config (discoveryConfig: ${discoveryConfig})`);
|
|
50
|
+
|
|
51
|
+
// Configure audit with lazy DB initialization
|
|
52
|
+
// DB will only be initialized when the first audit event needs persistence
|
|
53
|
+
configureAudit(config.audit, config.db);
|
|
54
|
+
const auditEnabled = config.audit?.enabled !== false;
|
|
55
|
+
const auditDbPath = config.audit?.db?.path ?? config.db?.path ?? '.mcps.db';
|
|
56
|
+
log.info(`Audit configured: enabled=${auditEnabled}, db=${auditDbPath} (lazy init)`);
|
|
57
|
+
|
|
58
|
+
// Unified cache for all MCP servers (both pre-configured and dynamic)
|
|
59
|
+
// Keyed by cache key from def.getCacheKey() or `config::${name}` for pre-configured
|
|
60
|
+
const serverCache = new LRUCache<string, McpServerInstance>({
|
|
61
|
+
max: 100,
|
|
62
|
+
dispose: (value, key) => {
|
|
63
|
+
log.info(`Closing expired MCP server: ${key}`);
|
|
64
|
+
value.close?.().catch((e: unknown) => log.error('Failed to close server', e));
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// =========================================================================
|
|
69
|
+
// Register MCP routes (pre-configured and dynamic endpoints)
|
|
70
|
+
// =========================================================================
|
|
71
|
+
registerMcpRoutes({ app, config, serverCache });
|
|
72
|
+
|
|
73
|
+
// =========================================================================
|
|
74
|
+
// Register Chat/LLM Gateway routes
|
|
75
|
+
// =========================================================================
|
|
76
|
+
registerChatRoutes({ app, config });
|
|
77
|
+
|
|
78
|
+
// =========================================================================
|
|
79
|
+
// Health and info endpoints
|
|
80
|
+
// =========================================================================
|
|
81
|
+
app.get('/health', (c) => c.json({ status: 'ok' }));
|
|
82
|
+
|
|
83
|
+
app.get('/', (c) => {
|
|
84
|
+
return c.json({ message: 'hello' });
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Server info - only available in dev mode or when discoveryConfig is enabled
|
|
88
|
+
if (isDevelopment || discoveryConfig) {
|
|
89
|
+
app.get('/info', (c) => {
|
|
90
|
+
// Dynamically get routes from Hono app
|
|
91
|
+
const routes = app.routes
|
|
92
|
+
.map((r) => ({ method: r.method, path: r.path }))
|
|
93
|
+
.filter((r) => r.method !== 'ALL' || r.path.startsWith('/mcp/'))
|
|
94
|
+
.sort((a, b) => a.path.localeCompare(b.path) || a.method.localeCompare(b.method));
|
|
95
|
+
|
|
96
|
+
// Group routes by category
|
|
97
|
+
const mcpRoutes = routes.filter((r) => r.path.startsWith('/mcp/')).map((r) => r.path);
|
|
98
|
+
const apiRoutes = routes.filter((r) => r.path.startsWith('/api/')).map((r) => `${r.method} ${r.path}`);
|
|
99
|
+
const chatRoutes = routes.filter((r) => r.path.startsWith('/v1/')).map((r) => `${r.method} ${r.path}`);
|
|
100
|
+
|
|
101
|
+
// Get unique MCP paths
|
|
102
|
+
const uniqueMcpPaths = [...new Set(mcpRoutes)];
|
|
103
|
+
|
|
104
|
+
return c.json({
|
|
105
|
+
name: '@wener/mcps',
|
|
106
|
+
version: '0.1.0',
|
|
107
|
+
servers: Object.keys(config.servers),
|
|
108
|
+
serverTypes: findMcpServerDef().map((d) => d.name),
|
|
109
|
+
models: config.models ? config.models.map((m) => m.name) : [],
|
|
110
|
+
endpoints: {
|
|
111
|
+
mcp: uniqueMcpPaths,
|
|
112
|
+
api: [...new Set(apiRoutes)],
|
|
113
|
+
chat: [...new Set(chatRoutes)],
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// =========================================================================
|
|
120
|
+
// Register oRPC API routes
|
|
121
|
+
// =========================================================================
|
|
122
|
+
registerApiRoutes({ app, config });
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Print available endpoints grouped by category
|
|
126
|
+
*/
|
|
127
|
+
const printEndpoints = () => {
|
|
128
|
+
const routes = app.routes;
|
|
129
|
+
const mcpPaths = new Set<string>();
|
|
130
|
+
const apiPaths = new Set<string>();
|
|
131
|
+
const chatPaths = new Set<string>();
|
|
132
|
+
const otherPaths = new Set<string>();
|
|
133
|
+
|
|
134
|
+
for (const route of routes) {
|
|
135
|
+
const path = route.path;
|
|
136
|
+
if (path.startsWith('/mcp/')) {
|
|
137
|
+
mcpPaths.add(path);
|
|
138
|
+
} else if (path.startsWith('/api/')) {
|
|
139
|
+
apiPaths.add(`${route.method} ${path}`);
|
|
140
|
+
} else if (path.startsWith('/v1/')) {
|
|
141
|
+
chatPaths.add(`${route.method} ${path}`);
|
|
142
|
+
} else if (route.method !== 'ALL') {
|
|
143
|
+
otherPaths.add(`${route.method} ${path}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
log.info('Available endpoints:');
|
|
148
|
+
if (mcpPaths.size > 0) {
|
|
149
|
+
log.info(` MCP: ${[...mcpPaths].sort().join(', ')}`);
|
|
150
|
+
}
|
|
151
|
+
if (chatPaths.size > 0) {
|
|
152
|
+
log.info(` Chat: ${[...chatPaths].sort().join(', ')}`);
|
|
153
|
+
}
|
|
154
|
+
if (apiPaths.size > 0) {
|
|
155
|
+
log.info(` API: ${[...apiPaths].sort().join(', ')}`);
|
|
156
|
+
}
|
|
157
|
+
if (otherPaths.size > 0) {
|
|
158
|
+
log.info(` Other: ${[...otherPaths].sort().join(', ')}`);
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
return { app, config, serverCache, printEndpoints };
|
|
163
|
+
}
|