@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,82 @@
1
+ import consola from "consola";
2
+ import { createChatHandler } from "../chat/index.js";
3
+ import { registerAgentRoutes } from "../chat/agent.js";
4
+ const log = consola.withTag("mcps");
5
+ /**
6
+ * Register Chat/LLM Gateway routes
7
+ */ export function registerChatRoutes({ app, config }) {
8
+ if (!config.models || config.models.length === 0) {
9
+ return;
10
+ }
11
+ log.info(`Registering chat gateway with ${config.models.length} models`);
12
+ const chatHandler = createChatHandler({
13
+ config: {
14
+ models: config.models
15
+ }
16
+ });
17
+ app.route("/", chatHandler);
18
+ // Helper function to resolve model config
19
+ function resolveModelConfig(modelName) {
20
+ const models = config.models || [];
21
+ // Exact match first
22
+ for (const modelConfig of models) {
23
+ if (modelConfig.name === modelName)
24
+ return modelConfig;
25
+ }
26
+ // Wildcard match
27
+ for (const modelConfig of models) {
28
+ const pattern = modelConfig.name;
29
+ if (pattern.includes("*")) {
30
+ const regex = new RegExp(`^${pattern.replace(/\*/g, ".*")}$`);
31
+ if (regex.test(modelName))
32
+ return modelConfig;
33
+ }
34
+ }
35
+ return null;
36
+ }
37
+ // Register agent routes with tool loop support
38
+ registerAgentRoutes(app, {
39
+ resolveModelConfig,
40
+ // MCP tools will be loaded on-demand from configured servers
41
+ getMcpTools: async (serverNames) => {
42
+ if (!serverNames || serverNames.length === 0) {
43
+ return {};
44
+ }
45
+ const allTools = {};
46
+ const { createMCPClient } = await import("@ai-sdk/mcp");
47
+ for (const serverName of serverNames) {
48
+ const serverConfig = config.servers[serverName];
49
+ if (!serverConfig) {
50
+ log.warn(`MCP server not found: ${serverName}`);
51
+ continue;
52
+ }
53
+ try {
54
+ // Get the server URL - for configured servers, connect via HTTP
55
+ // The server exposes MCP at /mcp/{serverName}
56
+ const port = process.env.PORT || "3001";
57
+ const serverUrl = `http://127.0.0.1:${port}/mcp/${serverName}`;
58
+ // Create MCP client using SSE transport for HTTP-based MCP
59
+ const { SSEClientTransport } = await import("@modelcontextprotocol/sdk/client/sse.js");
60
+ const client = await createMCPClient({
61
+ transport: new SSEClientTransport(new URL(serverUrl))
62
+ });
63
+ // Get tools from this server
64
+ const serverTools = await client.tools();
65
+ for (const [toolName, tool] of Object.entries(serverTools)) {
66
+ allTools[`${serverName}_${toolName}`] = tool;
67
+ }
68
+ log.info(`Loaded ${Object.keys(serverTools).length} tools from ${serverName}`);
69
+ // Close the client after getting tools
70
+ await client.close();
71
+ }
72
+ catch (err) {
73
+ log.error(`Failed to load tools from ${serverName}:`, err);
74
+ }
75
+ }
76
+ log.info(`Agent loaded ${Object.keys(allTools).length} total tools from servers: ${serverNames.join(", ")}`);
77
+ return allTools;
78
+ }
79
+ });
80
+ log.info("Registered agent routes at /v1/agent/*");
81
+ }
82
+ //# sourceMappingURL=chat-routes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/server/chat-routes.ts"],"sourcesContent":["import consola from 'consola';\nimport type { Hono } from 'hono';\nimport { createChatHandler } from '../chat';\nimport { registerAgentRoutes } from '../chat/agent';\nimport type { McpsConfig, ModelConfig } from './schema';\n\nconst log = consola.withTag('mcps');\n\nexport interface RegisterChatRoutesOptions {\n\tapp: Hono;\n\tconfig: McpsConfig;\n}\n\n/**\n * Register Chat/LLM Gateway routes\n */\nexport function registerChatRoutes({ app, config }: RegisterChatRoutesOptions) {\n\tif (!config.models || config.models.length === 0) {\n\t\treturn;\n\t}\n\n\tlog.info(`Registering chat gateway with ${config.models.length} models`);\n\tconst chatHandler = createChatHandler({ config: { models: config.models } });\n\tapp.route('/', chatHandler);\n\n\t// Helper function to resolve model config\n\tfunction resolveModelConfig(modelName: string): ModelConfig | null {\n\t\tconst models = config.models || [];\n\t\t// Exact match first\n\t\tfor (const modelConfig of models) {\n\t\t\tif (modelConfig.name === modelName) return modelConfig;\n\t\t}\n\t\t// Wildcard match\n\t\tfor (const modelConfig of models) {\n\t\t\tconst pattern = modelConfig.name;\n\t\t\tif (pattern.includes('*')) {\n\t\t\t\tconst regex = new RegExp(`^${pattern.replace(/\\*/g, '.*')}$`);\n\t\t\t\tif (regex.test(modelName)) return modelConfig;\n\t\t\t}\n\t\t}\n\t\treturn null;\n\t}\n\n\t// Register agent routes with tool loop support\n\tregisterAgentRoutes(app, {\n\t\tresolveModelConfig,\n\t\t// MCP tools will be loaded on-demand from configured servers\n\t\tgetMcpTools: async (serverNames) => {\n\t\t\tif (!serverNames || serverNames.length === 0) {\n\t\t\t\treturn {};\n\t\t\t}\n\n\t\t\tconst allTools: Record<string, any> = {};\n\t\t\tconst { createMCPClient } = await import('@ai-sdk/mcp');\n\n\t\t\tfor (const serverName of serverNames) {\n\t\t\t\tconst serverConfig = config.servers[serverName];\n\t\t\t\tif (!serverConfig) {\n\t\t\t\t\tlog.warn(`MCP server not found: ${serverName}`);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\ttry {\n\t\t\t\t\t// Get the server URL - for configured servers, connect via HTTP\n\t\t\t\t\t// The server exposes MCP at /mcp/{serverName}\n\t\t\t\t\tconst port = process.env.PORT || '3001';\n\t\t\t\t\tconst serverUrl = `http://127.0.0.1:${port}/mcp/${serverName}`;\n\n\t\t\t\t\t// Create MCP client using SSE transport for HTTP-based MCP\n\t\t\t\t\tconst { SSEClientTransport } = await import('@modelcontextprotocol/sdk/client/sse.js');\n\t\t\t\t\tconst client = await createMCPClient({\n\t\t\t\t\t\ttransport: new SSEClientTransport(new URL(serverUrl)),\n\t\t\t\t\t});\n\n\t\t\t\t\t// Get tools from this server\n\t\t\t\t\tconst serverTools = await client.tools();\n\t\t\t\t\tfor (const [toolName, tool] of Object.entries(serverTools)) {\n\t\t\t\t\t\tallTools[`${serverName}_${toolName}`] = tool;\n\t\t\t\t\t}\n\n\t\t\t\t\tlog.info(`Loaded ${Object.keys(serverTools).length} tools from ${serverName}`);\n\n\t\t\t\t\t// Close the client after getting tools\n\t\t\t\t\tawait client.close();\n\t\t\t\t} catch (err) {\n\t\t\t\t\tlog.error(`Failed to load tools from ${serverName}:`, err);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tlog.info(`Agent loaded ${Object.keys(allTools).length} total tools from servers: ${serverNames.join(', ')}`);\n\t\t\treturn allTools;\n\t\t},\n\t});\n\tlog.info('Registered agent routes at /v1/agent/*');\n}\n"],"names":["consola","createChatHandler","registerAgentRoutes","log","withTag","registerChatRoutes","app","config","models","length","info","chatHandler","route","resolveModelConfig","modelName","modelConfig","name","pattern","includes","regex","RegExp","replace","test","getMcpTools","serverNames","allTools","createMCPClient","serverName","serverConfig","servers","warn","port","process","env","PORT","serverUrl","SSEClientTransport","client","transport","URL","serverTools","tools","toolName","tool","Object","entries","keys","close","err","error","join"],"mappings":"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"}
@@ -0,0 +1,223 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import consola from "consola";
4
+ import YAML from "yaml";
5
+ import { McpsConfigSchema } from "./schema.js";
6
+ const log = consola.withTag("config");
7
+ /**
8
+ * Load .env files into process.env
9
+ * Priority: .env.local > .env (later files override earlier)
10
+ */ export function loadEnvFiles(cwd = process.cwd()) {
11
+ const envFiles = [
12
+ ".env",
13
+ ".env.local"
14
+ ];
15
+ for (const envFile of envFiles) {
16
+ const filePath = resolve(cwd, envFile);
17
+ if (!existsSync(filePath))
18
+ continue;
19
+ try {
20
+ const content = readFileSync(filePath, "utf-8");
21
+ const lines = content.split("\n");
22
+ for (const line of lines) {
23
+ const trimmed = line.trim();
24
+ // Skip empty lines and comments
25
+ if (!trimmed || trimmed.startsWith("#"))
26
+ continue;
27
+ const eqIndex = trimmed.indexOf("=");
28
+ if (eqIndex === -1)
29
+ continue;
30
+ const key = trimmed.slice(0, eqIndex).trim();
31
+ let value = trimmed.slice(eqIndex + 1).trim();
32
+ // Remove quotes if present
33
+ if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) {
34
+ value = value.slice(1, -1);
35
+ }
36
+ // Only set if not already set (don't override existing env vars)
37
+ if (process.env[key] === undefined) {
38
+ process.env[key] = value;
39
+ log.debug(`Loaded env var ${key} from ${envFile}`);
40
+ }
41
+ }
42
+ log.info(`Loaded env file: ${envFile}`);
43
+ }
44
+ catch (e) {
45
+ log.warn(`Failed to load ${envFile}:`, e);
46
+ }
47
+ }
48
+ }
49
+ /**
50
+ * Parse config file content based on format
51
+ */ function parseConfigContent(content, format) {
52
+ if (format === "yaml") {
53
+ return YAML.parse(content);
54
+ }
55
+ return JSON.parse(content);
56
+ }
57
+ /**
58
+ * Load and parse a single config file
59
+ */ function loadConfigFile(filePath, format) {
60
+ if (!existsSync(filePath)) {
61
+ return null;
62
+ }
63
+ try {
64
+ const content = readFileSync(filePath, "utf-8");
65
+ const parsed = parseConfigContent(content, format);
66
+ const result = McpsConfigSchema.safeParse(parsed);
67
+ if (result.success) {
68
+ log.info(`Loaded config from ${filePath}`);
69
+ return {
70
+ data: result.data,
71
+ path: filePath
72
+ };
73
+ }
74
+ else {
75
+ log.warn(`Invalid config in ${filePath}: ${result.error.message}`);
76
+ return null;
77
+ }
78
+ }
79
+ catch (e) {
80
+ log.error(`Failed to load ${filePath}:`, e);
81
+ return null;
82
+ }
83
+ }
84
+ /**
85
+ * Load config from multiple config files with priority merging
86
+ *
87
+ * Priority (highest to lowest):
88
+ * 1. .mcps.local.yaml/.yml/.json (local overrides for mcps)
89
+ * 2. .mcps.yaml/.yml/.json (base mcps config)
90
+ * 3. .mcp.local.yaml/.yml/.json (local overrides for mcp)
91
+ * 4. .mcp.yaml/.yml/.json (base mcp config)
92
+ *
93
+ * Within each group, YAML has higher priority than JSON.
94
+ * All found configs are merged, with higher priority configs overriding lower ones.
95
+ */ export function loadConfig(cwd = process.cwd()) {
96
+ const config = {
97
+ servers: {}
98
+ };
99
+ // Load configs in reverse priority order (lowest first, so higher priority overwrites)
100
+ // We want: base configs first, then local configs
101
+ // And within each: json first, then yaml (yaml overwrites json)
102
+ const loadOrder = [
103
+ // Base MCP configs (lowest priority)
104
+ {
105
+ path: ".mcp.json",
106
+ format: "json"
107
+ },
108
+ {
109
+ path: ".mcp.yml",
110
+ format: "yaml"
111
+ },
112
+ {
113
+ path: ".mcp.yaml",
114
+ format: "yaml"
115
+ },
116
+ // Local MCP configs
117
+ {
118
+ path: ".mcp.local.json",
119
+ format: "json"
120
+ },
121
+ {
122
+ path: ".mcp.local.yml",
123
+ format: "yaml"
124
+ },
125
+ {
126
+ path: ".mcp.local.yaml",
127
+ format: "yaml"
128
+ },
129
+ // Base MCPS configs
130
+ {
131
+ path: ".mcps.json",
132
+ format: "json"
133
+ },
134
+ {
135
+ path: ".mcps.yml",
136
+ format: "yaml"
137
+ },
138
+ {
139
+ path: ".mcps.yaml",
140
+ format: "yaml"
141
+ },
142
+ // Local MCPS configs (highest priority)
143
+ {
144
+ path: ".mcps.local.json",
145
+ format: "json"
146
+ },
147
+ {
148
+ path: ".mcps.local.yml",
149
+ format: "yaml"
150
+ },
151
+ {
152
+ path: ".mcps.local.yaml",
153
+ format: "yaml"
154
+ }
155
+ ];
156
+ for (const { path: configPath, format } of loadOrder) {
157
+ const fullPath = resolve(cwd, configPath);
158
+ const result = loadConfigFile(fullPath, format);
159
+ if (result) {
160
+ // Merge servers, later configs take precedence
161
+ Object.assign(config.servers, result.data.servers);
162
+ // Merge models config (array format - later configs append/override by name)
163
+ if (result.data.models && result.data.models.length > 0) {
164
+ if (!config.models) {
165
+ config.models = [];
166
+ }
167
+ // Merge by name - later config overrides earlier ones with same name
168
+ for (const model of result.data.models) {
169
+ const existingIndex = config.models.findIndex((m) => m.name === model.name);
170
+ if (existingIndex >= 0) {
171
+ config.models[existingIndex] = model;
172
+ }
173
+ else {
174
+ config.models.push(model);
175
+ }
176
+ }
177
+ }
178
+ }
179
+ }
180
+ // Filter disabled servers
181
+ for (const [name, serverConfig] of Object.entries(config.servers)) {
182
+ if (serverConfig.disabled) {
183
+ log.debug(`Server ${name} is disabled`);
184
+ delete config.servers[name];
185
+ }
186
+ }
187
+ return config;
188
+ }
189
+ /**
190
+ * Substitute environment variables in config values
191
+ * Supports ${VAR_NAME} syntax
192
+ */ export function substituteEnvVars(config) {
193
+ const result = {
194
+ servers: {}
195
+ };
196
+ for (const [name, serverConfig] of Object.entries(config.servers)) {
197
+ result.servers[name] = substituteEnvVarsInObject(serverConfig);
198
+ }
199
+ // Process models config
200
+ if (config.models) {
201
+ result.models = substituteEnvVarsInObject(config.models);
202
+ }
203
+ return result;
204
+ }
205
+ function substituteEnvVarsInObject(obj) {
206
+ if (typeof obj === "string") {
207
+ return obj.replace(/\$\{([^}]+)\}/g, (_, varName) => {
208
+ return process.env[varName] ?? "";
209
+ });
210
+ }
211
+ if (Array.isArray(obj)) {
212
+ return obj.map(substituteEnvVarsInObject);
213
+ }
214
+ if (obj && typeof obj === "object") {
215
+ const result = {};
216
+ for (const [key, value] of Object.entries(obj)) {
217
+ result[key] = substituteEnvVarsInObject(value);
218
+ }
219
+ return result;
220
+ }
221
+ return obj;
222
+ }
223
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/server/config.ts"],"sourcesContent":["import { existsSync, readFileSync } from 'node:fs';\nimport { resolve } from 'node:path';\nimport consola from 'consola';\nimport YAML from 'yaml';\nimport { McpsConfigSchema, type McpsConfig, type ServerConfig } from './schema';\n\nconst log = consola.withTag('config');\n\n/**\n * Load .env files into process.env\n * Priority: .env.local > .env (later files override earlier)\n */\nexport function loadEnvFiles(cwd: string = process.cwd()): void {\n\tconst envFiles = ['.env', '.env.local'];\n\n\tfor (const envFile of envFiles) {\n\t\tconst filePath = resolve(cwd, envFile);\n\t\tif (!existsSync(filePath)) continue;\n\n\t\ttry {\n\t\t\tconst content = readFileSync(filePath, 'utf-8');\n\t\t\tconst lines = content.split('\\n');\n\n\t\t\tfor (const line of lines) {\n\t\t\t\tconst trimmed = line.trim();\n\t\t\t\t// Skip empty lines and comments\n\t\t\t\tif (!trimmed || trimmed.startsWith('#')) continue;\n\n\t\t\t\tconst eqIndex = trimmed.indexOf('=');\n\t\t\t\tif (eqIndex === -1) continue;\n\n\t\t\t\tconst key = trimmed.slice(0, eqIndex).trim();\n\t\t\t\tlet value = trimmed.slice(eqIndex + 1).trim();\n\n\t\t\t\t// Remove quotes if present\n\t\t\t\tif ((value.startsWith('\"') && value.endsWith('\"')) || (value.startsWith(\"'\") && value.endsWith(\"'\"))) {\n\t\t\t\t\tvalue = value.slice(1, -1);\n\t\t\t\t}\n\n\t\t\t\t// Only set if not already set (don't override existing env vars)\n\t\t\t\tif (process.env[key] === undefined) {\n\t\t\t\t\tprocess.env[key] = value;\n\t\t\t\t\tlog.debug(`Loaded env var ${key} from ${envFile}`);\n\t\t\t\t}\n\t\t\t}\n\t\t\tlog.info(`Loaded env file: ${envFile}`);\n\t\t} catch (e) {\n\t\t\tlog.warn(`Failed to load ${envFile}:`, e);\n\t\t}\n\t}\n}\n\n/**\n * Parse config file content based on format\n */\nfunction parseConfigContent(content: string, format: 'yaml' | 'json'): unknown {\n\tif (format === 'yaml') {\n\t\treturn YAML.parse(content);\n\t}\n\treturn JSON.parse(content);\n}\n\n/**\n * Load and parse a single config file\n */\nfunction loadConfigFile(filePath: string, format: 'yaml' | 'json'): { data: McpsConfig; path: string } | null {\n\tif (!existsSync(filePath)) {\n\t\treturn null;\n\t}\n\n\ttry {\n\t\tconst content = readFileSync(filePath, 'utf-8');\n\t\tconst parsed = parseConfigContent(content, format);\n\t\tconst result = McpsConfigSchema.safeParse(parsed);\n\n\t\tif (result.success) {\n\t\t\tlog.info(`Loaded config from ${filePath}`);\n\t\t\treturn { data: result.data, path: filePath };\n\t\t} else {\n\t\t\tlog.warn(`Invalid config in ${filePath}: ${result.error.message}`);\n\t\t\treturn null;\n\t\t}\n\t} catch (e) {\n\t\tlog.error(`Failed to load ${filePath}:`, e);\n\t\treturn null;\n\t}\n}\n\n/**\n * Load config from multiple config files with priority merging\n *\n * Priority (highest to lowest):\n * 1. .mcps.local.yaml/.yml/.json (local overrides for mcps)\n * 2. .mcps.yaml/.yml/.json (base mcps config)\n * 3. .mcp.local.yaml/.yml/.json (local overrides for mcp)\n * 4. .mcp.yaml/.yml/.json (base mcp config)\n *\n * Within each group, YAML has higher priority than JSON.\n * All found configs are merged, with higher priority configs overriding lower ones.\n */\nexport function loadConfig(cwd: string = process.cwd()): McpsConfig {\n\tconst config: McpsConfig = { servers: {} };\n\n\t// Load configs in reverse priority order (lowest first, so higher priority overwrites)\n\t// We want: base configs first, then local configs\n\t// And within each: json first, then yaml (yaml overwrites json)\n\tconst loadOrder = [\n\t\t// Base MCP configs (lowest priority)\n\t\t{ path: '.mcp.json', format: 'json' as const },\n\t\t{ path: '.mcp.yml', format: 'yaml' as const },\n\t\t{ path: '.mcp.yaml', format: 'yaml' as const },\n\t\t// Local MCP configs\n\t\t{ path: '.mcp.local.json', format: 'json' as const },\n\t\t{ path: '.mcp.local.yml', format: 'yaml' as const },\n\t\t{ path: '.mcp.local.yaml', format: 'yaml' as const },\n\t\t// Base MCPS configs\n\t\t{ path: '.mcps.json', format: 'json' as const },\n\t\t{ path: '.mcps.yml', format: 'yaml' as const },\n\t\t{ path: '.mcps.yaml', format: 'yaml' as const },\n\t\t// Local MCPS configs (highest priority)\n\t\t{ path: '.mcps.local.json', format: 'json' as const },\n\t\t{ path: '.mcps.local.yml', format: 'yaml' as const },\n\t\t{ path: '.mcps.local.yaml', format: 'yaml' as const },\n\t];\n\n\tfor (const { path: configPath, format } of loadOrder) {\n\t\tconst fullPath = resolve(cwd, configPath);\n\t\tconst result = loadConfigFile(fullPath, format);\n\t\tif (result) {\n\t\t\t// Merge servers, later configs take precedence\n\t\t\tObject.assign(config.servers, result.data.servers);\n\n\t\t\t// Merge models config (array format - later configs append/override by name)\n\t\t\tif (result.data.models && result.data.models.length > 0) {\n\t\t\t\tif (!config.models) {\n\t\t\t\t\tconfig.models = [];\n\t\t\t\t}\n\t\t\t\t// Merge by name - later config overrides earlier ones with same name\n\t\t\t\tfor (const model of result.data.models) {\n\t\t\t\t\tconst existingIndex = config.models.findIndex((m) => m.name === model.name);\n\t\t\t\t\tif (existingIndex >= 0) {\n\t\t\t\t\t\tconfig.models[existingIndex] = model;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tconfig.models.push(model);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Filter disabled servers\n\tfor (const [name, serverConfig] of Object.entries(config.servers)) {\n\t\tif (serverConfig.disabled) {\n\t\t\tlog.debug(`Server ${name} is disabled`);\n\t\t\tdelete config.servers[name];\n\t\t}\n\t}\n\n\treturn config;\n}\n\n/**\n * Substitute environment variables in config values\n * Supports ${VAR_NAME} syntax\n */\nexport function substituteEnvVars(config: McpsConfig): McpsConfig {\n\tconst result: McpsConfig = { servers: {} };\n\n\tfor (const [name, serverConfig] of Object.entries(config.servers)) {\n\t\tresult.servers[name] = substituteEnvVarsInObject(serverConfig) as ServerConfig;\n\t}\n\n\t// Process models config\n\tif (config.models) {\n\t\tresult.models = substituteEnvVarsInObject(config.models);\n\t}\n\n\treturn result;\n}\n\nfunction substituteEnvVarsInObject<T>(obj: T): T {\n\tif (typeof obj === 'string') {\n\t\treturn obj.replace(/\\$\\{([^}]+)\\}/g, (_, varName) => {\n\t\t\treturn process.env[varName] ?? '';\n\t\t}) as T;\n\t}\n\tif (Array.isArray(obj)) {\n\t\treturn obj.map(substituteEnvVarsInObject) as T;\n\t}\n\tif (obj && typeof obj === 'object') {\n\t\tconst result: Record<string, unknown> = {};\n\t\tfor (const [key, value] of Object.entries(obj)) {\n\t\t\tresult[key] = substituteEnvVarsInObject(value);\n\t\t}\n\t\treturn result as T;\n\t}\n\treturn obj;\n}\n"],"names":["existsSync","readFileSync","resolve","consola","YAML","McpsConfigSchema","log","withTag","loadEnvFiles","cwd","process","envFiles","envFile","filePath","content","lines","split","line","trimmed","trim","startsWith","eqIndex","indexOf","key","slice","value","endsWith","env","undefined","debug","info","e","warn","parseConfigContent","format","parse","JSON","loadConfigFile","parsed","result","safeParse","success","data","path","error","message","loadConfig","config","servers","loadOrder","configPath","fullPath","Object","assign","models","length","model","existingIndex","findIndex","m","name","push","serverConfig","entries","disabled","substituteEnvVars","substituteEnvVarsInObject","obj","replace","_","varName","Array","isArray","map"],"mappings":"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"}
@@ -0,0 +1,97 @@
1
+ import { MikroORM } from "@mikro-orm/core";
2
+ import { SqliteDriver } from "@mikro-orm/sqlite";
3
+ import { ChatRequestEntity } from "../entities/ChatRequestEntity.js";
4
+ import { McpRequestEntity } from "../entities/McpRequestEntity.js";
5
+ import { RequestLogEntity } from "../entities/RequestLogEntity.js";
6
+ import { ResponseEntity } from "../entities/ResponseEntity.js";
7
+ let orm = null;
8
+ let initPromise = null;
9
+ let storedDbConfig;
10
+ /**
11
+ * Get MikroORM configuration
12
+ */ export function getOrmConfig(dbConfig) {
13
+ const dbPath = dbConfig?.path || ".mcps.db";
14
+ return {
15
+ driver: SqliteDriver,
16
+ dbName: dbPath,
17
+ entities: [
18
+ ChatRequestEntity,
19
+ McpRequestEntity,
20
+ RequestLogEntity,
21
+ ResponseEntity
22
+ ],
23
+ // Enable debug in development
24
+ debug: process.env.NODE_ENV === "development",
25
+ // Allow global context for simpler usage
26
+ allowGlobalContext: true
27
+ };
28
+ }
29
+ /**
30
+ * Initialize MikroORM and sync schema
31
+ */ export async function initializeDb(dbConfig) {
32
+ if (orm) {
33
+ return orm;
34
+ }
35
+ // If already initializing, wait for the existing promise
36
+ if (initPromise) {
37
+ return initPromise;
38
+ }
39
+ storedDbConfig = dbConfig;
40
+ initPromise = (async () => {
41
+ const config = getOrmConfig(dbConfig);
42
+ orm = await MikroORM.init(config);
43
+ // Sync schema (create tables if not exist)
44
+ await orm.schema.update();
45
+ return orm;
46
+ })();
47
+ try {
48
+ return await initPromise;
49
+ }
50
+ catch (e) {
51
+ // Reset on failure so retry is possible
52
+ initPromise = null;
53
+ throw e;
54
+ }
55
+ }
56
+ /**
57
+ * Configure DB for lazy initialization (stores config without initializing)
58
+ */ export function configureDb(dbConfig) {
59
+ storedDbConfig = dbConfig;
60
+ }
61
+ /**
62
+ * Ensure DB is initialized (lazy init on first call)
63
+ * Returns the ORM instance, initializing if needed
64
+ */ export async function ensureDbInitialized() {
65
+ if (orm) {
66
+ return orm;
67
+ }
68
+ return initializeDb(storedDbConfig);
69
+ }
70
+ /**
71
+ * Get MikroORM instance (must be initialized first)
72
+ */ export function getOrm() {
73
+ if (!orm) {
74
+ throw new Error("Database not initialized. Call initializeDb() first.");
75
+ }
76
+ return orm;
77
+ }
78
+ /**
79
+ * Get EntityManager
80
+ */ export function getEntityManager() {
81
+ return getOrm().em;
82
+ }
83
+ /**
84
+ * Close database connection
85
+ */ export async function closeDb() {
86
+ if (orm) {
87
+ await orm.close();
88
+ orm = null;
89
+ initPromise = null;
90
+ }
91
+ }
92
+ /**
93
+ * Check if database is initialized
94
+ */ export function isDbInitialized() {
95
+ return orm !== null;
96
+ }
97
+ //# sourceMappingURL=db.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/server/db.ts"],"sourcesContent":["import { MikroORM, type Options } from '@mikro-orm/core';\nimport { SqliteDriver } from '@mikro-orm/sqlite';\nimport { ChatRequestEntity } from '../entities/ChatRequestEntity';\nimport { McpRequestEntity } from '../entities/McpRequestEntity';\nimport { RequestLogEntity } from '../entities/RequestLogEntity';\nimport { ResponseEntity } from '../entities/ResponseEntity';\nimport type { DbConfig } from './schema';\n\nlet orm: MikroORM<SqliteDriver> | null = null;\nlet initPromise: Promise<MikroORM<SqliteDriver>> | null = null;\nlet storedDbConfig: DbConfig | undefined;\n\n/**\n * Get MikroORM configuration\n */\nexport function getOrmConfig(dbConfig?: DbConfig): Options<SqliteDriver> {\n\tconst dbPath = dbConfig?.path || '.mcps.db';\n\n\treturn {\n\t\tdriver: SqliteDriver,\n\t\tdbName: dbPath,\n\t\tentities: [ChatRequestEntity, McpRequestEntity, RequestLogEntity, ResponseEntity],\n\t\t// Enable debug in development\n\t\tdebug: process.env.NODE_ENV === 'development',\n\t\t// Allow global context for simpler usage\n\t\tallowGlobalContext: true,\n\t};\n}\n\n/**\n * Initialize MikroORM and sync schema\n */\nexport async function initializeDb(dbConfig?: DbConfig): Promise<MikroORM<SqliteDriver>> {\n\tif (orm) {\n\t\treturn orm;\n\t}\n\n\t// If already initializing, wait for the existing promise\n\tif (initPromise) {\n\t\treturn initPromise;\n\t}\n\n\tstoredDbConfig = dbConfig;\n\n\tinitPromise = (async () => {\n\t\tconst config = getOrmConfig(dbConfig);\n\t\torm = await MikroORM.init(config);\n\n\t\t// Sync schema (create tables if not exist)\n\t\tawait orm.schema.update();\n\n\t\treturn orm;\n\t})();\n\n\ttry {\n\t\treturn await initPromise;\n\t} catch (e) {\n\t\t// Reset on failure so retry is possible\n\t\tinitPromise = null;\n\t\tthrow e;\n\t}\n}\n\n/**\n * Configure DB for lazy initialization (stores config without initializing)\n */\nexport function configureDb(dbConfig?: DbConfig): void {\n\tstoredDbConfig = dbConfig;\n}\n\n/**\n * Ensure DB is initialized (lazy init on first call)\n * Returns the ORM instance, initializing if needed\n */\nexport async function ensureDbInitialized(): Promise<MikroORM<SqliteDriver>> {\n\tif (orm) {\n\t\treturn orm;\n\t}\n\treturn initializeDb(storedDbConfig);\n}\n\n/**\n * Get MikroORM instance (must be initialized first)\n */\nexport function getOrm(): MikroORM<SqliteDriver> {\n\tif (!orm) {\n\t\tthrow new Error('Database not initialized. Call initializeDb() first.');\n\t}\n\treturn orm;\n}\n\n/**\n * Get EntityManager\n */\nexport function getEntityManager() {\n\treturn getOrm().em;\n}\n\n/**\n * Close database connection\n */\nexport async function closeDb(): Promise<void> {\n\tif (orm) {\n\t\tawait orm.close();\n\t\torm = null;\n\t\tinitPromise = null;\n\t}\n}\n\n/**\n * Check if database is initialized\n */\nexport function isDbInitialized(): boolean {\n\treturn orm !== null;\n}\n"],"names":["MikroORM","SqliteDriver","ChatRequestEntity","McpRequestEntity","RequestLogEntity","ResponseEntity","orm","initPromise","storedDbConfig","getOrmConfig","dbConfig","dbPath","path","driver","dbName","entities","debug","process","env","NODE_ENV","allowGlobalContext","initializeDb","config","init","schema","update","e","configureDb","ensureDbInitialized","getOrm","Error","getEntityManager","em","closeDb","close","isDbInitialized"],"mappings":"AAAA,SAASA,QAAQ,QAAsB,kBAAkB;AACzD,SAASC,YAAY,QAAQ,oBAAoB;AACjD,SAASC,iBAAiB,QAAQ,gCAAgC;AAClE,SAASC,gBAAgB,QAAQ,+BAA+B;AAChE,SAASC,gBAAgB,QAAQ,+BAA+B;AAChE,SAASC,cAAc,QAAQ,6BAA6B;AAG5D,IAAIC,MAAqC;AACzC,IAAIC,cAAsD;AAC1D,IAAIC;AAEJ;;CAEC,GACD,OAAO,SAASC,aAAaC,QAAmB;IAC/C,MAAMC,SAASD,UAAUE,QAAQ;IAEjC,OAAO;QACNC,QAAQZ;QACRa,QAAQH;QACRI,UAAU;YAACb;YAAmBC;YAAkBC;YAAkBC;SAAe;QACjF,8BAA8B;QAC9BW,OAAOC,QAAQC,GAAG,CAACC,QAAQ,KAAK;QAChC,yCAAyC;QACzCC,oBAAoB;IACrB;AACD;AAEA;;CAEC,GACD,OAAO,eAAeC,aAAaX,QAAmB;IACrD,IAAIJ,KAAK;QACR,OAAOA;IACR;IAEA,yDAAyD;IACzD,IAAIC,aAAa;QAChB,OAAOA;IACR;IAEAC,iBAAiBE;IAEjBH,cAAc,AAAC,CAAA;QACd,MAAMe,SAASb,aAAaC;QAC5BJ,MAAM,MAAMN,SAASuB,IAAI,CAACD;QAE1B,2CAA2C;QAC3C,MAAMhB,IAAIkB,MAAM,CAACC,MAAM;QAEvB,OAAOnB;IACR,CAAA;IAEA,IAAI;QACH,OAAO,MAAMC;IACd,EAAE,OAAOmB,GAAG;QACX,wCAAwC;QACxCnB,cAAc;QACd,MAAMmB;IACP;AACD;AAEA;;CAEC,GACD,OAAO,SAASC,YAAYjB,QAAmB;IAC9CF,iBAAiBE;AAClB;AAEA;;;CAGC,GACD,OAAO,eAAekB;IACrB,IAAItB,KAAK;QACR,OAAOA;IACR;IACA,OAAOe,aAAab;AACrB;AAEA;;CAEC,GACD,OAAO,SAASqB;IACf,IAAI,CAACvB,KAAK;QACT,MAAM,IAAIwB,MAAM;IACjB;IACA,OAAOxB;AACR;AAEA;;CAEC,GACD,OAAO,SAASyB;IACf,OAAOF,SAASG,EAAE;AACnB;AAEA;;CAEC,GACD,OAAO,eAAeC;IACrB,IAAI3B,KAAK;QACR,MAAMA,IAAI4B,KAAK;QACf5B,MAAM;QACNC,cAAc;IACf;AACD;AAEA;;CAEC,GACD,OAAO,SAAS4B;IACf,OAAO7B,QAAQ;AAChB"}
@@ -0,0 +1,2 @@
1
+ export { createServer } from "./server.js";
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/server/index.ts"],"sourcesContent":["export { createServer } from './server';\n"],"names":["createServer"],"mappings":"AAAA,SAASA,YAAY,QAAQ,WAAW"}
@@ -0,0 +1,167 @@
1
+ import consola from "consola";
2
+ import { HeaderNames } from "./schema.js";
3
+ const mcpLog = consola.withTag("mcp");
4
+ /**
5
+ * Check if a pattern matches a string (simple glob support)
6
+ */ function matchPattern(pattern, value) {
7
+ // Convert glob to regex
8
+ const regex = new RegExp(`^${pattern.replace(/\*/g, ".*").replace(/\?/g, ".")}$`, "i");
9
+ return regex.test(value);
10
+ }
11
+ /**
12
+ * Filter tools based on filter options
13
+ */ export function filterTools(tools, options) {
14
+ let filtered = tools;
15
+ // Filter by readonly annotation
16
+ if (options.readonlyOnly) {
17
+ filtered = filtered.filter((tool) => {
18
+ const readOnlyHint = tool.annotations?.readOnlyHint;
19
+ return readOnlyHint === true;
20
+ });
21
+ }
22
+ // Filter by include patterns
23
+ if (options.includePatterns && options.includePatterns.length > 0) {
24
+ filtered = filtered.filter((tool) => options.includePatterns?.some((pattern) => matchPattern(pattern, tool.name)));
25
+ }
26
+ // Filter by exclude patterns
27
+ if (options.excludePatterns && options.excludePatterns.length > 0) {
28
+ filtered = filtered.filter((tool) => !options.excludePatterns?.some((pattern) => matchPattern(pattern, tool.name)));
29
+ }
30
+ return filtered;
31
+ }
32
+ /**
33
+ * Create a logging and filtering wrapper for MCP transport
34
+ * - Logs tool calls and their results with duration
35
+ * - Filters tools/list response based on X-MCP-Readonly, X-MCP-Include, X-MCP-Exclude headers
36
+ */ export function createMcpLoggingHandler(transport, serverName) {
37
+ const originalHandleRequest = transport.handleRequest.bind(transport);
38
+ return async (c) => {
39
+ const startTime = Date.now();
40
+ // Get filter headers
41
+ const readonlyHeader = c.req.header(HeaderNames.MCP_READONLY);
42
+ const includeHeader = c.req.header(HeaderNames.MCP_INCLUDE);
43
+ const excludeHeader = c.req.header(HeaderNames.MCP_EXCLUDE);
44
+ const filterOptions = {};
45
+ if (readonlyHeader?.toLowerCase() === "true") {
46
+ filterOptions.readonlyOnly = true;
47
+ }
48
+ if (includeHeader) {
49
+ filterOptions.includePatterns = includeHeader.split(",").map((p) => p.trim());
50
+ }
51
+ if (excludeHeader) {
52
+ filterOptions.excludePatterns = excludeHeader.split(",").map((p) => p.trim());
53
+ }
54
+ const needsFiltering = filterOptions.readonlyOnly || filterOptions.includePatterns || filterOptions.excludePatterns;
55
+ // Log incoming request (clone body to avoid consuming it)
56
+ const contentType = c.req.header("content-type");
57
+ const isPost = c.req.method === "POST";
58
+ let isToolsList = false;
59
+ let isToolsCall = false;
60
+ let toolName = "";
61
+ let parsedBody;
62
+ if (isPost && contentType?.includes("application/json")) {
63
+ try {
64
+ // Clone the request to read body without consuming it
65
+ const clonedReq = c.req.raw.clone();
66
+ parsedBody = await clonedReq.json();
67
+ // JSON-RPC request
68
+ if (parsedBody.method === "tools/call" && parsedBody.params) {
69
+ const { name, arguments: args } = parsedBody.params;
70
+ toolName = name;
71
+ isToolsCall = true;
72
+ mcpLog.info(`→ [${serverName}] tools/call: ${name}`, args ? JSON.stringify(args).slice(0, 200) : "");
73
+ }
74
+ else if (parsedBody.method === "tools/list") {
75
+ mcpLog.debug(`→ [${serverName}] tools/list`);
76
+ isToolsList = true;
77
+ }
78
+ else if (parsedBody.method) {
79
+ mcpLog.debug(`→ [${serverName}] ${parsedBody.method}`);
80
+ }
81
+ }
82
+ catch {
83
+ // Ignore parse errors, let the transport handle them
84
+ }
85
+ }
86
+ // Call original handler with error handling
87
+ let response;
88
+ try {
89
+ response = await originalHandleRequest(c);
90
+ }
91
+ catch (e) {
92
+ const duration = Date.now() - startTime;
93
+ mcpLog.error(`✗ [${serverName}] handler error (${duration}ms):`, e);
94
+ throw e;
95
+ }
96
+ // For tools/call, we need to wait for the SSE stream to complete to get accurate duration
97
+ // Clone the response to read it without consuming the original
98
+ if (isToolsCall && response instanceof Response) {
99
+ const clonedResponse = response.clone();
100
+ // Read the cloned response in the background to measure actual duration
101
+ clonedResponse.text().then(() => {
102
+ const duration = Date.now() - startTime;
103
+ mcpLog.info(`← [${serverName}] tools/call: ${toolName} (${duration}ms)`);
104
+ }).catch(() => {
105
+ // Ignore read errors
106
+ });
107
+ }
108
+ // If this is a tools/list response and we need filtering, intercept and modify
109
+ if (isToolsList && needsFiltering && response instanceof Response) {
110
+ try {
111
+ const responseText = await response.clone().text();
112
+ const contentTypeHeader = response.headers.get("content-type") || "";
113
+ // Handle SSE format (event: message\ndata: {...})
114
+ if (contentTypeHeader.includes("text/event-stream")) {
115
+ // Parse SSE data - find the data line
116
+ const lines = responseText.split("\n");
117
+ let jsonData = "";
118
+ for (const line of lines) {
119
+ if (line.startsWith("data: ")) {
120
+ jsonData = line.slice(6);
121
+ break;
122
+ }
123
+ }
124
+ if (jsonData) {
125
+ const responseData = JSON.parse(jsonData);
126
+ if (responseData.result?.tools && Array.isArray(responseData.result.tools)) {
127
+ const originalCount = responseData.result.tools.length;
128
+ responseData.result.tools = filterTools(responseData.result.tools, filterOptions);
129
+ const filteredCount = responseData.result.tools.length;
130
+ if (filteredCount !== originalCount) {
131
+ mcpLog.info(`← [${serverName}] tools/list: filtered ${originalCount} → ${filteredCount} tools (readonly=${filterOptions.readonlyOnly || false})`);
132
+ }
133
+ // Return new SSE response with filtered tools
134
+ const newSseData = `event: message\ndata: ${JSON.stringify(responseData)}\n\n`;
135
+ return new Response(newSseData, {
136
+ status: response.status,
137
+ headers: response.headers
138
+ });
139
+ }
140
+ }
141
+ }
142
+ else {
143
+ // Handle plain JSON response
144
+ const responseData = JSON.parse(responseText);
145
+ if (responseData.result?.tools && Array.isArray(responseData.result.tools)) {
146
+ const originalCount = responseData.result.tools.length;
147
+ responseData.result.tools = filterTools(responseData.result.tools, filterOptions);
148
+ const filteredCount = responseData.result.tools.length;
149
+ if (filteredCount !== originalCount) {
150
+ mcpLog.info(`← [${serverName}] tools/list: filtered ${originalCount} → ${filteredCount} tools (readonly=${filterOptions.readonlyOnly || false})`);
151
+ }
152
+ return new Response(JSON.stringify(responseData), {
153
+ status: response.status,
154
+ headers: response.headers
155
+ });
156
+ }
157
+ }
158
+ }
159
+ catch (e) {
160
+ // If we can't parse/modify, return original
161
+ mcpLog.warn(`[${serverName}] Failed to filter tools/list response: ${e instanceof Error ? e.message : e}`);
162
+ }
163
+ }
164
+ return response;
165
+ };
166
+ }
167
+ //# sourceMappingURL=mcp-handler.js.map