@agimon-ai/mcp-proxy 0.4.0

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.
@@ -0,0 +1,4975 @@
1
+ //#region rolldown:runtime
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") {
10
+ for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
11
+ key = keys[i];
12
+ if (!__hasOwnProp.call(to, key) && key !== except) {
13
+ __defProp(to, key, {
14
+ get: ((k) => from[k]).bind(null, key),
15
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
16
+ });
17
+ }
18
+ }
19
+ }
20
+ return to;
21
+ };
22
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
23
+ value: mod,
24
+ enumerable: true
25
+ }) : target, mod));
26
+
27
+ //#endregion
28
+ let __modelcontextprotocol_sdk_server_index_js = require("@modelcontextprotocol/sdk/server/index.js");
29
+ let __modelcontextprotocol_sdk_types_js = require("@modelcontextprotocol/sdk/types.js");
30
+ let node_crypto = require("node:crypto");
31
+ let node_fs_promises = require("node:fs/promises");
32
+ let node_os = require("node:os");
33
+ let node_path = require("node:path");
34
+ let js_yaml = require("js-yaml");
35
+ js_yaml = __toESM(js_yaml);
36
+ let zod = require("zod");
37
+ let node_fs = require("node:fs");
38
+ let liquidjs = require("liquidjs");
39
+ let __agimon_ai_log_sink_mcp = require("@agimon-ai/log-sink-mcp");
40
+ let __modelcontextprotocol_sdk_client_index_js = require("@modelcontextprotocol/sdk/client/index.js");
41
+ let __modelcontextprotocol_sdk_client_sse_js = require("@modelcontextprotocol/sdk/client/sse.js");
42
+ let __modelcontextprotocol_sdk_client_stdio_js = require("@modelcontextprotocol/sdk/client/stdio.js");
43
+ let __modelcontextprotocol_sdk_client_streamableHttp_js = require("@modelcontextprotocol/sdk/client/streamableHttp.js");
44
+ let node_child_process = require("node:child_process");
45
+ let node_events = require("node:events");
46
+ let node_util = require("node:util");
47
+ let __modelcontextprotocol_sdk_server_streamableHttp_js = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
48
+ let express = require("express");
49
+ express = __toESM(express);
50
+ let __modelcontextprotocol_sdk_server_sse_js = require("@modelcontextprotocol/sdk/server/sse.js");
51
+ let __modelcontextprotocol_sdk_server_stdio_js = require("@modelcontextprotocol/sdk/server/stdio.js");
52
+
53
+ //#region src/utils/mcpConfigSchema.ts
54
+ /**
55
+ * mcpConfigSchema Utilities
56
+ *
57
+ * DESIGN PATTERNS:
58
+ * - Schema-based validation using Zod
59
+ * - Pure functions with no side effects
60
+ * - Type inference from schemas
61
+ * - Transformation from Claude Code format to internal format
62
+ *
63
+ * CODING STANDARDS:
64
+ * - Export individual functions and schemas
65
+ * - Use descriptive function names with verbs
66
+ * - Add JSDoc comments for complex logic
67
+ * - Keep functions small and focused
68
+ *
69
+ * AVOID:
70
+ * - Side effects (mutating external state)
71
+ * - Stateful logic (use services for state)
72
+ * - Loosely typed configs (use Zod for runtime safety)
73
+ */
74
+ /**
75
+ * Interpolate environment variables in a string
76
+ * Supports ${VAR_NAME} syntax
77
+ *
78
+ * This function replaces environment variable placeholders with their actual values.
79
+ * If an environment variable is not defined, the placeholder is kept as-is and a warning is logged.
80
+ *
81
+ * Examples:
82
+ * - "${HOME}/data" → "/Users/username/data"
83
+ * - "Bearer ${API_KEY}" → "Bearer sk-abc123xyz"
84
+ * - "${DATABASE_URL}/api" → "postgres://localhost:5432/mydb/api"
85
+ *
86
+ * Supported locations for environment variable interpolation:
87
+ * - Stdio config: command, args, env values
88
+ * - HTTP/SSE config: url, header values
89
+ *
90
+ * @param value - String that may contain environment variable references
91
+ * @returns String with environment variables replaced
92
+ */
93
+ function interpolateEnvVars(value) {
94
+ return value.replace(/\$\{([^}]+)\}/g, (_, varName) => {
95
+ const envValue = process.env[varName];
96
+ if (envValue === void 0) {
97
+ console.warn(`Environment variable ${varName} is not defined, keeping placeholder`);
98
+ return `\${${varName}}`;
99
+ }
100
+ return envValue;
101
+ });
102
+ }
103
+ /**
104
+ * Recursively interpolate environment variables in an object
105
+ *
106
+ * @param obj - Object that may contain environment variable references
107
+ * @returns Object with environment variables replaced
108
+ */
109
+ function interpolateEnvVarsInObject(obj) {
110
+ if (typeof obj === "string") return interpolateEnvVars(obj);
111
+ if (Array.isArray(obj)) return obj.map((item) => interpolateEnvVarsInObject(item));
112
+ if (obj !== null && typeof obj === "object") {
113
+ const result = {};
114
+ for (const [key, value] of Object.entries(obj)) result[key] = interpolateEnvVarsInObject(value);
115
+ return result;
116
+ }
117
+ return obj;
118
+ }
119
+ /**
120
+ * Private IP range patterns for SSRF protection
121
+ * Covers both IPv4 and IPv6 loopback, private, and link-local ranges
122
+ */
123
+ const PRIVATE_IP_PATTERNS = [
124
+ /^127\./,
125
+ /^10\./,
126
+ /^172\.(1[6-9]|2\d|3[01])\./,
127
+ /^192\.168\./,
128
+ /^169\.254\./,
129
+ /^0\./,
130
+ /^224\./,
131
+ /^240\./,
132
+ /^localhost$/i,
133
+ /^.*\.localhost$/i,
134
+ /^\[::\]/,
135
+ /^\[::1\]/,
136
+ /^\[0:0:0:0:0:0:0:1\]/,
137
+ /^\[0{1,4}:0{1,4}:0{1,4}:0{1,4}:0{1,4}:0{1,4}:0{1,4}:1\]/i,
138
+ /^\[fe80:/i,
139
+ /^\[fc00:/i,
140
+ /^\[fd00:/i,
141
+ /^\[::ffff:127\./i,
142
+ /^\[::ffff:7f[0-9a-f]{2}:/i,
143
+ /^\[::ffff:10\./i,
144
+ /^\[::ffff:a[0-9a-f]{2}:/i,
145
+ /^\[::ffff:172\.(1[6-9]|2\d|3[01])\./i,
146
+ /^\[::ffff:ac1[0-9a-f]:/i,
147
+ /^\[::ffff:192\.168\./i,
148
+ /^\[::ffff:c0a8:/i,
149
+ /^\[::ffff:169\.254\./i,
150
+ /^\[::ffff:a9fe:/i,
151
+ /^\[::ffff:0\./i,
152
+ /^\[::127\./i,
153
+ /^\[::7f[0-9a-f]{2}:/i,
154
+ /^\[::10\./i,
155
+ /^\[::a[0-9a-f]{2}:/i,
156
+ /^\[::192\.168\./i,
157
+ /^\[::c0a8:/i
158
+ ];
159
+ /**
160
+ * Validate URL for SSRF protection
161
+ *
162
+ * @param url - The URL to validate (after env var interpolation)
163
+ * @param security - Security settings
164
+ * @throws Error if URL is unsafe
165
+ */
166
+ function validateUrlSecurity(url, security) {
167
+ const allowPrivateIPs = security?.allowPrivateIPs ?? false;
168
+ const enforceHttps = security?.enforceHttps ?? true;
169
+ let parsedUrl;
170
+ try {
171
+ parsedUrl = new URL(url);
172
+ } catch (_error) {
173
+ throw new Error(`Invalid URL format: ${url}`);
174
+ }
175
+ const protocol = parsedUrl.protocol.replace(":", "");
176
+ if (enforceHttps && protocol !== "https") throw new Error(`HTTPS is required for security. URL uses '${protocol}://'. Set security.enforceHttps: false to allow HTTP.`);
177
+ if (protocol !== "http" && protocol !== "https") throw new Error(`Invalid URL protocol '${protocol}://'. Only http:// and https:// are allowed.`);
178
+ if (!allowPrivateIPs) {
179
+ const hostname = parsedUrl.hostname.toLowerCase();
180
+ if (PRIVATE_IP_PATTERNS.some((pattern) => pattern.test(hostname))) throw new Error(`Private IP addresses and localhost are blocked for security (${hostname}). Set security.allowPrivateIPs: true to allow internal networks.`);
181
+ }
182
+ }
183
+ /**
184
+ * Validate a remote config source against its validation rules
185
+ *
186
+ * @param source - Remote config source with validation rules
187
+ * @throws Error if validation fails
188
+ */
189
+ function validateRemoteConfigSource(source) {
190
+ const interpolatedUrl = interpolateEnvVars(source.url);
191
+ validateUrlSecurity(interpolatedUrl, source.security);
192
+ if (!source.validation) return;
193
+ if (source.validation.url) {
194
+ if (!new RegExp(source.validation.url).test(interpolatedUrl)) throw new Error(`Remote config URL "${interpolatedUrl}" does not match validation pattern: ${source.validation.url}`);
195
+ }
196
+ if (source.validation.headers && Object.keys(source.validation.headers).length > 0) {
197
+ if (!source.headers) {
198
+ const requiredHeaders = Object.keys(source.validation.headers);
199
+ throw new Error(`Remote config is missing required headers: ${requiredHeaders.join(", ")}`);
200
+ }
201
+ for (const [headerName, pattern] of Object.entries(source.validation.headers)) {
202
+ if (!(headerName in source.headers)) throw new Error(`Remote config is missing required header: ${headerName}`);
203
+ const interpolatedHeaderValue = interpolateEnvVars(source.headers[headerName]);
204
+ if (!new RegExp(pattern).test(interpolatedHeaderValue)) throw new Error(`Remote config header "${headerName}" value "${interpolatedHeaderValue}" does not match validation pattern: ${pattern}`);
205
+ }
206
+ }
207
+ }
208
+ /**
209
+ * Claude Code / Claude Desktop standard MCP config format
210
+ * This is the format users write in their config files
211
+ */
212
+ /**
213
+ * Prompt skill configuration schema
214
+ * Converts a prompt to an executable skill
215
+ */
216
+ const PromptSkillConfigSchema = zod.z.object({
217
+ name: zod.z.string(),
218
+ description: zod.z.string(),
219
+ folder: zod.z.string().optional()
220
+ });
221
+ /**
222
+ * Prompt configuration schema
223
+ * Supports converting prompts to skills
224
+ */
225
+ const PromptConfigSchema = zod.z.object({ skill: PromptSkillConfigSchema.optional() });
226
+ const AdditionalConfigSchema = zod.z.object({
227
+ instruction: zod.z.string().optional(),
228
+ toolBlacklist: zod.z.array(zod.z.string()).optional(),
229
+ omitToolDescription: zod.z.boolean().optional(),
230
+ prompts: zod.z.record(zod.z.string(), PromptConfigSchema).optional()
231
+ }).optional();
232
+ const ClaudeCodeStdioServerSchema = zod.z.object({
233
+ command: zod.z.string(),
234
+ args: zod.z.array(zod.z.string()).optional(),
235
+ env: zod.z.record(zod.z.string(), zod.z.string()).optional(),
236
+ disabled: zod.z.boolean().optional(),
237
+ instruction: zod.z.string().optional(),
238
+ timeout: zod.z.number().positive().optional(),
239
+ requestTimeout: zod.z.number().positive().optional(),
240
+ config: AdditionalConfigSchema
241
+ });
242
+ const ClaudeCodeHttpServerSchema = zod.z.object({
243
+ url: zod.z.string().url(),
244
+ headers: zod.z.record(zod.z.string(), zod.z.string()).optional(),
245
+ type: zod.z.enum(["http", "sse"]).optional(),
246
+ disabled: zod.z.boolean().optional(),
247
+ instruction: zod.z.string().optional(),
248
+ timeout: zod.z.number().positive().optional(),
249
+ requestTimeout: zod.z.number().positive().optional(),
250
+ config: AdditionalConfigSchema
251
+ });
252
+ const ClaudeCodeServerConfigSchema = zod.z.union([ClaudeCodeStdioServerSchema, ClaudeCodeHttpServerSchema]);
253
+ const RemoteConfigValidationSchema = zod.z.object({
254
+ url: zod.z.string().optional(),
255
+ headers: zod.z.record(zod.z.string(), zod.z.string()).optional()
256
+ }).optional();
257
+ const RemoteConfigSecuritySchema = zod.z.object({
258
+ allowPrivateIPs: zod.z.boolean().optional(),
259
+ enforceHttps: zod.z.boolean().optional()
260
+ }).optional();
261
+ const RemoteConfigSourceSchema = zod.z.object({
262
+ url: zod.z.string(),
263
+ headers: zod.z.record(zod.z.string(), zod.z.string()).optional(),
264
+ validation: RemoteConfigValidationSchema,
265
+ security: RemoteConfigSecuritySchema,
266
+ mergeStrategy: zod.z.enum([
267
+ "local-priority",
268
+ "remote-priority",
269
+ "merge-deep"
270
+ ]).optional()
271
+ });
272
+ /**
273
+ * Skills configuration schema
274
+ */
275
+ const SkillsConfigSchema = zod.z.object({ paths: zod.z.array(zod.z.string()) });
276
+ /**
277
+ * Full Claude Code MCP configuration schema
278
+ */
279
+ const ClaudeCodeMcpConfigSchema = zod.z.object({
280
+ id: zod.z.string().optional(),
281
+ mcpServers: zod.z.record(zod.z.string(), ClaudeCodeServerConfigSchema),
282
+ remoteConfigs: zod.z.array(RemoteConfigSourceSchema).optional(),
283
+ skills: SkillsConfigSchema.optional()
284
+ });
285
+ /**
286
+ * Internal MCP config format
287
+ * This is the normalized format used internally by the proxy
288
+ */
289
+ const McpStdioConfigSchema = zod.z.object({
290
+ command: zod.z.string(),
291
+ args: zod.z.array(zod.z.string()).optional(),
292
+ env: zod.z.record(zod.z.string(), zod.z.string()).optional()
293
+ });
294
+ const McpHttpConfigSchema = zod.z.object({
295
+ url: zod.z.string().url(),
296
+ headers: zod.z.record(zod.z.string(), zod.z.string()).optional()
297
+ });
298
+ const McpSseConfigSchema = zod.z.object({
299
+ url: zod.z.string().url(),
300
+ headers: zod.z.record(zod.z.string(), zod.z.string()).optional()
301
+ });
302
+ /**
303
+ * Internal prompt skill configuration schema
304
+ */
305
+ const InternalPromptSkillConfigSchema = zod.z.object({
306
+ name: zod.z.string(),
307
+ description: zod.z.string(),
308
+ folder: zod.z.string().optional()
309
+ });
310
+ /**
311
+ * Internal prompt configuration schema
312
+ */
313
+ const InternalPromptConfigSchema = zod.z.object({ skill: InternalPromptSkillConfigSchema.optional() });
314
+ const McpServerConfigSchema = zod.z.discriminatedUnion("transport", [
315
+ zod.z.object({
316
+ name: zod.z.string(),
317
+ instruction: zod.z.string().optional(),
318
+ toolBlacklist: zod.z.array(zod.z.string()).optional(),
319
+ omitToolDescription: zod.z.boolean().optional(),
320
+ prompts: zod.z.record(zod.z.string(), InternalPromptConfigSchema).optional(),
321
+ timeout: zod.z.number().positive().optional(),
322
+ requestTimeout: zod.z.number().positive().optional(),
323
+ transport: zod.z.literal("stdio"),
324
+ config: McpStdioConfigSchema
325
+ }),
326
+ zod.z.object({
327
+ name: zod.z.string(),
328
+ instruction: zod.z.string().optional(),
329
+ toolBlacklist: zod.z.array(zod.z.string()).optional(),
330
+ omitToolDescription: zod.z.boolean().optional(),
331
+ prompts: zod.z.record(zod.z.string(), InternalPromptConfigSchema).optional(),
332
+ timeout: zod.z.number().positive().optional(),
333
+ requestTimeout: zod.z.number().positive().optional(),
334
+ transport: zod.z.literal("http"),
335
+ config: McpHttpConfigSchema
336
+ }),
337
+ zod.z.object({
338
+ name: zod.z.string(),
339
+ instruction: zod.z.string().optional(),
340
+ toolBlacklist: zod.z.array(zod.z.string()).optional(),
341
+ omitToolDescription: zod.z.boolean().optional(),
342
+ prompts: zod.z.record(zod.z.string(), InternalPromptConfigSchema).optional(),
343
+ timeout: zod.z.number().positive().optional(),
344
+ requestTimeout: zod.z.number().positive().optional(),
345
+ transport: zod.z.literal("sse"),
346
+ config: McpSseConfigSchema
347
+ })
348
+ ]);
349
+ /**
350
+ * Full internal MCP configuration schema
351
+ */
352
+ const InternalMcpConfigSchema = zod.z.object({
353
+ id: zod.z.string().optional(),
354
+ mcpServers: zod.z.record(zod.z.string(), McpServerConfigSchema),
355
+ skills: SkillsConfigSchema.optional()
356
+ });
357
+ /**
358
+ * Transform Claude Code config to internal format
359
+ * Converts standard Claude Code MCP configuration to normalized internal format
360
+ *
361
+ * @param claudeConfig - Claude Code format configuration
362
+ * @returns Internal format configuration
363
+ */
364
+ function transformClaudeCodeConfig(claudeConfig) {
365
+ const transformedServers = {};
366
+ for (const [serverName, serverConfig] of Object.entries(claudeConfig.mcpServers)) {
367
+ if ("disabled" in serverConfig && serverConfig.disabled === true) continue;
368
+ if ("command" in serverConfig) {
369
+ const stdioConfig = serverConfig;
370
+ const interpolatedCommand = interpolateEnvVars(stdioConfig.command);
371
+ const interpolatedArgs = stdioConfig.args?.map((arg) => interpolateEnvVars(arg));
372
+ const interpolatedEnv = stdioConfig.env ? interpolateEnvVarsInObject(stdioConfig.env) : void 0;
373
+ transformedServers[serverName] = {
374
+ name: serverName,
375
+ instruction: stdioConfig.instruction || stdioConfig.config?.instruction,
376
+ toolBlacklist: stdioConfig.config?.toolBlacklist,
377
+ omitToolDescription: stdioConfig.config?.omitToolDescription,
378
+ prompts: stdioConfig.config?.prompts,
379
+ timeout: stdioConfig.timeout,
380
+ requestTimeout: stdioConfig.requestTimeout,
381
+ transport: "stdio",
382
+ config: {
383
+ command: interpolatedCommand,
384
+ args: interpolatedArgs,
385
+ env: interpolatedEnv
386
+ }
387
+ };
388
+ } else if ("url" in serverConfig) {
389
+ const httpConfig = serverConfig;
390
+ const transport = httpConfig.type === "sse" ? "sse" : "http";
391
+ const interpolatedUrl = interpolateEnvVars(httpConfig.url);
392
+ const interpolatedHeaders = httpConfig.headers ? interpolateEnvVarsInObject(httpConfig.headers) : void 0;
393
+ transformedServers[serverName] = {
394
+ name: serverName,
395
+ instruction: httpConfig.instruction || httpConfig.config?.instruction,
396
+ toolBlacklist: httpConfig.config?.toolBlacklist,
397
+ omitToolDescription: httpConfig.config?.omitToolDescription,
398
+ prompts: httpConfig.config?.prompts,
399
+ timeout: httpConfig.timeout,
400
+ requestTimeout: httpConfig.requestTimeout,
401
+ transport,
402
+ config: {
403
+ url: interpolatedUrl,
404
+ headers: interpolatedHeaders
405
+ }
406
+ };
407
+ }
408
+ }
409
+ return {
410
+ id: claudeConfig.id,
411
+ mcpServers: transformedServers,
412
+ skills: claudeConfig.skills
413
+ };
414
+ }
415
+ /**
416
+ * Parse and validate MCP config from raw JSON
417
+ * Validates against Claude Code format, transforms to internal format, and validates result
418
+ *
419
+ * @param rawConfig - Raw JSON configuration object
420
+ * @returns Validated and transformed internal configuration
421
+ * @throws ZodError if validation fails
422
+ */
423
+ function parseMcpConfig(rawConfig) {
424
+ const internalConfig = transformClaudeCodeConfig(ClaudeCodeMcpConfigSchema.parse(rawConfig));
425
+ return InternalMcpConfigSchema.parse(internalConfig);
426
+ }
427
+
428
+ //#endregion
429
+ //#region src/utils/findConfigFile.ts
430
+ /**
431
+ * Config File Finder Utility
432
+ *
433
+ * DESIGN PATTERNS:
434
+ * - Utility function pattern for reusable logic
435
+ * - Fail-fast pattern with early returns
436
+ * - Environment variable configuration pattern
437
+ *
438
+ * CODING STANDARDS:
439
+ * - Use sync filesystem operations for config discovery (performance)
440
+ * - Check PROJECT_PATH environment variable first
441
+ * - Fall back to current working directory
442
+ * - Support both .yaml and .json extensions
443
+ * - Return null if no config file is found
444
+ *
445
+ * AVOID:
446
+ * - Throwing errors (return null instead for optional config)
447
+ * - Hardcoded file names without extension variants
448
+ * - Ignoring environment variables
449
+ */
450
+ /**
451
+ * Find MCP configuration file by checking PROJECT_PATH first, then cwd
452
+ * Looks for both mcp-config.yaml and mcp-config.json
453
+ *
454
+ * @returns Absolute path to config file, or null if not found
455
+ */
456
+ function findConfigFile() {
457
+ const configFileNames = [
458
+ "mcp-config.yaml",
459
+ "mcp-config.yml",
460
+ "mcp-config.json"
461
+ ];
462
+ const projectPath = process.env.PROJECT_PATH;
463
+ if (projectPath) for (const fileName of configFileNames) {
464
+ const configPath = (0, node_path.resolve)(projectPath, fileName);
465
+ if ((0, node_fs.existsSync)(configPath)) return configPath;
466
+ }
467
+ const cwd = process.cwd();
468
+ for (const fileName of configFileNames) {
469
+ const configPath = (0, node_path.join)(cwd, fileName);
470
+ if ((0, node_fs.existsSync)(configPath)) return configPath;
471
+ }
472
+ return null;
473
+ }
474
+
475
+ //#endregion
476
+ //#region src/utils/parseToolName.ts
477
+ /**
478
+ * Parse tool name to extract server and actual tool name
479
+ * Supports both plain tool names and prefixed format: {serverName}__{toolName}
480
+ *
481
+ * @param toolName - The tool name to parse (e.g., "my_tool" or "server__my_tool")
482
+ * @returns Parsed result with optional serverName and actualToolName
483
+ *
484
+ * @example
485
+ * parseToolName("my_tool") // { actualToolName: "my_tool" }
486
+ * parseToolName("server__my_tool") // { serverName: "server", actualToolName: "my_tool" }
487
+ */
488
+ function parseToolName(toolName) {
489
+ const separatorIndex = toolName.indexOf("__");
490
+ if (separatorIndex > 0) return {
491
+ serverName: toolName.substring(0, separatorIndex),
492
+ actualToolName: toolName.substring(separatorIndex + 2)
493
+ };
494
+ return { actualToolName: toolName };
495
+ }
496
+
497
+ //#endregion
498
+ //#region src/utils/parseFrontMatter.ts
499
+ /**
500
+ * Parses YAML front matter from a string content.
501
+ * Front matter must be at the start of the content, delimited by `---`.
502
+ *
503
+ * Supports:
504
+ * - Simple key: value pairs
505
+ * - Literal block scalar (|) for multi-line preserving newlines
506
+ * - Folded block scalar (>) for multi-line folding to single line
507
+ *
508
+ * @param content - The content string that may contain front matter
509
+ * @returns Object with parsed front matter (or null) and remaining content
510
+ *
511
+ * @example
512
+ * const result = parseFrontMatter(`---
513
+ * name: my-skill
514
+ * description: A skill description
515
+ * ---
516
+ * The actual content here`);
517
+ * // result.frontMatter = { name: 'my-skill', description: 'A skill description' }
518
+ * // result.content = 'The actual content here'
519
+ *
520
+ * @example
521
+ * // Multi-line with literal block scalar
522
+ * const result = parseFrontMatter(`---
523
+ * name: my-skill
524
+ * description: |
525
+ * Line 1
526
+ * Line 2
527
+ * ---
528
+ * Content`);
529
+ * // result.frontMatter.description = 'Line 1\nLine 2'
530
+ */
531
+ function parseFrontMatter(content) {
532
+ const trimmedContent = content.trimStart();
533
+ if (!trimmedContent.startsWith("---")) return {
534
+ frontMatter: null,
535
+ content
536
+ };
537
+ const endDelimiterIndex = trimmedContent.indexOf("\n---", 3);
538
+ if (endDelimiterIndex === -1) return {
539
+ frontMatter: null,
540
+ content
541
+ };
542
+ const yamlContent = trimmedContent.slice(4, endDelimiterIndex).trim();
543
+ if (!yamlContent) return {
544
+ frontMatter: null,
545
+ content
546
+ };
547
+ const frontMatter = {};
548
+ const lines = yamlContent.split("\n");
549
+ let currentKey = null;
550
+ let currentValue = [];
551
+ let multiLineMode = null;
552
+ let baseIndent = 0;
553
+ const saveCurrentKey = () => {
554
+ if (currentKey && currentValue.length > 0) if (multiLineMode === "literal") frontMatter[currentKey] = currentValue.join("\n").trimEnd();
555
+ else if (multiLineMode === "folded") frontMatter[currentKey] = currentValue.join(" ").trim();
556
+ else frontMatter[currentKey] = currentValue.join("").trim();
557
+ currentKey = null;
558
+ currentValue = [];
559
+ multiLineMode = null;
560
+ baseIndent = 0;
561
+ };
562
+ for (let i = 0; i < lines.length; i++) {
563
+ const line = lines[i];
564
+ const trimmedLine = line.trim();
565
+ const colonIndex = line.indexOf(":");
566
+ if (colonIndex !== -1 && !line.startsWith(" ") && !line.startsWith(" ")) {
567
+ saveCurrentKey();
568
+ const key = line.slice(0, colonIndex).trim();
569
+ let value = line.slice(colonIndex + 1).trim();
570
+ if (value === "|" || value === "|-") {
571
+ currentKey = key;
572
+ multiLineMode = "literal";
573
+ if (i + 1 < lines.length) {
574
+ const match = lines[i + 1].match(/^(\s+)/);
575
+ baseIndent = match ? match[1].length : 2;
576
+ }
577
+ } else if (value === ">" || value === ">-") {
578
+ currentKey = key;
579
+ multiLineMode = "folded";
580
+ if (i + 1 < lines.length) {
581
+ const match = lines[i + 1].match(/^(\s+)/);
582
+ baseIndent = match ? match[1].length : 2;
583
+ }
584
+ } else {
585
+ if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
586
+ if (key && value) frontMatter[key] = value;
587
+ }
588
+ } else if (multiLineMode && currentKey) {
589
+ const lineIndent = line.match(/^(\s*)/)?.[1].length || 0;
590
+ if (trimmedLine === "") currentValue.push("");
591
+ else if (lineIndent >= baseIndent) {
592
+ const unindentedLine = line.slice(baseIndent);
593
+ currentValue.push(unindentedLine);
594
+ } else saveCurrentKey();
595
+ }
596
+ }
597
+ saveCurrentKey();
598
+ return {
599
+ frontMatter,
600
+ content: trimmedContent.slice(endDelimiterIndex + 4).trimStart()
601
+ };
602
+ }
603
+ /**
604
+ * Checks if parsed front matter contains valid skill metadata.
605
+ * A valid skill front matter must have both `name` and `description` fields.
606
+ *
607
+ * @param frontMatter - The parsed front matter object
608
+ * @returns True if front matter contains valid skill metadata
609
+ */
610
+ function isValidSkillFrontMatter(frontMatter) {
611
+ return frontMatter !== null && typeof frontMatter.name === "string" && frontMatter.name.length > 0 && typeof frontMatter.description === "string" && frontMatter.description.length > 0;
612
+ }
613
+ /**
614
+ * Extracts skill front matter from content if present and valid.
615
+ *
616
+ * @param content - The content string that may contain skill front matter
617
+ * @returns Object with skill metadata and content, or null if no valid skill front matter
618
+ */
619
+ function extractSkillFrontMatter(content) {
620
+ const { frontMatter, content: remainingContent } = parseFrontMatter(content);
621
+ if (frontMatter && isValidSkillFrontMatter(frontMatter)) return {
622
+ skill: {
623
+ name: frontMatter.name,
624
+ description: frontMatter.description
625
+ },
626
+ content: remainingContent
627
+ };
628
+ return null;
629
+ }
630
+
631
+ //#endregion
632
+ //#region src/utils/generateServerId.ts
633
+ /**
634
+ * generateServerId Utilities
635
+ *
636
+ * DESIGN PATTERNS:
637
+ * - Pure functions with no side effects
638
+ * - Single responsibility per function
639
+ * - Functional programming approach
640
+ *
641
+ * CODING STANDARDS:
642
+ * - Export individual functions, not classes
643
+ * - Use descriptive function names with verbs
644
+ * - Add JSDoc comments for complex logic
645
+ * - Keep functions small and focused
646
+ *
647
+ * AVOID:
648
+ * - Side effects (mutating external state)
649
+ * - Stateful logic (use services for state)
650
+ * - Complex external dependencies
651
+ */
652
+ /**
653
+ * Character set for generating human-readable IDs.
654
+ * Excludes confusing characters: 0, O, 1, l, I
655
+ */
656
+ const CHARSET = "23456789abcdefghjkmnpqrstuvwxyz";
657
+ /**
658
+ * Default length for generated server IDs (6 characters)
659
+ */
660
+ const DEFAULT_ID_LENGTH = 6;
661
+ /**
662
+ * Generate a short, human-readable server ID.
663
+ *
664
+ * Uses Node.js crypto.randomBytes for cryptographically secure randomness
665
+ * with rejection sampling to avoid modulo bias.
666
+ *
667
+ * The generated ID:
668
+ * - Is 6 characters long by default
669
+ * - Uses only lowercase alphanumeric characters
670
+ * - Excludes confusing characters (0, O, 1, l, I)
671
+ *
672
+ * @param length - Length of the ID to generate (default: 6)
673
+ * @returns A random, human-readable ID
674
+ *
675
+ * @example
676
+ * generateServerId() // "abc234"
677
+ * generateServerId(4) // "x7mn"
678
+ */
679
+ function generateServerId(length = DEFAULT_ID_LENGTH) {
680
+ const charsetLength = 31;
681
+ const maxUnbiased = Math.floor(256 / charsetLength) * charsetLength - 1;
682
+ let result = "";
683
+ let remaining = length;
684
+ while (remaining > 0) {
685
+ const bytes = (0, node_crypto.randomBytes)(remaining);
686
+ for (let i = 0; i < bytes.length && remaining > 0; i++) {
687
+ const byte = bytes[i];
688
+ if (byte > maxUnbiased) continue;
689
+ result += CHARSET[byte % charsetLength];
690
+ remaining--;
691
+ }
692
+ }
693
+ return result;
694
+ }
695
+
696
+ //#endregion
697
+ //#region src/constants/index.ts
698
+ /**
699
+ * Shared constants for mcp-proxy package
700
+ */
701
+ /**
702
+ * Prefix added to skill names when they clash with MCP tool names.
703
+ * This ensures skills can be uniquely identified even when a tool has the same name.
704
+ */
705
+ const SKILL_PREFIX = "skill__";
706
+ /**
707
+ * Log prefix for skill detection messages.
708
+ * Used to easily filter skill detection logs in stderr output.
709
+ */
710
+ const LOG_PREFIX_SKILL_DETECTION = "[skill-detection]";
711
+ /**
712
+ * Log prefix for general MCP capability discovery messages.
713
+ */
714
+ const LOG_PREFIX_CAPABILITY_DISCOVERY = "[capability-discovery]";
715
+ /**
716
+ * Prefix for prompt-based skill locations.
717
+ * Format: "prompt:{serverName}:{promptName}"
718
+ */
719
+ const PROMPT_LOCATION_PREFIX = "prompt:";
720
+ /**
721
+ * Default server ID used when no ID is provided via CLI or config.
722
+ * This fallback is used when auto-generation also fails.
723
+ */
724
+ const DEFAULT_SERVER_ID = "unknown";
725
+
726
+ //#endregion
727
+ //#region src/services/DefinitionsCacheService.ts
728
+ /**
729
+ * DefinitionsCacheService
730
+ *
731
+ * Provides shared discovery, caching, and serialization for startup-time MCP
732
+ * capability metadata. This avoids repeated remote enumeration during
733
+ * mcp-serve startup and describe_tools generation.
734
+ */
735
+ function isYamlPath(filePath) {
736
+ return filePath.endsWith(".yaml") || filePath.endsWith(".yml");
737
+ }
738
+ function toErrorMessage$3(error) {
739
+ return error instanceof Error ? error.message : String(error);
740
+ }
741
+ function sanitizeConfigPathForFilename(configFilePath) {
742
+ const absoluteConfigPath = (0, node_path.resolve)(configFilePath);
743
+ const normalizedPath = absoluteConfigPath.length >= 2 && absoluteConfigPath[1] === ":" && (absoluteConfigPath[0] >= "A" && absoluteConfigPath[0] <= "Z" || absoluteConfigPath[0] >= "a" && absoluteConfigPath[0] <= "z") ? `${absoluteConfigPath[0].toLowerCase()}${absoluteConfigPath.slice(1)}` : absoluteConfigPath;
744
+ let result = "";
745
+ let previousWasUnderscore = false;
746
+ for (const char of normalizedPath) {
747
+ if (char >= "a" && char <= "z" || char >= "A" && char <= "Z" || char >= "0" && char <= "9" || char === "." || char === "_" || char === "-") {
748
+ result += char;
749
+ previousWasUnderscore = false;
750
+ continue;
751
+ }
752
+ if (!previousWasUnderscore) {
753
+ result += "_";
754
+ previousWasUnderscore = true;
755
+ }
756
+ }
757
+ let start = 0;
758
+ let end = result.length;
759
+ while (start < end && result[start] === "_") start += 1;
760
+ while (end > start && result[end - 1] === "_") end -= 1;
761
+ return result.slice(start, end);
762
+ }
763
+ function cloneCache(cache) {
764
+ return {
765
+ ...cache,
766
+ failures: [...cache.failures ?? []],
767
+ skills: (cache.skills ?? []).map((skill) => ({ ...skill })),
768
+ servers: Object.fromEntries(Object.entries(cache.servers).map(([serverName, server]) => [serverName, {
769
+ ...server,
770
+ tools: (server.tools ?? []).map((tool) => ({ ...tool })),
771
+ resources: (server.resources ?? []).map((resource) => ({ ...resource })),
772
+ prompts: (server.prompts ?? []).map((prompt) => ({
773
+ ...prompt,
774
+ arguments: prompt.arguments?.map((arg) => ({ ...arg }))
775
+ })),
776
+ promptSkills: (server.promptSkills ?? []).map((promptSkill) => ({
777
+ ...promptSkill,
778
+ skill: { ...promptSkill.skill }
779
+ }))
780
+ }]))
781
+ };
782
+ }
783
+ var DefinitionsCacheService = class {
784
+ clientManager;
785
+ skillService;
786
+ cacheData;
787
+ liveDefinitionsPromise = null;
788
+ mergedDefinitionsPromise = null;
789
+ logger;
790
+ constructor(clientManager, skillService, options, logger = console) {
791
+ this.clientManager = clientManager;
792
+ this.skillService = skillService;
793
+ this.cacheData = options?.cacheData;
794
+ this.logger = logger;
795
+ }
796
+ static async readFromFile(filePath) {
797
+ const content = await (0, node_fs_promises.readFile)(filePath, "utf-8");
798
+ const parsed = isYamlPath(filePath) ? js_yaml.default.load(content) : JSON.parse(content);
799
+ if (!parsed || typeof parsed !== "object") throw new Error("Definitions cache must be an object");
800
+ const cache = parsed;
801
+ if (cache.version !== 1 || !cache.servers) throw new Error("Definitions cache is missing required fields");
802
+ return {
803
+ ...cache,
804
+ failures: Array.isArray(cache.failures) ? cache.failures : [],
805
+ skills: Array.isArray(cache.skills) ? cache.skills : [],
806
+ servers: Object.fromEntries(Object.entries(cache.servers).map(([serverName, server]) => [serverName, {
807
+ ...server,
808
+ tools: Array.isArray(server.tools) ? server.tools : [],
809
+ resources: Array.isArray(server.resources) ? server.resources : [],
810
+ prompts: Array.isArray(server.prompts) ? server.prompts : [],
811
+ promptSkills: Array.isArray(server.promptSkills) ? server.promptSkills : []
812
+ }]))
813
+ };
814
+ }
815
+ static async writeToFile(filePath, cache) {
816
+ const serialized = isYamlPath(filePath) ? js_yaml.default.dump(cache, { noRefs: true }) : JSON.stringify(cache, null, 2);
817
+ await (0, node_fs_promises.mkdir)((0, node_path.dirname)(filePath), { recursive: true });
818
+ await (0, node_fs_promises.writeFile)(filePath, serialized, "utf-8");
819
+ }
820
+ static getDefaultCachePath(configFilePath) {
821
+ const sanitizedPath = sanitizeConfigPathForFilename(configFilePath);
822
+ return (0, node_path.join)((0, node_os.homedir)(), ".aicode-toolkit", `${sanitizedPath}.definitions-cache.json`);
823
+ }
824
+ static generateConfigHash(config) {
825
+ return (0, node_crypto.createHash)("sha256").update(JSON.stringify(config)).digest("hex");
826
+ }
827
+ static isCacheValid(cache, options) {
828
+ if (options.configHash && cache.configHash && cache.configHash !== options.configHash) return false;
829
+ if (options.oneMcpVersion && cache.oneMcpVersion && cache.oneMcpVersion !== options.oneMcpVersion) return false;
830
+ return true;
831
+ }
832
+ static async clearFile(filePath) {
833
+ await (0, node_fs_promises.rm)(filePath, { force: true });
834
+ }
835
+ clearLiveCache() {
836
+ this.liveDefinitionsPromise = null;
837
+ this.mergedDefinitionsPromise = null;
838
+ }
839
+ setCacheData(cacheData) {
840
+ this.cacheData = cacheData;
841
+ this.mergedDefinitionsPromise = null;
842
+ }
843
+ async collectForCache(options) {
844
+ const liveDefinitions = await this.collectLiveDefinitions(options);
845
+ this.setCacheData(liveDefinitions);
846
+ this.liveDefinitionsPromise = Promise.resolve(cloneCache(liveDefinitions));
847
+ return cloneCache(liveDefinitions);
848
+ }
849
+ async getDefinitions() {
850
+ if (this.mergedDefinitionsPromise) return this.mergedDefinitionsPromise;
851
+ this.mergedDefinitionsPromise = (async () => {
852
+ const clients = this.clientManager.getAllClients();
853
+ if (!this.cacheData) return this.getLiveDefinitions();
854
+ const missingServers = clients.map((client) => client.serverName).filter((serverName) => !this.cacheData?.servers[serverName]);
855
+ if (missingServers.length === 0) return cloneCache(this.cacheData);
856
+ const liveDefinitions = await this.getLiveDefinitions();
857
+ const merged = cloneCache(this.cacheData);
858
+ for (const serverName of missingServers) {
859
+ const serverDefinition = liveDefinitions.servers[serverName];
860
+ if (serverDefinition) merged.servers[serverName] = serverDefinition;
861
+ }
862
+ const failureMap = /* @__PURE__ */ new Map();
863
+ for (const failure of [...merged.failures, ...liveDefinitions.failures]) failureMap.set(failure.serverName, failure.error);
864
+ merged.failures = Array.from(failureMap.entries()).map(([serverName, error]) => ({
865
+ serverName,
866
+ error
867
+ }));
868
+ if (merged.skills.length === 0 && liveDefinitions.skills.length > 0) merged.skills = liveDefinitions.skills.map((skill) => ({ ...skill }));
869
+ return merged;
870
+ })();
871
+ return this.mergedDefinitionsPromise;
872
+ }
873
+ async getServerDefinitions() {
874
+ const definitions = await this.getDefinitions();
875
+ const serverOrder = this.clientManager.getKnownServerNames();
876
+ if (serverOrder.length === 0) return Object.values(definitions.servers);
877
+ return serverOrder.map((serverName) => definitions.servers[serverName]).filter((server) => server !== void 0);
878
+ }
879
+ async getServersForTool(toolName) {
880
+ return (await this.getServerDefinitions()).filter((serverDefinition) => serverDefinition.tools.some((tool) => tool.name === toolName)).map((serverDefinition) => serverDefinition.serverName);
881
+ }
882
+ async getServersForResource(uri) {
883
+ return (await this.getServerDefinitions()).filter((serverDefinition) => serverDefinition.resources.some((resource) => resource.uri === uri)).map((serverDefinition) => serverDefinition.serverName);
884
+ }
885
+ async getPromptSkillByName(skillName) {
886
+ const definitions = await this.getDefinitions();
887
+ for (const [serverName, server] of Object.entries(definitions.servers)) for (const promptSkill of server.promptSkills) if (promptSkill.skill.name === skillName) return {
888
+ serverName,
889
+ promptName: promptSkill.promptName,
890
+ skill: promptSkill.skill,
891
+ autoDetected: promptSkill.autoDetected
892
+ };
893
+ }
894
+ async getCachedFileSkills() {
895
+ return (await this.getDefinitions()).skills.map((skill) => ({ ...skill }));
896
+ }
897
+ async getLiveDefinitions() {
898
+ if (!this.liveDefinitionsPromise) this.liveDefinitionsPromise = this.collectLiveDefinitions();
899
+ return this.liveDefinitionsPromise;
900
+ }
901
+ async collectLiveDefinitions(options) {
902
+ const clients = this.clientManager.getAllClients();
903
+ const failures = [];
904
+ const servers = {};
905
+ const serverResults = await Promise.all(clients.map(async (client) => {
906
+ try {
907
+ const tools = await client.listTools();
908
+ const resources = await this.listResourcesSafe(client);
909
+ const prompts = await this.listPromptsSafe(client);
910
+ const blacklist = new Set(client.toolBlacklist || []);
911
+ const filteredTools = tools.filter((tool) => !blacklist.has(tool.name));
912
+ const promptSkills = await this.collectPromptSkillsForClient(client, prompts);
913
+ return {
914
+ serverName: client.serverName,
915
+ serverInstruction: client.serverInstruction,
916
+ omitToolDescription: client.omitToolDescription,
917
+ toolBlacklist: client.toolBlacklist,
918
+ tools: filteredTools.map((tool) => ({
919
+ name: tool.name,
920
+ description: tool.description,
921
+ inputSchema: tool.inputSchema,
922
+ _meta: tool._meta
923
+ })),
924
+ resources,
925
+ prompts,
926
+ promptSkills
927
+ };
928
+ } catch (error) {
929
+ failures.push({
930
+ serverName: client.serverName,
931
+ error: toErrorMessage$3(error)
932
+ });
933
+ return null;
934
+ }
935
+ }));
936
+ for (const serverDefinition of serverResults) if (serverDefinition) servers[serverDefinition.serverName] = serverDefinition;
937
+ return {
938
+ version: 1,
939
+ oneMcpVersion: options?.oneMcpVersion,
940
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
941
+ configPath: options?.configPath,
942
+ configHash: options?.configHash,
943
+ serverId: options?.serverId,
944
+ servers,
945
+ skills: await this.collectFileSkills(),
946
+ failures
947
+ };
948
+ }
949
+ async collectFileSkills() {
950
+ if (!this.skillService) return [];
951
+ return (await this.skillService.getSkills()).map((skill) => this.toCachedFileSkill(skill));
952
+ }
953
+ toCachedFileSkill(skill) {
954
+ return {
955
+ name: skill.name,
956
+ description: skill.description,
957
+ location: skill.location,
958
+ basePath: skill.basePath
959
+ };
960
+ }
961
+ async listPromptsSafe(client) {
962
+ try {
963
+ return (await client.listPrompts()).map((prompt) => ({
964
+ name: prompt.name,
965
+ description: prompt.description,
966
+ arguments: prompt.arguments?.map((arg) => ({ ...arg }))
967
+ }));
968
+ } catch (error) {
969
+ this.logger.warn(`${LOG_PREFIX_SKILL_DETECTION} Failed to list prompts from ${client.serverName}: ${toErrorMessage$3(error)}`);
970
+ return [];
971
+ }
972
+ }
973
+ async listResourcesSafe(client) {
974
+ try {
975
+ return (await client.listResources()).map((resource) => ({
976
+ uri: resource.uri,
977
+ name: resource.name,
978
+ description: resource.description,
979
+ mimeType: resource.mimeType
980
+ }));
981
+ } catch (error) {
982
+ this.logger.warn(`${LOG_PREFIX_CAPABILITY_DISCOVERY} Failed to list resources from ${client.serverName}: ${toErrorMessage$3(error)}`);
983
+ return [];
984
+ }
985
+ }
986
+ async collectPromptSkillsForClient(client, prompts) {
987
+ const configuredPromptNames = new Set(client.prompts ? Object.keys(client.prompts) : []);
988
+ const promptSkills = [];
989
+ if (client.prompts) {
990
+ for (const [promptName, promptConfig] of Object.entries(client.prompts)) if (promptConfig.skill) promptSkills.push({
991
+ promptName,
992
+ skill: { ...promptConfig.skill }
993
+ });
994
+ }
995
+ const autoDetectedSkills = await Promise.all(prompts.map(async (prompt) => {
996
+ if (configuredPromptNames.has(prompt.name)) return null;
997
+ try {
998
+ const skillExtraction = extractSkillFrontMatter((await client.getPrompt(prompt.name)).messages?.map((message) => {
999
+ const content = message.content;
1000
+ if (typeof content === "string") return content;
1001
+ if (content && typeof content === "object" && "text" in content) return String(content.text);
1002
+ return "";
1003
+ }).join("\n") || "");
1004
+ if (!skillExtraction) return null;
1005
+ return {
1006
+ promptName: prompt.name,
1007
+ skill: skillExtraction.skill,
1008
+ autoDetected: true
1009
+ };
1010
+ } catch (error) {
1011
+ this.logger.warn(`${LOG_PREFIX_SKILL_DETECTION} Failed to fetch prompt '${prompt.name}' from ${client.serverName}: ${toErrorMessage$3(error)}`);
1012
+ return null;
1013
+ }
1014
+ }));
1015
+ for (const autoDetectedSkill of autoDetectedSkills) if (autoDetectedSkill) promptSkills.push(autoDetectedSkill);
1016
+ return promptSkills;
1017
+ }
1018
+ };
1019
+
1020
+ //#endregion
1021
+ //#region src/templates/toolkit-description.liquid?raw
1022
+ var toolkit_description_default = "<toolkit id=\"{{ serverId }}\">\n<instruction>\nBefore you use any capabilities below, you MUST call this tool with a list of names to learn how to use them properly; this includes:\n- For tools: Arguments schema needed to pass to use_tool\n- For skills: Detailed instructions that will expand when invoked (Prefer to be explored first when relevant)\n\nThis tool is optimized for batch queries - you can request multiple capabilities at once for better performance.\n\nHow to invoke:\n- For MCP tools: Use use_tool with toolName and toolArgs based on the schema\n- For skills: Use this tool with the skill name to get expanded instructions\n</instruction>\n\n<available_capabilities>\n{% for server in servers -%}\n<group name=\"{{ server.name }}\">\n{% if server.instruction -%}\n<group_instruction>{{ server.instruction }}</group_instruction>\n{% endif -%}\n{% if server.omitToolDescription -%}\n{% for toolName in server.toolNames -%}\n<item name=\"{{ toolName }}\"></item>\n{% endfor -%}\n{% else -%}\n{% for tool in server.tools -%}\n<item name=\"{{ tool.displayName }}\"><description>{{ tool.description | default: \"No description\" }}</description></item>\n{% endfor -%}\n{% endif -%}\n</group>\n{% endfor -%}\n{% if skills.size > 0 -%}\n<group name=\"skills\">\n{% for skill in skills -%}\n<item name=\"{{ skill.displayName }}\"><description>{{ skill.description }}</description></item>\n{% endfor -%}\n</group>\n{% endif -%}\n</available_capabilities>\n</toolkit>\n";
1023
+
1024
+ //#endregion
1025
+ //#region src/tools/DescribeToolsTool.ts
1026
+ /**
1027
+ * Formats skill instructions with the loading command message prefix.
1028
+ * This message is used by Claude Code to indicate that a skill is being loaded.
1029
+ *
1030
+ * @param name - The skill name
1031
+ * @param instructions - The raw skill instructions/content
1032
+ * @returns Formatted instructions with command message prefix
1033
+ */
1034
+ function formatSkillInstructions(name, instructions) {
1035
+ return `<command-message>The "${name}" skill is loading</command-message>\n${instructions}`;
1036
+ }
1037
+ /**
1038
+ * DescribeToolsTool provides progressive disclosure of MCP tools and skills.
1039
+ *
1040
+ * This tool lists available tools from all connected MCP servers and skills
1041
+ * from the configured skills directories. Users can query for specific tools
1042
+ * or skills to get detailed input schemas and descriptions.
1043
+ *
1044
+ * Tool naming conventions:
1045
+ * - Unique tools: use plain name (e.g., "browser_click")
1046
+ * - Clashing tools: use serverName__toolName format (e.g., "playwright__click")
1047
+ * - Skills: use skill__skillName format (e.g., "skill__pdf")
1048
+ *
1049
+ * @example
1050
+ * const tool = new DescribeToolsTool(clientManager, skillService);
1051
+ * const definition = await tool.getDefinition();
1052
+ * const result = await tool.execute({ toolNames: ['browser_click', 'skill__pdf'] });
1053
+ */
1054
+ var DescribeToolsTool = class DescribeToolsTool {
1055
+ static TOOL_NAME = "describe_tools";
1056
+ clientManager;
1057
+ skillService;
1058
+ definitionsCacheService;
1059
+ liquid = new liquidjs.Liquid();
1060
+ /** Cache for auto-detected skills from prompt front-matter */
1061
+ autoDetectedSkillsCache = null;
1062
+ /** Unique server identifier for this mcp-proxy instance */
1063
+ serverId;
1064
+ /**
1065
+ * Creates a new DescribeToolsTool instance
1066
+ * @param clientManager - The MCP client manager for accessing remote servers
1067
+ * @param skillService - Optional skill service for loading skills
1068
+ * @param serverId - Unique server identifier for this mcp-proxy instance
1069
+ */
1070
+ constructor(clientManager, skillService, serverId, definitionsCacheService) {
1071
+ this.clientManager = clientManager;
1072
+ this.skillService = skillService;
1073
+ this.serverId = serverId || DEFAULT_SERVER_ID;
1074
+ this.definitionsCacheService = definitionsCacheService || new DefinitionsCacheService(clientManager, skillService);
1075
+ }
1076
+ /**
1077
+ * Clears the cached auto-detected skills from prompt front-matter.
1078
+ * Use this when prompt configurations may have changed or when
1079
+ * the skill service cache is invalidated.
1080
+ */
1081
+ clearAutoDetectedSkillsCache() {
1082
+ this.autoDetectedSkillsCache = null;
1083
+ this.definitionsCacheService.clearLiveCache();
1084
+ }
1085
+ /**
1086
+ * Detects and caches skills from prompt front-matter across all connected MCP servers.
1087
+ * Fetches all prompts and checks their content for YAML front-matter with name/description.
1088
+ * Results are cached to avoid repeated fetches.
1089
+ *
1090
+ * Error Handling Strategy:
1091
+ * - Errors are logged to stderr but do not fail the overall detection process
1092
+ * - This ensures partial results are returned even if some servers/prompts fail
1093
+ * - Common failure reasons: server temporarily unavailable, prompt requires arguments,
1094
+ * network timeout, or server doesn't support listPrompts
1095
+ * - Errors are prefixed with [skill-detection] for easy filtering in logs
1096
+ *
1097
+ * @returns Array of auto-detected skills from prompt front-matter
1098
+ */
1099
+ async detectSkillsFromPromptFrontMatter() {
1100
+ if (this.autoDetectedSkillsCache !== null) return this.autoDetectedSkillsCache;
1101
+ const clients = this.clientManager.getAllClients();
1102
+ let listPromptsFailures = 0;
1103
+ let fetchPromptFailures = 0;
1104
+ const autoDetectedSkills = (await Promise.all(clients.map(async (client) => {
1105
+ const detectedSkills = [];
1106
+ const configuredPromptNames = new Set(client.prompts ? Object.keys(client.prompts) : []);
1107
+ try {
1108
+ const prompts = await client.listPrompts();
1109
+ if (!prompts || prompts.length === 0) return detectedSkills;
1110
+ const promptResults = await Promise.all(prompts.map(async (promptInfo) => {
1111
+ if (configuredPromptNames.has(promptInfo.name)) return null;
1112
+ try {
1113
+ const skillExtraction = extractSkillFrontMatter(((await client.getPrompt(promptInfo.name)).messages || []).map((m) => {
1114
+ const content = m.content;
1115
+ if (typeof content === "string") return content;
1116
+ if (content && typeof content === "object" && "text" in content) return String(content.text);
1117
+ return "";
1118
+ }).join("\n"));
1119
+ if (skillExtraction) return {
1120
+ serverName: client.serverName,
1121
+ promptName: promptInfo.name,
1122
+ skill: skillExtraction.skill
1123
+ };
1124
+ return null;
1125
+ } catch (error) {
1126
+ fetchPromptFailures++;
1127
+ console.error(`${LOG_PREFIX_SKILL_DETECTION} Failed to fetch prompt '${promptInfo.name}' from ${client.serverName}: ${error instanceof Error ? error.message : "Unknown error"}`);
1128
+ return null;
1129
+ }
1130
+ }));
1131
+ for (const result of promptResults) if (result) detectedSkills.push(result);
1132
+ } catch (error) {
1133
+ listPromptsFailures++;
1134
+ console.error(`${LOG_PREFIX_SKILL_DETECTION} Failed to list prompts from ${client.serverName}: ${error instanceof Error ? error.message : "Unknown error"}`);
1135
+ }
1136
+ return detectedSkills;
1137
+ }))).flat();
1138
+ if (listPromptsFailures > 0 || fetchPromptFailures > 0) console.error(`${LOG_PREFIX_SKILL_DETECTION} Completed with ${listPromptsFailures} server failure(s) and ${fetchPromptFailures} prompt failure(s). Detected ${autoDetectedSkills.length} skill(s).`);
1139
+ this.autoDetectedSkillsCache = autoDetectedSkills;
1140
+ return autoDetectedSkills;
1141
+ }
1142
+ /**
1143
+ * Collects skills derived from prompt configurations across all connected MCP servers.
1144
+ * Includes both explicitly configured prompts and auto-detected skills from front-matter.
1145
+ *
1146
+ * @returns Array of skill template data derived from prompts
1147
+ */
1148
+ async collectPromptSkills() {
1149
+ const promptSkills = [];
1150
+ const serverDefinitions = await this.definitionsCacheService.getServerDefinitions();
1151
+ for (const serverDefinition of serverDefinitions) for (const promptSkill of serverDefinition.promptSkills) promptSkills.push({
1152
+ name: promptSkill.skill.name,
1153
+ displayName: promptSkill.skill.name,
1154
+ description: promptSkill.skill.description
1155
+ });
1156
+ return promptSkills;
1157
+ }
1158
+ /**
1159
+ * Finds a prompt-based skill by name from all connected MCP servers.
1160
+ * Searches both explicitly configured prompts and auto-detected skills from front-matter.
1161
+ *
1162
+ * @param skillName - The skill name to search for
1163
+ * @returns Object with serverName, promptName, and skill config, or undefined if not found
1164
+ */
1165
+ async findPromptSkill(skillName) {
1166
+ if (!skillName) return void 0;
1167
+ return await this.definitionsCacheService.getPromptSkillByName(skillName);
1168
+ }
1169
+ /**
1170
+ * Retrieves skill content from a prompt-based skill configuration.
1171
+ * Fetches the prompt from the MCP server and extracts text content.
1172
+ * Handles both explicitly configured prompts and auto-detected skills from front-matter.
1173
+ *
1174
+ * @param skillName - The skill name being requested
1175
+ * @returns SkillDescription if found and successfully fetched, undefined otherwise
1176
+ */
1177
+ async getPromptSkillContent(skillName) {
1178
+ const promptSkill = await this.findPromptSkill(skillName);
1179
+ if (!promptSkill) return void 0;
1180
+ try {
1181
+ const rawInstructions = (await (await this.clientManager.ensureConnected(promptSkill.serverName)).getPrompt(promptSkill.promptName)).messages?.map((m) => {
1182
+ const content = m.content;
1183
+ if (typeof content === "string") return content;
1184
+ if (content && typeof content === "object" && "text" in content) return String(content.text);
1185
+ return "";
1186
+ }).join("\n") || "";
1187
+ return {
1188
+ name: promptSkill.skill.name,
1189
+ location: promptSkill.skill.folder || `${PROMPT_LOCATION_PREFIX}${promptSkill.serverName}/${promptSkill.promptName}`,
1190
+ instructions: formatSkillInstructions(promptSkill.skill.name, rawInstructions)
1191
+ };
1192
+ } catch (error) {
1193
+ console.error(`Failed to get prompt-based skill '${skillName}': ${error instanceof Error ? error.message : "Unknown error"}`);
1194
+ return;
1195
+ }
1196
+ }
1197
+ /**
1198
+ * Builds the combined toolkit description using a single Liquid template.
1199
+ *
1200
+ * Collects all tools from connected MCP servers and all skills, then renders
1201
+ * them together using the toolkit-description.liquid template.
1202
+ *
1203
+ * Tool names are prefixed with serverName__ when the same tool exists
1204
+ * on multiple servers. Skill names are prefixed with skill__ when they
1205
+ * clash with MCP tools or other skills.
1206
+ *
1207
+ * @returns Object with rendered description and set of all tool names
1208
+ */
1209
+ async buildToolkitDescription() {
1210
+ const serverDefinitions = await this.definitionsCacheService.getServerDefinitions();
1211
+ const toolToServers = /* @__PURE__ */ new Map();
1212
+ for (const serverDefinition of serverDefinitions) for (const tool of serverDefinition.tools) {
1213
+ if (!toolToServers.has(tool.name)) toolToServers.set(tool.name, []);
1214
+ toolToServers.get(tool.name)?.push(serverDefinition.serverName);
1215
+ }
1216
+ /**
1217
+ * Formats tool name with server prefix if the tool exists on multiple servers
1218
+ */
1219
+ const formatToolName = (toolName, serverName) => {
1220
+ return (toolToServers.get(toolName) || []).length > 1 ? `${serverName}__${toolName}` : toolName;
1221
+ };
1222
+ const allToolNames = /* @__PURE__ */ new Set();
1223
+ const servers = serverDefinitions.map((serverDefinition) => {
1224
+ const formattedTools = serverDefinition.tools.map((t) => ({
1225
+ displayName: formatToolName(t.name, serverDefinition.serverName),
1226
+ description: t.description
1227
+ }));
1228
+ for (const tool of formattedTools) allToolNames.add(tool.displayName);
1229
+ return {
1230
+ name: serverDefinition.serverName,
1231
+ instruction: serverDefinition.serverInstruction,
1232
+ omitToolDescription: serverDefinition.omitToolDescription || false,
1233
+ tools: formattedTools,
1234
+ toolNames: formattedTools.map((t) => t.displayName)
1235
+ };
1236
+ });
1237
+ const [rawSkills, cachedSkills, promptSkills] = await Promise.all([
1238
+ this.skillService ? this.skillService.getSkills() : Promise.resolve([]),
1239
+ this.definitionsCacheService.getCachedFileSkills(),
1240
+ this.collectPromptSkills()
1241
+ ]);
1242
+ const seenSkillNames = /* @__PURE__ */ new Set();
1243
+ const allSkillsData = [];
1244
+ for (const skill of rawSkills) if (!seenSkillNames.has(skill.name)) {
1245
+ seenSkillNames.add(skill.name);
1246
+ allSkillsData.push({
1247
+ name: skill.name,
1248
+ displayName: skill.name,
1249
+ description: skill.description
1250
+ });
1251
+ }
1252
+ for (const skill of cachedSkills) if (!seenSkillNames.has(skill.name)) {
1253
+ seenSkillNames.add(skill.name);
1254
+ allSkillsData.push({
1255
+ name: skill.name,
1256
+ displayName: skill.name,
1257
+ description: skill.description
1258
+ });
1259
+ }
1260
+ for (const skill of promptSkills) if (!seenSkillNames.has(skill.name)) {
1261
+ seenSkillNames.add(skill.name);
1262
+ allSkillsData.push(skill);
1263
+ }
1264
+ const skills = allSkillsData.map((skill) => {
1265
+ const clashesWithMcpTool = allToolNames.has(skill.name);
1266
+ return {
1267
+ name: skill.name,
1268
+ displayName: clashesWithMcpTool ? `${SKILL_PREFIX}${skill.name}` : skill.name,
1269
+ description: skill.description
1270
+ };
1271
+ });
1272
+ return {
1273
+ content: await this.liquid.parseAndRender(toolkit_description_default, {
1274
+ servers,
1275
+ skills,
1276
+ serverId: this.serverId
1277
+ }),
1278
+ toolNames: allToolNames
1279
+ };
1280
+ }
1281
+ /**
1282
+ * Gets the tool definition including available tools and skills in a unified format.
1283
+ *
1284
+ * The definition includes:
1285
+ * - All MCP tools from connected servers
1286
+ * - All available skills (file-based and prompt-based)
1287
+ * - Unified instructions for querying capability details
1288
+ *
1289
+ * Tool names are prefixed with serverName__ when clashing.
1290
+ * Skill names are prefixed with skill__ when clashing.
1291
+ *
1292
+ * @returns Tool definition with description and input schema
1293
+ */
1294
+ async getDefinition() {
1295
+ const { content } = await this.buildToolkitDescription();
1296
+ return {
1297
+ name: DescribeToolsTool.TOOL_NAME,
1298
+ description: content,
1299
+ inputSchema: {
1300
+ type: "object",
1301
+ properties: { toolNames: {
1302
+ type: "array",
1303
+ items: {
1304
+ type: "string",
1305
+ minLength: 1
1306
+ },
1307
+ description: "List of tool names to get detailed information about",
1308
+ minItems: 1
1309
+ } },
1310
+ required: ["toolNames"],
1311
+ additionalProperties: false
1312
+ }
1313
+ };
1314
+ }
1315
+ /**
1316
+ * Executes tool description lookup for the requested tool and skill names.
1317
+ *
1318
+ * Handles three types of lookups:
1319
+ * 1. skill__name - Returns skill information from SkillService
1320
+ * 2. serverName__toolName - Returns tool from specific server
1321
+ * 3. plainToolName - Returns tool(s) from all servers (multiple if clashing)
1322
+ *
1323
+ * @param input - Object containing toolNames array
1324
+ * @returns CallToolResult with tool/skill descriptions or error
1325
+ */
1326
+ async execute(input) {
1327
+ try {
1328
+ const { toolNames } = input;
1329
+ const serverDefinitions = await this.definitionsCacheService.getServerDefinitions();
1330
+ if (!toolNames || toolNames.length === 0) return {
1331
+ content: [{
1332
+ type: "text",
1333
+ text: "No tool names provided. Please specify at least one tool name."
1334
+ }],
1335
+ isError: true
1336
+ };
1337
+ const serverToolsMap = /* @__PURE__ */ new Map();
1338
+ const toolToServers = /* @__PURE__ */ new Map();
1339
+ for (const serverDefinition of serverDefinitions) {
1340
+ const typedTools = serverDefinition.tools.map((tool) => ({
1341
+ name: tool.name,
1342
+ description: tool.description,
1343
+ inputSchema: tool.inputSchema
1344
+ }));
1345
+ serverToolsMap.set(serverDefinition.serverName, typedTools);
1346
+ for (const tool of typedTools) {
1347
+ if (!toolToServers.has(tool.name)) toolToServers.set(tool.name, []);
1348
+ toolToServers.get(tool.name)?.push(serverDefinition.serverName);
1349
+ }
1350
+ }
1351
+ const lookupResults = await Promise.all(toolNames.map(async (requestedName) => {
1352
+ const result$1 = {
1353
+ tools: [],
1354
+ skills: [],
1355
+ notFound: null
1356
+ };
1357
+ if (requestedName.startsWith(SKILL_PREFIX)) {
1358
+ const skillName = requestedName.slice(SKILL_PREFIX.length);
1359
+ if (this.skillService) {
1360
+ const skill = await this.skillService.getSkill(skillName);
1361
+ if (skill) {
1362
+ result$1.skills.push({
1363
+ name: skill.name,
1364
+ location: skill.basePath,
1365
+ instructions: formatSkillInstructions(skill.name, skill.content)
1366
+ });
1367
+ return result$1;
1368
+ }
1369
+ }
1370
+ const promptSkillContent = await this.getPromptSkillContent(skillName);
1371
+ if (promptSkillContent) {
1372
+ result$1.skills.push(promptSkillContent);
1373
+ return result$1;
1374
+ }
1375
+ result$1.notFound = requestedName;
1376
+ return result$1;
1377
+ }
1378
+ const { serverName, actualToolName } = parseToolName(requestedName);
1379
+ if (serverName) {
1380
+ const serverTools = serverToolsMap.get(serverName);
1381
+ if (!serverTools) {
1382
+ result$1.notFound = requestedName;
1383
+ return result$1;
1384
+ }
1385
+ const tool = serverTools.find((t) => t.name === actualToolName);
1386
+ if (tool) result$1.tools.push({
1387
+ server: serverName,
1388
+ tool: {
1389
+ name: tool.name,
1390
+ description: tool.description,
1391
+ inputSchema: tool.inputSchema
1392
+ }
1393
+ });
1394
+ else result$1.notFound = requestedName;
1395
+ return result$1;
1396
+ }
1397
+ const servers = toolToServers.get(actualToolName);
1398
+ if (!servers || servers.length === 0) {
1399
+ if (this.skillService) {
1400
+ const skill = await this.skillService.getSkill(actualToolName);
1401
+ if (skill) {
1402
+ result$1.skills.push({
1403
+ name: skill.name,
1404
+ location: skill.basePath,
1405
+ instructions: formatSkillInstructions(skill.name, skill.content)
1406
+ });
1407
+ return result$1;
1408
+ }
1409
+ }
1410
+ const promptSkillContent = await this.getPromptSkillContent(actualToolName);
1411
+ if (promptSkillContent) {
1412
+ result$1.skills.push(promptSkillContent);
1413
+ return result$1;
1414
+ }
1415
+ result$1.notFound = requestedName;
1416
+ return result$1;
1417
+ }
1418
+ if (servers.length === 1) {
1419
+ const server = servers[0];
1420
+ const tool = serverToolsMap.get(server).find((t) => t.name === actualToolName);
1421
+ result$1.tools.push({
1422
+ server,
1423
+ tool: {
1424
+ name: tool.name,
1425
+ description: tool.description,
1426
+ inputSchema: tool.inputSchema
1427
+ }
1428
+ });
1429
+ } else for (const server of servers) {
1430
+ const tool = serverToolsMap.get(server).find((t) => t.name === actualToolName);
1431
+ result$1.tools.push({
1432
+ server,
1433
+ tool: {
1434
+ name: tool.name,
1435
+ description: tool.description,
1436
+ inputSchema: tool.inputSchema
1437
+ }
1438
+ });
1439
+ }
1440
+ return result$1;
1441
+ }));
1442
+ const foundTools = [];
1443
+ const foundSkills = [];
1444
+ const notFoundItems = [];
1445
+ for (const result$1 of lookupResults) {
1446
+ foundTools.push(...result$1.tools);
1447
+ foundSkills.push(...result$1.skills);
1448
+ if (result$1.notFound) notFoundItems.push(result$1.notFound);
1449
+ }
1450
+ if (foundTools.length === 0 && foundSkills.length === 0) return {
1451
+ content: [{
1452
+ type: "text",
1453
+ text: `None of the requested tools/skills found.\nRequested: ${toolNames.join(", ")}\nUse describe_tools to see available tools and skills.`
1454
+ }],
1455
+ isError: true
1456
+ };
1457
+ const result = {};
1458
+ const nextSteps = [];
1459
+ if (foundTools.length > 0) {
1460
+ result.tools = foundTools;
1461
+ nextSteps.push("For MCP tools: Use the use_tool function with toolName and toolArgs based on the inputSchema above.");
1462
+ }
1463
+ if (foundSkills.length > 0) {
1464
+ result.skills = foundSkills;
1465
+ nextSteps.push(`For skill, just follow skill's description to continue.`);
1466
+ }
1467
+ if (nextSteps.length > 0) result.nextSteps = nextSteps;
1468
+ if (notFoundItems.length > 0) {
1469
+ result.notFound = notFoundItems;
1470
+ result.warnings = [`Items not found: ${notFoundItems.join(", ")}`];
1471
+ }
1472
+ return { content: [{
1473
+ type: "text",
1474
+ text: JSON.stringify(result, null, 2)
1475
+ }] };
1476
+ } catch (error) {
1477
+ return {
1478
+ content: [{
1479
+ type: "text",
1480
+ text: `Error describing tools: ${error instanceof Error ? error.message : "Unknown error"}`
1481
+ }],
1482
+ isError: true
1483
+ };
1484
+ }
1485
+ }
1486
+ };
1487
+
1488
+ //#endregion
1489
+ //#region src/utils/toolCapabilities.ts
1490
+ const TOOL_CAPABILITIES_META_KEY = "agiflowai/capabilities";
1491
+ function getToolCapabilities(tool) {
1492
+ const rawCapabilities = tool._meta?.[TOOL_CAPABILITIES_META_KEY];
1493
+ if (!Array.isArray(rawCapabilities)) return [];
1494
+ return rawCapabilities.filter((value) => typeof value === "string");
1495
+ }
1496
+ function getUniqueSortedCapabilities(tools) {
1497
+ return Array.from(new Set(tools.flatMap((tool) => getToolCapabilities(tool)))).sort();
1498
+ }
1499
+
1500
+ //#endregion
1501
+ //#region src/tools/SearchListToolsTool.ts
1502
+ var SearchListToolsTool = class SearchListToolsTool {
1503
+ static TOOL_NAME = "list_tools";
1504
+ constructor(_clientManager, definitionsCacheService) {
1505
+ this._clientManager = _clientManager;
1506
+ this.definitionsCacheService = definitionsCacheService;
1507
+ }
1508
+ formatToolName(toolName, serverName, toolToServers) {
1509
+ return (toolToServers.get(toolName) || []).length > 1 ? `${serverName}__${toolName}` : toolName;
1510
+ }
1511
+ async getDefinition() {
1512
+ const serverDefinitions = await this.definitionsCacheService.getServerDefinitions();
1513
+ const capabilitySummary = serverDefinitions.length > 0 ? serverDefinitions.map((server) => {
1514
+ const capabilities = getUniqueSortedCapabilities(server.tools);
1515
+ const summary = capabilities.length > 0 ? capabilities.join(", ") : server.serverInstruction || "No capability summary available";
1516
+ return `${server.serverName}: ${summary}`;
1517
+ }).join("\n") : "No proxied servers available.";
1518
+ return {
1519
+ name: SearchListToolsTool.TOOL_NAME,
1520
+ description: `Search proxied MCP tools by server capability summary.\n\nAvailable capabilities:\n${capabilitySummary}`,
1521
+ inputSchema: {
1522
+ type: "object",
1523
+ properties: {
1524
+ capability: {
1525
+ type: "string",
1526
+ description: "Optional capability filter. Matches explicit capability tags first, then server summaries, server names, tool names, and tool descriptions."
1527
+ },
1528
+ serverName: {
1529
+ type: "string",
1530
+ description: "Optional server name filter."
1531
+ }
1532
+ },
1533
+ additionalProperties: false
1534
+ }
1535
+ };
1536
+ }
1537
+ async execute(input) {
1538
+ const serverDefinitions = await this.definitionsCacheService.getServerDefinitions();
1539
+ const capabilityFilter = input.capability?.trim().toLowerCase();
1540
+ const serverNameFilter = input.serverName?.trim().toLowerCase();
1541
+ const toolToServers = /* @__PURE__ */ new Map();
1542
+ for (const serverDefinition of serverDefinitions) for (const tool of serverDefinition.tools) {
1543
+ if (!toolToServers.has(tool.name)) toolToServers.set(tool.name, []);
1544
+ toolToServers.get(tool.name)?.push(serverDefinition.serverName);
1545
+ }
1546
+ const filteredServers = serverDefinitions.filter((serverDefinition) => {
1547
+ if (serverNameFilter && serverDefinition.serverName.toLowerCase() !== serverNameFilter) return false;
1548
+ if (!capabilityFilter) return true;
1549
+ if ((serverDefinition.serverInstruction?.toLowerCase() || "").includes(capabilityFilter)) return true;
1550
+ if (getUniqueSortedCapabilities(serverDefinition.tools).some((capability) => capability.toLowerCase().includes(capabilityFilter))) return true;
1551
+ return serverDefinition.tools.some((tool) => {
1552
+ const toolName = this.formatToolName(tool.name, serverDefinition.serverName, toolToServers);
1553
+ const toolCapabilities = getToolCapabilities(tool);
1554
+ return toolName.toLowerCase().includes(capabilityFilter) || (tool.description || "").toLowerCase().includes(capabilityFilter) || toolCapabilities.some((capability) => capability.toLowerCase().includes(capabilityFilter));
1555
+ });
1556
+ }).map((serverDefinition) => ({
1557
+ server: serverDefinition.serverName,
1558
+ capabilities: getUniqueSortedCapabilities(serverDefinition.tools),
1559
+ summary: serverDefinition.serverInstruction,
1560
+ tools: serverDefinition.tools.map((tool) => ({
1561
+ name: this.formatToolName(tool.name, serverDefinition.serverName, toolToServers),
1562
+ description: serverDefinition.omitToolDescription ? void 0 : tool.description,
1563
+ capabilities: getToolCapabilities(tool)
1564
+ }))
1565
+ })).filter((server) => server.tools.length > 0);
1566
+ const result = { servers: filteredServers };
1567
+ return {
1568
+ content: [{
1569
+ type: "text",
1570
+ text: JSON.stringify(result, null, 2)
1571
+ }],
1572
+ isError: filteredServers.length === 0 ? true : void 0
1573
+ };
1574
+ }
1575
+ };
1576
+
1577
+ //#endregion
1578
+ //#region src/tools/UseToolTool.ts
1579
+ /**
1580
+ * UseToolTool executes MCP tools and skills with proper error handling.
1581
+ *
1582
+ * This tool supports three invocation patterns:
1583
+ * 1. skill__skillName - Invokes a skill from the configured skills directory
1584
+ * 2. serverName__toolName - Invokes a tool on a specific MCP server
1585
+ * 3. plainToolName - Searches all servers for a unique tool match
1586
+ *
1587
+ * @example
1588
+ * const tool = new UseToolTool(clientManager, skillService);
1589
+ * await tool.execute({ toolName: 'skill__pdf' }); // Invoke a skill
1590
+ * await tool.execute({ toolName: 'playwright__browser_click', toolArgs: { ref: 'btn' } });
1591
+ */
1592
+ var UseToolTool = class UseToolTool {
1593
+ static TOOL_NAME = "use_tool";
1594
+ clientManager;
1595
+ skillService;
1596
+ definitionsCacheService;
1597
+ /** Unique server identifier for this mcp-proxy instance */
1598
+ serverId;
1599
+ /**
1600
+ * Creates a new UseToolTool instance
1601
+ * @param clientManager - The MCP client manager for accessing remote servers
1602
+ * @param skillService - Optional skill service for loading and executing skills
1603
+ * @param serverId - Unique server identifier for this mcp-proxy instance
1604
+ */
1605
+ constructor(clientManager, skillService, serverId, definitionsCacheService) {
1606
+ this.clientManager = clientManager;
1607
+ this.skillService = skillService;
1608
+ this.definitionsCacheService = definitionsCacheService || new DefinitionsCacheService(clientManager, skillService);
1609
+ this.serverId = serverId || DEFAULT_SERVER_ID;
1610
+ }
1611
+ /**
1612
+ * Returns the MCP tool definition with name, description, and input schema.
1613
+ *
1614
+ * The definition describes how to use this tool to execute MCP tools or skills,
1615
+ * including the skill__ prefix format for skill invocations.
1616
+ *
1617
+ * @returns The tool definition conforming to MCP spec
1618
+ */
1619
+ getDefinition() {
1620
+ return {
1621
+ name: UseToolTool.TOOL_NAME,
1622
+ description: `Execute an MCP tool (NOT Skill) with provided arguments. You MUST call describe_tools first to discover the tool's correct arguments. Then to use tool:
1623
+ - Provide toolName and toolArgs based on the schema
1624
+ - If multiple servers provide the same tool, specify serverName
1625
+
1626
+ IMPORTANT: Only use tools discovered from describe_tools with id="${this.serverId}".
1627
+ `,
1628
+ inputSchema: {
1629
+ type: "object",
1630
+ properties: {
1631
+ toolName: {
1632
+ type: "string",
1633
+ description: "Name of the tool to execute",
1634
+ minLength: 1
1635
+ },
1636
+ toolArgs: {
1637
+ type: "object",
1638
+ description: "Arguments to pass to the tool, as discovered from describe_tools"
1639
+ }
1640
+ },
1641
+ required: ["toolName"],
1642
+ additionalProperties: false
1643
+ }
1644
+ };
1645
+ }
1646
+ /**
1647
+ * Returns guidance message for skill invocation.
1648
+ *
1649
+ * Skills are not executed via use_tool - they provide instructions that should
1650
+ * be followed directly. This method returns a message directing users to use
1651
+ * describe_tools to get the skill details and follow its instructions.
1652
+ *
1653
+ * @param skill - The skill that was requested
1654
+ * @returns CallToolResult with guidance message
1655
+ */
1656
+ executeSkill(skill) {
1657
+ return { content: [{
1658
+ type: "text",
1659
+ text: `Skill "${skill.name}" found. Skills provide instructions and should not be executed via use_tool.\n\nUse describe_tools to view the skill details at: ${skill.basePath}\n\nThen follow the skill's instructions directly.`
1660
+ }] };
1661
+ }
1662
+ /**
1663
+ * Finds a prompt-based skill by name from all connected MCP servers.
1664
+ *
1665
+ * @param skillName - The skill name to search for
1666
+ * @returns PromptSkillMatch if found, undefined otherwise
1667
+ */
1668
+ async findPromptSkill(skillName) {
1669
+ if (!skillName) return void 0;
1670
+ return await this.definitionsCacheService.getPromptSkillByName(skillName);
1671
+ }
1672
+ /**
1673
+ * Returns guidance message for prompt-based skill invocation.
1674
+ *
1675
+ * @param promptSkill - The prompt skill match that was found
1676
+ * @returns CallToolResult with guidance message
1677
+ */
1678
+ executePromptSkill(promptSkill) {
1679
+ const location = promptSkill.skill.folder || `prompt:${promptSkill.serverName}/${promptSkill.promptName}`;
1680
+ return { content: [{
1681
+ type: "text",
1682
+ text: `Skill "${promptSkill.skill.name}" found. Skills provide instructions and should not be executed via use_tool.\n\nUse describe_tools to view the skill details at: ${location}\n\nThen follow the skill's instructions directly.`
1683
+ }] };
1684
+ }
1685
+ /**
1686
+ * Executes a tool or skill based on the provided input.
1687
+ *
1688
+ * Handles three invocation patterns:
1689
+ * 1. skill__skillName - Routes to skill execution via SkillService
1690
+ * 2. serverName__toolName - Routes to specific MCP server
1691
+ * 3. plainToolName - Searches all servers for unique match
1692
+ *
1693
+ * Edge cases:
1694
+ * - Returns error if skill not found when using skill__ prefix
1695
+ * - Returns error if tool is blacklisted on target server
1696
+ * - Returns disambiguation message if tool exists on multiple servers
1697
+ *
1698
+ * @param input - The tool/skill name and optional arguments
1699
+ * @returns CallToolResult with execution output or error
1700
+ */
1701
+ async execute(input) {
1702
+ try {
1703
+ const { toolName: inputToolName, toolArgs = {} } = input;
1704
+ if (inputToolName.startsWith(SKILL_PREFIX)) {
1705
+ const skillName = inputToolName.slice(SKILL_PREFIX.length);
1706
+ if (this.skillService) {
1707
+ const skill = await this.skillService.getSkill(skillName);
1708
+ if (skill) return this.executeSkill(skill);
1709
+ }
1710
+ const promptSkill = await this.findPromptSkill(skillName);
1711
+ if (promptSkill) return this.executePromptSkill(promptSkill);
1712
+ return {
1713
+ content: [{
1714
+ type: "text",
1715
+ text: `Skill "${skillName}" not found. Use describe_tools to see available skills.`
1716
+ }],
1717
+ isError: true
1718
+ };
1719
+ }
1720
+ const knownServerNames = this.clientManager.getKnownServerNames();
1721
+ const { serverName, actualToolName } = parseToolName(inputToolName);
1722
+ if (serverName) try {
1723
+ const client = await this.clientManager.ensureConnected(serverName);
1724
+ if (client.toolBlacklist?.includes(actualToolName)) return {
1725
+ content: [{
1726
+ type: "text",
1727
+ text: `Tool "${actualToolName}" is blacklisted on server "${serverName}" and cannot be executed.`
1728
+ }],
1729
+ isError: true
1730
+ };
1731
+ const reqTimeout = this.clientManager.getServerRequestTimeout(serverName);
1732
+ return await client.callTool(actualToolName, toolArgs, reqTimeout ? { timeout: reqTimeout } : void 0);
1733
+ } catch (error) {
1734
+ return {
1735
+ content: [{
1736
+ type: "text",
1737
+ text: `Failed to call tool "${actualToolName}" on server "${serverName}". Available servers: ${knownServerNames.join(", ")}. ${error instanceof Error ? error.message : "Unknown error"}`
1738
+ }],
1739
+ isError: true
1740
+ };
1741
+ }
1742
+ const matchingServers = await this.definitionsCacheService.getServersForTool(actualToolName);
1743
+ if (matchingServers.length === 0) {
1744
+ if (this.skillService) {
1745
+ const skill = await this.skillService.getSkill(actualToolName);
1746
+ if (skill) return this.executeSkill(skill);
1747
+ }
1748
+ const promptSkill = await this.findPromptSkill(actualToolName);
1749
+ if (promptSkill) return this.executePromptSkill(promptSkill);
1750
+ return {
1751
+ content: [{
1752
+ type: "text",
1753
+ text: `Tool or skill "${actualToolName}" not found. Use describe_tools to see available tools and skills.`
1754
+ }],
1755
+ isError: true
1756
+ };
1757
+ }
1758
+ if (matchingServers.length > 1) return {
1759
+ content: [{
1760
+ type: "text",
1761
+ text: `Tool "${actualToolName}" found on multiple servers. Use prefixed format to specify: ${matchingServers.map((s) => `${s}__${actualToolName}`).join(", ")}`
1762
+ }],
1763
+ isError: true
1764
+ };
1765
+ try {
1766
+ const targetServerName = matchingServers[0];
1767
+ const client = await this.clientManager.ensureConnected(targetServerName);
1768
+ const targetReqTimeout = this.clientManager.getServerRequestTimeout(targetServerName);
1769
+ return await client.callTool(actualToolName, toolArgs, targetReqTimeout ? { timeout: targetReqTimeout } : void 0);
1770
+ } catch (error) {
1771
+ return {
1772
+ content: [{
1773
+ type: "text",
1774
+ text: `Failed to call tool "${actualToolName}": ${error instanceof Error ? error.message : "Unknown error"}`
1775
+ }],
1776
+ isError: true
1777
+ };
1778
+ }
1779
+ } catch (error) {
1780
+ return {
1781
+ content: [{
1782
+ type: "text",
1783
+ text: `Error executing tool: ${error instanceof Error ? error.message : "Unknown error"}`
1784
+ }],
1785
+ isError: true
1786
+ };
1787
+ }
1788
+ }
1789
+ };
1790
+
1791
+ //#endregion
1792
+ //#region src/services/RemoteConfigCacheService.ts
1793
+ /**
1794
+ * RemoteConfigCacheService
1795
+ *
1796
+ * DESIGN PATTERNS:
1797
+ * - Service pattern for cache management
1798
+ * - Single responsibility principle
1799
+ * - File-based caching with TTL support
1800
+ *
1801
+ * CODING STANDARDS:
1802
+ * - Use async/await for asynchronous operations
1803
+ * - Handle file system errors gracefully
1804
+ * - Keep cache organized by URL hash
1805
+ * - Implement automatic cache expiration
1806
+ *
1807
+ * AVOID:
1808
+ * - Storing sensitive data in cache (headers with tokens)
1809
+ * - Unbounded cache growth
1810
+ * - Missing error handling for file operations
1811
+ */
1812
+ /**
1813
+ * Service for caching remote MCP configurations
1814
+ */
1815
+ var RemoteConfigCacheService = class {
1816
+ cacheDir;
1817
+ cacheTTL;
1818
+ readEnabled;
1819
+ writeEnabled;
1820
+ logger;
1821
+ constructor(options, logger = console) {
1822
+ this.cacheDir = (0, node_path.join)((0, node_os.tmpdir)(), "mcp-proxy-cache", "remote-configs");
1823
+ this.cacheTTL = options?.ttl || 3600 * 1e3;
1824
+ this.readEnabled = options?.readEnabled !== void 0 ? options.readEnabled : true;
1825
+ this.writeEnabled = options?.writeEnabled !== void 0 ? options.writeEnabled : true;
1826
+ this.logger = logger;
1827
+ }
1828
+ /**
1829
+ * Generate a hash key from remote config URL
1830
+ * Only uses URL for hashing to avoid caching credentials in the key
1831
+ */
1832
+ generateCacheKey(url) {
1833
+ return (0, node_crypto.createHash)("sha256").update(url).digest("hex");
1834
+ }
1835
+ /**
1836
+ * Get the cache file path for a given cache key
1837
+ */
1838
+ getCacheFilePath(cacheKey) {
1839
+ return (0, node_path.join)(this.cacheDir, `${cacheKey}.json`);
1840
+ }
1841
+ /**
1842
+ * Initialize cache directory
1843
+ * Uses mkdir with recursive option which handles existing directories gracefully
1844
+ * (no TOCTOU race condition from existsSync check)
1845
+ */
1846
+ async ensureCacheDir() {
1847
+ try {
1848
+ await (0, node_fs_promises.mkdir)(this.cacheDir, { recursive: true });
1849
+ } catch (error) {
1850
+ if (error?.code !== "EEXIST") throw error;
1851
+ }
1852
+ }
1853
+ /**
1854
+ * Get cached data for a remote config URL
1855
+ */
1856
+ async get(url) {
1857
+ if (!this.readEnabled) return null;
1858
+ try {
1859
+ await this.ensureCacheDir();
1860
+ const cacheKey = this.generateCacheKey(url);
1861
+ const cacheFilePath = this.getCacheFilePath(cacheKey);
1862
+ if (!(0, node_fs.existsSync)(cacheFilePath)) return null;
1863
+ const cacheContent = await (0, node_fs_promises.readFile)(cacheFilePath, "utf-8");
1864
+ const cacheEntry = JSON.parse(cacheContent);
1865
+ const now = Date.now();
1866
+ if (now > cacheEntry.expiresAt) {
1867
+ await (0, node_fs_promises.unlink)(cacheFilePath).catch(() => {});
1868
+ return null;
1869
+ }
1870
+ const expiresInSeconds = Math.round((cacheEntry.expiresAt - now) / 1e3);
1871
+ this.logger.debug(`Remote config cache hit for ${url} (expires in ${expiresInSeconds}s)`);
1872
+ return cacheEntry.data;
1873
+ } catch (error) {
1874
+ this.logger.warn(`Failed to read remote config cache for ${url}`, error);
1875
+ return null;
1876
+ }
1877
+ }
1878
+ /**
1879
+ * Set cached data for a remote config URL
1880
+ */
1881
+ async set(url, data) {
1882
+ if (!this.writeEnabled) return;
1883
+ try {
1884
+ await this.ensureCacheDir();
1885
+ const cacheKey = this.generateCacheKey(url);
1886
+ const cacheFilePath = this.getCacheFilePath(cacheKey);
1887
+ const now = Date.now();
1888
+ const cacheEntry = {
1889
+ data,
1890
+ timestamp: now,
1891
+ expiresAt: now + this.cacheTTL,
1892
+ url
1893
+ };
1894
+ await (0, node_fs_promises.writeFile)(cacheFilePath, JSON.stringify(cacheEntry, null, 2), "utf-8");
1895
+ this.logger.debug(`Cached remote config for ${url} (TTL: ${Math.round(this.cacheTTL / 1e3)}s)`);
1896
+ } catch (error) {
1897
+ this.logger.warn(`Failed to write remote config cache for ${url}`, error);
1898
+ }
1899
+ }
1900
+ /**
1901
+ * Clear cache for a specific URL
1902
+ */
1903
+ async clear(url) {
1904
+ try {
1905
+ const cacheKey = this.generateCacheKey(url);
1906
+ const cacheFilePath = this.getCacheFilePath(cacheKey);
1907
+ if ((0, node_fs.existsSync)(cacheFilePath)) {
1908
+ await (0, node_fs_promises.unlink)(cacheFilePath);
1909
+ this.logger.info(`Cleared remote config cache for ${url}`);
1910
+ }
1911
+ } catch (error) {
1912
+ this.logger.warn(`Failed to clear remote config cache for ${url}`, error);
1913
+ }
1914
+ }
1915
+ /**
1916
+ * Clear all cached remote configs
1917
+ */
1918
+ async clearAll() {
1919
+ try {
1920
+ if (!(0, node_fs.existsSync)(this.cacheDir)) return;
1921
+ const files = await (0, node_fs_promises.readdir)(this.cacheDir);
1922
+ const deletePromises = files.filter((file) => file.endsWith(".json")).map((file) => (0, node_fs_promises.unlink)((0, node_path.join)(this.cacheDir, file)).catch((error) => {
1923
+ this.logger.debug(`Failed to delete remote config cache file ${file}`, error);
1924
+ }));
1925
+ await Promise.all(deletePromises);
1926
+ this.logger.info(`Cleared all remote config cache entries (${files.length} files)`);
1927
+ } catch (error) {
1928
+ this.logger.warn("Failed to clear all remote config cache", error);
1929
+ }
1930
+ }
1931
+ /**
1932
+ * Clean up expired cache entries
1933
+ */
1934
+ async cleanExpired() {
1935
+ try {
1936
+ if (!(0, node_fs.existsSync)(this.cacheDir)) return;
1937
+ const now = Date.now();
1938
+ const jsonFiles = (await (0, node_fs_promises.readdir)(this.cacheDir)).filter((file) => file.endsWith(".json"));
1939
+ const expiredCount = (await Promise.all(jsonFiles.map(async (file) => {
1940
+ const filePath = (0, node_path.join)(this.cacheDir, file);
1941
+ try {
1942
+ const content = await (0, node_fs_promises.readFile)(filePath, "utf-8");
1943
+ if (now > JSON.parse(content).expiresAt) {
1944
+ await (0, node_fs_promises.unlink)(filePath);
1945
+ return true;
1946
+ }
1947
+ return false;
1948
+ } catch (error) {
1949
+ await (0, node_fs_promises.unlink)(filePath).catch(() => {
1950
+ this.logger.debug(`Failed to delete corrupt cache file ${filePath}`, error);
1951
+ });
1952
+ this.logger.warn(`Removed unreadable remote config cache file ${filePath}`, error);
1953
+ return true;
1954
+ }
1955
+ }))).filter(Boolean).length;
1956
+ if (expiredCount > 0) this.logger.info(`Cleaned up ${expiredCount} expired remote config cache entries`);
1957
+ } catch (error) {
1958
+ this.logger.warn("Failed to clean expired remote config cache", error);
1959
+ }
1960
+ }
1961
+ /**
1962
+ * Get cache statistics
1963
+ */
1964
+ async getStats() {
1965
+ try {
1966
+ if (!(0, node_fs.existsSync)(this.cacheDir)) return {
1967
+ totalEntries: 0,
1968
+ totalSize: 0
1969
+ };
1970
+ const jsonFiles = (await (0, node_fs_promises.readdir)(this.cacheDir)).filter((file) => file.endsWith(".json"));
1971
+ const totalSize = (await Promise.all(jsonFiles.map(async (file) => {
1972
+ const filePath = (0, node_path.join)(this.cacheDir, file);
1973
+ try {
1974
+ const content = await (0, node_fs_promises.readFile)(filePath, "utf-8");
1975
+ return Buffer.byteLength(content, "utf-8");
1976
+ } catch {
1977
+ return 0;
1978
+ }
1979
+ }))).reduce((sum, size) => sum + size, 0);
1980
+ return {
1981
+ totalEntries: jsonFiles.length,
1982
+ totalSize
1983
+ };
1984
+ } catch (error) {
1985
+ this.logger.warn("Failed to get remote config cache stats", error);
1986
+ return {
1987
+ totalEntries: 0,
1988
+ totalSize: 0
1989
+ };
1990
+ }
1991
+ }
1992
+ /**
1993
+ * Check if read from cache is enabled
1994
+ */
1995
+ isReadEnabled() {
1996
+ return this.readEnabled;
1997
+ }
1998
+ /**
1999
+ * Check if write to cache is enabled
2000
+ */
2001
+ isWriteEnabled() {
2002
+ return this.writeEnabled;
2003
+ }
2004
+ /**
2005
+ * Set read enabled state
2006
+ */
2007
+ setReadEnabled(enabled) {
2008
+ this.readEnabled = enabled;
2009
+ }
2010
+ /**
2011
+ * Set write enabled state
2012
+ */
2013
+ setWriteEnabled(enabled) {
2014
+ this.writeEnabled = enabled;
2015
+ }
2016
+ };
2017
+
2018
+ //#endregion
2019
+ //#region src/services/ConfigFetcherService.ts
2020
+ /**
2021
+ * ConfigFetcherService
2022
+ *
2023
+ * DESIGN PATTERNS:
2024
+ * - Service pattern for business logic encapsulation
2025
+ * - Single responsibility principle
2026
+ * - Caching pattern for performance optimization
2027
+ *
2028
+ * CODING STANDARDS:
2029
+ * - Use async/await for asynchronous operations
2030
+ * - Throw descriptive errors for error cases
2031
+ * - Keep methods focused and well-named
2032
+ * - Document complex logic with comments
2033
+ *
2034
+ * AVOID:
2035
+ * - Mixing concerns (keep focused on single domain)
2036
+ * - Direct tool implementation (services should be tool-agnostic)
2037
+ */
2038
+ /**
2039
+ * Service for fetching and caching MCP server configurations from local file and remote sources
2040
+ * Supports merging multiple remote configs with local config
2041
+ */
2042
+ var ConfigFetcherService = class {
2043
+ configFilePath;
2044
+ cacheTtlMs;
2045
+ cachedConfig = null;
2046
+ lastFetchTime = 0;
2047
+ remoteConfigCache;
2048
+ logger;
2049
+ constructor(options, logger = console) {
2050
+ this.configFilePath = options.configFilePath;
2051
+ this.cacheTtlMs = options.cacheTtlMs || 6e4;
2052
+ this.logger = logger;
2053
+ const useCache = options.useCache !== void 0 ? options.useCache : true;
2054
+ this.remoteConfigCache = new RemoteConfigCacheService({
2055
+ ttl: options.remoteCacheTtlMs || 3600 * 1e3,
2056
+ readEnabled: useCache,
2057
+ writeEnabled: true
2058
+ }, logger);
2059
+ if (!this.configFilePath) throw new Error("configFilePath must be provided");
2060
+ }
2061
+ /**
2062
+ * Fetch MCP configuration from local file and remote sources with caching
2063
+ * Merges remote configs with local config based on merge strategy
2064
+ * @param forceRefresh - Force reload from source, bypassing cache
2065
+ */
2066
+ async fetchConfiguration(forceRefresh = false) {
2067
+ const now = Date.now();
2068
+ if (!forceRefresh && this.cachedConfig && now - this.lastFetchTime < this.cacheTtlMs) return this.cachedConfig;
2069
+ const localConfigData = await this.loadRawConfigFromFile();
2070
+ const remoteConfigSources = localConfigData.remoteConfigs || [];
2071
+ let mergedConfig = await this.parseConfig(localConfigData);
2072
+ const remoteConfigPromises = remoteConfigSources.map(async (remoteSource) => {
2073
+ try {
2074
+ validateRemoteConfigSource(remoteSource);
2075
+ return {
2076
+ config: await this.loadFromUrl(remoteSource),
2077
+ mergeStrategy: remoteSource.mergeStrategy || "local-priority",
2078
+ url: remoteSource.url
2079
+ };
2080
+ } catch (error) {
2081
+ this.logger.warn(`Failed to fetch remote config from ${remoteSource.url}`, error);
2082
+ return null;
2083
+ }
2084
+ });
2085
+ const remoteConfigResults = await Promise.all(remoteConfigPromises);
2086
+ for (const result of remoteConfigResults) if (result !== null) mergedConfig = this.mergeConfigurations(mergedConfig, result.config, result.mergeStrategy);
2087
+ if (!mergedConfig.mcpServers || typeof mergedConfig.mcpServers !== "object") throw new Error("Invalid MCP configuration: missing or invalid mcpServers");
2088
+ this.cachedConfig = mergedConfig;
2089
+ this.lastFetchTime = now;
2090
+ return mergedConfig;
2091
+ }
2092
+ /**
2093
+ * Load raw configuration data from a local file (supports JSON and YAML)
2094
+ * Returns unparsed config data to allow access to remoteConfigs
2095
+ */
2096
+ async loadRawConfigFromFile() {
2097
+ if (!this.configFilePath) throw new Error("No config file path provided");
2098
+ if (!(0, node_fs.existsSync)(this.configFilePath)) throw new Error(`Config file not found: ${this.configFilePath}`);
2099
+ try {
2100
+ const content = await (0, node_fs_promises.readFile)(this.configFilePath, "utf-8");
2101
+ let rawConfig;
2102
+ if (this.configFilePath.endsWith(".yaml") || this.configFilePath.endsWith(".yml")) rawConfig = js_yaml.default.load(content);
2103
+ else rawConfig = JSON.parse(content);
2104
+ return rawConfig;
2105
+ } catch (error) {
2106
+ if (error instanceof Error) throw new Error(`Failed to load config file: ${error.message}`);
2107
+ throw new Error("Failed to load config file: Unknown error");
2108
+ }
2109
+ }
2110
+ /**
2111
+ * Parse raw config data using Zod schema
2112
+ * Filters out remoteConfigs to avoid including them in the final config
2113
+ */
2114
+ async parseConfig(rawConfig) {
2115
+ try {
2116
+ const { remoteConfigs, ...configWithoutRemote } = rawConfig;
2117
+ return parseMcpConfig(configWithoutRemote);
2118
+ } catch (error) {
2119
+ if (error instanceof Error) throw new Error(`Failed to parse config: ${error.message}`);
2120
+ throw new Error("Failed to parse config: Unknown error");
2121
+ }
2122
+ }
2123
+ /**
2124
+ * Load configuration from a remote URL with caching
2125
+ *
2126
+ * SECURITY NOTE: This method fetches remote configs based on URLs from the local config file.
2127
+ * This is intentional and safe because:
2128
+ * 1. URLs are user-controlled via their local config file (not external input)
2129
+ * 2. SSRF protection validates URLs before fetching (blocks private IPs, enforces HTTPS)
2130
+ * 3. Users explicitly opt-in to remote configs in their local configuration
2131
+ * 4. This enables centralized config management (intended feature, not a vulnerability)
2132
+ *
2133
+ * CodeQL alert "file-access-to-http" is a false positive here - we're not leaking
2134
+ * file contents to arbitrary URLs, we're fetching configs from user-specified sources.
2135
+ */
2136
+ async loadFromUrl(source) {
2137
+ try {
2138
+ const interpolatedUrl = this.interpolateEnvVars(source.url);
2139
+ const cachedConfig = await this.remoteConfigCache.get(interpolatedUrl);
2140
+ if (cachedConfig) return cachedConfig;
2141
+ const interpolatedHeaders = source.headers ? Object.fromEntries(Object.entries(source.headers).map(([key, value]) => [key, this.interpolateEnvVars(value)])) : {};
2142
+ const response = await fetch(interpolatedUrl, { headers: interpolatedHeaders });
2143
+ if (!response.ok) throw new Error(`Failed to fetch remote config: ${response.status} ${response.statusText}`);
2144
+ const config = parseMcpConfig(await response.json());
2145
+ await this.remoteConfigCache.set(interpolatedUrl, config);
2146
+ return config;
2147
+ } catch (error) {
2148
+ if (error instanceof Error) throw new Error(`Failed to fetch remote config from ${source.url}: ${error.message}`, { cause: error });
2149
+ throw new Error(`Failed to fetch remote config from ${source.url}: Unknown error`);
2150
+ }
2151
+ }
2152
+ /**
2153
+ * Interpolate environment variables in a string
2154
+ * Supports ${VAR_NAME} syntax
2155
+ */
2156
+ interpolateEnvVars(value) {
2157
+ return value.replace(/\$\{([^}]+)\}/g, (_, varName) => {
2158
+ const envValue = process.env[varName];
2159
+ if (envValue === void 0) {
2160
+ this.logger.warn(`Environment variable ${varName} is not defined, keeping placeholder`);
2161
+ return `\${${varName}}`;
2162
+ }
2163
+ return envValue;
2164
+ });
2165
+ }
2166
+ /**
2167
+ * Merge two MCP configurations based on the specified merge strategy
2168
+ * @param localConfig Configuration loaded from local file
2169
+ * @param remoteConfig Configuration loaded from remote URL
2170
+ * @param mergeStrategy Strategy for merging configs
2171
+ * @returns Merged configuration
2172
+ */
2173
+ mergeConfigurations(localConfig, remoteConfig, mergeStrategy) {
2174
+ switch (mergeStrategy) {
2175
+ case "local-priority": return {
2176
+ id: localConfig.id ?? remoteConfig.id,
2177
+ mcpServers: {
2178
+ ...remoteConfig.mcpServers,
2179
+ ...localConfig.mcpServers
2180
+ },
2181
+ skills: localConfig.skills ?? remoteConfig.skills
2182
+ };
2183
+ case "remote-priority": return {
2184
+ id: remoteConfig.id ?? localConfig.id,
2185
+ mcpServers: {
2186
+ ...localConfig.mcpServers,
2187
+ ...remoteConfig.mcpServers
2188
+ },
2189
+ skills: remoteConfig.skills ?? localConfig.skills
2190
+ };
2191
+ case "merge-deep": {
2192
+ const merged = { ...remoteConfig.mcpServers };
2193
+ for (const [serverName, localServerConfig] of Object.entries(localConfig.mcpServers)) if (merged[serverName]) {
2194
+ const remoteServer = merged[serverName];
2195
+ const mergedConfig = {
2196
+ ...remoteServer.config,
2197
+ ...localServerConfig.config
2198
+ };
2199
+ const remoteEnv = "env" in remoteServer.config ? remoteServer.config.env : void 0;
2200
+ const localEnv = "env" in localServerConfig.config ? localServerConfig.config.env : void 0;
2201
+ if (remoteEnv || localEnv) mergedConfig.env = {
2202
+ ...remoteEnv || {},
2203
+ ...localEnv || {}
2204
+ };
2205
+ const remoteHeaders = "headers" in remoteServer.config ? remoteServer.config.headers : void 0;
2206
+ const localHeaders = "headers" in localServerConfig.config ? localServerConfig.config.headers : void 0;
2207
+ if (remoteHeaders || localHeaders) mergedConfig.headers = {
2208
+ ...remoteHeaders || {},
2209
+ ...localHeaders || {}
2210
+ };
2211
+ merged[serverName] = {
2212
+ ...remoteServer,
2213
+ ...localServerConfig,
2214
+ config: mergedConfig
2215
+ };
2216
+ } else merged[serverName] = localServerConfig;
2217
+ return {
2218
+ id: localConfig.id ?? remoteConfig.id,
2219
+ mcpServers: merged,
2220
+ skills: localConfig.skills ?? remoteConfig.skills
2221
+ };
2222
+ }
2223
+ default: throw new Error(`Unknown merge strategy: ${mergeStrategy}`);
2224
+ }
2225
+ }
2226
+ /**
2227
+ * Clear the cached configuration
2228
+ */
2229
+ clearCache() {
2230
+ this.cachedConfig = null;
2231
+ this.lastFetchTime = 0;
2232
+ }
2233
+ /**
2234
+ * Check if cache is valid
2235
+ */
2236
+ isCacheValid() {
2237
+ const now = Date.now();
2238
+ return this.cachedConfig !== null && now - this.lastFetchTime < this.cacheTtlMs;
2239
+ }
2240
+ };
2241
+
2242
+ //#endregion
2243
+ //#region src/services/logger.ts
2244
+ function isRecord(value) {
2245
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2246
+ }
2247
+ function toLogOptions(data) {
2248
+ if (data === void 0) return;
2249
+ if (data instanceof Error) return { exception: data };
2250
+ if (isRecord(data) && ("attributes" in data || "exception" in data || "context" in data)) return data;
2251
+ if (isRecord(data)) return { attributes: data };
2252
+ return { attributes: { value: data } };
2253
+ }
2254
+ function createLoggerFacade(handle) {
2255
+ const logger = handle.logger;
2256
+ return {
2257
+ backend: handle.backend,
2258
+ enabled: handle.enabled,
2259
+ endpoint: handle.endpoint,
2260
+ filePath: handle.filePath,
2261
+ trace(message, data) {
2262
+ logger.trace(message, toLogOptions(data));
2263
+ },
2264
+ debug(message, data) {
2265
+ logger.debug(message, toLogOptions(data));
2266
+ },
2267
+ info(message, data) {
2268
+ logger.info(message, toLogOptions(data));
2269
+ },
2270
+ warn(message, data) {
2271
+ logger.warn(message, toLogOptions(data));
2272
+ },
2273
+ error(message, data) {
2274
+ logger.error(message, toLogOptions(data));
2275
+ },
2276
+ getTraceContext() {
2277
+ return logger.getTraceContext();
2278
+ },
2279
+ flush: () => handle.flush(),
2280
+ shutdown: () => handle.shutdown()
2281
+ };
2282
+ }
2283
+ async function createProxyLogger(options = {}) {
2284
+ return createLoggerFacade(await (0, __agimon_ai_log_sink_mcp.createNodeTelemetry)({
2285
+ env: options.env,
2286
+ workspaceRoot: options.workspaceRoot,
2287
+ serviceName: options.serviceName ?? "@agimon-ai/mcp-proxy",
2288
+ serviceVersion: options.serviceVersion,
2289
+ logDir: options.logDir,
2290
+ logFileName: options.logFileName,
2291
+ maxFileSizeBytes: options.maxFileSizeBytes,
2292
+ maxFileCount: options.maxFileCount
2293
+ }));
2294
+ }
2295
+
2296
+ //#endregion
2297
+ //#region src/services/McpClientManagerService.ts
2298
+ /** Default connection timeout in milliseconds (30 seconds) */
2299
+ const DEFAULT_CONNECTION_TIMEOUT_MS = 3e4;
2300
+ /**
2301
+ * Checks if an error is a session-related error from an HTTP backend
2302
+ * (e.g., downstream server restarted and no longer recognizes the session ID).
2303
+ */
2304
+ function isSessionError(error) {
2305
+ const message = error instanceof Error ? error.message : String(error);
2306
+ return message.includes("unknown session") || message.includes("Session not found");
2307
+ }
2308
+ /**
2309
+ * MCP Client wrapper for managing individual server connections
2310
+ * This is an internal class used by McpClientManagerService
2311
+ */
2312
+ var McpClient = class {
2313
+ serverName;
2314
+ serverInstruction;
2315
+ toolBlacklist;
2316
+ omitToolDescription;
2317
+ prompts;
2318
+ transport;
2319
+ logger;
2320
+ client;
2321
+ childProcess;
2322
+ connected = false;
2323
+ reconnectFn;
2324
+ constructor(serverName, transport, client, logger, config) {
2325
+ this.serverName = serverName;
2326
+ this.serverInstruction = config.instruction;
2327
+ this.toolBlacklist = config.toolBlacklist;
2328
+ this.omitToolDescription = config.omitToolDescription;
2329
+ this.prompts = config.prompts;
2330
+ this.transport = transport;
2331
+ this.logger = logger;
2332
+ this.client = client;
2333
+ }
2334
+ setChildProcess(process$1) {
2335
+ this.childProcess = process$1;
2336
+ }
2337
+ setConnected(connected) {
2338
+ this.connected = connected;
2339
+ }
2340
+ /**
2341
+ * Sets a reconnection function that creates a fresh Client and transport.
2342
+ * Called automatically by withSessionRetry when a session error is detected
2343
+ * (e.g., downstream HTTP server restarted and the old session ID is invalid).
2344
+ */
2345
+ setReconnectFn(fn) {
2346
+ this.reconnectFn = fn;
2347
+ }
2348
+ /**
2349
+ * Wraps an operation with automatic retry on session errors.
2350
+ * If the operation fails with a session error (e.g., downstream server restarted),
2351
+ * reconnects and retries once.
2352
+ */
2353
+ async withSessionRetry(operation) {
2354
+ try {
2355
+ return await operation();
2356
+ } catch (error) {
2357
+ if (!this.reconnectFn || !isSessionError(error)) throw error;
2358
+ this.logger.warn(`Session error for ${this.serverName}, reconnecting: ${error instanceof Error ? error.message : String(error)}`);
2359
+ try {
2360
+ await this.client.close();
2361
+ } catch (closeError) {
2362
+ this.logger.warn(`Failed to close stale client for ${this.serverName}`, closeError);
2363
+ }
2364
+ const result = await this.reconnectFn();
2365
+ this.client = result.client;
2366
+ if (result.childProcess) this.childProcess = result.childProcess;
2367
+ return await operation();
2368
+ }
2369
+ }
2370
+ async listTools() {
2371
+ if (!this.connected) throw new Error(`Client for ${this.serverName} is not connected`);
2372
+ return this.withSessionRetry(async () => {
2373
+ return (await this.client.listTools()).tools;
2374
+ });
2375
+ }
2376
+ async listResources() {
2377
+ if (!this.connected) throw new Error(`Client for ${this.serverName} is not connected`);
2378
+ return this.withSessionRetry(async () => {
2379
+ return (await this.client.listResources()).resources;
2380
+ });
2381
+ }
2382
+ async listPrompts() {
2383
+ if (!this.connected) throw new Error(`Client for ${this.serverName} is not connected`);
2384
+ return this.withSessionRetry(async () => {
2385
+ return (await this.client.listPrompts()).prompts;
2386
+ });
2387
+ }
2388
+ async callTool(name, args, options) {
2389
+ if (!this.connected) throw new Error(`Client for ${this.serverName} is not connected`);
2390
+ return this.withSessionRetry(async () => {
2391
+ const requestOptions = options?.timeout ? { timeout: options.timeout } : void 0;
2392
+ return await this.client.callTool({
2393
+ name,
2394
+ arguments: args
2395
+ }, void 0, requestOptions);
2396
+ });
2397
+ }
2398
+ async readResource(uri) {
2399
+ if (!this.connected) throw new Error(`Client for ${this.serverName} is not connected`);
2400
+ return this.withSessionRetry(async () => {
2401
+ return await this.client.readResource({ uri });
2402
+ });
2403
+ }
2404
+ async getPrompt(name, args) {
2405
+ if (!this.connected) throw new Error(`Client for ${this.serverName} is not connected`);
2406
+ return this.withSessionRetry(async () => {
2407
+ return await this.client.getPrompt({
2408
+ name,
2409
+ arguments: args
2410
+ });
2411
+ });
2412
+ }
2413
+ async close() {
2414
+ if (this.childProcess) this.childProcess.kill();
2415
+ await this.client.close();
2416
+ this.connected = false;
2417
+ }
2418
+ };
2419
+ /**
2420
+ * Service for managing MCP client connections to remote servers
2421
+ */
2422
+ var McpClientManagerService = class {
2423
+ clients = /* @__PURE__ */ new Map();
2424
+ serverConfigs = /* @__PURE__ */ new Map();
2425
+ connectionPromises = /* @__PURE__ */ new Map();
2426
+ logger;
2427
+ constructor(logger = console) {
2428
+ this.logger = logger;
2429
+ }
2430
+ /**
2431
+ * Synchronously kill all stdio MCP server child processes.
2432
+ * Must be called by the owner (e.g. transport/command layer) during shutdown.
2433
+ */
2434
+ cleanupChildProcesses() {
2435
+ for (const [serverName, client] of this.clients) try {
2436
+ const childProcess = client["childProcess"];
2437
+ if (childProcess && !childProcess.killed) {
2438
+ this.logger.info(`Killing stdio MCP server: ${serverName} (PID: ${childProcess.pid})`);
2439
+ childProcess.kill("SIGTERM");
2440
+ setTimeout(() => {
2441
+ if (!childProcess.killed) {
2442
+ this.logger.warn(`Force killing stdio MCP server: ${serverName} (PID: ${childProcess.pid})`);
2443
+ childProcess.kill("SIGKILL");
2444
+ }
2445
+ }, 1e3);
2446
+ }
2447
+ } catch (error) {
2448
+ this.logger.warn(`Failed to kill child process for ${serverName}`, error);
2449
+ }
2450
+ }
2451
+ /**
2452
+ * Connect to an MCP server based on its configuration with timeout
2453
+ * Uses the timeout from server config, falling back to default (30s)
2454
+ */
2455
+ async connectToServer(serverName, config) {
2456
+ this.serverConfigs.set(serverName, config);
2457
+ await this.ensureConnected(serverName);
2458
+ }
2459
+ registerServerConfigs(configs) {
2460
+ for (const [serverName, config] of Object.entries(configs)) this.serverConfigs.set(serverName, config);
2461
+ }
2462
+ getKnownServerNames() {
2463
+ return Array.from(this.serverConfigs.keys());
2464
+ }
2465
+ getServerRequestTimeout(serverName) {
2466
+ return this.serverConfigs.get(serverName)?.requestTimeout;
2467
+ }
2468
+ async ensureConnected(serverName) {
2469
+ const existingClient = this.clients.get(serverName);
2470
+ if (existingClient) return existingClient;
2471
+ const inflightConnection = this.connectionPromises.get(serverName);
2472
+ if (inflightConnection) return await inflightConnection;
2473
+ const config = this.serverConfigs.get(serverName);
2474
+ if (!config) throw new Error(`No configuration found for server "${serverName}"`);
2475
+ const connectionPromise = this.createConnection(serverName, config);
2476
+ this.connectionPromises.set(serverName, connectionPromise);
2477
+ try {
2478
+ return await connectionPromise;
2479
+ } finally {
2480
+ this.connectionPromises.delete(serverName);
2481
+ }
2482
+ }
2483
+ async createConnection(serverName, config) {
2484
+ const timeoutMs = config.timeout ?? DEFAULT_CONNECTION_TIMEOUT_MS;
2485
+ const client = new __modelcontextprotocol_sdk_client_index_js.Client({
2486
+ name: `@agimon-ai/mcp-proxy-client`,
2487
+ version: "0.1.0"
2488
+ }, { capabilities: {} });
2489
+ const mcpClient = new McpClient(serverName, config.transport, client, this.logger, {
2490
+ instruction: config.instruction,
2491
+ toolBlacklist: config.toolBlacklist,
2492
+ omitToolDescription: config.omitToolDescription,
2493
+ prompts: config.prompts
2494
+ });
2495
+ try {
2496
+ await Promise.race([this.performConnection(mcpClient, config), new Promise((_, reject) => setTimeout(() => reject(/* @__PURE__ */ new Error(`Connection timeout after ${timeoutMs}ms`)), timeoutMs))]);
2497
+ mcpClient.setConnected(true);
2498
+ if (config.transport === "http" || config.transport === "sse") mcpClient.setReconnectFn(async () => {
2499
+ try {
2500
+ const newClient = new __modelcontextprotocol_sdk_client_index_js.Client({
2501
+ name: "@agimon-ai/mcp-proxy-client",
2502
+ version: "0.1.0"
2503
+ }, { capabilities: {} });
2504
+ const newMcpClient = new McpClient(serverName, config.transport, newClient, this.logger, {});
2505
+ await this.performConnection(newMcpClient, config);
2506
+ return { client: newClient };
2507
+ } catch (error) {
2508
+ this.logger.warn(`Failed to reconnect to ${serverName}`, error);
2509
+ throw error;
2510
+ }
2511
+ });
2512
+ if (!mcpClient.serverInstruction) try {
2513
+ const serverInstruction = mcpClient["client"].getInstructions();
2514
+ if (serverInstruction) mcpClient.serverInstruction = serverInstruction;
2515
+ } catch (error) {
2516
+ this.logger.warn(`Failed to get server instruction from ${serverName}`, error);
2517
+ }
2518
+ this.clients.set(serverName, mcpClient);
2519
+ return mcpClient;
2520
+ } catch (error) {
2521
+ await mcpClient.close();
2522
+ throw error;
2523
+ }
2524
+ }
2525
+ /**
2526
+ * Perform the actual connection to MCP server
2527
+ */
2528
+ async performConnection(mcpClient, config) {
2529
+ if (config.transport === "stdio") await this.connectStdioClient(mcpClient, config.config);
2530
+ else if (config.transport === "http") await this.connectHttpClient(mcpClient, config.config);
2531
+ else if (config.transport === "sse") await this.connectSseClient(mcpClient, config.config);
2532
+ else throw new Error(`Unsupported transport type: ${config.transport}`);
2533
+ }
2534
+ async connectStdioClient(mcpClient, config) {
2535
+ const transport = new __modelcontextprotocol_sdk_client_stdio_js.StdioClientTransport({
2536
+ command: config.command,
2537
+ args: config.args,
2538
+ env: {
2539
+ ...process.env,
2540
+ ...config.env ?? {}
2541
+ }
2542
+ });
2543
+ await mcpClient["client"].connect(transport);
2544
+ const childProcess = transport["_process"];
2545
+ if (childProcess) mcpClient.setChildProcess(childProcess);
2546
+ }
2547
+ async connectHttpClient(mcpClient, config) {
2548
+ const transport = new __modelcontextprotocol_sdk_client_streamableHttp_js.StreamableHTTPClientTransport(new URL(config.url), { requestInit: config.headers ? { headers: config.headers } : void 0 });
2549
+ await mcpClient["client"].connect(transport);
2550
+ }
2551
+ async connectSseClient(mcpClient, config) {
2552
+ const transport = new __modelcontextprotocol_sdk_client_sse_js.SSEClientTransport(new URL(config.url));
2553
+ await mcpClient["client"].connect(transport);
2554
+ }
2555
+ /**
2556
+ * Get a connected client by server name
2557
+ */
2558
+ getClient(serverName) {
2559
+ return this.clients.get(serverName);
2560
+ }
2561
+ /**
2562
+ * Get all connected clients
2563
+ */
2564
+ getAllClients() {
2565
+ return Array.from(this.clients.values());
2566
+ }
2567
+ /**
2568
+ * Disconnect from a specific server
2569
+ */
2570
+ async disconnectServer(serverName) {
2571
+ const client = this.clients.get(serverName);
2572
+ if (client) {
2573
+ await client.close();
2574
+ this.clients.delete(serverName);
2575
+ }
2576
+ }
2577
+ /**
2578
+ * Disconnect from all servers
2579
+ */
2580
+ async disconnectAll() {
2581
+ const disconnectPromises = Array.from(this.clients.values()).map((client) => client.close());
2582
+ await Promise.all(disconnectPromises);
2583
+ this.clients.clear();
2584
+ this.connectionPromises.clear();
2585
+ }
2586
+ /**
2587
+ * Check if a server is connected
2588
+ */
2589
+ isConnected(serverName) {
2590
+ return this.clients.has(serverName);
2591
+ }
2592
+ };
2593
+
2594
+ //#endregion
2595
+ //#region src/services/RuntimeStateService.ts
2596
+ /**
2597
+ * RuntimeStateService
2598
+ *
2599
+ * Persists runtime metadata for HTTP mcp-proxy instances so external commands
2600
+ * (for example `mcp-proxy stop`) can discover and target the correct server.
2601
+ */
2602
+ const RUNTIME_DIR_NAME = "runtimes";
2603
+ const RUNTIME_FILE_SUFFIX = ".runtime.json";
2604
+ function isObject(value) {
2605
+ return typeof value === "object" && value !== null;
2606
+ }
2607
+ function isRuntimeStateRecord(value) {
2608
+ if (!isObject(value)) return false;
2609
+ return typeof value.serverId === "string" && typeof value.host === "string" && typeof value.port === "number" && value.transport === "http" && typeof value.shutdownToken === "string" && typeof value.startedAt === "string" && typeof value.pid === "number" && (value.configPath === void 0 || typeof value.configPath === "string");
2610
+ }
2611
+ function toErrorMessage$2(error) {
2612
+ return error instanceof Error ? error.message : String(error);
2613
+ }
2614
+ /**
2615
+ * Runtime state persistence implementation.
2616
+ */
2617
+ var RuntimeStateService = class RuntimeStateService {
2618
+ runtimeDir;
2619
+ logger;
2620
+ constructor(runtimeDir, logger = console) {
2621
+ this.runtimeDir = runtimeDir ?? RuntimeStateService.getDefaultRuntimeDir();
2622
+ this.logger = logger;
2623
+ }
2624
+ /**
2625
+ * Resolve default runtime directory under the user's home cache path.
2626
+ * @returns Absolute runtime directory path
2627
+ */
2628
+ static getDefaultRuntimeDir() {
2629
+ return (0, node_path.join)((0, node_os.homedir)(), ".aicode-toolkit", "mcp-proxy", RUNTIME_DIR_NAME);
2630
+ }
2631
+ /**
2632
+ * Build runtime state file path for a given server ID.
2633
+ * @param serverId - Target mcp-proxy server identifier
2634
+ * @returns Absolute runtime file path
2635
+ */
2636
+ getRecordPath(serverId) {
2637
+ return (0, node_path.join)(this.runtimeDir, `${serverId}${RUNTIME_FILE_SUFFIX}`);
2638
+ }
2639
+ /**
2640
+ * Persist a runtime state record.
2641
+ * @param record - Runtime metadata to persist
2642
+ * @returns Promise that resolves when write completes
2643
+ */
2644
+ async write(record) {
2645
+ await (0, node_fs_promises.mkdir)(this.runtimeDir, { recursive: true });
2646
+ await (0, node_fs_promises.writeFile)(this.getRecordPath(record.serverId), JSON.stringify(record, null, 2), "utf-8");
2647
+ }
2648
+ /**
2649
+ * Read a runtime state record by server ID.
2650
+ * @param serverId - Target mcp-proxy server identifier
2651
+ * @returns Matching runtime record, or null when no record exists
2652
+ */
2653
+ async read(serverId) {
2654
+ const filePath = this.getRecordPath(serverId);
2655
+ try {
2656
+ const content = await (0, node_fs_promises.readFile)(filePath, "utf-8");
2657
+ const parsed = JSON.parse(content);
2658
+ return isRuntimeStateRecord(parsed) ? parsed : null;
2659
+ } catch (error) {
2660
+ if (isObject(error) && "code" in error && error.code === "ENOENT") return null;
2661
+ throw new Error(`Failed to read runtime state for server '${serverId}' from '${filePath}': ${toErrorMessage$2(error)}`);
2662
+ }
2663
+ }
2664
+ /**
2665
+ * List all persisted runtime records.
2666
+ * @returns Array of runtime records
2667
+ */
2668
+ async list() {
2669
+ try {
2670
+ const files = (await (0, node_fs_promises.readdir)(this.runtimeDir, { withFileTypes: true })).filter((entry) => entry.isFile() && entry.name.endsWith(RUNTIME_FILE_SUFFIX));
2671
+ return (await Promise.all(files.map(async (file) => {
2672
+ try {
2673
+ const content = await (0, node_fs_promises.readFile)((0, node_path.join)(this.runtimeDir, file.name), "utf-8");
2674
+ const parsed = JSON.parse(content);
2675
+ return isRuntimeStateRecord(parsed) ? parsed : null;
2676
+ } catch (error) {
2677
+ this.logger.debug(`Skipping unreadable runtime state file ${file.name}`, error);
2678
+ return null;
2679
+ }
2680
+ }))).filter((record) => record !== null);
2681
+ } catch (error) {
2682
+ if (isObject(error) && "code" in error && error.code === "ENOENT") return [];
2683
+ throw new Error(`Failed to list runtime states from '${this.runtimeDir}': ${toErrorMessage$2(error)}`);
2684
+ }
2685
+ }
2686
+ /**
2687
+ * Remove a runtime state record by server ID.
2688
+ * @param serverId - Target mcp-proxy server identifier
2689
+ * @returns Promise that resolves when delete completes
2690
+ */
2691
+ async remove(serverId) {
2692
+ await (0, node_fs_promises.rm)(this.getRecordPath(serverId), { force: true });
2693
+ }
2694
+ };
2695
+
2696
+ //#endregion
2697
+ //#region src/services/StopServerService/constants.ts
2698
+ /**
2699
+ * StopServerService constants.
2700
+ */
2701
+ /** Maximum time in milliseconds to wait for a shutdown to complete. */
2702
+ const DEFAULT_STOP_TIMEOUT_MS = 5e3;
2703
+ /** Minimum timeout in milliseconds for individual health check requests. */
2704
+ const HEALTH_REQUEST_TIMEOUT_FLOOR_MS = 250;
2705
+ /** Delay in milliseconds between shutdown polling attempts. */
2706
+ const SHUTDOWN_POLL_INTERVAL_MS = 200;
2707
+ /** Path for the runtime health check endpoint. */
2708
+ const HEALTH_CHECK_PATH = "/health";
2709
+ /** Path for the authenticated admin shutdown endpoint. */
2710
+ const ADMIN_SHUTDOWN_PATH = "/admin/shutdown";
2711
+ /** HTTP GET method identifier. */
2712
+ const HTTP_METHOD_GET = "GET";
2713
+ /** HTTP POST method identifier. */
2714
+ const HTTP_METHOD_POST = "POST";
2715
+ /** HTTP header name for bearer token authorization. */
2716
+ const AUTHORIZATION_HEADER_NAME = "Authorization";
2717
+ /** Prefix for bearer token values in the Authorization header. */
2718
+ const BEARER_TOKEN_PREFIX = "Bearer ";
2719
+ /** HTTP protocol scheme prefix for URL construction. */
2720
+ const HTTP_PROTOCOL = "http://";
2721
+ /** Separator between host and port in URL construction. */
2722
+ const URL_PORT_SEPARATOR = ":";
2723
+ /** Loopback hostname. */
2724
+ const LOOPBACK_HOST_LOCALHOST = "localhost";
2725
+ /** IPv4 loopback address. */
2726
+ const LOOPBACK_HOST_IPV4 = "127.0.0.1";
2727
+ /** IPv6 loopback address. */
2728
+ const LOOPBACK_HOST_IPV6 = "::1";
2729
+ /** Hosts that are safe to send admin requests to (loopback only). */
2730
+ const ALLOWED_HOSTS = new Set([
2731
+ LOOPBACK_HOST_LOCALHOST,
2732
+ LOOPBACK_HOST_IPV4,
2733
+ LOOPBACK_HOST_IPV6
2734
+ ]);
2735
+ /** Expected status value in a healthy runtime response. */
2736
+ const HEALTH_STATUS_OK = "ok";
2737
+ /** Expected transport value in a healthy runtime response. */
2738
+ const HEALTH_TRANSPORT_HTTP = "http";
2739
+ /** Property key for status field in health responses. */
2740
+ const KEY_STATUS = "status";
2741
+ /** Property key for transport field in health responses. */
2742
+ const KEY_TRANSPORT = "transport";
2743
+ /** Property key for serverId field in runtime responses. */
2744
+ const KEY_SERVER_ID = "serverId";
2745
+ /** Property key for ok field in shutdown responses. */
2746
+ const KEY_OK = "ok";
2747
+ /** Property key for message field in shutdown responses. */
2748
+ const KEY_MESSAGE = "message";
2749
+
2750
+ //#endregion
2751
+ //#region src/services/StopServerService/types.ts
2752
+ /**
2753
+ * Safely cast a non-null object to a string-keyed record for property access.
2754
+ * @param value - Object value already verified as non-null
2755
+ * @returns The same value typed as a record
2756
+ */
2757
+ function toRecord(value) {
2758
+ return value;
2759
+ }
2760
+ /**
2761
+ * Type guard for health responses.
2762
+ * @param value - Candidate payload to validate
2763
+ * @returns True when payload matches health response shape
2764
+ */
2765
+ function isHealthResponse(value) {
2766
+ if (typeof value !== "object" || value === null) return false;
2767
+ const record = toRecord(value);
2768
+ return KEY_STATUS in record && record[KEY_STATUS] === HEALTH_STATUS_OK && KEY_TRANSPORT in record && record[KEY_TRANSPORT] === HEALTH_TRANSPORT_HTTP && (!(KEY_SERVER_ID in record) || record[KEY_SERVER_ID] === void 0 || typeof record[KEY_SERVER_ID] === "string");
2769
+ }
2770
+ /**
2771
+ * Type guard for shutdown responses.
2772
+ * @param value - Candidate payload to validate
2773
+ * @returns True when payload matches shutdown response shape
2774
+ */
2775
+ function isShutdownResponse(value) {
2776
+ if (typeof value !== "object" || value === null) return false;
2777
+ const record = toRecord(value);
2778
+ return KEY_OK in record && typeof record[KEY_OK] === "boolean" && KEY_MESSAGE in record && typeof record[KEY_MESSAGE] === "string" && (!(KEY_SERVER_ID in record) || record[KEY_SERVER_ID] === void 0 || typeof record[KEY_SERVER_ID] === "string");
2779
+ }
2780
+
2781
+ //#endregion
2782
+ //#region src/services/StopServerService/StopServerService.ts
2783
+ /**
2784
+ * Format runtime endpoint URL after validating the host is a loopback address.
2785
+ * Rejects non-loopback hosts to prevent SSRF via tampered runtime state files.
2786
+ * @param runtime - Runtime record to format
2787
+ * @param path - Request path to append
2788
+ * @returns Full runtime URL
2789
+ */
2790
+ function buildRuntimeUrl(runtime, path) {
2791
+ if (!ALLOWED_HOSTS.has(runtime.host)) throw new Error(`Refusing to connect to non-loopback host '${runtime.host}'. Only ${Array.from(ALLOWED_HOSTS).join(", ")} are allowed.`);
2792
+ return `${HTTP_PROTOCOL}${runtime.host}${URL_PORT_SEPARATOR}${runtime.port}${path}`;
2793
+ }
2794
+ function toErrorMessage$1(error) {
2795
+ return error instanceof Error ? error.message : String(error);
2796
+ }
2797
+ function sleep(delayMs) {
2798
+ return new Promise((resolve$2) => {
2799
+ setTimeout(resolve$2, delayMs);
2800
+ });
2801
+ }
2802
+ /**
2803
+ * Service for resolving runtime targets and stopping them safely.
2804
+ */
2805
+ var StopServerService = class {
2806
+ runtimeStateService;
2807
+ logger;
2808
+ constructor(runtimeStateService = new RuntimeStateService(), logger = console) {
2809
+ this.runtimeStateService = runtimeStateService;
2810
+ this.logger = logger;
2811
+ }
2812
+ /**
2813
+ * Resolve a target runtime and stop it cooperatively.
2814
+ * @param request - Stop request options
2815
+ * @returns Stop result payload
2816
+ */
2817
+ async stop(request) {
2818
+ const timeoutMs = request.timeoutMs ?? DEFAULT_STOP_TIMEOUT_MS;
2819
+ const runtime = await this.resolveRuntime(request);
2820
+ const health = await this.fetchHealth(runtime, timeoutMs);
2821
+ if (!health.reachable) {
2822
+ await this.runtimeStateService.remove(runtime.serverId);
2823
+ throw new Error(`Runtime '${runtime.serverId}' is not reachable at http://${runtime.host}:${runtime.port}. Removed stale runtime record.`);
2824
+ }
2825
+ if (!request.force && health.payload?.serverId && health.payload.serverId !== runtime.serverId) throw new Error(`Refusing to stop runtime at http://${runtime.host}:${runtime.port}: expected server ID '${runtime.serverId}' but health endpoint reported '${health.payload.serverId}'. Use --force to override.`);
2826
+ const shutdownToken = request.token ?? runtime.shutdownToken;
2827
+ if (!shutdownToken) throw new Error(`No shutdown token available for runtime '${runtime.serverId}'.`);
2828
+ const shutdownResponse = await this.requestShutdown(runtime, shutdownToken, timeoutMs);
2829
+ await this.waitForShutdown(runtime, timeoutMs);
2830
+ await this.runtimeStateService.remove(runtime.serverId);
2831
+ return {
2832
+ ok: true,
2833
+ serverId: runtime.serverId,
2834
+ host: runtime.host,
2835
+ port: runtime.port,
2836
+ message: shutdownResponse.message
2837
+ };
2838
+ }
2839
+ /**
2840
+ * Resolve a runtime record from explicit ID or a unique host/port pair.
2841
+ * @param request - Stop request options
2842
+ * @returns Matching runtime record
2843
+ */
2844
+ async resolveRuntime(request) {
2845
+ if (request.serverId) {
2846
+ const runtime = await this.runtimeStateService.read(request.serverId);
2847
+ if (!runtime) throw new Error(`No runtime record found for server ID '${request.serverId}'. Start the server with 'mcp-proxy mcp-serve --type http' first.`);
2848
+ return runtime;
2849
+ }
2850
+ if (request.host === void 0 || request.port === void 0) throw new Error("Provide --id or both --host and --port to select a runtime.");
2851
+ const matches = (await this.runtimeStateService.list()).filter((runtime) => runtime.host === request.host && runtime.port === request.port);
2852
+ if (matches.length === 0) throw new Error(`No runtime record found for http://${request.host}:${request.port}. Start the server with 'mcp-proxy mcp-serve --type http' first.`);
2853
+ if (matches.length > 1) throw new Error(`Multiple runtime records match http://${request.host}:${request.port}. Retry with --id to avoid stopping the wrong server.`);
2854
+ return matches[0];
2855
+ }
2856
+ /**
2857
+ * Read the runtime health payload.
2858
+ * @param runtime - Runtime to query
2859
+ * @param timeoutMs - Request timeout in milliseconds
2860
+ * @returns Reachability status and optional payload
2861
+ */
2862
+ async fetchHealth(runtime, timeoutMs) {
2863
+ try {
2864
+ const response = await this.fetchWithTimeout(buildRuntimeUrl(runtime, HEALTH_CHECK_PATH), { method: HTTP_METHOD_GET }, timeoutMs);
2865
+ if (!response.ok) return { reachable: false };
2866
+ const payload = await response.json();
2867
+ if (!isHealthResponse(payload)) throw new Error("Received invalid health response payload.");
2868
+ return {
2869
+ reachable: true,
2870
+ payload
2871
+ };
2872
+ } catch (error) {
2873
+ this.logger.debug(`Health check failed for ${runtime.serverId}`, error);
2874
+ return { reachable: false };
2875
+ }
2876
+ }
2877
+ /**
2878
+ * Send authenticated shutdown request to the admin endpoint.
2879
+ * @param runtime - Runtime to stop
2880
+ * @param shutdownToken - Bearer token for the admin endpoint
2881
+ * @param timeoutMs - Request timeout in milliseconds
2882
+ * @returns Parsed shutdown response payload
2883
+ */
2884
+ async requestShutdown(runtime, shutdownToken, timeoutMs) {
2885
+ const response = await this.fetchWithTimeout(buildRuntimeUrl(runtime, ADMIN_SHUTDOWN_PATH), {
2886
+ method: HTTP_METHOD_POST,
2887
+ headers: { [AUTHORIZATION_HEADER_NAME]: `${BEARER_TOKEN_PREFIX}${shutdownToken}` }
2888
+ }, timeoutMs);
2889
+ const payload = await response.json();
2890
+ if (!isShutdownResponse(payload)) throw new Error("Received invalid shutdown response payload.");
2891
+ if (!response.ok || !payload.ok) throw new Error(payload.message);
2892
+ return payload;
2893
+ }
2894
+ /**
2895
+ * Poll until the target runtime is no longer reachable.
2896
+ * @param runtime - Runtime expected to stop
2897
+ * @param timeoutMs - Maximum wait time in milliseconds
2898
+ * @returns Promise that resolves when shutdown is observed
2899
+ */
2900
+ async waitForShutdown(runtime, timeoutMs) {
2901
+ const deadline = Date.now() + timeoutMs;
2902
+ while (Date.now() < deadline) {
2903
+ if (!(await this.fetchHealth(runtime, Math.max(HEALTH_REQUEST_TIMEOUT_FLOOR_MS, deadline - Date.now()))).reachable) return;
2904
+ await sleep(SHUTDOWN_POLL_INTERVAL_MS);
2905
+ }
2906
+ throw new Error(`Timed out waiting for runtime '${runtime.serverId}' to stop at http://${runtime.host}:${runtime.port}.`);
2907
+ }
2908
+ /**
2909
+ * Perform a fetch with an abort timeout.
2910
+ * @param url - Target URL
2911
+ * @param init - Fetch options
2912
+ * @param timeoutMs - Timeout in milliseconds
2913
+ * @returns Fetch response
2914
+ */
2915
+ async fetchWithTimeout(url, init, timeoutMs) {
2916
+ const controller = new AbortController();
2917
+ const timeoutId = setTimeout(() => {
2918
+ controller.abort();
2919
+ }, timeoutMs);
2920
+ try {
2921
+ return await fetch(url, {
2922
+ ...init,
2923
+ signal: controller.signal
2924
+ });
2925
+ } catch (error) {
2926
+ throw new Error(`Request to '${url}' failed: ${toErrorMessage$1(error)}`);
2927
+ } finally {
2928
+ clearTimeout(timeoutId);
2929
+ }
2930
+ }
2931
+ };
2932
+
2933
+ //#endregion
2934
+ //#region src/services/SkillService.ts
2935
+ /**
2936
+ * SkillService
2937
+ *
2938
+ * DESIGN PATTERNS:
2939
+ * - Service pattern for business logic encapsulation
2940
+ * - Single responsibility principle
2941
+ * - Lazy loading pattern for skill discovery
2942
+ *
2943
+ * CODING STANDARDS:
2944
+ * - Use async/await for asynchronous operations
2945
+ * - Throw descriptive errors for error cases
2946
+ * - Keep methods focused and well-named
2947
+ * - Document complex logic with comments
2948
+ *
2949
+ * AVOID:
2950
+ * - Mixing concerns (keep focused on single domain)
2951
+ * - Direct tool implementation (services should be tool-agnostic)
2952
+ */
2953
+ /**
2954
+ * Error thrown when skill loading fails
2955
+ */
2956
+ var SkillLoadError = class extends Error {
2957
+ constructor(message, filePath, cause) {
2958
+ super(message);
2959
+ this.filePath = filePath;
2960
+ this.cause = cause;
2961
+ this.name = "SkillLoadError";
2962
+ }
2963
+ };
2964
+ /**
2965
+ * Check if a path exists asynchronously
2966
+ * @param path - Path to check
2967
+ * @returns true if path exists, false otherwise
2968
+ * @throws Error for unexpected filesystem errors (permission denied, etc.)
2969
+ */
2970
+ async function pathExists(path) {
2971
+ try {
2972
+ await (0, node_fs_promises.access)(path);
2973
+ return true;
2974
+ } catch (error) {
2975
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") return false;
2976
+ throw new Error(`Failed to check path existence for "${path}": ${error instanceof Error ? error.message : "Unknown error"}`);
2977
+ }
2978
+ }
2979
+ /**
2980
+ * Service for loading and managing skills from configured skill directories.
2981
+ *
2982
+ * Skills are markdown files with YAML frontmatter that can be invoked via
2983
+ * the skill__ prefix in describe_tools and use_tool.
2984
+ *
2985
+ * Skills are only enabled when explicitly configured via the `skills.paths` array
2986
+ * in the MCP config.
2987
+ *
2988
+ * @example
2989
+ * // Config with skills enabled:
2990
+ * // skills:
2991
+ * // paths:
2992
+ * // - ".claude/skills"
2993
+ * // - "/absolute/path/to/skills"
2994
+ *
2995
+ * const skillService = new SkillService('/project/root', ['.claude/skills']);
2996
+ * const skills = await skillService.getSkills();
2997
+ */
2998
+ var SkillService = class {
2999
+ cwd;
3000
+ skillPaths;
3001
+ cachedSkills = null;
3002
+ skillsByName = null;
3003
+ /** Active file watchers for skill directories */
3004
+ watchers = [];
3005
+ /** Polling timers used when native file watching is unavailable */
3006
+ pollingTimers = [];
3007
+ /** Callback invoked when cache is invalidated due to file changes */
3008
+ onCacheInvalidated;
3009
+ logger;
3010
+ /**
3011
+ * Creates a new SkillService instance
3012
+ * @param cwd - Current working directory for resolving relative paths
3013
+ * @param skillPaths - Array of paths to skills directories
3014
+ * @param options - Optional configuration
3015
+ * @param options.onCacheInvalidated - Callback invoked when cache is invalidated due to file changes
3016
+ */
3017
+ constructor(cwd, skillPaths, options, logger = console) {
3018
+ this.cwd = cwd;
3019
+ this.skillPaths = skillPaths;
3020
+ this.onCacheInvalidated = options?.onCacheInvalidated;
3021
+ this.logger = logger;
3022
+ }
3023
+ /**
3024
+ * Get all available skills from configured directories.
3025
+ * Results are cached after first load.
3026
+ *
3027
+ * Skills from earlier entries in the config take precedence over
3028
+ * skills with the same name from later entries.
3029
+ *
3030
+ * @returns Array of loaded skills
3031
+ * @throws SkillLoadError if a critical error occurs during loading
3032
+ */
3033
+ async getSkills() {
3034
+ if (this.cachedSkills !== null) return this.cachedSkills;
3035
+ const skills = [];
3036
+ const loadedSkillNames = /* @__PURE__ */ new Set();
3037
+ const allDirSkills = await Promise.all(this.skillPaths.map(async (skillPath) => {
3038
+ const skillsDir = (0, node_path.isAbsolute)(skillPath) ? skillPath : (0, node_path.join)(this.cwd, skillPath);
3039
+ return this.loadSkillsFromDirectory(skillsDir, "project");
3040
+ }));
3041
+ for (const dirSkills of allDirSkills) for (const skill of dirSkills) if (!loadedSkillNames.has(skill.name)) {
3042
+ skills.push(skill);
3043
+ loadedSkillNames.add(skill.name);
3044
+ }
3045
+ this.cachedSkills = skills;
3046
+ this.skillsByName = new Map(skills.map((skill) => [skill.name, skill]));
3047
+ return skills;
3048
+ }
3049
+ /**
3050
+ * Get a specific skill by name with O(1) lookup from cache.
3051
+ * @param name - The skill name (without skill__ prefix)
3052
+ * @returns The skill if found, undefined otherwise
3053
+ */
3054
+ async getSkill(name) {
3055
+ if (this.skillsByName === null) await this.getSkills();
3056
+ return this.skillsByName?.get(name);
3057
+ }
3058
+ /**
3059
+ * Clears the cached skills to force a fresh reload on the next getSkills() or getSkill() call.
3060
+ * Use this when skill files have been modified on disk.
3061
+ */
3062
+ clearCache() {
3063
+ this.cachedSkills = null;
3064
+ this.skillsByName = null;
3065
+ }
3066
+ /**
3067
+ * Starts watching skill directories for changes to SKILL.md files.
3068
+ * When changes are detected, the cache is automatically invalidated.
3069
+ *
3070
+ * Uses Node.js fs.watch with recursive option for efficient directory monitoring.
3071
+ * Only invalidates cache when SKILL.md files are modified.
3072
+ *
3073
+ * @example
3074
+ * const skillService = new SkillService(cwd, skillPaths, {
3075
+ * onCacheInvalidated: () => console.log('Skills cache invalidated')
3076
+ * });
3077
+ * await skillService.startWatching();
3078
+ */
3079
+ async startWatching() {
3080
+ this.stopWatching();
3081
+ const existenceChecks = await Promise.all(this.skillPaths.map(async (skillPath) => {
3082
+ const skillsDir = (0, node_path.isAbsolute)(skillPath) ? skillPath : (0, node_path.join)(this.cwd, skillPath);
3083
+ return {
3084
+ skillsDir,
3085
+ exists: await pathExists(skillsDir)
3086
+ };
3087
+ }));
3088
+ for (const { skillsDir, exists } of existenceChecks) {
3089
+ if (!exists) continue;
3090
+ const abortController = new AbortController();
3091
+ this.watchers.push(abortController);
3092
+ this.watchDirectory(skillsDir, abortController.signal).catch((error) => {
3093
+ if (error?.name !== "AbortError") {
3094
+ if (this.isWatchResourceLimitError(error)) {
3095
+ this.startPollingDirectory(skillsDir, abortController.signal);
3096
+ return;
3097
+ }
3098
+ this.logger.warn(`[skill-watcher] Error watching ${skillsDir}: ${error instanceof Error ? error.message : "Unknown error"}`);
3099
+ }
3100
+ });
3101
+ }
3102
+ }
3103
+ /**
3104
+ * Stops all active file watchers.
3105
+ * Should be called when the service is being disposed.
3106
+ */
3107
+ stopWatching() {
3108
+ for (const controller of this.watchers) controller.abort();
3109
+ this.watchers = [];
3110
+ for (const timer of this.pollingTimers) clearInterval(timer);
3111
+ this.pollingTimers = [];
3112
+ }
3113
+ /**
3114
+ * Watches a directory for changes to SKILL.md files.
3115
+ * @param dirPath - Directory path to watch
3116
+ * @param signal - AbortSignal to stop watching
3117
+ */
3118
+ async watchDirectory(dirPath, signal) {
3119
+ const watcher = (0, node_fs_promises.watch)(dirPath, {
3120
+ recursive: true,
3121
+ signal
3122
+ });
3123
+ for await (const event of watcher) if (event.filename?.endsWith("SKILL.md")) this.invalidateCache();
3124
+ }
3125
+ invalidateCache() {
3126
+ this.clearCache();
3127
+ this.onCacheInvalidated?.();
3128
+ }
3129
+ isWatchResourceLimitError(error) {
3130
+ return error instanceof Error && "code" in error && (error.code === "EMFILE" || error.code === "ENOSPC");
3131
+ }
3132
+ startPollingDirectory(dirPath, signal) {
3133
+ this.createSkillSnapshot(dirPath).then((initialSnapshot) => {
3134
+ let previousSnapshot = initialSnapshot;
3135
+ const timer = setInterval(() => {
3136
+ if (signal.aborted) {
3137
+ clearInterval(timer);
3138
+ return;
3139
+ }
3140
+ this.createSkillSnapshot(dirPath).then((nextSnapshot) => {
3141
+ if (!this.snapshotsEqual(previousSnapshot, nextSnapshot)) {
3142
+ previousSnapshot = nextSnapshot;
3143
+ this.invalidateCache();
3144
+ }
3145
+ }).catch((error) => {
3146
+ this.logger.warn(`[skill-watcher] Polling failed for ${dirPath}: ${error instanceof Error ? error.message : "Unknown error"}`);
3147
+ });
3148
+ }, 100);
3149
+ this.pollingTimers.push(timer);
3150
+ signal.addEventListener("abort", () => {
3151
+ clearInterval(timer);
3152
+ this.pollingTimers = this.pollingTimers.filter((activeTimer) => activeTimer !== timer);
3153
+ }, { once: true });
3154
+ });
3155
+ }
3156
+ async createSkillSnapshot(dirPath) {
3157
+ const snapshot = /* @__PURE__ */ new Map();
3158
+ await this.collectSkillSnapshots(dirPath, snapshot);
3159
+ return snapshot;
3160
+ }
3161
+ async collectSkillSnapshots(dirPath, snapshot) {
3162
+ let entries;
3163
+ try {
3164
+ entries = await (0, node_fs_promises.readdir)(dirPath);
3165
+ } catch (error) {
3166
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") return;
3167
+ throw error;
3168
+ }
3169
+ await Promise.all(entries.map(async (entry) => {
3170
+ const entryPath = (0, node_path.join)(dirPath, entry);
3171
+ const entryStat = await (0, node_fs_promises.stat)(entryPath);
3172
+ if (entryStat.isDirectory()) {
3173
+ await this.collectSkillSnapshots(entryPath, snapshot);
3174
+ return;
3175
+ }
3176
+ if (entry === "SKILL.md") snapshot.set(entryPath, entryStat.mtimeMs);
3177
+ }));
3178
+ }
3179
+ snapshotsEqual(left, right) {
3180
+ if (left.size !== right.size) return false;
3181
+ for (const [filePath, mtimeMs] of left) if (right.get(filePath) !== mtimeMs) return false;
3182
+ return true;
3183
+ }
3184
+ /**
3185
+ * Load skills from a directory.
3186
+ * Supports both flat structure (SKILL.md) and nested structure (name/SKILL.md).
3187
+ *
3188
+ * @param dirPath - Path to the skills directory
3189
+ * @param location - Whether this is a 'project' or 'user' skill directory
3190
+ * @returns Array of successfully loaded skills (skips invalid skills)
3191
+ * @throws SkillLoadError if there's a critical I/O error
3192
+ *
3193
+ * @example
3194
+ * // Load skills from project directory
3195
+ * const skills = await this.loadSkillsFromDirectory('/path/to/.claude/skills', 'project');
3196
+ * // Returns: [{ name: 'pdf', description: '...', location: 'project', ... }]
3197
+ */
3198
+ async loadSkillsFromDirectory(dirPath, location) {
3199
+ const skills = [];
3200
+ try {
3201
+ if (!await pathExists(dirPath)) return skills;
3202
+ } catch (error) {
3203
+ throw new SkillLoadError(`Cannot access skills directory: ${error instanceof Error ? error.message : "Unknown error"}`, dirPath, error instanceof Error ? error : void 0);
3204
+ }
3205
+ let entries;
3206
+ try {
3207
+ entries = await (0, node_fs_promises.readdir)(dirPath);
3208
+ } catch (error) {
3209
+ throw new SkillLoadError(`Failed to read skills directory: ${error instanceof Error ? error.message : "Unknown error"}`, dirPath, error instanceof Error ? error : void 0);
3210
+ }
3211
+ const entryStats = await Promise.all(entries.map(async (entry) => {
3212
+ const entryPath = (0, node_path.join)(dirPath, entry);
3213
+ try {
3214
+ return {
3215
+ entry,
3216
+ entryPath,
3217
+ stat: await (0, node_fs_promises.stat)(entryPath),
3218
+ error: null
3219
+ };
3220
+ } catch (error) {
3221
+ this.logger.warn(`Skipping entry ${entryPath}: ${error instanceof Error ? error.message : "Unknown error"}`);
3222
+ return {
3223
+ entry,
3224
+ entryPath,
3225
+ stat: null,
3226
+ error
3227
+ };
3228
+ }
3229
+ }));
3230
+ const skillFilesToLoad = [];
3231
+ for (const { entry, entryPath, stat: entryStat } of entryStats) {
3232
+ if (!entryStat) continue;
3233
+ if (entryStat.isDirectory()) {
3234
+ const skillFilePath = (0, node_path.join)(entryPath, "SKILL.md");
3235
+ skillFilesToLoad.push({
3236
+ filePath: skillFilePath,
3237
+ isRootLevel: false
3238
+ });
3239
+ } else if (entry === "SKILL.md") skillFilesToLoad.push({
3240
+ filePath: entryPath,
3241
+ isRootLevel: true
3242
+ });
3243
+ }
3244
+ const loadResults = await Promise.all(skillFilesToLoad.map(async ({ filePath, isRootLevel }) => {
3245
+ try {
3246
+ if (!isRootLevel && !await pathExists(filePath)) return null;
3247
+ return await this.loadSkillFile(filePath, location);
3248
+ } catch (error) {
3249
+ this.logger.warn(`Skipping skill at ${filePath}: ${error instanceof Error ? error.message : "Unknown error"}`);
3250
+ return null;
3251
+ }
3252
+ }));
3253
+ for (const skill of loadResults) if (skill) skills.push(skill);
3254
+ return skills;
3255
+ }
3256
+ /**
3257
+ * Load a single skill file and parse its frontmatter.
3258
+ * Supports multi-line YAML values using literal (|) and folded (>) block scalars.
3259
+ *
3260
+ * @param filePath - Path to the SKILL.md file
3261
+ * @param location - Whether this is a 'project' or 'user' skill
3262
+ * @returns The loaded skill, or null if the file is invalid (missing required frontmatter)
3263
+ * @throws SkillLoadError if there's an I/O error reading the file
3264
+ *
3265
+ * @example
3266
+ * // Load a skill from a file
3267
+ * const skill = await this.loadSkillFile('/path/to/pdf/SKILL.md', 'project');
3268
+ * // Returns: { name: 'pdf', description: 'PDF skill', location: 'project', content: '...', basePath: '/path/to/pdf' }
3269
+ * // Returns null if frontmatter is missing name or description
3270
+ */
3271
+ async loadSkillFile(filePath, location) {
3272
+ let fileContent;
3273
+ try {
3274
+ fileContent = await (0, node_fs_promises.readFile)(filePath, "utf-8");
3275
+ } catch (error) {
3276
+ throw new SkillLoadError(`Failed to read skill file: ${error instanceof Error ? error.message : "Unknown error"}`, filePath, error instanceof Error ? error : void 0);
3277
+ }
3278
+ const { frontMatter, content } = parseFrontMatter(fileContent);
3279
+ if (!frontMatter || !frontMatter.name || !frontMatter.description) return null;
3280
+ return {
3281
+ name: frontMatter.name,
3282
+ description: frontMatter.description,
3283
+ location,
3284
+ content,
3285
+ basePath: (0, node_path.dirname)(filePath)
3286
+ };
3287
+ }
3288
+ };
3289
+
3290
+ //#endregion
3291
+ //#region src/services/PrefetchService/constants.ts
3292
+ /**
3293
+ * PrefetchService Constants
3294
+ *
3295
+ * Constants for package manager commands and process configuration.
3296
+ */
3297
+ /** Transport type for stdio-based MCP servers */
3298
+ const TRANSPORT_STDIO = "stdio";
3299
+ /** npx command name */
3300
+ const COMMAND_NPX = "npx";
3301
+ /** npm command name */
3302
+ const COMMAND_NPM = "npm";
3303
+ /** pnpx command name (pnpm's npx equivalent) */
3304
+ const COMMAND_PNPX = "pnpx";
3305
+ /** pnpm command name */
3306
+ const COMMAND_PNPM = "pnpm";
3307
+ /** uvx command name */
3308
+ const COMMAND_UVX = "uvx";
3309
+ /** uv command name */
3310
+ const COMMAND_UV = "uv";
3311
+ /** Path suffix for npx command */
3312
+ const COMMAND_NPX_SUFFIX = "/npx";
3313
+ /** Path suffix for pnpx command */
3314
+ const COMMAND_PNPX_SUFFIX = "/pnpx";
3315
+ /** Path suffix for uvx command */
3316
+ const COMMAND_UVX_SUFFIX = "/uvx";
3317
+ /** Path suffix for uv command */
3318
+ const COMMAND_UV_SUFFIX = "/uv";
3319
+ /** Run subcommand for uv */
3320
+ const ARG_RUN = "run";
3321
+ /** Tool subcommand for uv */
3322
+ const ARG_TOOL = "tool";
3323
+ /** Install subcommand for uv tool and npm/pnpm */
3324
+ const ARG_INSTALL = "install";
3325
+ /** Add subcommand for pnpm */
3326
+ const ARG_ADD = "add";
3327
+ /** Global flag for npm/pnpm install */
3328
+ const ARG_GLOBAL = "-g";
3329
+ /** Flag prefix for command arguments */
3330
+ const FLAG_PREFIX = "-";
3331
+ /** npx --package flag (long form) */
3332
+ const FLAG_PACKAGE_LONG = "--package";
3333
+ /** npx -p flag (short form) */
3334
+ const FLAG_PACKAGE_SHORT = "-p";
3335
+ /** Equals delimiter used in flag=value patterns */
3336
+ const EQUALS_DELIMITER = "=";
3337
+ /**
3338
+ * Regex pattern for valid package names (npm, pnpm, uvx, uv)
3339
+ * Allows: @scope/package-name@version, package-name, package_name
3340
+ * Prevents shell metacharacters that could enable command injection
3341
+ * @example
3342
+ * // Valid: '@scope/package@1.0.0', 'my-package', 'my_package', '@org/pkg'
3343
+ * // Invalid: 'pkg; rm -rf /', 'pkg$(cmd)', 'pkg`whoami`', 'pkg|cat /etc/passwd'
3344
+ */
3345
+ const VALID_PACKAGE_NAME_PATTERN = /^(@[a-zA-Z0-9_-]+\/)?[a-zA-Z0-9._-]+(@[a-zA-Z0-9._-]+)?$/;
3346
+ /** Windows platform identifier */
3347
+ const PLATFORM_WIN32 = "win32";
3348
+ /** Success exit code */
3349
+ const EXIT_CODE_SUCCESS = 0;
3350
+ /** Stdio option to ignore stream */
3351
+ const STDIO_IGNORE = "ignore";
3352
+ /** Stdio option to pipe stream */
3353
+ const STDIO_PIPE = "pipe";
3354
+
3355
+ //#endregion
3356
+ //#region src/services/PrefetchService/PrefetchService.ts
3357
+ /**
3358
+ * PrefetchService
3359
+ *
3360
+ * DESIGN PATTERNS:
3361
+ * - Service pattern for business logic encapsulation
3362
+ * - Single responsibility principle
3363
+ *
3364
+ * CODING STANDARDS:
3365
+ * - Use async/await for asynchronous operations
3366
+ * - Throw descriptive errors for error cases
3367
+ * - Keep methods focused and well-named
3368
+ * - Document complex logic with comments
3369
+ *
3370
+ * AVOID:
3371
+ * - Mixing concerns (keep focused on single domain)
3372
+ * - Direct tool implementation (services should be tool-agnostic)
3373
+ */
3374
+ /**
3375
+ * Type guard to check if a config object is an McpStdioConfig
3376
+ * @param config - Config object to check
3377
+ * @returns True if config has required McpStdioConfig properties
3378
+ */
3379
+ function isMcpStdioConfig(config) {
3380
+ return typeof config === "object" && config !== null && "command" in config;
3381
+ }
3382
+ /**
3383
+ * PrefetchService handles pre-downloading packages used by MCP servers.
3384
+ * Supports npx (Node.js), uvx (Python/uv), and uv run commands.
3385
+ *
3386
+ * @example
3387
+ * ```typescript
3388
+ * const service = new PrefetchService({
3389
+ * mcpConfig: await configFetcher.fetchConfiguration(),
3390
+ * parallel: true,
3391
+ * });
3392
+ * const packages = service.extractPackages();
3393
+ * const summary = await service.prefetch();
3394
+ * ```
3395
+ */
3396
+ var PrefetchService = class {
3397
+ config;
3398
+ /**
3399
+ * Creates a new PrefetchService instance
3400
+ * @param config - Service configuration options
3401
+ */
3402
+ constructor(config) {
3403
+ this.config = config;
3404
+ }
3405
+ /**
3406
+ * Extract all prefetchable packages from the MCP configuration
3407
+ * @returns Array of package info objects
3408
+ */
3409
+ extractPackages() {
3410
+ const packages = [];
3411
+ const { mcpConfig, filter } = this.config;
3412
+ for (const [serverName, serverConfig] of Object.entries(mcpConfig.mcpServers)) {
3413
+ if (serverConfig.disabled) continue;
3414
+ if (serverConfig.transport !== TRANSPORT_STDIO) continue;
3415
+ if (!isMcpStdioConfig(serverConfig.config)) continue;
3416
+ const packageInfo = this.extractPackageInfo(serverName, serverConfig.config);
3417
+ if (packageInfo) {
3418
+ if (filter && packageInfo.packageManager !== filter) continue;
3419
+ packages.push(packageInfo);
3420
+ }
3421
+ }
3422
+ return packages;
3423
+ }
3424
+ /**
3425
+ * Prefetch all packages from the configuration
3426
+ * @returns Summary of prefetch results
3427
+ * @throws Error if prefetch operation fails unexpectedly
3428
+ */
3429
+ async prefetch() {
3430
+ try {
3431
+ const packages = this.extractPackages();
3432
+ const results = [];
3433
+ if (packages.length === 0) return {
3434
+ totalPackages: 0,
3435
+ successful: 0,
3436
+ failed: 0,
3437
+ results: []
3438
+ };
3439
+ if (this.config.parallel) {
3440
+ const promises = packages.map(async (pkg) => this.prefetchPackage(pkg));
3441
+ results.push(...await Promise.all(promises));
3442
+ } else for (const pkg of packages) {
3443
+ const result = await this.prefetchPackage(pkg);
3444
+ results.push(result);
3445
+ }
3446
+ const successful = results.filter((r) => r.success).length;
3447
+ const failed = results.filter((r) => !r.success).length;
3448
+ return {
3449
+ totalPackages: packages.length,
3450
+ successful,
3451
+ failed,
3452
+ results
3453
+ };
3454
+ } catch (error) {
3455
+ throw new Error(`Failed to prefetch packages: ${error instanceof Error ? error.message : String(error)}`);
3456
+ }
3457
+ }
3458
+ /**
3459
+ * Prefetch a single package
3460
+ * @param pkg - Package info to prefetch
3461
+ * @returns Result of the prefetch operation
3462
+ */
3463
+ async prefetchPackage(pkg) {
3464
+ try {
3465
+ const [command, ...args] = pkg.fullCommand;
3466
+ const result = await this.runCommand(command, args);
3467
+ return {
3468
+ package: pkg,
3469
+ success: result.success,
3470
+ output: result.output
3471
+ };
3472
+ } catch (error) {
3473
+ return {
3474
+ package: pkg,
3475
+ success: false,
3476
+ output: error instanceof Error ? error.message : String(error)
3477
+ };
3478
+ }
3479
+ }
3480
+ /**
3481
+ * Validate package name to prevent command injection
3482
+ * @param packageName - Package name to validate
3483
+ * @returns True if package name is safe, false otherwise
3484
+ * @remarks Rejects package names containing shell metacharacters
3485
+ * @example
3486
+ * isValidPackageName('@scope/package') // true
3487
+ * isValidPackageName('my-package@1.0.0') // true
3488
+ * isValidPackageName('pkg; rm -rf /') // false (shell injection)
3489
+ * isValidPackageName('pkg$(whoami)') // false (command substitution)
3490
+ */
3491
+ isValidPackageName(packageName) {
3492
+ return VALID_PACKAGE_NAME_PATTERN.test(packageName);
3493
+ }
3494
+ /**
3495
+ * Extract package info from a server's stdio config
3496
+ * @param serverName - Name of the MCP server
3497
+ * @param config - Stdio configuration for the server
3498
+ * @returns Package info if extractable, null otherwise
3499
+ */
3500
+ extractPackageInfo(serverName, config) {
3501
+ const command = config.command.toLowerCase();
3502
+ const args = config.args || [];
3503
+ if (command === COMMAND_NPX || command.endsWith(COMMAND_NPX_SUFFIX)) {
3504
+ const packageName = this.extractNpxPackage(args);
3505
+ if (packageName && this.isValidPackageName(packageName)) return {
3506
+ serverName,
3507
+ packageManager: COMMAND_NPX,
3508
+ packageName,
3509
+ fullCommand: [
3510
+ COMMAND_NPM,
3511
+ ARG_INSTALL,
3512
+ ARG_GLOBAL,
3513
+ packageName
3514
+ ]
3515
+ };
3516
+ }
3517
+ if (command === COMMAND_PNPX || command.endsWith(COMMAND_PNPX_SUFFIX)) {
3518
+ const packageName = this.extractNpxPackage(args);
3519
+ if (packageName && this.isValidPackageName(packageName)) return {
3520
+ serverName,
3521
+ packageManager: COMMAND_PNPX,
3522
+ packageName,
3523
+ fullCommand: [
3524
+ COMMAND_PNPM,
3525
+ ARG_ADD,
3526
+ ARG_GLOBAL,
3527
+ packageName
3528
+ ]
3529
+ };
3530
+ }
3531
+ if (command === COMMAND_UVX || command.endsWith(COMMAND_UVX_SUFFIX)) {
3532
+ const packageName = this.extractUvxPackage(args);
3533
+ if (packageName && this.isValidPackageName(packageName)) return {
3534
+ serverName,
3535
+ packageManager: COMMAND_UVX,
3536
+ packageName,
3537
+ fullCommand: [COMMAND_UVX, packageName]
3538
+ };
3539
+ }
3540
+ if ((command === COMMAND_UV || command.endsWith(COMMAND_UV_SUFFIX)) && args.includes(ARG_RUN)) {
3541
+ const packageName = this.extractUvRunPackage(args);
3542
+ if (packageName && this.isValidPackageName(packageName)) return {
3543
+ serverName,
3544
+ packageManager: COMMAND_UV,
3545
+ packageName,
3546
+ fullCommand: [
3547
+ COMMAND_UV,
3548
+ ARG_TOOL,
3549
+ ARG_INSTALL,
3550
+ packageName
3551
+ ]
3552
+ };
3553
+ }
3554
+ return null;
3555
+ }
3556
+ /**
3557
+ * Extract package name from npx command args
3558
+ * @param args - Command arguments
3559
+ * @returns Package name or null
3560
+ * @remarks Handles --package=value, --package value, -p value patterns.
3561
+ * Falls back to first non-flag argument if no --package/-p flag found.
3562
+ * Returns null if flag has no value or is followed by another flag.
3563
+ * When multiple --package flags exist, returns the first valid one.
3564
+ * @example
3565
+ * extractNpxPackage(['--package=@scope/pkg']) // returns '@scope/pkg'
3566
+ * extractNpxPackage(['--package', 'pkg-name']) // returns 'pkg-name'
3567
+ * extractNpxPackage(['-p', 'pkg']) // returns 'pkg'
3568
+ * extractNpxPackage(['-y', 'pkg-name', '--flag']) // returns 'pkg-name' (fallback)
3569
+ * extractNpxPackage(['--package=']) // returns null (empty value)
3570
+ */
3571
+ extractNpxPackage(args) {
3572
+ for (let i = 0; i < args.length; i++) {
3573
+ const arg = args[i];
3574
+ if (arg.startsWith(FLAG_PACKAGE_LONG + EQUALS_DELIMITER)) return arg.slice(FLAG_PACKAGE_LONG.length + EQUALS_DELIMITER.length) || null;
3575
+ if (arg === FLAG_PACKAGE_LONG && i + 1 < args.length) {
3576
+ const nextArg = args[i + 1];
3577
+ if (!nextArg.startsWith(FLAG_PREFIX)) return nextArg;
3578
+ }
3579
+ if (arg === FLAG_PACKAGE_SHORT && i + 1 < args.length) {
3580
+ const nextArg = args[i + 1];
3581
+ if (!nextArg.startsWith(FLAG_PREFIX)) return nextArg;
3582
+ }
3583
+ }
3584
+ for (const arg of args) {
3585
+ if (arg.startsWith(FLAG_PREFIX)) continue;
3586
+ return arg;
3587
+ }
3588
+ return null;
3589
+ }
3590
+ /**
3591
+ * Extract package name from uvx command args
3592
+ * @param args - Command arguments
3593
+ * @returns Package name or null
3594
+ * @remarks Assumes the first non-flag argument is the package name.
3595
+ * Handles both single (-) and double (--) dash flags.
3596
+ * @example
3597
+ * extractUvxPackage(['mcp-server-fetch']) // returns 'mcp-server-fetch'
3598
+ * extractUvxPackage(['--quiet', 'pkg-name']) // returns 'pkg-name'
3599
+ */
3600
+ extractUvxPackage(args) {
3601
+ for (const arg of args) {
3602
+ if (arg.startsWith(FLAG_PREFIX)) continue;
3603
+ return arg;
3604
+ }
3605
+ return null;
3606
+ }
3607
+ /**
3608
+ * Extract package name from uv run command args
3609
+ * @param args - Command arguments
3610
+ * @returns Package name or null
3611
+ * @remarks Looks for the first non-flag argument after the 'run' subcommand.
3612
+ * Returns null if 'run' is not found in args.
3613
+ * @example
3614
+ * extractUvRunPackage(['run', 'mcp-server']) // returns 'mcp-server'
3615
+ * extractUvRunPackage(['run', '--verbose', 'pkg']) // returns 'pkg'
3616
+ * extractUvRunPackage(['install', 'pkg']) // returns null (no 'run')
3617
+ */
3618
+ extractUvRunPackage(args) {
3619
+ const runIndex = args.indexOf(ARG_RUN);
3620
+ if (runIndex === -1) return null;
3621
+ for (let i = runIndex + 1; i < args.length; i++) {
3622
+ const arg = args[i];
3623
+ if (arg.startsWith(FLAG_PREFIX)) continue;
3624
+ return arg;
3625
+ }
3626
+ return null;
3627
+ }
3628
+ /**
3629
+ * Run a shell command and capture output
3630
+ * @param command - Command to run
3631
+ * @param args - Command arguments
3632
+ * @returns Promise with success status and output
3633
+ */
3634
+ runCommand(command, args) {
3635
+ return new Promise((resolve$2) => {
3636
+ const proc = (0, node_child_process.spawn)(command, args, {
3637
+ stdio: [
3638
+ STDIO_IGNORE,
3639
+ STDIO_PIPE,
3640
+ STDIO_PIPE
3641
+ ],
3642
+ shell: process.platform === PLATFORM_WIN32
3643
+ });
3644
+ let stdout = "";
3645
+ let stderr = "";
3646
+ proc.stdout?.on("data", (data) => {
3647
+ stdout += data.toString();
3648
+ });
3649
+ proc.stderr?.on("data", (data) => {
3650
+ stderr += data.toString();
3651
+ });
3652
+ proc.on("close", (code) => {
3653
+ resolve$2({
3654
+ success: code === EXIT_CODE_SUCCESS,
3655
+ output: stdout || stderr
3656
+ });
3657
+ });
3658
+ proc.on("error", (error) => {
3659
+ resolve$2({
3660
+ success: false,
3661
+ output: error.message
3662
+ });
3663
+ });
3664
+ });
3665
+ }
3666
+ };
3667
+
3668
+ //#endregion
3669
+ //#region src/transports/http.ts
3670
+ /**
3671
+ * HTTP Transport Handler
3672
+ *
3673
+ * DESIGN PATTERNS:
3674
+ * - Transport handler pattern implementing TransportHandler interface
3675
+ * - Session management for stateful connections
3676
+ * - Streamable HTTP protocol (2025-03-26) with resumability support
3677
+ * - Factory pattern for creating MCP server instances per session
3678
+ *
3679
+ * CODING STANDARDS:
3680
+ * - Use async/await for all asynchronous operations
3681
+ * - Implement proper session lifecycle management
3682
+ * - Handle errors gracefully with appropriate HTTP status codes
3683
+ * - Provide health check endpoint for monitoring
3684
+ * - Clean up resources on shutdown
3685
+ *
3686
+ * AVOID:
3687
+ * - Sharing MCP server instances across sessions (use factory pattern)
3688
+ * - Forgetting to clean up sessions on disconnect
3689
+ * - Missing error handling for request processing
3690
+ * - Hardcoded configuration (use TransportConfig)
3691
+ */
3692
+ /**
3693
+ * HTTP session manager
3694
+ */
3695
+ var HttpFullSessionManager = class {
3696
+ sessions = /* @__PURE__ */ new Map();
3697
+ closingSessions = /* @__PURE__ */ new Set();
3698
+ getSession(sessionId) {
3699
+ return this.sessions.get(sessionId);
3700
+ }
3701
+ setSession(sessionId, transport, server) {
3702
+ this.sessions.set(sessionId, {
3703
+ transport,
3704
+ server
3705
+ });
3706
+ }
3707
+ removeSession(sessionId) {
3708
+ this.sessions.delete(sessionId);
3709
+ }
3710
+ isClosing(sessionId) {
3711
+ return this.closingSessions.has(sessionId);
3712
+ }
3713
+ async closeSession(sessionId) {
3714
+ const session = this.sessions.get(sessionId);
3715
+ if (session) {
3716
+ this.closingSessions.add(sessionId);
3717
+ try {
3718
+ await session.server.close();
3719
+ } catch (error) {
3720
+ throw new Error(`Failed to close MCP server for session '${sessionId}': ${toErrorMessage(error)}`);
3721
+ } finally {
3722
+ this.sessions.delete(sessionId);
3723
+ this.closingSessions.delete(sessionId);
3724
+ }
3725
+ }
3726
+ }
3727
+ hasSession(sessionId) {
3728
+ return this.sessions.has(sessionId);
3729
+ }
3730
+ async clear() {
3731
+ try {
3732
+ await Promise.all(Array.from(this.sessions.keys()).map(async (sessionId) => this.closeSession(sessionId)));
3733
+ } catch (error) {
3734
+ throw new Error(`Failed to clear sessions: ${toErrorMessage(error)}`);
3735
+ }
3736
+ }
3737
+ };
3738
+ function toErrorMessage(error) {
3739
+ return error instanceof Error ? error.message : String(error);
3740
+ }
3741
+ const ADMIN_RATE_LIMIT_WINDOW_MS = 6e4;
3742
+ const ADMIN_RATE_LIMIT_MAX_REQUESTS = 5;
3743
+ /**
3744
+ * Simple in-memory rate limiter for the admin shutdown endpoint.
3745
+ * Tracks request timestamps per IP within a sliding window.
3746
+ */
3747
+ var AdminRateLimiter = class {
3748
+ requests = /* @__PURE__ */ new Map();
3749
+ isAllowed(ip) {
3750
+ const now = Date.now();
3751
+ const windowStart = now - ADMIN_RATE_LIMIT_WINDOW_MS;
3752
+ const timestamps = (this.requests.get(ip) ?? []).filter((t) => t > windowStart);
3753
+ if (timestamps.length >= ADMIN_RATE_LIMIT_MAX_REQUESTS) {
3754
+ this.requests.set(ip, timestamps);
3755
+ return false;
3756
+ }
3757
+ timestamps.push(now);
3758
+ this.requests.set(ip, timestamps);
3759
+ return true;
3760
+ }
3761
+ };
3762
+ /**
3763
+ * HTTP transport handler using Streamable HTTP (protocol version 2025-03-26)
3764
+ * Provides stateful session management with resumability support
3765
+ */
3766
+ var HttpTransportHandler = class {
3767
+ serverFactory;
3768
+ app;
3769
+ server = null;
3770
+ sessionManager;
3771
+ config;
3772
+ adminOptions;
3773
+ adminRateLimiter = new AdminRateLimiter();
3774
+ logger;
3775
+ constructor(serverFactory, config, adminOptions, logger = console) {
3776
+ this.serverFactory = serverFactory;
3777
+ this.app = (0, express.default)();
3778
+ this.sessionManager = new HttpFullSessionManager();
3779
+ this.config = {
3780
+ mode: config.mode,
3781
+ port: config.port ?? 3e3,
3782
+ host: config.host ?? "localhost"
3783
+ };
3784
+ this.adminOptions = adminOptions;
3785
+ this.logger = logger;
3786
+ this.setupMiddleware();
3787
+ this.setupRoutes();
3788
+ }
3789
+ setupMiddleware() {
3790
+ this.app.use(express.default.json());
3791
+ }
3792
+ setupRoutes() {
3793
+ this.app.post("/mcp", async (req, res) => {
3794
+ try {
3795
+ await this.handlePostRequest(req, res);
3796
+ } catch (error) {
3797
+ this.logger.error(`Failed to handle MCP POST request: ${toErrorMessage(error)}`);
3798
+ res.status(500).json({
3799
+ jsonrpc: "2.0",
3800
+ error: {
3801
+ code: -32603,
3802
+ message: "Failed to handle MCP POST request."
3803
+ },
3804
+ id: null
3805
+ });
3806
+ }
3807
+ });
3808
+ this.app.get("/mcp", async (req, res) => {
3809
+ try {
3810
+ await this.handleGetRequest(req, res);
3811
+ } catch (error) {
3812
+ this.logger.error(`Failed to handle MCP GET request: ${toErrorMessage(error)}`);
3813
+ res.status(500).send("Failed to handle MCP GET request.");
3814
+ }
3815
+ });
3816
+ this.app.delete("/mcp", async (req, res) => {
3817
+ try {
3818
+ await this.handleDeleteRequest(req, res);
3819
+ } catch (error) {
3820
+ this.logger.error(`Failed to handle MCP DELETE request: ${toErrorMessage(error)}`);
3821
+ res.status(500).send("Failed to handle MCP DELETE request.");
3822
+ }
3823
+ });
3824
+ this.app.get("/health", (_req, res) => {
3825
+ const payload = {
3826
+ status: "ok",
3827
+ transport: "http",
3828
+ serverId: this.adminOptions?.serverId
3829
+ };
3830
+ res.json(payload);
3831
+ });
3832
+ this.app.post("/admin/shutdown", async (req, res) => {
3833
+ try {
3834
+ const clientIp = req.ip ?? req.socket.remoteAddress ?? "unknown";
3835
+ if (!this.adminRateLimiter.isAllowed(clientIp)) {
3836
+ const payload = {
3837
+ ok: false,
3838
+ message: "Too many shutdown requests. Try again later.",
3839
+ serverId: this.adminOptions?.serverId
3840
+ };
3841
+ res.status(429).json(payload);
3842
+ return;
3843
+ }
3844
+ await this.handleAdminShutdownRequest(req, res);
3845
+ } catch (error) {
3846
+ this.logger.error(`Failed to process shutdown request: ${toErrorMessage(error)}`);
3847
+ const payload = {
3848
+ ok: false,
3849
+ message: "Failed to process shutdown request.",
3850
+ serverId: this.adminOptions?.serverId
3851
+ };
3852
+ res.status(500).json(payload);
3853
+ }
3854
+ });
3855
+ }
3856
+ isAuthorizedShutdownRequest(req) {
3857
+ const expectedToken = this.adminOptions?.shutdownToken;
3858
+ if (!expectedToken) return false;
3859
+ const authHeader = req.headers.authorization;
3860
+ if (typeof authHeader === "string" && authHeader.startsWith("Bearer ")) return authHeader.slice(7) === expectedToken;
3861
+ const tokenHeader = req.headers["x-mcp-proxy-shutdown-token"];
3862
+ return typeof tokenHeader === "string" && tokenHeader === expectedToken;
3863
+ }
3864
+ async handleAdminShutdownRequest(req, res) {
3865
+ try {
3866
+ if (!this.adminOptions?.onShutdownRequested) {
3867
+ const payload$1 = {
3868
+ ok: false,
3869
+ message: "Shutdown endpoint is not enabled for this server instance.",
3870
+ serverId: this.adminOptions?.serverId
3871
+ };
3872
+ res.status(404).json(payload$1);
3873
+ return;
3874
+ }
3875
+ if (!this.isAuthorizedShutdownRequest(req)) {
3876
+ const payload$1 = {
3877
+ ok: false,
3878
+ message: "Unauthorized shutdown request: invalid or missing shutdown token.",
3879
+ serverId: this.adminOptions?.serverId
3880
+ };
3881
+ res.status(401).json(payload$1);
3882
+ return;
3883
+ }
3884
+ const payload = {
3885
+ ok: true,
3886
+ message: "Shutdown request accepted. Stopping server gracefully.",
3887
+ serverId: this.adminOptions?.serverId
3888
+ };
3889
+ res.json(payload);
3890
+ await this.adminOptions.onShutdownRequested();
3891
+ } catch (error) {
3892
+ throw new Error(`Failed to handle admin shutdown request: ${toErrorMessage(error)}`);
3893
+ }
3894
+ }
3895
+ async handlePostRequest(req, res) {
3896
+ const sessionId = req.headers["mcp-session-id"];
3897
+ let transport;
3898
+ if (sessionId && this.sessionManager.hasSession(sessionId)) transport = this.sessionManager.getSession(sessionId).transport;
3899
+ else if (!sessionId && (0, __modelcontextprotocol_sdk_types_js.isInitializeRequest)(req.body)) {
3900
+ const mcpServer = await this.serverFactory();
3901
+ transport = new __modelcontextprotocol_sdk_server_streamableHttp_js.StreamableHTTPServerTransport({
3902
+ sessionIdGenerator: () => (0, node_crypto.randomUUID)(),
3903
+ enableJsonResponse: true,
3904
+ onsessioninitialized: (initializedSessionId) => {
3905
+ this.sessionManager.setSession(initializedSessionId, transport, mcpServer);
3906
+ }
3907
+ });
3908
+ transport.onclose = async () => {
3909
+ if (transport.sessionId) try {
3910
+ if (!this.sessionManager.isClosing(transport.sessionId)) this.sessionManager.removeSession(transport.sessionId);
3911
+ } catch (error) {
3912
+ this.logger.warn(`Failed to clean up session '${transport.sessionId}': ${toErrorMessage(error)}`);
3913
+ }
3914
+ };
3915
+ try {
3916
+ await mcpServer.connect(transport);
3917
+ } catch (error) {
3918
+ throw new Error(`Failed to connect MCP server transport for initialization request: ${toErrorMessage(error)}`);
3919
+ }
3920
+ } else {
3921
+ res.status(400).json({
3922
+ jsonrpc: "2.0",
3923
+ error: {
3924
+ code: -32e3,
3925
+ message: sessionId === void 0 ? "Bad Request: missing session ID and request body is not an initialize request." : `Bad Request: unknown session ID '${sessionId}'.`
3926
+ },
3927
+ id: null
3928
+ });
3929
+ return;
3930
+ }
3931
+ try {
3932
+ await transport.handleRequest(req, res, req.body);
3933
+ } catch (error) {
3934
+ throw new Error(`Failed handling MCP transport request: ${toErrorMessage(error)}`);
3935
+ }
3936
+ }
3937
+ async handleGetRequest(req, res) {
3938
+ const sessionId = req.headers["mcp-session-id"];
3939
+ if (!sessionId || !this.sessionManager.hasSession(sessionId)) {
3940
+ res.status(400).send("Invalid or missing session ID");
3941
+ return;
3942
+ }
3943
+ const session = this.sessionManager.getSession(sessionId);
3944
+ try {
3945
+ await session.transport.handleRequest(req, res);
3946
+ } catch (error) {
3947
+ throw new Error(`Failed handling MCP GET request for session '${sessionId}': ${toErrorMessage(error)}`);
3948
+ }
3949
+ }
3950
+ async handleDeleteRequest(req, res) {
3951
+ const sessionId = req.headers["mcp-session-id"];
3952
+ if (!sessionId || !this.sessionManager.hasSession(sessionId)) {
3953
+ res.status(400).send("Invalid or missing session ID");
3954
+ return;
3955
+ }
3956
+ const session = this.sessionManager.getSession(sessionId);
3957
+ try {
3958
+ await session.transport.handleRequest(req, res);
3959
+ } catch (error) {
3960
+ throw new Error(`Failed handling MCP DELETE request for session '${sessionId}': ${toErrorMessage(error)}`);
3961
+ }
3962
+ this.sessionManager.removeSession(sessionId);
3963
+ }
3964
+ async start() {
3965
+ try {
3966
+ const server = this.app.listen(this.config.port, this.config.host);
3967
+ this.server = server;
3968
+ const listeningPromise = (async () => {
3969
+ await (0, node_events.once)(server, "listening");
3970
+ })();
3971
+ const errorPromise = (async () => {
3972
+ const [error] = await (0, node_events.once)(server, "error");
3973
+ throw error instanceof Error ? error : new Error(String(error));
3974
+ })();
3975
+ await Promise.race([listeningPromise, errorPromise]);
3976
+ this.logger.info(`@agimon-ai/mcp-proxy MCP server started on http://${this.config.host}:${this.config.port}/mcp`);
3977
+ this.logger.info(`Health check: http://${this.config.host}:${this.config.port}/health`);
3978
+ } catch (error) {
3979
+ this.server = null;
3980
+ throw new Error(`Failed to start HTTP transport: ${toErrorMessage(error)}`);
3981
+ }
3982
+ }
3983
+ async stop() {
3984
+ if (!this.server) return;
3985
+ try {
3986
+ await this.sessionManager.clear();
3987
+ } catch (error) {
3988
+ throw new Error(`Failed to clear sessions during HTTP transport stop: ${toErrorMessage(error)}`);
3989
+ }
3990
+ const closeServer = (0, node_util.promisify)(this.server.close.bind(this.server));
3991
+ try {
3992
+ await closeServer();
3993
+ this.server = null;
3994
+ } catch (error) {
3995
+ throw new Error(`Failed to stop HTTP transport: ${toErrorMessage(error)}`);
3996
+ }
3997
+ }
3998
+ getPort() {
3999
+ return this.config.port;
4000
+ }
4001
+ getHost() {
4002
+ return this.config.host;
4003
+ }
4004
+ };
4005
+
4006
+ //#endregion
4007
+ //#region src/transports/sse.ts
4008
+ /**
4009
+ * Session manager for SSE transports
4010
+ */
4011
+ var SseSessionManager = class {
4012
+ sessions = /* @__PURE__ */ new Map();
4013
+ closingSessions = /* @__PURE__ */ new Set();
4014
+ getSession(sessionId) {
4015
+ return this.sessions.get(sessionId)?.transport;
4016
+ }
4017
+ setSession(sessionId, transport, server) {
4018
+ this.sessions.set(sessionId, {
4019
+ transport,
4020
+ server
4021
+ });
4022
+ }
4023
+ removeSession(sessionId) {
4024
+ this.sessions.delete(sessionId);
4025
+ }
4026
+ isClosing(sessionId) {
4027
+ return this.closingSessions.has(sessionId);
4028
+ }
4029
+ async closeSession(sessionId) {
4030
+ const session = this.sessions.get(sessionId);
4031
+ if (session) {
4032
+ this.closingSessions.add(sessionId);
4033
+ try {
4034
+ await session.server.close();
4035
+ } finally {
4036
+ this.sessions.delete(sessionId);
4037
+ this.closingSessions.delete(sessionId);
4038
+ }
4039
+ }
4040
+ }
4041
+ async clear() {
4042
+ await Promise.all(Array.from(this.sessions.keys()).map(async (sessionId) => this.closeSession(sessionId)));
4043
+ }
4044
+ hasSession(sessionId) {
4045
+ return this.sessions.has(sessionId);
4046
+ }
4047
+ };
4048
+ /**
4049
+ * SSE (Server-Sent Events) transport handler
4050
+ * Legacy transport for backwards compatibility (protocol version 2024-11-05)
4051
+ * Uses separate endpoints: /sse for SSE stream (GET) and /messages for client messages (POST)
4052
+ */
4053
+ var SseTransportHandler = class {
4054
+ serverFactory;
4055
+ app;
4056
+ server = null;
4057
+ sessionManager;
4058
+ config;
4059
+ logger;
4060
+ constructor(serverFactory, config, logger = console) {
4061
+ this.serverFactory = typeof serverFactory === "function" ? serverFactory : () => serverFactory;
4062
+ this.app = (0, express.default)();
4063
+ this.sessionManager = new SseSessionManager();
4064
+ this.config = {
4065
+ mode: config.mode,
4066
+ port: config.port ?? 3e3,
4067
+ host: config.host ?? "localhost"
4068
+ };
4069
+ this.logger = logger;
4070
+ this.setupMiddleware();
4071
+ this.setupRoutes();
4072
+ }
4073
+ setupMiddleware() {
4074
+ this.app.use(express.default.json());
4075
+ }
4076
+ setupRoutes() {
4077
+ this.app.get("/sse", async (req, res) => {
4078
+ await this.handleSseConnection(req, res);
4079
+ });
4080
+ this.app.post("/messages", async (req, res) => {
4081
+ await this.handlePostMessage(req, res);
4082
+ });
4083
+ this.app.get("/health", (_req, res) => {
4084
+ res.json({
4085
+ status: "ok",
4086
+ transport: "sse"
4087
+ });
4088
+ });
4089
+ }
4090
+ async handleSseConnection(_req, res) {
4091
+ try {
4092
+ const mcpServer = this.serverFactory();
4093
+ const transport = new __modelcontextprotocol_sdk_server_sse_js.SSEServerTransport("/messages", res);
4094
+ this.sessionManager.setSession(transport.sessionId, transport, mcpServer);
4095
+ res.on("close", () => {
4096
+ const sessionId = transport.sessionId;
4097
+ if (sessionId && !this.sessionManager.isClosing(sessionId)) this.sessionManager.removeSession(sessionId);
4098
+ });
4099
+ await mcpServer.connect(transport);
4100
+ this.logger.info(`SSE session established: ${transport.sessionId}`);
4101
+ } catch (error) {
4102
+ this.logger.error("Error handling SSE connection", error);
4103
+ if (!res.headersSent) res.status(500).send("Internal Server Error");
4104
+ }
4105
+ }
4106
+ async handlePostMessage(req, res) {
4107
+ const sessionId = req.query.sessionId;
4108
+ if (!sessionId) {
4109
+ res.status(400).send("Missing sessionId query parameter");
4110
+ return;
4111
+ }
4112
+ const transport = this.sessionManager.getSession(sessionId);
4113
+ if (!transport) {
4114
+ res.status(404).send("No transport found for sessionId");
4115
+ return;
4116
+ }
4117
+ try {
4118
+ await transport.handlePostMessage(req, res, req.body);
4119
+ } catch (error) {
4120
+ this.logger.error("Error handling post message", error);
4121
+ if (!res.headersSent) res.status(500).send("Internal Server Error");
4122
+ }
4123
+ }
4124
+ async start() {
4125
+ return new Promise((resolve$2, reject) => {
4126
+ try {
4127
+ this.server = this.app.listen(this.config.port, this.config.host, () => {
4128
+ this.logger.info(`@agimon-ai/mcp-proxy MCP server started with SSE transport on http://${this.config.host}:${this.config.port}`);
4129
+ this.logger.info(`SSE endpoint: http://${this.config.host}:${this.config.port}/sse`);
4130
+ this.logger.info(`Messages endpoint: http://${this.config.host}:${this.config.port}/messages`);
4131
+ this.logger.info(`Health check: http://${this.config.host}:${this.config.port}/health`);
4132
+ resolve$2();
4133
+ });
4134
+ this.server.on("error", (error) => {
4135
+ reject(error);
4136
+ });
4137
+ } catch (error) {
4138
+ reject(error);
4139
+ }
4140
+ });
4141
+ }
4142
+ async stop() {
4143
+ return new Promise((resolve$2, reject) => {
4144
+ if (this.server) {
4145
+ const server = this.server;
4146
+ (async () => {
4147
+ try {
4148
+ await this.sessionManager.clear();
4149
+ server.close((err) => {
4150
+ if (err) reject(err);
4151
+ else {
4152
+ this.server = null;
4153
+ resolve$2();
4154
+ }
4155
+ });
4156
+ } catch (error) {
4157
+ reject(error);
4158
+ }
4159
+ })();
4160
+ } else resolve$2();
4161
+ });
4162
+ }
4163
+ getPort() {
4164
+ return this.config.port;
4165
+ }
4166
+ getHost() {
4167
+ return this.config.host;
4168
+ }
4169
+ };
4170
+
4171
+ //#endregion
4172
+ //#region src/transports/stdio.ts
4173
+ /**
4174
+ * Stdio transport handler for MCP server
4175
+ * Used for command-line and direct integrations
4176
+ */
4177
+ var StdioTransportHandler = class {
4178
+ server;
4179
+ transport = null;
4180
+ logger;
4181
+ constructor(server, logger = console) {
4182
+ this.server = server;
4183
+ this.logger = logger;
4184
+ }
4185
+ async start() {
4186
+ this.transport = new __modelcontextprotocol_sdk_server_stdio_js.StdioServerTransport();
4187
+ await this.server.connect(this.transport);
4188
+ this.logger.info("@agimon-ai/mcp-proxy MCP server started on stdio");
4189
+ }
4190
+ async stop() {
4191
+ if (this.transport) {
4192
+ await this.transport.close();
4193
+ this.transport = null;
4194
+ }
4195
+ }
4196
+ };
4197
+
4198
+ //#endregion
4199
+ //#region src/transports/stdio-http.ts
4200
+ /**
4201
+ * STDIO-HTTP Proxy Transport
4202
+ *
4203
+ * DESIGN PATTERNS:
4204
+ * - Transport handler pattern implementing TransportHandler interface
4205
+ * - STDIO transport with MCP request forwarding to HTTP backend
4206
+ * - Graceful cleanup with error isolation
4207
+ *
4208
+ * CODING STANDARDS:
4209
+ * - Use StdioServerTransport for stdio communication
4210
+ * - Reuse a single StreamableHTTP client connection
4211
+ * - Wrap async operations with try-catch and descriptive errors
4212
+ *
4213
+ * AVOID:
4214
+ * - Starting HTTP server lifecycle in this transport entry point
4215
+ * - Recreating HTTP client per request
4216
+ * - Swallowing cleanup failures silently
4217
+ */
4218
+ /**
4219
+ * Transport that serves MCP over stdio and forwards MCP requests to an HTTP endpoint.
4220
+ */
4221
+ var StdioHttpTransportHandler = class {
4222
+ endpoint;
4223
+ stdioProxyServer = null;
4224
+ stdioTransport = null;
4225
+ httpClient = null;
4226
+ logger;
4227
+ constructor(config, logger = console) {
4228
+ this.endpoint = config.endpoint;
4229
+ this.logger = logger;
4230
+ }
4231
+ async start() {
4232
+ try {
4233
+ const httpClientTransport = new __modelcontextprotocol_sdk_client_streamableHttp_js.StreamableHTTPClientTransport(this.endpoint);
4234
+ const client = new __modelcontextprotocol_sdk_client_index_js.Client({
4235
+ name: "@agimon-ai/mcp-proxy-stdio-http-proxy",
4236
+ version: "0.1.0"
4237
+ }, { capabilities: {} });
4238
+ await client.connect(httpClientTransport);
4239
+ this.httpClient = client;
4240
+ this.stdioProxyServer = this.createProxyServer(client);
4241
+ this.stdioTransport = new __modelcontextprotocol_sdk_server_stdio_js.StdioServerTransport();
4242
+ await this.stdioProxyServer.connect(this.stdioTransport);
4243
+ this.logger.info(`@agimon-ai/mcp-proxy MCP stdio proxy connected to ${this.endpoint.toString()}`);
4244
+ } catch (error) {
4245
+ await this.stop();
4246
+ throw new Error(`Failed to start stdio-http proxy transport: ${error instanceof Error ? error.message : String(error)}`);
4247
+ }
4248
+ }
4249
+ async stop() {
4250
+ const stdioTransport = this.stdioTransport;
4251
+ const stdioProxyServer = this.stdioProxyServer;
4252
+ const httpClient = this.httpClient;
4253
+ this.stdioTransport = null;
4254
+ this.stdioProxyServer = null;
4255
+ this.httpClient = null;
4256
+ const cleanupErrors = [];
4257
+ await Promise.all([
4258
+ (async () => {
4259
+ try {
4260
+ if (stdioTransport) await stdioTransport.close();
4261
+ } catch (error) {
4262
+ cleanupErrors.push(`failed closing stdio transport: ${error instanceof Error ? error.message : String(error)}`);
4263
+ }
4264
+ })(),
4265
+ (async () => {
4266
+ try {
4267
+ if (stdioProxyServer) await stdioProxyServer.close();
4268
+ } catch (error) {
4269
+ cleanupErrors.push(`failed closing stdio proxy server: ${error instanceof Error ? error.message : String(error)}`);
4270
+ }
4271
+ })(),
4272
+ (async () => {
4273
+ try {
4274
+ if (httpClient) await httpClient.close();
4275
+ } catch (error) {
4276
+ cleanupErrors.push(`failed closing http client: ${error instanceof Error ? error.message : String(error)}`);
4277
+ }
4278
+ })()
4279
+ ]);
4280
+ if (cleanupErrors.length > 0) throw new Error(`Failed to stop stdio-http proxy transport: ${cleanupErrors.join("; ")}`);
4281
+ }
4282
+ createProxyServer(client) {
4283
+ const proxyServer = new __modelcontextprotocol_sdk_server_index_js.Server({
4284
+ name: "@agimon-ai/mcp-proxy-stdio-http-proxy",
4285
+ version: "0.1.0"
4286
+ }, { capabilities: {
4287
+ tools: {},
4288
+ resources: {},
4289
+ prompts: {}
4290
+ } });
4291
+ proxyServer.setRequestHandler(__modelcontextprotocol_sdk_types_js.ListToolsRequestSchema, async () => {
4292
+ try {
4293
+ return await client.listTools();
4294
+ } catch (error) {
4295
+ throw new Error(`Failed forwarding tools/list to HTTP backend: ${error instanceof Error ? error.message : String(error)}`);
4296
+ }
4297
+ });
4298
+ proxyServer.setRequestHandler(__modelcontextprotocol_sdk_types_js.CallToolRequestSchema, async (request) => {
4299
+ try {
4300
+ return await client.callTool({
4301
+ name: request.params.name,
4302
+ arguments: request.params.arguments
4303
+ });
4304
+ } catch (error) {
4305
+ throw new Error(`Failed forwarding tools/call (${request.params.name}) to HTTP backend: ${error instanceof Error ? error.message : String(error)}`);
4306
+ }
4307
+ });
4308
+ proxyServer.setRequestHandler(__modelcontextprotocol_sdk_types_js.ListResourcesRequestSchema, async () => {
4309
+ try {
4310
+ return await client.listResources();
4311
+ } catch (error) {
4312
+ throw new Error(`Failed forwarding resources/list to HTTP backend: ${error instanceof Error ? error.message : String(error)}`);
4313
+ }
4314
+ });
4315
+ proxyServer.setRequestHandler(__modelcontextprotocol_sdk_types_js.ReadResourceRequestSchema, async (request) => {
4316
+ try {
4317
+ return await client.readResource({ uri: request.params.uri });
4318
+ } catch (error) {
4319
+ throw new Error(`Failed forwarding resources/read (${request.params.uri}) to HTTP backend: ${error instanceof Error ? error.message : String(error)}`);
4320
+ }
4321
+ });
4322
+ proxyServer.setRequestHandler(__modelcontextprotocol_sdk_types_js.ListPromptsRequestSchema, async () => {
4323
+ try {
4324
+ return await client.listPrompts();
4325
+ } catch (error) {
4326
+ throw new Error(`Failed forwarding prompts/list to HTTP backend: ${error instanceof Error ? error.message : String(error)}`);
4327
+ }
4328
+ });
4329
+ proxyServer.setRequestHandler(__modelcontextprotocol_sdk_types_js.GetPromptRequestSchema, async (request) => {
4330
+ try {
4331
+ return await client.getPrompt({
4332
+ name: request.params.name,
4333
+ arguments: request.params.arguments
4334
+ });
4335
+ } catch (error) {
4336
+ throw new Error(`Failed forwarding prompts/get (${request.params.name}) to HTTP backend: ${error instanceof Error ? error.message : String(error)}`);
4337
+ }
4338
+ });
4339
+ return proxyServer;
4340
+ }
4341
+ };
4342
+
4343
+ //#endregion
4344
+ //#region package.json
4345
+ var version = "0.3.19";
4346
+
4347
+ //#endregion
4348
+ //#region src/container/index.ts
4349
+ /**
4350
+ * Proxy Container
4351
+ *
4352
+ * DESIGN PATTERNS:
4353
+ * - Composition root pattern for wiring proxy services
4354
+ * - Factory-based service construction
4355
+ * - Single place for runtime service initialization
4356
+ *
4357
+ * CODING STANDARDS:
4358
+ * - Keep service wiring centralized here
4359
+ * - Prefer lazy initialization for cache-heavy services
4360
+ * - Return a dispose function for owned resources
4361
+ *
4362
+ * AVOID:
4363
+ * - Instantiating proxy services directly in transport or CLI layers
4364
+ * - Spreading construction logic across commands
4365
+ */
4366
+ function createProxyIoCContainer(logger = console) {
4367
+ return {
4368
+ createConfigFetcherService: (options) => new ConfigFetcherService(options, logger),
4369
+ createClientManagerService: () => new McpClientManagerService(logger),
4370
+ createRuntimeStateService: (runtimeDir) => new RuntimeStateService(runtimeDir, logger),
4371
+ createStopServerService: (runtimeStateService) => new StopServerService(runtimeStateService, logger),
4372
+ createSkillService: (cwd, skillPaths, options) => new SkillService(cwd, skillPaths, options, logger),
4373
+ createDefinitionsCacheService: (clientManager, skillService, options) => new DefinitionsCacheService(clientManager, skillService, options, logger),
4374
+ createPrefetchService: (config) => new PrefetchService(config),
4375
+ createDescribeToolsTool: (clientManager, skillService, serverId, definitionsCacheService) => new DescribeToolsTool(clientManager, skillService, serverId, definitionsCacheService),
4376
+ createUseToolTool: (clientManager, skillService, serverId, definitionsCacheService) => new UseToolTool(clientManager, skillService, serverId, definitionsCacheService),
4377
+ createSearchListToolsTool: (clientManager, definitionsCacheService) => new SearchListToolsTool(clientManager, definitionsCacheService),
4378
+ createStdioTransportHandler: async (createServer$1) => {
4379
+ return new StdioTransportHandler(await createServer$1(), logger);
4380
+ },
4381
+ createSseTransportHandler: async (createServer$1, config) => {
4382
+ return new SseTransportHandler(await createServer$1(), config, logger);
4383
+ },
4384
+ createHttpTransportHandler: (createServer$1, config, adminOptions) => new HttpTransportHandler(createServer$1, config, adminOptions, logger),
4385
+ createStdioHttpTransportHandler: (endpoint) => new StdioHttpTransportHandler({ endpoint }, logger)
4386
+ };
4387
+ }
4388
+ /**
4389
+ * Create the shared proxy container for a session or server startup.
4390
+ *
4391
+ * This is the composition root for the package: it owns service wiring,
4392
+ * cache warmup, and cleanup for all proxy-specific resources.
4393
+ */
4394
+ async function createProxyContainer(options) {
4395
+ const telemetry = await createProxyLogger({
4396
+ workspaceRoot: process.cwd(),
4397
+ serviceName: "@agimon-ai/mcp-proxy"
4398
+ });
4399
+ const logger = telemetry;
4400
+ const container = createProxyIoCContainer(logger);
4401
+ const clientManager = container.createClientManagerService();
4402
+ try {
4403
+ let configSkills;
4404
+ let configId;
4405
+ let configHash;
4406
+ let effectiveDefinitionsCachePath;
4407
+ let shouldStartFromCache = false;
4408
+ if (options?.configFilePath) {
4409
+ let config;
4410
+ try {
4411
+ config = await container.createConfigFetcherService({
4412
+ configFilePath: options.configFilePath,
4413
+ useCache: !options.noCache
4414
+ }).fetchConfiguration(options.noCache || false);
4415
+ } catch (error) {
4416
+ throw new Error(`Failed to load MCP configuration from '${options.configFilePath}': ${error instanceof Error ? error.message : String(error)}`);
4417
+ }
4418
+ configSkills = config.skills;
4419
+ configId = config.id;
4420
+ configHash = DefinitionsCacheService.generateConfigHash(config);
4421
+ effectiveDefinitionsCachePath = options.definitionsCachePath || DefinitionsCacheService.getDefaultCachePath(options.configFilePath);
4422
+ clientManager.registerServerConfigs(config.mcpServers);
4423
+ if (options.clearDefinitionsCache && effectiveDefinitionsCachePath) {
4424
+ await DefinitionsCacheService.clearFile(effectiveDefinitionsCachePath);
4425
+ logger.info(`[definitions-cache] Cleared ${effectiveDefinitionsCachePath}`);
4426
+ }
4427
+ if (effectiveDefinitionsCachePath) try {
4428
+ const cacheData = await DefinitionsCacheService.readFromFile(effectiveDefinitionsCachePath);
4429
+ if (DefinitionsCacheService.isCacheValid(cacheData, {
4430
+ configHash,
4431
+ oneMcpVersion: version
4432
+ })) shouldStartFromCache = true;
4433
+ } catch (error) {
4434
+ logger.warn(`[definitions-cache] Failed to inspect ${effectiveDefinitionsCachePath}; falling back to live discovery`, error);
4435
+ }
4436
+ if (!shouldStartFromCache) {
4437
+ const failedConnections = [];
4438
+ const connectionPromises = Object.entries(config.mcpServers).map(async ([serverName, serverConfig]) => {
4439
+ try {
4440
+ await clientManager.connectToServer(serverName, serverConfig);
4441
+ logger.info(`Connected to MCP server: ${serverName}`);
4442
+ } catch (error) {
4443
+ const err = error instanceof Error ? error : new Error(String(error));
4444
+ failedConnections.push({
4445
+ serverName,
4446
+ error: err
4447
+ });
4448
+ logger.warn(`Failed to connect to ${serverName}`, error);
4449
+ }
4450
+ });
4451
+ await Promise.all(connectionPromises);
4452
+ if (failedConnections.length > 0 && failedConnections.length < Object.keys(config.mcpServers).length) logger.warn(`Warning: Some MCP server connections failed: ${failedConnections.map((f) => f.serverName).join(", ")}`);
4453
+ if (failedConnections.length > 0 && failedConnections.length === Object.keys(config.mcpServers).length) throw new Error(`All MCP server connections failed: ${failedConnections.map((f) => `${f.serverName}: ${f.error.message}`).join(", ")}`);
4454
+ } else logger.info(`[definitions-cache] Using cached definitions from ${effectiveDefinitionsCachePath}`);
4455
+ }
4456
+ const serverId = options?.serverId || configId || generateServerId();
4457
+ logger.info(`[mcp-proxy] Server ID: ${serverId}`);
4458
+ const skillPaths = (options?.skills || configSkills)?.paths ?? [];
4459
+ const toolsRef = { describeTools: null };
4460
+ const skillService = skillPaths.length > 0 ? container.createSkillService(process.cwd(), skillPaths, { onCacheInvalidated: () => {
4461
+ toolsRef.describeTools?.clearAutoDetectedSkillsCache();
4462
+ } }) : void 0;
4463
+ let definitionsCacheService;
4464
+ if (effectiveDefinitionsCachePath) try {
4465
+ const cacheData = await DefinitionsCacheService.readFromFile(effectiveDefinitionsCachePath);
4466
+ if (DefinitionsCacheService.isCacheValid(cacheData, {
4467
+ configHash,
4468
+ oneMcpVersion: version
4469
+ })) definitionsCacheService = container.createDefinitionsCacheService(clientManager, skillService, { cacheData });
4470
+ else definitionsCacheService = container.createDefinitionsCacheService(clientManager, skillService);
4471
+ } catch (error) {
4472
+ logger.warn(`[definitions-cache] Failed to load ${effectiveDefinitionsCachePath}, falling back to live discovery: ${error instanceof Error ? error.message : "Unknown error"}`);
4473
+ definitionsCacheService = container.createDefinitionsCacheService(clientManager, skillService);
4474
+ }
4475
+ else definitionsCacheService = container.createDefinitionsCacheService(clientManager, skillService);
4476
+ const proxyMode = options?.proxyMode || "meta";
4477
+ const describeTools = container.createDescribeToolsTool(clientManager, skillService, serverId, definitionsCacheService);
4478
+ const useTool = container.createUseToolTool(clientManager, skillService, serverId, definitionsCacheService);
4479
+ const searchListTools = container.createSearchListToolsTool(clientManager, definitionsCacheService);
4480
+ toolsRef.describeTools = describeTools;
4481
+ if (skillService) skillService.startWatching().catch((error) => {
4482
+ logger.warn(`[skill-watcher] File watcher failed (non-critical): ${error instanceof Error ? error.message : "Unknown error"}`);
4483
+ });
4484
+ if (!shouldStartFromCache && effectiveDefinitionsCachePath && options?.configFilePath) definitionsCacheService.collectForCache({
4485
+ configPath: options.configFilePath,
4486
+ configHash,
4487
+ oneMcpVersion: version,
4488
+ serverId
4489
+ }).then((definitionsCache) => DefinitionsCacheService.writeToFile(effectiveDefinitionsCachePath, definitionsCache)).then(() => {
4490
+ logger.info(`[definitions-cache] Wrote ${effectiveDefinitionsCachePath}`);
4491
+ }).catch((error) => {
4492
+ logger.warn(`[definitions-cache] Failed to persist ${effectiveDefinitionsCachePath}: ${error instanceof Error ? error.message : "Unknown error"}`);
4493
+ });
4494
+ const dispose = async () => {
4495
+ try {
4496
+ await clientManager.disconnectAll();
4497
+ } finally {
4498
+ if (skillService) skillService.stopWatching();
4499
+ await telemetry.shutdown();
4500
+ }
4501
+ };
4502
+ return {
4503
+ clientManager,
4504
+ definitionsCacheService,
4505
+ skillService,
4506
+ describeTools,
4507
+ useTool,
4508
+ searchListTools,
4509
+ serverId,
4510
+ proxyMode,
4511
+ dispose
4512
+ };
4513
+ } catch (error) {
4514
+ await telemetry.shutdown();
4515
+ throw error;
4516
+ }
4517
+ }
4518
+ /**
4519
+ * Create a sessionless stdio transport handler from the shared server factory.
4520
+ */
4521
+ async function createStdioTransportHandler(createServer$1) {
4522
+ const server = await createServer$1();
4523
+ return createProxyIoCContainer().createStdioTransportHandler(() => Promise.resolve(server));
4524
+ }
4525
+ /**
4526
+ * Create an SSE transport handler from the shared server factory.
4527
+ */
4528
+ async function createSseTransportHandler(createServer$1, config) {
4529
+ const server = await createServer$1();
4530
+ return createProxyIoCContainer().createSseTransportHandler(() => Promise.resolve(server), config);
4531
+ }
4532
+ /**
4533
+ * Create an HTTP transport handler from shared services.
4534
+ */
4535
+ function createHttpTransportHandler(createServer$1, config, adminOptions) {
4536
+ return createProxyIoCContainer().createHttpTransportHandler(createServer$1, config, adminOptions);
4537
+ }
4538
+ /**
4539
+ * Create a stdio-http transport handler from an endpoint URL.
4540
+ */
4541
+ function createStdioHttpTransportHandler(endpoint) {
4542
+ return createProxyIoCContainer().createStdioHttpTransportHandler(endpoint);
4543
+ }
4544
+ /**
4545
+ * Backward-compatible alias for the shared-service composition root.
4546
+ */
4547
+ async function initializeSharedServices(options) {
4548
+ return createProxyContainer(options);
4549
+ }
4550
+
4551
+ //#endregion
4552
+ //#region src/server/index.ts
4553
+ /**
4554
+ * MCP Server Setup
4555
+ *
4556
+ * DESIGN PATTERNS:
4557
+ * - Factory pattern for server creation
4558
+ * - Tool registration pattern
4559
+ * - Dependency injection for services
4560
+ * - Shared services pattern for multi-session HTTP transport
4561
+ *
4562
+ * CODING STANDARDS:
4563
+ * - Register all tools, resources, and prompts here
4564
+ * - Keep server setup modular and extensible
4565
+ * - Import tools from ../tools/ and register them in the handlers
4566
+ */
4567
+ function summarizeServerTools(serverDefinition) {
4568
+ const toolNames = serverDefinition.tools.map((tool) => tool.name);
4569
+ const capabilities = getUniqueSortedCapabilities(serverDefinition.tools);
4570
+ const capabilitySummary = capabilities.length > 0 ? `; capabilities: ${capabilities.join(", ")}` : "";
4571
+ if (toolNames.length === 0) return `${serverDefinition.serverName} (no tools cached${capabilitySummary})`;
4572
+ return `${serverDefinition.serverName} (${toolNames.join(", ")})${capabilitySummary}`;
4573
+ }
4574
+ function buildFlatToolDescription(serverDefinition, tool) {
4575
+ const parts = [`Proxied from server "${serverDefinition.serverName}" as tool "${tool.name}".`];
4576
+ if (serverDefinition.serverInstruction) parts.push(`Server summary: ${serverDefinition.serverInstruction}`);
4577
+ if (tool.description && !serverDefinition.omitToolDescription) parts.push(tool.description);
4578
+ const capabilities = getToolCapabilities(tool);
4579
+ if (capabilities.length > 0) parts.push(`Capabilities: ${capabilities.join(", ")}`);
4580
+ return parts.join("\n\n");
4581
+ }
4582
+ function buildFlatToolDefinitions(serverDefinitions) {
4583
+ const toolToServers = /* @__PURE__ */ new Map();
4584
+ for (const serverDefinition of serverDefinitions) for (const tool of serverDefinition.tools) {
4585
+ if (!toolToServers.has(tool.name)) toolToServers.set(tool.name, []);
4586
+ toolToServers.get(tool.name)?.push(serverDefinition.serverName);
4587
+ }
4588
+ const definitions = [];
4589
+ for (const serverDefinition of serverDefinitions) for (const tool of serverDefinition.tools) {
4590
+ const hasClash = (toolToServers.get(tool.name) || []).length > 1;
4591
+ definitions.push({
4592
+ name: hasClash ? `${serverDefinition.serverName}__${tool.name}` : tool.name,
4593
+ description: buildFlatToolDescription(serverDefinition, tool),
4594
+ inputSchema: tool.inputSchema,
4595
+ _meta: tool._meta
4596
+ });
4597
+ }
4598
+ return definitions;
4599
+ }
4600
+ async function hasAnySkills(definitionsCacheService, skillService) {
4601
+ const [fileSkills, serverDefinitions] = await Promise.all([skillService ? skillService.getSkills() : definitionsCacheService.getCachedFileSkills(), definitionsCacheService.getServerDefinitions()]);
4602
+ return fileSkills.length > 0 || serverDefinitions.some((server) => server.promptSkills.length > 0);
4603
+ }
4604
+ function buildSkillsDescribeDefinition(serverDefinitions, serverId) {
4605
+ const proxySummary = serverDefinitions.length > 0 ? serverDefinitions.map(summarizeServerTools).join("; ") : "No proxied servers available.";
4606
+ return {
4607
+ name: DescribeToolsTool.TOOL_NAME,
4608
+ description: `Get detailed skill instructions for file-based skills and prompt-based skills proxied by mcp-proxy.\n\nProxy summary: ${proxySummary}\n\nUse this when you need the full instructions for a skill. For MCP tools, call the flat tool names directly. Only use skills discovered from describe_tools with id="${serverId}".`,
4609
+ inputSchema: {
4610
+ type: "object",
4611
+ properties: { toolNames: {
4612
+ type: "array",
4613
+ items: {
4614
+ type: "string",
4615
+ minLength: 1
4616
+ },
4617
+ description: "List of skill names to get detailed information about",
4618
+ minItems: 1
4619
+ } },
4620
+ required: ["toolNames"],
4621
+ additionalProperties: false
4622
+ }
4623
+ };
4624
+ }
4625
+ function buildSearchDescribeDefinition(serverDefinitions, serverId) {
4626
+ const summary = serverDefinitions.length > 0 ? serverDefinitions.map(summarizeServerTools).join("; ") : "No proxied servers available.";
4627
+ return {
4628
+ name: DescribeToolsTool.TOOL_NAME,
4629
+ description: `Get detailed schemas and skill instructions for proxied MCP capabilities.\n\nProxy summary: ${summary}\n\nUse list_tools first to search capability summaries and discover tool names. Then use describe_tools to fetch full schemas or skill instructions. Only use capabilities discovered from mcp-proxy id="${serverId}".`,
4630
+ inputSchema: {
4631
+ type: "object",
4632
+ properties: { toolNames: {
4633
+ type: "array",
4634
+ items: {
4635
+ type: "string",
4636
+ minLength: 1
4637
+ },
4638
+ description: "List of tool or skill names to get detailed information about",
4639
+ minItems: 1
4640
+ } },
4641
+ required: ["toolNames"],
4642
+ additionalProperties: false
4643
+ }
4644
+ };
4645
+ }
4646
+ function buildProxyInstructions(serverDefinitions, mode, includeSkillsTool) {
4647
+ const summary = serverDefinitions.length > 0 ? serverDefinitions.map(summarizeServerTools).join("; ") : "No proxied servers available.";
4648
+ if (mode === "flat") return [
4649
+ "mcp-proxy proxies downstream MCP servers and exposes their tools and resources directly.",
4650
+ `Proxied servers and tools: ${summary}`,
4651
+ includeSkillsTool ? "Skills are still exposed through describe_tools when file-based skills or prompt-backed skills are configured." : "No skills are currently exposed through describe_tools."
4652
+ ].join("\n\n");
4653
+ if (mode === "search") return [
4654
+ "mcp-proxy proxies downstream MCP servers in search mode.",
4655
+ `Proxied servers and tools: ${summary}`,
4656
+ "Use list_tools to search capability summaries and discover tool names, describe_tools to fetch schemas or skill instructions, and use_tool to execute tools."
4657
+ ].join("\n\n");
4658
+ return [
4659
+ "mcp-proxy proxies downstream MCP servers in meta mode.",
4660
+ `Proxied servers and tools: ${summary}`,
4661
+ "Use describe_tools to inspect capabilities and use_tool to execute them."
4662
+ ].join("\n\n");
4663
+ }
4664
+ /**
4665
+ * Initialize shared services and tools once for use across multiple sessions.
4666
+ * Use with createSessionServer() for HTTP transport where multiple agents
4667
+ * connect concurrently. This avoids duplicating downstream connections,
4668
+ * file watchers, caches, and tool instances per session.
4669
+ */
4670
+ /**
4671
+ * Create a lightweight per-session MCP Server instance that delegates
4672
+ * to shared services and tools. Use with createProxyContainer()
4673
+ * for multi-session HTTP transport.
4674
+ */
4675
+ async function createSessionServer(shared) {
4676
+ const { clientManager, definitionsCacheService, skillService, describeTools, useTool: useToolWithCache, searchListTools, serverId, proxyMode } = shared;
4677
+ const server = new __modelcontextprotocol_sdk_server_index_js.Server({
4678
+ name: "@agimon-ai/mcp-proxy",
4679
+ version: "0.1.0"
4680
+ }, {
4681
+ capabilities: {
4682
+ tools: {},
4683
+ resources: {},
4684
+ prompts: {}
4685
+ },
4686
+ instructions: buildProxyInstructions(await definitionsCacheService.getServerDefinitions(), proxyMode, await hasAnySkills(definitionsCacheService, skillService))
4687
+ });
4688
+ server.setRequestHandler(__modelcontextprotocol_sdk_types_js.ListToolsRequestSchema, async () => ({ tools: proxyMode === "flat" ? await (async () => {
4689
+ const currentServerDefinitions = await definitionsCacheService.getServerDefinitions();
4690
+ const shouldIncludeSkillsTool = await hasAnySkills(definitionsCacheService, skillService);
4691
+ return [...buildFlatToolDefinitions(currentServerDefinitions), ...shouldIncludeSkillsTool ? [buildSkillsDescribeDefinition(currentServerDefinitions, serverId)] : []];
4692
+ })() : proxyMode === "search" ? await (async () => {
4693
+ return [
4694
+ buildSearchDescribeDefinition(await definitionsCacheService.getServerDefinitions(), serverId),
4695
+ await searchListTools.getDefinition(),
4696
+ useToolWithCache.getDefinition()
4697
+ ];
4698
+ })() : [await describeTools.getDefinition(), useToolWithCache.getDefinition()] }));
4699
+ server.setRequestHandler(__modelcontextprotocol_sdk_types_js.CallToolRequestSchema, async (request) => {
4700
+ const { name, arguments: args } = request.params;
4701
+ if (name === DescribeToolsTool.TOOL_NAME) try {
4702
+ return await describeTools.execute(args);
4703
+ } catch (error) {
4704
+ throw new Error(`Failed to execute ${name}: ${error instanceof Error ? error.message : String(error)}`);
4705
+ }
4706
+ if (name === UseToolTool.TOOL_NAME) try {
4707
+ return await useToolWithCache.execute(args);
4708
+ } catch (error) {
4709
+ throw new Error(`Failed to execute ${name}: ${error instanceof Error ? error.message : String(error)}`);
4710
+ }
4711
+ if (name === SearchListToolsTool.TOOL_NAME && proxyMode === "search") try {
4712
+ return await searchListTools.execute(args);
4713
+ } catch (error) {
4714
+ throw new Error(`Failed to execute ${name}: ${error instanceof Error ? error.message : String(error)}`);
4715
+ }
4716
+ if (proxyMode === "flat") return await useToolWithCache.execute({
4717
+ toolName: name,
4718
+ toolArgs: args || {}
4719
+ });
4720
+ throw new Error(`Unknown tool: ${name}`);
4721
+ });
4722
+ server.setRequestHandler(__modelcontextprotocol_sdk_types_js.ListResourcesRequestSchema, async () => {
4723
+ const currentServerDefinitions = await definitionsCacheService.getServerDefinitions();
4724
+ const resourceToServers = /* @__PURE__ */ new Map();
4725
+ for (const serverDefinition of currentServerDefinitions) for (const resource of serverDefinition.resources) {
4726
+ if (!resourceToServers.has(resource.uri)) resourceToServers.set(resource.uri, []);
4727
+ resourceToServers.get(resource.uri)?.push(serverDefinition.serverName);
4728
+ }
4729
+ const resources = [];
4730
+ for (const serverDefinition of currentServerDefinitions) for (const resource of serverDefinition.resources) {
4731
+ const hasClash = (resourceToServers.get(resource.uri) || []).length > 1;
4732
+ resources.push({
4733
+ ...resource,
4734
+ uri: hasClash ? `${serverDefinition.serverName}__${resource.uri}` : resource.uri
4735
+ });
4736
+ }
4737
+ return { resources };
4738
+ });
4739
+ server.setRequestHandler(__modelcontextprotocol_sdk_types_js.ReadResourceRequestSchema, async (request) => {
4740
+ const { uri } = request.params;
4741
+ const { serverName, actualToolName: actualUri } = parseToolName(uri);
4742
+ if (serverName) return await (await clientManager.ensureConnected(serverName)).readResource(actualUri);
4743
+ const matchingServers = await definitionsCacheService.getServersForResource(actualUri);
4744
+ if (matchingServers.length === 0) throw new Error(`Resource not found: ${uri}`);
4745
+ if (matchingServers.length > 1) throw new Error(`Resource "${actualUri}" exists on multiple servers: ${matchingServers.join(", ")}. Use the prefixed format (e.g., "${matchingServers[0]}__${actualUri}") to specify which server to use.`);
4746
+ return await (await clientManager.ensureConnected(matchingServers[0])).readResource(actualUri);
4747
+ });
4748
+ server.setRequestHandler(__modelcontextprotocol_sdk_types_js.ListPromptsRequestSchema, async () => {
4749
+ const currentServerDefinitions = await definitionsCacheService.getServerDefinitions();
4750
+ const promptToServers = /* @__PURE__ */ new Map();
4751
+ const serverPromptsMap = /* @__PURE__ */ new Map();
4752
+ for (const serverDefinition of currentServerDefinitions) {
4753
+ serverPromptsMap.set(serverDefinition.serverName, serverDefinition.prompts);
4754
+ for (const prompt of serverDefinition.prompts) {
4755
+ if (!promptToServers.has(prompt.name)) promptToServers.set(prompt.name, []);
4756
+ promptToServers.get(prompt.name).push(serverDefinition.serverName);
4757
+ }
4758
+ }
4759
+ const aggregatedPrompts = [];
4760
+ for (const serverDefinition of currentServerDefinitions) {
4761
+ const prompts = serverPromptsMap.get(serverDefinition.serverName) || [];
4762
+ for (const prompt of prompts) {
4763
+ const hasClash = (promptToServers.get(prompt.name) || []).length > 1;
4764
+ aggregatedPrompts.push({
4765
+ name: hasClash ? `${serverDefinition.serverName}__${prompt.name}` : prompt.name,
4766
+ description: prompt.description,
4767
+ arguments: prompt.arguments
4768
+ });
4769
+ }
4770
+ }
4771
+ return { prompts: aggregatedPrompts };
4772
+ });
4773
+ server.setRequestHandler(__modelcontextprotocol_sdk_types_js.GetPromptRequestSchema, async (request) => {
4774
+ const { name, arguments: args } = request.params;
4775
+ const currentServerDefinitions = await definitionsCacheService.getServerDefinitions();
4776
+ const { serverName, actualToolName: actualPromptName } = parseToolName(name);
4777
+ if (serverName) return await (await clientManager.ensureConnected(serverName)).getPrompt(actualPromptName, args);
4778
+ const serversWithPrompt = [];
4779
+ for (const serverDefinition of currentServerDefinitions) if (serverDefinition.prompts.some((prompt) => prompt.name === name)) serversWithPrompt.push(serverDefinition.serverName);
4780
+ if (serversWithPrompt.length === 0) throw new Error(`Prompt not found: ${name}`);
4781
+ if (serversWithPrompt.length > 1) throw new Error(`Prompt "${name}" exists on multiple servers: ${serversWithPrompt.join(", ")}. Use the prefixed format (e.g., "${serversWithPrompt[0]}__${name}") to specify which server to use.`);
4782
+ const client = clientManager.getClient(serversWithPrompt[0]);
4783
+ if (!client) return await (await clientManager.ensureConnected(serversWithPrompt[0])).getPrompt(name, args);
4784
+ return await client.getPrompt(name, args);
4785
+ });
4786
+ return server;
4787
+ }
4788
+ /**
4789
+ * Create a single MCP server instance (backward-compatible wrapper).
4790
+ * For multi-session HTTP transport, use createProxyContainer() + createSessionServer() instead.
4791
+ */
4792
+ async function createServer(options) {
4793
+ return createSessionServer(await createProxyContainer(options));
4794
+ }
4795
+
4796
+ //#endregion
4797
+ //#region src/types/index.ts
4798
+ /**
4799
+ * Transport mode constants
4800
+ */
4801
+ const TRANSPORT_MODE = {
4802
+ STDIO: "stdio",
4803
+ HTTP: "http",
4804
+ SSE: "sse"
4805
+ };
4806
+
4807
+ //#endregion
4808
+ Object.defineProperty(exports, 'ConfigFetcherService', {
4809
+ enumerable: true,
4810
+ get: function () {
4811
+ return ConfigFetcherService;
4812
+ }
4813
+ });
4814
+ Object.defineProperty(exports, 'DefinitionsCacheService', {
4815
+ enumerable: true,
4816
+ get: function () {
4817
+ return DefinitionsCacheService;
4818
+ }
4819
+ });
4820
+ Object.defineProperty(exports, 'DescribeToolsTool', {
4821
+ enumerable: true,
4822
+ get: function () {
4823
+ return DescribeToolsTool;
4824
+ }
4825
+ });
4826
+ Object.defineProperty(exports, 'HttpTransportHandler', {
4827
+ enumerable: true,
4828
+ get: function () {
4829
+ return HttpTransportHandler;
4830
+ }
4831
+ });
4832
+ Object.defineProperty(exports, 'McpClientManagerService', {
4833
+ enumerable: true,
4834
+ get: function () {
4835
+ return McpClientManagerService;
4836
+ }
4837
+ });
4838
+ Object.defineProperty(exports, 'RuntimeStateService', {
4839
+ enumerable: true,
4840
+ get: function () {
4841
+ return RuntimeStateService;
4842
+ }
4843
+ });
4844
+ Object.defineProperty(exports, 'SearchListToolsTool', {
4845
+ enumerable: true,
4846
+ get: function () {
4847
+ return SearchListToolsTool;
4848
+ }
4849
+ });
4850
+ Object.defineProperty(exports, 'SkillService', {
4851
+ enumerable: true,
4852
+ get: function () {
4853
+ return SkillService;
4854
+ }
4855
+ });
4856
+ Object.defineProperty(exports, 'SseTransportHandler', {
4857
+ enumerable: true,
4858
+ get: function () {
4859
+ return SseTransportHandler;
4860
+ }
4861
+ });
4862
+ Object.defineProperty(exports, 'StdioHttpTransportHandler', {
4863
+ enumerable: true,
4864
+ get: function () {
4865
+ return StdioHttpTransportHandler;
4866
+ }
4867
+ });
4868
+ Object.defineProperty(exports, 'StdioTransportHandler', {
4869
+ enumerable: true,
4870
+ get: function () {
4871
+ return StdioTransportHandler;
4872
+ }
4873
+ });
4874
+ Object.defineProperty(exports, 'StopServerService', {
4875
+ enumerable: true,
4876
+ get: function () {
4877
+ return StopServerService;
4878
+ }
4879
+ });
4880
+ Object.defineProperty(exports, 'TRANSPORT_MODE', {
4881
+ enumerable: true,
4882
+ get: function () {
4883
+ return TRANSPORT_MODE;
4884
+ }
4885
+ });
4886
+ Object.defineProperty(exports, 'UseToolTool', {
4887
+ enumerable: true,
4888
+ get: function () {
4889
+ return UseToolTool;
4890
+ }
4891
+ });
4892
+ Object.defineProperty(exports, '__toESM', {
4893
+ enumerable: true,
4894
+ get: function () {
4895
+ return __toESM;
4896
+ }
4897
+ });
4898
+ Object.defineProperty(exports, 'createHttpTransportHandler', {
4899
+ enumerable: true,
4900
+ get: function () {
4901
+ return createHttpTransportHandler;
4902
+ }
4903
+ });
4904
+ Object.defineProperty(exports, 'createProxyContainer', {
4905
+ enumerable: true,
4906
+ get: function () {
4907
+ return createProxyContainer;
4908
+ }
4909
+ });
4910
+ Object.defineProperty(exports, 'createProxyIoCContainer', {
4911
+ enumerable: true,
4912
+ get: function () {
4913
+ return createProxyIoCContainer;
4914
+ }
4915
+ });
4916
+ Object.defineProperty(exports, 'createProxyLogger', {
4917
+ enumerable: true,
4918
+ get: function () {
4919
+ return createProxyLogger;
4920
+ }
4921
+ });
4922
+ Object.defineProperty(exports, 'createServer', {
4923
+ enumerable: true,
4924
+ get: function () {
4925
+ return createServer;
4926
+ }
4927
+ });
4928
+ Object.defineProperty(exports, 'createSessionServer', {
4929
+ enumerable: true,
4930
+ get: function () {
4931
+ return createSessionServer;
4932
+ }
4933
+ });
4934
+ Object.defineProperty(exports, 'createSseTransportHandler', {
4935
+ enumerable: true,
4936
+ get: function () {
4937
+ return createSseTransportHandler;
4938
+ }
4939
+ });
4940
+ Object.defineProperty(exports, 'createStdioHttpTransportHandler', {
4941
+ enumerable: true,
4942
+ get: function () {
4943
+ return createStdioHttpTransportHandler;
4944
+ }
4945
+ });
4946
+ Object.defineProperty(exports, 'createStdioTransportHandler', {
4947
+ enumerable: true,
4948
+ get: function () {
4949
+ return createStdioTransportHandler;
4950
+ }
4951
+ });
4952
+ Object.defineProperty(exports, 'findConfigFile', {
4953
+ enumerable: true,
4954
+ get: function () {
4955
+ return findConfigFile;
4956
+ }
4957
+ });
4958
+ Object.defineProperty(exports, 'generateServerId', {
4959
+ enumerable: true,
4960
+ get: function () {
4961
+ return generateServerId;
4962
+ }
4963
+ });
4964
+ Object.defineProperty(exports, 'initializeSharedServices', {
4965
+ enumerable: true,
4966
+ get: function () {
4967
+ return initializeSharedServices;
4968
+ }
4969
+ });
4970
+ Object.defineProperty(exports, 'version', {
4971
+ enumerable: true,
4972
+ get: function () {
4973
+ return version;
4974
+ }
4975
+ });