@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.
Files changed (141) hide show
  1. package/LICENSE +21 -0
  2. package/dist/index.mjs +15 -0
  3. package/dist/mcps-cli.mjs +174727 -0
  4. package/lib/chat/agent.js +187 -0
  5. package/lib/chat/agent.js.map +1 -0
  6. package/lib/chat/audit.js +238 -0
  7. package/lib/chat/audit.js.map +1 -0
  8. package/lib/chat/converters.js +467 -0
  9. package/lib/chat/converters.js.map +1 -0
  10. package/lib/chat/handler.js +1068 -0
  11. package/lib/chat/handler.js.map +1 -0
  12. package/lib/chat/index.js +12 -0
  13. package/lib/chat/index.js.map +1 -0
  14. package/lib/chat/types.js +35 -0
  15. package/lib/chat/types.js.map +1 -0
  16. package/lib/contracts/AuditContract.js +85 -0
  17. package/lib/contracts/AuditContract.js.map +1 -0
  18. package/lib/contracts/McpsContract.js +113 -0
  19. package/lib/contracts/McpsContract.js.map +1 -0
  20. package/lib/contracts/index.js +3 -0
  21. package/lib/contracts/index.js.map +1 -0
  22. package/lib/dev.server.js +7 -0
  23. package/lib/dev.server.js.map +1 -0
  24. package/lib/entities/ChatRequestEntity.js +318 -0
  25. package/lib/entities/ChatRequestEntity.js.map +1 -0
  26. package/lib/entities/McpRequestEntity.js +271 -0
  27. package/lib/entities/McpRequestEntity.js.map +1 -0
  28. package/lib/entities/RequestLogEntity.js +177 -0
  29. package/lib/entities/RequestLogEntity.js.map +1 -0
  30. package/lib/entities/ResponseEntity.js +150 -0
  31. package/lib/entities/ResponseEntity.js.map +1 -0
  32. package/lib/entities/index.js +11 -0
  33. package/lib/entities/index.js.map +1 -0
  34. package/lib/entities/types.js +11 -0
  35. package/lib/entities/types.js.map +1 -0
  36. package/lib/index.js +3 -0
  37. package/lib/index.js.map +1 -0
  38. package/lib/mcps-cli.js +44 -0
  39. package/lib/mcps-cli.js.map +1 -0
  40. package/lib/providers/McpServerHandlerDef.js +40 -0
  41. package/lib/providers/McpServerHandlerDef.js.map +1 -0
  42. package/lib/providers/findMcpServerDef.js +26 -0
  43. package/lib/providers/findMcpServerDef.js.map +1 -0
  44. package/lib/providers/prometheus/def.js +24 -0
  45. package/lib/providers/prometheus/def.js.map +1 -0
  46. package/lib/providers/prometheus/index.js +2 -0
  47. package/lib/providers/prometheus/index.js.map +1 -0
  48. package/lib/providers/relay/def.js +32 -0
  49. package/lib/providers/relay/def.js.map +1 -0
  50. package/lib/providers/relay/index.js +2 -0
  51. package/lib/providers/relay/index.js.map +1 -0
  52. package/lib/providers/sql/def.js +31 -0
  53. package/lib/providers/sql/def.js.map +1 -0
  54. package/lib/providers/sql/index.js +2 -0
  55. package/lib/providers/sql/index.js.map +1 -0
  56. package/lib/providers/tencent-cls/def.js +44 -0
  57. package/lib/providers/tencent-cls/def.js.map +1 -0
  58. package/lib/providers/tencent-cls/index.js +2 -0
  59. package/lib/providers/tencent-cls/index.js.map +1 -0
  60. package/lib/scripts/bundle.js +90 -0
  61. package/lib/scripts/bundle.js.map +1 -0
  62. package/lib/server/api-routes.js +96 -0
  63. package/lib/server/api-routes.js.map +1 -0
  64. package/lib/server/audit.js +274 -0
  65. package/lib/server/audit.js.map +1 -0
  66. package/lib/server/chat-routes.js +82 -0
  67. package/lib/server/chat-routes.js.map +1 -0
  68. package/lib/server/config.js +223 -0
  69. package/lib/server/config.js.map +1 -0
  70. package/lib/server/db.js +97 -0
  71. package/lib/server/db.js.map +1 -0
  72. package/lib/server/index.js +2 -0
  73. package/lib/server/index.js.map +1 -0
  74. package/lib/server/mcp-handler.js +167 -0
  75. package/lib/server/mcp-handler.js.map +1 -0
  76. package/lib/server/mcp-routes.js +112 -0
  77. package/lib/server/mcp-routes.js.map +1 -0
  78. package/lib/server/mcps-router.js +119 -0
  79. package/lib/server/mcps-router.js.map +1 -0
  80. package/lib/server/schema.js +129 -0
  81. package/lib/server/schema.js.map +1 -0
  82. package/lib/server/server.js +166 -0
  83. package/lib/server/server.js.map +1 -0
  84. package/lib/web/ChatPage.js +827 -0
  85. package/lib/web/ChatPage.js.map +1 -0
  86. package/lib/web/McpInspectorPage.js +214 -0
  87. package/lib/web/McpInspectorPage.js.map +1 -0
  88. package/lib/web/ServersPage.js +93 -0
  89. package/lib/web/ServersPage.js.map +1 -0
  90. package/lib/web/main.js +541 -0
  91. package/lib/web/main.js.map +1 -0
  92. package/package.json +83 -0
  93. package/src/chat/agent.ts +240 -0
  94. package/src/chat/audit.ts +377 -0
  95. package/src/chat/converters.test.ts +325 -0
  96. package/src/chat/converters.ts +459 -0
  97. package/src/chat/handler.test.ts +137 -0
  98. package/src/chat/handler.ts +1233 -0
  99. package/src/chat/index.ts +16 -0
  100. package/src/chat/types.ts +72 -0
  101. package/src/contracts/AuditContract.ts +93 -0
  102. package/src/contracts/McpsContract.ts +141 -0
  103. package/src/contracts/index.ts +18 -0
  104. package/src/dev.server.ts +7 -0
  105. package/src/entities/ChatRequestEntity.ts +157 -0
  106. package/src/entities/McpRequestEntity.ts +149 -0
  107. package/src/entities/RequestLogEntity.ts +78 -0
  108. package/src/entities/ResponseEntity.ts +75 -0
  109. package/src/entities/index.ts +12 -0
  110. package/src/entities/types.ts +188 -0
  111. package/src/index.ts +1 -0
  112. package/src/mcps-cli.ts +59 -0
  113. package/src/providers/McpServerHandlerDef.ts +105 -0
  114. package/src/providers/findMcpServerDef.ts +31 -0
  115. package/src/providers/prometheus/def.ts +21 -0
  116. package/src/providers/prometheus/index.ts +1 -0
  117. package/src/providers/relay/def.ts +31 -0
  118. package/src/providers/relay/index.ts +1 -0
  119. package/src/providers/relay/relay.test.ts +47 -0
  120. package/src/providers/sql/def.ts +33 -0
  121. package/src/providers/sql/index.ts +1 -0
  122. package/src/providers/tencent-cls/def.ts +38 -0
  123. package/src/providers/tencent-cls/index.ts +1 -0
  124. package/src/scripts/bundle.ts +82 -0
  125. package/src/server/api-routes.ts +98 -0
  126. package/src/server/audit.ts +310 -0
  127. package/src/server/chat-routes.ts +95 -0
  128. package/src/server/config.test.ts +162 -0
  129. package/src/server/config.ts +198 -0
  130. package/src/server/db.ts +115 -0
  131. package/src/server/index.ts +1 -0
  132. package/src/server/mcp-handler.ts +209 -0
  133. package/src/server/mcp-routes.ts +133 -0
  134. package/src/server/mcps-router.ts +133 -0
  135. package/src/server/schema.ts +175 -0
  136. package/src/server/server.ts +163 -0
  137. package/src/web/ChatPage.tsx +1005 -0
  138. package/src/web/McpInspectorPage.tsx +254 -0
  139. package/src/web/ServersPage.tsx +139 -0
  140. package/src/web/main.tsx +600 -0
  141. 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
+ }