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