@agiflowai/one-mcp 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.cjs +17 -14
- package/dist/cli.mjs +17 -14
- package/dist/{http-B5WVqLzz.cjs → http-3v8zyDO3.cjs} +661 -160
- package/dist/{http-B1EDyxR_.mjs → http-CzQfsUEI.mjs} +657 -162
- package/dist/index.cjs +1 -1
- package/dist/index.mjs +1 -1
- package/package.json +2 -2
|
@@ -28,6 +28,9 @@ let node_fs = require("node:fs");
|
|
|
28
28
|
let js_yaml = require("js-yaml");
|
|
29
29
|
js_yaml = __toESM(js_yaml);
|
|
30
30
|
let zod = require("zod");
|
|
31
|
+
let node_crypto = require("node:crypto");
|
|
32
|
+
let node_path = require("node:path");
|
|
33
|
+
let node_os = require("node:os");
|
|
31
34
|
let __modelcontextprotocol_sdk_client_index_js = require("@modelcontextprotocol/sdk/client/index.js");
|
|
32
35
|
let __modelcontextprotocol_sdk_client_stdio_js = require("@modelcontextprotocol/sdk/client/stdio.js");
|
|
33
36
|
let __modelcontextprotocol_sdk_client_sse_js = require("@modelcontextprotocol/sdk/client/sse.js");
|
|
@@ -35,7 +38,6 @@ let __modelcontextprotocol_sdk_server_stdio_js = require("@modelcontextprotocol/
|
|
|
35
38
|
let __modelcontextprotocol_sdk_server_sse_js = require("@modelcontextprotocol/sdk/server/sse.js");
|
|
36
39
|
let express = require("express");
|
|
37
40
|
express = __toESM(express);
|
|
38
|
-
let node_crypto = require("node:crypto");
|
|
39
41
|
let __modelcontextprotocol_sdk_server_streamableHttp_js = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
|
|
40
42
|
|
|
41
43
|
//#region src/utils/mcpConfigSchema.ts
|
|
@@ -105,6 +107,95 @@ function interpolateEnvVarsInObject(obj) {
|
|
|
105
107
|
return obj;
|
|
106
108
|
}
|
|
107
109
|
/**
|
|
110
|
+
* Private IP range patterns for SSRF protection
|
|
111
|
+
* Covers both IPv4 and IPv6 loopback, private, and link-local ranges
|
|
112
|
+
*/
|
|
113
|
+
const PRIVATE_IP_PATTERNS = [
|
|
114
|
+
/^127\./,
|
|
115
|
+
/^10\./,
|
|
116
|
+
/^172\.(1[6-9]|2\d|3[01])\./,
|
|
117
|
+
/^192\.168\./,
|
|
118
|
+
/^169\.254\./,
|
|
119
|
+
/^0\./,
|
|
120
|
+
/^224\./,
|
|
121
|
+
/^240\./,
|
|
122
|
+
/^localhost$/i,
|
|
123
|
+
/^.*\.localhost$/i,
|
|
124
|
+
/^\[::\]/,
|
|
125
|
+
/^\[::1\]/,
|
|
126
|
+
/^\[0:0:0:0:0:0:0:1\]/,
|
|
127
|
+
/^\[0{1,4}:0{1,4}:0{1,4}:0{1,4}:0{1,4}:0{1,4}:0{1,4}:1\]/i,
|
|
128
|
+
/^\[fe80:/i,
|
|
129
|
+
/^\[fc00:/i,
|
|
130
|
+
/^\[fd00:/i,
|
|
131
|
+
/^\[::ffff:127\./i,
|
|
132
|
+
/^\[::ffff:7f[0-9a-f]{2}:/i,
|
|
133
|
+
/^\[::ffff:10\./i,
|
|
134
|
+
/^\[::ffff:a[0-9a-f]{2}:/i,
|
|
135
|
+
/^\[::ffff:172\.(1[6-9]|2\d|3[01])\./i,
|
|
136
|
+
/^\[::ffff:ac1[0-9a-f]:/i,
|
|
137
|
+
/^\[::ffff:192\.168\./i,
|
|
138
|
+
/^\[::ffff:c0a8:/i,
|
|
139
|
+
/^\[::ffff:169\.254\./i,
|
|
140
|
+
/^\[::ffff:a9fe:/i,
|
|
141
|
+
/^\[::ffff:0\./i,
|
|
142
|
+
/^\[::127\./i,
|
|
143
|
+
/^\[::7f[0-9a-f]{2}:/i,
|
|
144
|
+
/^\[::10\./i,
|
|
145
|
+
/^\[::a[0-9a-f]{2}:/i,
|
|
146
|
+
/^\[::192\.168\./i,
|
|
147
|
+
/^\[::c0a8:/i
|
|
148
|
+
];
|
|
149
|
+
/**
|
|
150
|
+
* Validate URL for SSRF protection
|
|
151
|
+
*
|
|
152
|
+
* @param url - The URL to validate (after env var interpolation)
|
|
153
|
+
* @param security - Security settings
|
|
154
|
+
* @throws Error if URL is unsafe
|
|
155
|
+
*/
|
|
156
|
+
function validateUrlSecurity(url, security) {
|
|
157
|
+
const allowPrivateIPs = security?.allowPrivateIPs ?? false;
|
|
158
|
+
const enforceHttps = security?.enforceHttps ?? true;
|
|
159
|
+
let parsedUrl;
|
|
160
|
+
try {
|
|
161
|
+
parsedUrl = new URL(url);
|
|
162
|
+
} catch (error) {
|
|
163
|
+
throw new Error(`Invalid URL format: ${url}`);
|
|
164
|
+
}
|
|
165
|
+
const protocol = parsedUrl.protocol.replace(":", "");
|
|
166
|
+
if (enforceHttps && protocol !== "https") throw new Error(`HTTPS is required for security. URL uses '${protocol}://'. Set security.enforceHttps: false to allow HTTP.`);
|
|
167
|
+
if (protocol !== "http" && protocol !== "https") throw new Error(`Invalid URL protocol '${protocol}://'. Only http:// and https:// are allowed.`);
|
|
168
|
+
if (!allowPrivateIPs) {
|
|
169
|
+
const hostname = parsedUrl.hostname.toLowerCase();
|
|
170
|
+
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.`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Validate a remote config source against its validation rules
|
|
175
|
+
*
|
|
176
|
+
* @param source - Remote config source with validation rules
|
|
177
|
+
* @throws Error if validation fails
|
|
178
|
+
*/
|
|
179
|
+
function validateRemoteConfigSource(source) {
|
|
180
|
+
const interpolatedUrl = interpolateEnvVars(source.url);
|
|
181
|
+
validateUrlSecurity(interpolatedUrl, source.security);
|
|
182
|
+
if (!source.validation) return;
|
|
183
|
+
if (source.validation.url) {
|
|
184
|
+
if (!new RegExp(source.validation.url).test(interpolatedUrl)) throw new Error(`Remote config URL "${interpolatedUrl}" does not match validation pattern: ${source.validation.url}`);
|
|
185
|
+
}
|
|
186
|
+
if (source.validation.headers && Object.keys(source.validation.headers).length > 0) {
|
|
187
|
+
if (!source.headers) {
|
|
188
|
+
const requiredHeaders = Object.keys(source.validation.headers);
|
|
189
|
+
throw new Error(`Remote config is missing required headers: ${requiredHeaders.join(", ")}`);
|
|
190
|
+
}
|
|
191
|
+
for (const [headerName, pattern] of Object.entries(source.validation.headers)) {
|
|
192
|
+
if (!(headerName in source.headers)) throw new Error(`Remote config is missing required header: ${headerName}`);
|
|
193
|
+
const interpolatedHeaderValue = interpolateEnvVars(source.headers[headerName]);
|
|
194
|
+
if (!new RegExp(pattern).test(interpolatedHeaderValue)) throw new Error(`Remote config header "${headerName}" value "${interpolatedHeaderValue}" does not match validation pattern: ${pattern}`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
108
199
|
* Claude Code / Claude Desktop standard MCP config format
|
|
109
200
|
* This is the format users write in their config files
|
|
110
201
|
*/
|
|
@@ -130,10 +221,32 @@ const ClaudeCodeHttpServerSchema = zod.z.object({
|
|
|
130
221
|
config: AdditionalConfigSchema
|
|
131
222
|
});
|
|
132
223
|
const ClaudeCodeServerConfigSchema = zod.z.union([ClaudeCodeStdioServerSchema, ClaudeCodeHttpServerSchema]);
|
|
224
|
+
const RemoteConfigValidationSchema = zod.z.object({
|
|
225
|
+
url: zod.z.string().optional(),
|
|
226
|
+
headers: zod.z.record(zod.z.string(), zod.z.string()).optional()
|
|
227
|
+
}).optional();
|
|
228
|
+
const RemoteConfigSecuritySchema = zod.z.object({
|
|
229
|
+
allowPrivateIPs: zod.z.boolean().optional(),
|
|
230
|
+
enforceHttps: zod.z.boolean().optional()
|
|
231
|
+
}).optional();
|
|
232
|
+
const RemoteConfigSourceSchema = zod.z.object({
|
|
233
|
+
url: zod.z.string(),
|
|
234
|
+
headers: zod.z.record(zod.z.string(), zod.z.string()).optional(),
|
|
235
|
+
validation: RemoteConfigValidationSchema,
|
|
236
|
+
security: RemoteConfigSecuritySchema,
|
|
237
|
+
mergeStrategy: zod.z.enum([
|
|
238
|
+
"local-priority",
|
|
239
|
+
"remote-priority",
|
|
240
|
+
"merge-deep"
|
|
241
|
+
]).optional()
|
|
242
|
+
});
|
|
133
243
|
/**
|
|
134
244
|
* Full Claude Code MCP configuration schema
|
|
135
245
|
*/
|
|
136
|
-
const ClaudeCodeMcpConfigSchema = zod.z.object({
|
|
246
|
+
const ClaudeCodeMcpConfigSchema = zod.z.object({
|
|
247
|
+
mcpServers: zod.z.record(zod.z.string(), ClaudeCodeServerConfigSchema),
|
|
248
|
+
remoteConfigs: zod.z.array(RemoteConfigSourceSchema).optional()
|
|
249
|
+
});
|
|
137
250
|
/**
|
|
138
251
|
* Internal MCP config format
|
|
139
252
|
* This is the normalized format used internally by the proxy
|
|
@@ -242,6 +355,226 @@ function parseMcpConfig(rawConfig) {
|
|
|
242
355
|
return InternalMcpConfigSchema.parse(internalConfig);
|
|
243
356
|
}
|
|
244
357
|
|
|
358
|
+
//#endregion
|
|
359
|
+
//#region src/services/RemoteConfigCacheService.ts
|
|
360
|
+
/**
|
|
361
|
+
* RemoteConfigCacheService
|
|
362
|
+
*
|
|
363
|
+
* DESIGN PATTERNS:
|
|
364
|
+
* - Service pattern for cache management
|
|
365
|
+
* - Single responsibility principle
|
|
366
|
+
* - File-based caching with TTL support
|
|
367
|
+
*
|
|
368
|
+
* CODING STANDARDS:
|
|
369
|
+
* - Use async/await for asynchronous operations
|
|
370
|
+
* - Handle file system errors gracefully
|
|
371
|
+
* - Keep cache organized by URL hash
|
|
372
|
+
* - Implement automatic cache expiration
|
|
373
|
+
*
|
|
374
|
+
* AVOID:
|
|
375
|
+
* - Storing sensitive data in cache (headers with tokens)
|
|
376
|
+
* - Unbounded cache growth
|
|
377
|
+
* - Missing error handling for file operations
|
|
378
|
+
*/
|
|
379
|
+
/**
|
|
380
|
+
* Service for caching remote MCP configurations
|
|
381
|
+
*/
|
|
382
|
+
var RemoteConfigCacheService = class {
|
|
383
|
+
cacheDir;
|
|
384
|
+
cacheTTL;
|
|
385
|
+
readEnabled;
|
|
386
|
+
writeEnabled;
|
|
387
|
+
constructor(options) {
|
|
388
|
+
this.cacheDir = (0, node_path.join)((0, node_os.tmpdir)(), "one-mcp-cache", "remote-configs");
|
|
389
|
+
this.cacheTTL = options?.ttl || 3600 * 1e3;
|
|
390
|
+
this.readEnabled = options?.readEnabled !== void 0 ? options.readEnabled : true;
|
|
391
|
+
this.writeEnabled = options?.writeEnabled !== void 0 ? options.writeEnabled : true;
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Generate a hash key from remote config URL
|
|
395
|
+
* Only uses URL for hashing to avoid caching credentials in the key
|
|
396
|
+
*/
|
|
397
|
+
generateCacheKey(url) {
|
|
398
|
+
return (0, node_crypto.createHash)("sha256").update(url).digest("hex");
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Get the cache file path for a given cache key
|
|
402
|
+
*/
|
|
403
|
+
getCacheFilePath(cacheKey) {
|
|
404
|
+
return (0, node_path.join)(this.cacheDir, `${cacheKey}.json`);
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Initialize cache directory
|
|
408
|
+
* Uses mkdir with recursive option which handles existing directories gracefully
|
|
409
|
+
* (no TOCTOU race condition from existsSync check)
|
|
410
|
+
*/
|
|
411
|
+
async ensureCacheDir() {
|
|
412
|
+
try {
|
|
413
|
+
await (0, node_fs_promises.mkdir)(this.cacheDir, { recursive: true });
|
|
414
|
+
} catch (error) {
|
|
415
|
+
if (error?.code !== "EEXIST") throw error;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Get cached data for a remote config URL
|
|
420
|
+
*/
|
|
421
|
+
async get(url) {
|
|
422
|
+
if (!this.readEnabled) return null;
|
|
423
|
+
try {
|
|
424
|
+
await this.ensureCacheDir();
|
|
425
|
+
const cacheKey = this.generateCacheKey(url);
|
|
426
|
+
const cacheFilePath = this.getCacheFilePath(cacheKey);
|
|
427
|
+
if (!(0, node_fs.existsSync)(cacheFilePath)) return null;
|
|
428
|
+
const cacheContent = await (0, node_fs_promises.readFile)(cacheFilePath, "utf-8");
|
|
429
|
+
const cacheEntry = JSON.parse(cacheContent);
|
|
430
|
+
const now = Date.now();
|
|
431
|
+
if (now > cacheEntry.expiresAt) {
|
|
432
|
+
await (0, node_fs_promises.unlink)(cacheFilePath).catch(() => {});
|
|
433
|
+
return null;
|
|
434
|
+
}
|
|
435
|
+
const expiresInSeconds = Math.round((cacheEntry.expiresAt - now) / 1e3);
|
|
436
|
+
console.error(`Remote config cache hit for ${url} (expires in ${expiresInSeconds}s)`);
|
|
437
|
+
return cacheEntry.data;
|
|
438
|
+
} catch (error) {
|
|
439
|
+
console.error(`Failed to read remote config cache for ${url}:`, error);
|
|
440
|
+
return null;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Set cached data for a remote config URL
|
|
445
|
+
*/
|
|
446
|
+
async set(url, data) {
|
|
447
|
+
if (!this.writeEnabled) return;
|
|
448
|
+
try {
|
|
449
|
+
await this.ensureCacheDir();
|
|
450
|
+
const cacheKey = this.generateCacheKey(url);
|
|
451
|
+
const cacheFilePath = this.getCacheFilePath(cacheKey);
|
|
452
|
+
const now = Date.now();
|
|
453
|
+
const cacheEntry = {
|
|
454
|
+
data,
|
|
455
|
+
timestamp: now,
|
|
456
|
+
expiresAt: now + this.cacheTTL,
|
|
457
|
+
url
|
|
458
|
+
};
|
|
459
|
+
await (0, node_fs_promises.writeFile)(cacheFilePath, JSON.stringify(cacheEntry, null, 2), "utf-8");
|
|
460
|
+
console.error(`Cached remote config for ${url} (TTL: ${Math.round(this.cacheTTL / 1e3)}s)`);
|
|
461
|
+
} catch (error) {
|
|
462
|
+
console.error(`Failed to write remote config cache for ${url}:`, error);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* Clear cache for a specific URL
|
|
467
|
+
*/
|
|
468
|
+
async clear(url) {
|
|
469
|
+
try {
|
|
470
|
+
const cacheKey = this.generateCacheKey(url);
|
|
471
|
+
const cacheFilePath = this.getCacheFilePath(cacheKey);
|
|
472
|
+
if ((0, node_fs.existsSync)(cacheFilePath)) {
|
|
473
|
+
await (0, node_fs_promises.unlink)(cacheFilePath);
|
|
474
|
+
console.error(`Cleared remote config cache for ${url}`);
|
|
475
|
+
}
|
|
476
|
+
} catch (error) {
|
|
477
|
+
console.error(`Failed to clear remote config cache for ${url}:`, error);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* Clear all cached remote configs
|
|
482
|
+
*/
|
|
483
|
+
async clearAll() {
|
|
484
|
+
try {
|
|
485
|
+
if (!(0, node_fs.existsSync)(this.cacheDir)) return;
|
|
486
|
+
const files = await (0, node_fs_promises.readdir)(this.cacheDir);
|
|
487
|
+
const deletePromises = files.filter((file) => file.endsWith(".json")).map((file) => (0, node_fs_promises.unlink)((0, node_path.join)(this.cacheDir, file)).catch(() => {}));
|
|
488
|
+
await Promise.all(deletePromises);
|
|
489
|
+
console.error(`Cleared all remote config cache entries (${files.length} files)`);
|
|
490
|
+
} catch (error) {
|
|
491
|
+
console.error("Failed to clear all remote config cache:", error);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* Clean up expired cache entries
|
|
496
|
+
*/
|
|
497
|
+
async cleanExpired() {
|
|
498
|
+
try {
|
|
499
|
+
if (!(0, node_fs.existsSync)(this.cacheDir)) return;
|
|
500
|
+
const now = Date.now();
|
|
501
|
+
const files = await (0, node_fs_promises.readdir)(this.cacheDir);
|
|
502
|
+
let expiredCount = 0;
|
|
503
|
+
for (const file of files) {
|
|
504
|
+
if (!file.endsWith(".json")) continue;
|
|
505
|
+
const filePath = (0, node_path.join)(this.cacheDir, file);
|
|
506
|
+
try {
|
|
507
|
+
const content = await (0, node_fs_promises.readFile)(filePath, "utf-8");
|
|
508
|
+
if (now > JSON.parse(content).expiresAt) {
|
|
509
|
+
await (0, node_fs_promises.unlink)(filePath);
|
|
510
|
+
expiredCount++;
|
|
511
|
+
}
|
|
512
|
+
} catch (error) {
|
|
513
|
+
await (0, node_fs_promises.unlink)(filePath).catch(() => {});
|
|
514
|
+
expiredCount++;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
if (expiredCount > 0) console.error(`Cleaned up ${expiredCount} expired remote config cache entries`);
|
|
518
|
+
} catch (error) {
|
|
519
|
+
console.error("Failed to clean expired remote config cache:", error);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* Get cache statistics
|
|
524
|
+
*/
|
|
525
|
+
async getStats() {
|
|
526
|
+
try {
|
|
527
|
+
if (!(0, node_fs.existsSync)(this.cacheDir)) return {
|
|
528
|
+
totalEntries: 0,
|
|
529
|
+
totalSize: 0
|
|
530
|
+
};
|
|
531
|
+
const jsonFiles = (await (0, node_fs_promises.readdir)(this.cacheDir)).filter((file) => file.endsWith(".json"));
|
|
532
|
+
let totalSize = 0;
|
|
533
|
+
for (const file of jsonFiles) {
|
|
534
|
+
const filePath = (0, node_path.join)(this.cacheDir, file);
|
|
535
|
+
try {
|
|
536
|
+
const content = await (0, node_fs_promises.readFile)(filePath, "utf-8");
|
|
537
|
+
totalSize += Buffer.byteLength(content, "utf-8");
|
|
538
|
+
} catch {}
|
|
539
|
+
}
|
|
540
|
+
return {
|
|
541
|
+
totalEntries: jsonFiles.length,
|
|
542
|
+
totalSize
|
|
543
|
+
};
|
|
544
|
+
} catch (error) {
|
|
545
|
+
console.error("Failed to get remote config cache stats:", error);
|
|
546
|
+
return {
|
|
547
|
+
totalEntries: 0,
|
|
548
|
+
totalSize: 0
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* Check if read from cache is enabled
|
|
554
|
+
*/
|
|
555
|
+
isReadEnabled() {
|
|
556
|
+
return this.readEnabled;
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* Check if write to cache is enabled
|
|
560
|
+
*/
|
|
561
|
+
isWriteEnabled() {
|
|
562
|
+
return this.writeEnabled;
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* Set read enabled state
|
|
566
|
+
*/
|
|
567
|
+
setReadEnabled(enabled) {
|
|
568
|
+
this.readEnabled = enabled;
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Set write enabled state
|
|
572
|
+
*/
|
|
573
|
+
setWriteEnabled(enabled) {
|
|
574
|
+
this.writeEnabled = enabled;
|
|
575
|
+
}
|
|
576
|
+
};
|
|
577
|
+
|
|
245
578
|
//#endregion
|
|
246
579
|
//#region src/services/ConfigFetcherService.ts
|
|
247
580
|
/**
|
|
@@ -263,35 +596,62 @@ function parseMcpConfig(rawConfig) {
|
|
|
263
596
|
* - Direct tool implementation (services should be tool-agnostic)
|
|
264
597
|
*/
|
|
265
598
|
/**
|
|
266
|
-
* Service for fetching and caching MCP server configurations from local file
|
|
599
|
+
* Service for fetching and caching MCP server configurations from local file and remote sources
|
|
600
|
+
* Supports merging multiple remote configs with local config
|
|
267
601
|
*/
|
|
268
602
|
var ConfigFetcherService = class {
|
|
269
603
|
configFilePath;
|
|
270
604
|
cacheTtlMs;
|
|
271
605
|
cachedConfig = null;
|
|
272
606
|
lastFetchTime = 0;
|
|
607
|
+
remoteConfigCache;
|
|
273
608
|
constructor(options) {
|
|
274
609
|
this.configFilePath = options.configFilePath;
|
|
275
610
|
this.cacheTtlMs = options.cacheTtlMs || 6e4;
|
|
611
|
+
const useCache = options.useCache !== void 0 ? options.useCache : true;
|
|
612
|
+
this.remoteConfigCache = new RemoteConfigCacheService({
|
|
613
|
+
ttl: options.remoteCacheTtlMs || 3600 * 1e3,
|
|
614
|
+
readEnabled: useCache,
|
|
615
|
+
writeEnabled: true
|
|
616
|
+
});
|
|
276
617
|
if (!this.configFilePath) throw new Error("configFilePath must be provided");
|
|
277
618
|
}
|
|
278
619
|
/**
|
|
279
|
-
* Fetch MCP configuration from local file with caching
|
|
620
|
+
* Fetch MCP configuration from local file and remote sources with caching
|
|
621
|
+
* Merges remote configs with local config based on merge strategy
|
|
280
622
|
* @param forceRefresh - Force reload from source, bypassing cache
|
|
281
623
|
*/
|
|
282
624
|
async fetchConfiguration(forceRefresh = false) {
|
|
283
625
|
const now = Date.now();
|
|
284
626
|
if (!forceRefresh && this.cachedConfig && now - this.lastFetchTime < this.cacheTtlMs) return this.cachedConfig;
|
|
285
|
-
const
|
|
286
|
-
|
|
287
|
-
|
|
627
|
+
const localConfigData = await this.loadRawConfigFromFile();
|
|
628
|
+
const remoteConfigSources = localConfigData.remoteConfigs || [];
|
|
629
|
+
let mergedConfig = await this.parseConfig(localConfigData);
|
|
630
|
+
const remoteConfigPromises = remoteConfigSources.map(async (remoteSource) => {
|
|
631
|
+
try {
|
|
632
|
+
validateRemoteConfigSource(remoteSource);
|
|
633
|
+
return {
|
|
634
|
+
config: await this.loadFromUrl(remoteSource),
|
|
635
|
+
mergeStrategy: remoteSource.mergeStrategy || "local-priority",
|
|
636
|
+
url: remoteSource.url
|
|
637
|
+
};
|
|
638
|
+
} catch (error) {
|
|
639
|
+
if (error instanceof Error) console.error(`Failed to fetch remote config from ${remoteSource.url}: ${error.message}`);
|
|
640
|
+
return null;
|
|
641
|
+
}
|
|
642
|
+
});
|
|
643
|
+
const remoteConfigResults = await Promise.all(remoteConfigPromises);
|
|
644
|
+
for (const result of remoteConfigResults) if (result !== null) mergedConfig = this.mergeConfigurations(mergedConfig, result.config, result.mergeStrategy);
|
|
645
|
+
if (!mergedConfig.mcpServers || typeof mergedConfig.mcpServers !== "object") throw new Error("Invalid MCP configuration: missing or invalid mcpServers");
|
|
646
|
+
this.cachedConfig = mergedConfig;
|
|
288
647
|
this.lastFetchTime = now;
|
|
289
|
-
return
|
|
648
|
+
return mergedConfig;
|
|
290
649
|
}
|
|
291
650
|
/**
|
|
292
|
-
* Load configuration from a local file (supports JSON and YAML)
|
|
651
|
+
* Load raw configuration data from a local file (supports JSON and YAML)
|
|
652
|
+
* Returns unparsed config data to allow access to remoteConfigs
|
|
293
653
|
*/
|
|
294
|
-
async
|
|
654
|
+
async loadRawConfigFromFile() {
|
|
295
655
|
if (!this.configFilePath) throw new Error("No config file path provided");
|
|
296
656
|
if (!(0, node_fs.existsSync)(this.configFilePath)) throw new Error(`Config file not found: ${this.configFilePath}`);
|
|
297
657
|
try {
|
|
@@ -299,13 +659,117 @@ var ConfigFetcherService = class {
|
|
|
299
659
|
let rawConfig;
|
|
300
660
|
if (this.configFilePath.endsWith(".yaml") || this.configFilePath.endsWith(".yml")) rawConfig = js_yaml.default.load(content);
|
|
301
661
|
else rawConfig = JSON.parse(content);
|
|
302
|
-
return
|
|
662
|
+
return rawConfig;
|
|
303
663
|
} catch (error) {
|
|
304
664
|
if (error instanceof Error) throw new Error(`Failed to load config file: ${error.message}`);
|
|
305
665
|
throw new Error("Failed to load config file: Unknown error");
|
|
306
666
|
}
|
|
307
667
|
}
|
|
308
668
|
/**
|
|
669
|
+
* Parse raw config data using Zod schema
|
|
670
|
+
* Filters out remoteConfigs to avoid including them in the final config
|
|
671
|
+
*/
|
|
672
|
+
async parseConfig(rawConfig) {
|
|
673
|
+
try {
|
|
674
|
+
const { remoteConfigs, ...configWithoutRemote } = rawConfig;
|
|
675
|
+
return parseMcpConfig(configWithoutRemote);
|
|
676
|
+
} catch (error) {
|
|
677
|
+
if (error instanceof Error) throw new Error(`Failed to parse config: ${error.message}`);
|
|
678
|
+
throw new Error("Failed to parse config: Unknown error");
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
/**
|
|
682
|
+
* Load configuration from a remote URL with caching
|
|
683
|
+
*
|
|
684
|
+
* SECURITY NOTE: This method fetches remote configs based on URLs from the local config file.
|
|
685
|
+
* This is intentional and safe because:
|
|
686
|
+
* 1. URLs are user-controlled via their local config file (not external input)
|
|
687
|
+
* 2. SSRF protection validates URLs before fetching (blocks private IPs, enforces HTTPS)
|
|
688
|
+
* 3. Users explicitly opt-in to remote configs in their local configuration
|
|
689
|
+
* 4. This enables centralized config management (intended feature, not a vulnerability)
|
|
690
|
+
*
|
|
691
|
+
* CodeQL alert "file-access-to-http" is a false positive here - we're not leaking
|
|
692
|
+
* file contents to arbitrary URLs, we're fetching configs from user-specified sources.
|
|
693
|
+
*/
|
|
694
|
+
async loadFromUrl(source) {
|
|
695
|
+
try {
|
|
696
|
+
const interpolatedUrl = this.interpolateEnvVars(source.url);
|
|
697
|
+
const cachedConfig = await this.remoteConfigCache.get(interpolatedUrl);
|
|
698
|
+
if (cachedConfig) return cachedConfig;
|
|
699
|
+
const interpolatedHeaders = source.headers ? Object.fromEntries(Object.entries(source.headers).map(([key, value]) => [key, this.interpolateEnvVars(value)])) : {};
|
|
700
|
+
const response = await fetch(interpolatedUrl, { headers: interpolatedHeaders });
|
|
701
|
+
if (!response.ok) throw new Error(`Failed to fetch remote config: ${response.status} ${response.statusText}`);
|
|
702
|
+
const config = parseMcpConfig(await response.json());
|
|
703
|
+
await this.remoteConfigCache.set(interpolatedUrl, config);
|
|
704
|
+
return config;
|
|
705
|
+
} catch (error) {
|
|
706
|
+
if (error instanceof Error) throw new Error(`Failed to fetch remote config from ${source.url}: ${error.message}`);
|
|
707
|
+
throw new Error(`Failed to fetch remote config from ${source.url}: Unknown error`);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
/**
|
|
711
|
+
* Interpolate environment variables in a string
|
|
712
|
+
* Supports ${VAR_NAME} syntax
|
|
713
|
+
*/
|
|
714
|
+
interpolateEnvVars(value) {
|
|
715
|
+
return value.replace(/\$\{([^}]+)\}/g, (_, varName) => {
|
|
716
|
+
const envValue = process.env[varName];
|
|
717
|
+
if (envValue === void 0) {
|
|
718
|
+
console.warn(`Environment variable ${varName} is not defined, keeping placeholder`);
|
|
719
|
+
return `\${${varName}}`;
|
|
720
|
+
}
|
|
721
|
+
return envValue;
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
/**
|
|
725
|
+
* Merge two MCP configurations based on the specified merge strategy
|
|
726
|
+
* @param localConfig Configuration loaded from local file
|
|
727
|
+
* @param remoteConfig Configuration loaded from remote URL
|
|
728
|
+
* @param mergeStrategy Strategy for merging configs
|
|
729
|
+
* @returns Merged configuration
|
|
730
|
+
*/
|
|
731
|
+
mergeConfigurations(localConfig, remoteConfig, mergeStrategy) {
|
|
732
|
+
switch (mergeStrategy) {
|
|
733
|
+
case "local-priority": return { mcpServers: {
|
|
734
|
+
...remoteConfig.mcpServers,
|
|
735
|
+
...localConfig.mcpServers
|
|
736
|
+
} };
|
|
737
|
+
case "remote-priority": return { mcpServers: {
|
|
738
|
+
...localConfig.mcpServers,
|
|
739
|
+
...remoteConfig.mcpServers
|
|
740
|
+
} };
|
|
741
|
+
case "merge-deep": {
|
|
742
|
+
const merged = { ...remoteConfig.mcpServers };
|
|
743
|
+
for (const [serverName, localServerConfig] of Object.entries(localConfig.mcpServers)) if (merged[serverName]) {
|
|
744
|
+
const remoteServer = merged[serverName];
|
|
745
|
+
const mergedConfig = {
|
|
746
|
+
...remoteServer.config,
|
|
747
|
+
...localServerConfig.config
|
|
748
|
+
};
|
|
749
|
+
const remoteEnv = "env" in remoteServer.config ? remoteServer.config.env : void 0;
|
|
750
|
+
const localEnv = "env" in localServerConfig.config ? localServerConfig.config.env : void 0;
|
|
751
|
+
if (remoteEnv || localEnv) mergedConfig.env = {
|
|
752
|
+
...remoteEnv || {},
|
|
753
|
+
...localEnv || {}
|
|
754
|
+
};
|
|
755
|
+
const remoteHeaders = "headers" in remoteServer.config ? remoteServer.config.headers : void 0;
|
|
756
|
+
const localHeaders = "headers" in localServerConfig.config ? localServerConfig.config.headers : void 0;
|
|
757
|
+
if (remoteHeaders || localHeaders) mergedConfig.headers = {
|
|
758
|
+
...remoteHeaders || {},
|
|
759
|
+
...localHeaders || {}
|
|
760
|
+
};
|
|
761
|
+
merged[serverName] = {
|
|
762
|
+
...remoteServer,
|
|
763
|
+
...localServerConfig,
|
|
764
|
+
config: mergedConfig
|
|
765
|
+
};
|
|
766
|
+
} else merged[serverName] = localServerConfig;
|
|
767
|
+
return { mcpServers: merged };
|
|
768
|
+
}
|
|
769
|
+
default: throw new Error(`Unknown merge strategy: ${mergeStrategy}`);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
/**
|
|
309
773
|
* Clear the cached configuration
|
|
310
774
|
*/
|
|
311
775
|
clearCache() {
|
|
@@ -512,6 +976,75 @@ var McpClientManagerService = class {
|
|
|
512
976
|
}
|
|
513
977
|
};
|
|
514
978
|
|
|
979
|
+
//#endregion
|
|
980
|
+
//#region src/utils/findConfigFile.ts
|
|
981
|
+
/**
|
|
982
|
+
* Config File Finder Utility
|
|
983
|
+
*
|
|
984
|
+
* DESIGN PATTERNS:
|
|
985
|
+
* - Utility function pattern for reusable logic
|
|
986
|
+
* - Fail-fast pattern with early returns
|
|
987
|
+
* - Environment variable configuration pattern
|
|
988
|
+
*
|
|
989
|
+
* CODING STANDARDS:
|
|
990
|
+
* - Use sync filesystem operations for config discovery (performance)
|
|
991
|
+
* - Check PROJECT_PATH environment variable first
|
|
992
|
+
* - Fall back to current working directory
|
|
993
|
+
* - Support both .yaml and .json extensions
|
|
994
|
+
* - Return null if no config file is found
|
|
995
|
+
*
|
|
996
|
+
* AVOID:
|
|
997
|
+
* - Throwing errors (return null instead for optional config)
|
|
998
|
+
* - Hardcoded file names without extension variants
|
|
999
|
+
* - Ignoring environment variables
|
|
1000
|
+
*/
|
|
1001
|
+
/**
|
|
1002
|
+
* Find MCP configuration file by checking PROJECT_PATH first, then cwd
|
|
1003
|
+
* Looks for both mcp-config.yaml and mcp-config.json
|
|
1004
|
+
*
|
|
1005
|
+
* @returns Absolute path to config file, or null if not found
|
|
1006
|
+
*/
|
|
1007
|
+
function findConfigFile() {
|
|
1008
|
+
const configFileNames = [
|
|
1009
|
+
"mcp-config.yaml",
|
|
1010
|
+
"mcp-config.yml",
|
|
1011
|
+
"mcp-config.json"
|
|
1012
|
+
];
|
|
1013
|
+
const projectPath = process.env.PROJECT_PATH;
|
|
1014
|
+
if (projectPath) for (const fileName of configFileNames) {
|
|
1015
|
+
const configPath = (0, node_path.resolve)(projectPath, fileName);
|
|
1016
|
+
if ((0, node_fs.existsSync)(configPath)) return configPath;
|
|
1017
|
+
}
|
|
1018
|
+
const cwd = process.cwd();
|
|
1019
|
+
for (const fileName of configFileNames) {
|
|
1020
|
+
const configPath = (0, node_path.join)(cwd, fileName);
|
|
1021
|
+
if ((0, node_fs.existsSync)(configPath)) return configPath;
|
|
1022
|
+
}
|
|
1023
|
+
return null;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
//#endregion
|
|
1027
|
+
//#region src/utils/parseToolName.ts
|
|
1028
|
+
/**
|
|
1029
|
+
* Parse tool name to extract server and actual tool name
|
|
1030
|
+
* Supports both plain tool names and prefixed format: {serverName}__{toolName}
|
|
1031
|
+
*
|
|
1032
|
+
* @param toolName - The tool name to parse (e.g., "my_tool" or "server__my_tool")
|
|
1033
|
+
* @returns Parsed result with optional serverName and actualToolName
|
|
1034
|
+
*
|
|
1035
|
+
* @example
|
|
1036
|
+
* parseToolName("my_tool") // { actualToolName: "my_tool" }
|
|
1037
|
+
* parseToolName("server__my_tool") // { serverName: "server", actualToolName: "my_tool" }
|
|
1038
|
+
*/
|
|
1039
|
+
function parseToolName(toolName) {
|
|
1040
|
+
const separatorIndex = toolName.indexOf("__");
|
|
1041
|
+
if (separatorIndex > 0) return {
|
|
1042
|
+
serverName: toolName.substring(0, separatorIndex),
|
|
1043
|
+
actualToolName: toolName.substring(separatorIndex + 2)
|
|
1044
|
+
};
|
|
1045
|
+
return { actualToolName: toolName };
|
|
1046
|
+
}
|
|
1047
|
+
|
|
515
1048
|
//#endregion
|
|
516
1049
|
//#region src/tools/DescribeToolsTool.ts
|
|
517
1050
|
var DescribeToolsTool = class DescribeToolsTool {
|
|
@@ -522,46 +1055,50 @@ var DescribeToolsTool = class DescribeToolsTool {
|
|
|
522
1055
|
}
|
|
523
1056
|
async getDefinition() {
|
|
524
1057
|
const clients = this.clientManager.getAllClients();
|
|
525
|
-
const
|
|
1058
|
+
const toolToServers = /* @__PURE__ */ new Map();
|
|
1059
|
+
const serverToolsMap = /* @__PURE__ */ new Map();
|
|
1060
|
+
await Promise.all(clients.map(async (client) => {
|
|
526
1061
|
try {
|
|
527
1062
|
const tools = await client.listTools();
|
|
528
1063
|
const blacklist = new Set(client.toolBlacklist || []);
|
|
529
1064
|
const filteredTools = tools.filter((t) => !blacklist.has(t.name));
|
|
530
|
-
|
|
531
|
-
const
|
|
532
|
-
|
|
1065
|
+
serverToolsMap.set(client.serverName, filteredTools);
|
|
1066
|
+
for (const tool of filteredTools) {
|
|
1067
|
+
if (!toolToServers.has(tool.name)) toolToServers.set(tool.name, []);
|
|
1068
|
+
toolToServers.get(tool.name).push(client.serverName);
|
|
1069
|
+
}
|
|
533
1070
|
} catch (error) {
|
|
534
1071
|
console.error(`Failed to list tools from ${client.serverName}:`, error);
|
|
535
|
-
|
|
536
|
-
return `\n\n**Server: ${client.serverName}**${instructionLine}\n`;
|
|
1072
|
+
serverToolsMap.set(client.serverName, []);
|
|
537
1073
|
}
|
|
538
1074
|
}));
|
|
1075
|
+
const serverDescriptions = clients.map((client) => {
|
|
1076
|
+
const tools = serverToolsMap.get(client.serverName) || [];
|
|
1077
|
+
const formatToolName = (toolName) => {
|
|
1078
|
+
return (toolToServers.get(toolName) || []).length > 1 ? `${client.serverName}__${toolName}` : toolName;
|
|
1079
|
+
};
|
|
1080
|
+
const toolList = client.omitToolDescription ? tools.map((t) => formatToolName(t.name)).join(", ") : tools.map((t) => `${formatToolName(t.name)}: """${t.description || "No description"}"""`).join("\n");
|
|
1081
|
+
const instructionLine = client.serverInstruction ? `\n"""${client.serverInstruction}"""` : "";
|
|
1082
|
+
return `\n\n### Server: ${client.serverName}${instructionLine}\n\n- Available tools:\n${toolList || "No tools available"}`;
|
|
1083
|
+
});
|
|
539
1084
|
return {
|
|
540
1085
|
name: DescribeToolsTool.TOOL_NAME,
|
|
541
|
-
description:
|
|
542
|
-
|
|
543
|
-
## Available MCP Servers:${serverDescriptions.join("")}
|
|
1086
|
+
description: `## Available MCP Servers:${serverDescriptions.join("")}
|
|
544
1087
|
|
|
545
1088
|
## Usage:
|
|
546
|
-
|
|
1089
|
+
Before you use any tools above, you MUST call this tool with a list of tool names to learn how to use them properly before use_tool; this includes:
|
|
547
1090
|
- Arguments schema needed to pass to the tool use
|
|
548
1091
|
- Description about each tool
|
|
549
1092
|
|
|
550
1093
|
This tool is optimized for batch queries - you can request multiple tools at once for better performance.`,
|
|
551
1094
|
inputSchema: {
|
|
552
1095
|
type: "object",
|
|
553
|
-
properties: {
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
},
|
|
560
|
-
serverName: {
|
|
561
|
-
type: "string",
|
|
562
|
-
description: "Optional server name to search within. If not specified, searches all servers."
|
|
563
|
-
}
|
|
564
|
-
},
|
|
1096
|
+
properties: { toolNames: {
|
|
1097
|
+
type: "array",
|
|
1098
|
+
items: { type: "string" },
|
|
1099
|
+
description: "List of tool names to get detailed information about",
|
|
1100
|
+
minItems: 1
|
|
1101
|
+
} },
|
|
565
1102
|
required: ["toolNames"],
|
|
566
1103
|
additionalProperties: false
|
|
567
1104
|
}
|
|
@@ -569,7 +1106,7 @@ This tool is optimized for batch queries - you can request multiple tools at onc
|
|
|
569
1106
|
}
|
|
570
1107
|
async execute(input) {
|
|
571
1108
|
try {
|
|
572
|
-
const { toolNames
|
|
1109
|
+
const { toolNames } = input;
|
|
573
1110
|
const clients = this.clientManager.getAllClients();
|
|
574
1111
|
if (!toolNames || toolNames.length === 0) return {
|
|
575
1112
|
content: [{
|
|
@@ -578,127 +1115,85 @@ This tool is optimized for batch queries - you can request multiple tools at onc
|
|
|
578
1115
|
}],
|
|
579
1116
|
isError: true
|
|
580
1117
|
};
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
content: [{
|
|
585
|
-
type: "text",
|
|
586
|
-
text: `Server "${serverName}" not found. Available servers: ${clients.map((c) => c.serverName).join(", ")}`
|
|
587
|
-
}],
|
|
588
|
-
isError: true
|
|
589
|
-
};
|
|
590
|
-
const tools = await client.listTools();
|
|
591
|
-
const blacklist = new Set(client.toolBlacklist || []);
|
|
592
|
-
const filteredTools = tools.filter((t) => !blacklist.has(t.name));
|
|
593
|
-
const foundTools$1 = [];
|
|
594
|
-
const notFoundTools$1 = [];
|
|
595
|
-
for (const toolName of toolNames) {
|
|
596
|
-
if (blacklist.has(toolName)) {
|
|
597
|
-
notFoundTools$1.push(toolName);
|
|
598
|
-
continue;
|
|
599
|
-
}
|
|
600
|
-
const tool = filteredTools.find((t) => t.name === toolName);
|
|
601
|
-
if (tool) foundTools$1.push({
|
|
602
|
-
server: serverName,
|
|
603
|
-
tool: {
|
|
604
|
-
name: tool.name,
|
|
605
|
-
description: tool.description,
|
|
606
|
-
inputSchema: tool.inputSchema
|
|
607
|
-
}
|
|
608
|
-
});
|
|
609
|
-
else notFoundTools$1.push(toolName);
|
|
610
|
-
}
|
|
611
|
-
if (foundTools$1.length === 0) return {
|
|
612
|
-
content: [{
|
|
613
|
-
type: "text",
|
|
614
|
-
text: `None of the requested tools found on server "${serverName}".\nRequested: ${toolNames.join(", ")}\nAvailable tools: ${tools.map((t) => t.name).join(", ")}`
|
|
615
|
-
}],
|
|
616
|
-
isError: true
|
|
617
|
-
};
|
|
618
|
-
const result$1 = { tools: foundTools$1 };
|
|
619
|
-
if (notFoundTools$1.length > 0) {
|
|
620
|
-
result$1.notFound = notFoundTools$1;
|
|
621
|
-
result$1.warning = `Some tools were not found on server "${serverName}": ${notFoundTools$1.join(", ")}`;
|
|
622
|
-
}
|
|
623
|
-
return { content: [{
|
|
624
|
-
type: "text",
|
|
625
|
-
text: JSON.stringify(result$1, null, 2)
|
|
626
|
-
}] };
|
|
627
|
-
}
|
|
628
|
-
const foundTools = [];
|
|
629
|
-
const notFoundTools = [...toolNames];
|
|
630
|
-
const toolMatches = /* @__PURE__ */ new Map();
|
|
631
|
-
const results = await Promise.all(clients.map(async (client) => {
|
|
1118
|
+
const serverToolsMap = /* @__PURE__ */ new Map();
|
|
1119
|
+
const toolToServers = /* @__PURE__ */ new Map();
|
|
1120
|
+
await Promise.all(clients.map(async (client) => {
|
|
632
1121
|
try {
|
|
633
1122
|
const tools = await client.listTools();
|
|
634
1123
|
const blacklist = new Set(client.toolBlacklist || []);
|
|
635
1124
|
const filteredTools = tools.filter((t) => !blacklist.has(t.name));
|
|
636
|
-
|
|
637
|
-
for (const
|
|
638
|
-
if (
|
|
639
|
-
|
|
640
|
-
if (tool) matches.push({
|
|
641
|
-
toolName,
|
|
642
|
-
server: client.serverName,
|
|
643
|
-
tool
|
|
644
|
-
});
|
|
1125
|
+
serverToolsMap.set(client.serverName, filteredTools);
|
|
1126
|
+
for (const tool of filteredTools) {
|
|
1127
|
+
if (!toolToServers.has(tool.name)) toolToServers.set(tool.name, []);
|
|
1128
|
+
toolToServers.get(tool.name).push(client.serverName);
|
|
645
1129
|
}
|
|
646
|
-
return matches;
|
|
647
1130
|
} catch (error) {
|
|
648
1131
|
console.error(`Failed to list tools from ${client.serverName}:`, error);
|
|
649
|
-
|
|
1132
|
+
serverToolsMap.set(client.serverName, []);
|
|
650
1133
|
}
|
|
651
1134
|
}));
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
foundTools.push({
|
|
666
|
-
server: match.server,
|
|
1135
|
+
const foundTools = [];
|
|
1136
|
+
const notFoundTools = [];
|
|
1137
|
+
for (const requestedToolName of toolNames) {
|
|
1138
|
+
const { serverName, actualToolName } = parseToolName(requestedToolName);
|
|
1139
|
+
if (serverName) {
|
|
1140
|
+
const serverTools = serverToolsMap.get(serverName);
|
|
1141
|
+
if (!serverTools) {
|
|
1142
|
+
notFoundTools.push(requestedToolName);
|
|
1143
|
+
continue;
|
|
1144
|
+
}
|
|
1145
|
+
const tool = serverTools.find((t) => t.name === actualToolName);
|
|
1146
|
+
if (tool) foundTools.push({
|
|
1147
|
+
server: serverName,
|
|
667
1148
|
tool: {
|
|
668
|
-
name:
|
|
669
|
-
description:
|
|
670
|
-
inputSchema:
|
|
1149
|
+
name: tool.name,
|
|
1150
|
+
description: tool.description,
|
|
1151
|
+
inputSchema: tool.inputSchema
|
|
671
1152
|
}
|
|
672
1153
|
});
|
|
673
|
-
|
|
674
|
-
if (idx > -1) notFoundTools.splice(idx, 1);
|
|
1154
|
+
else notFoundTools.push(requestedToolName);
|
|
675
1155
|
} else {
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
if (
|
|
1156
|
+
const servers = toolToServers.get(actualToolName);
|
|
1157
|
+
if (!servers || servers.length === 0) {
|
|
1158
|
+
notFoundTools.push(requestedToolName);
|
|
1159
|
+
continue;
|
|
1160
|
+
}
|
|
1161
|
+
if (servers.length === 1) {
|
|
1162
|
+
const server = servers[0];
|
|
1163
|
+
const tool = serverToolsMap.get(server).find((t) => t.name === actualToolName);
|
|
1164
|
+
foundTools.push({
|
|
1165
|
+
server,
|
|
1166
|
+
tool: {
|
|
1167
|
+
name: tool.name,
|
|
1168
|
+
description: tool.description,
|
|
1169
|
+
inputSchema: tool.inputSchema
|
|
1170
|
+
}
|
|
1171
|
+
});
|
|
1172
|
+
} else for (const server of servers) {
|
|
1173
|
+
const tool = serverToolsMap.get(server).find((t) => t.name === actualToolName);
|
|
1174
|
+
foundTools.push({
|
|
1175
|
+
server,
|
|
1176
|
+
tool: {
|
|
1177
|
+
name: tool.name,
|
|
1178
|
+
description: tool.description,
|
|
1179
|
+
inputSchema: tool.inputSchema
|
|
1180
|
+
}
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
682
1183
|
}
|
|
683
1184
|
}
|
|
684
|
-
if (foundTools.length === 0
|
|
1185
|
+
if (foundTools.length === 0) return {
|
|
685
1186
|
content: [{
|
|
686
1187
|
type: "text",
|
|
687
|
-
text: `None of the requested tools found on any connected server.\nRequested: ${toolNames.join(", ")}\nUse describe_tools
|
|
1188
|
+
text: `None of the requested tools found on any connected server.\nRequested: ${toolNames.join(", ")}\nUse describe_tools to see available tools.`
|
|
688
1189
|
}],
|
|
689
1190
|
isError: true
|
|
690
1191
|
};
|
|
691
1192
|
const result = { tools: foundTools };
|
|
692
|
-
if (notFoundTools.length > 0)
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
message: `Tool "${item.toolName}" found on multiple servers: ${item.servers.join(", ")}. Please specify serverName to disambiguate.`
|
|
697
|
-
}));
|
|
698
|
-
const warnings = [];
|
|
699
|
-
if (notFoundTools.length > 0) warnings.push(`Tools not found: ${notFoundTools.join(", ")}`);
|
|
700
|
-
if (ambiguousTools.length > 0) warnings.push(`Ambiguous tools (specify serverName): ${ambiguousTools.map((t) => t.toolName).join(", ")}`);
|
|
701
|
-
if (warnings.length > 0) result.warnings = warnings;
|
|
1193
|
+
if (notFoundTools.length > 0) {
|
|
1194
|
+
result.notFound = notFoundTools;
|
|
1195
|
+
result.warnings = [`Tools not found: ${notFoundTools.join(", ")}`];
|
|
1196
|
+
}
|
|
702
1197
|
return { content: [{
|
|
703
1198
|
type: "text",
|
|
704
1199
|
text: JSON.stringify(result, null, 2)
|
|
@@ -740,10 +1235,6 @@ var UseToolTool = class UseToolTool {
|
|
|
740
1235
|
toolArgs: {
|
|
741
1236
|
type: "object",
|
|
742
1237
|
description: "Arguments to pass to the tool, as discovered from describe_tools"
|
|
743
|
-
},
|
|
744
|
-
serverName: {
|
|
745
|
-
type: "string",
|
|
746
|
-
description: "Optional server name to disambiguate when multiple servers have the same tool"
|
|
747
1238
|
}
|
|
748
1239
|
},
|
|
749
1240
|
required: ["toolName"],
|
|
@@ -753,8 +1244,9 @@ var UseToolTool = class UseToolTool {
|
|
|
753
1244
|
}
|
|
754
1245
|
async execute(input) {
|
|
755
1246
|
try {
|
|
756
|
-
const { toolName, toolArgs = {}
|
|
1247
|
+
const { toolName: inputToolName, toolArgs = {} } = input;
|
|
757
1248
|
const clients = this.clientManager.getAllClients();
|
|
1249
|
+
const { serverName, actualToolName } = parseToolName(inputToolName);
|
|
758
1250
|
if (serverName) {
|
|
759
1251
|
const client$1 = this.clientManager.getClient(serverName);
|
|
760
1252
|
if (!client$1) return {
|
|
@@ -764,20 +1256,20 @@ var UseToolTool = class UseToolTool {
|
|
|
764
1256
|
}],
|
|
765
1257
|
isError: true
|
|
766
1258
|
};
|
|
767
|
-
if (client$1.toolBlacklist && client$1.toolBlacklist.includes(
|
|
1259
|
+
if (client$1.toolBlacklist && client$1.toolBlacklist.includes(actualToolName)) return {
|
|
768
1260
|
content: [{
|
|
769
1261
|
type: "text",
|
|
770
|
-
text: `Tool "${
|
|
1262
|
+
text: `Tool "${actualToolName}" is blacklisted on server "${serverName}" and cannot be executed.`
|
|
771
1263
|
}],
|
|
772
1264
|
isError: true
|
|
773
1265
|
};
|
|
774
1266
|
try {
|
|
775
|
-
return await client$1.callTool(
|
|
1267
|
+
return await client$1.callTool(actualToolName, toolArgs);
|
|
776
1268
|
} catch (error) {
|
|
777
1269
|
return {
|
|
778
1270
|
content: [{
|
|
779
1271
|
type: "text",
|
|
780
|
-
text: `Failed to call tool "${
|
|
1272
|
+
text: `Failed to call tool "${actualToolName}" on server "${serverName}": ${error instanceof Error ? error.message : "Unknown error"}`
|
|
781
1273
|
}],
|
|
782
1274
|
isError: true
|
|
783
1275
|
};
|
|
@@ -786,8 +1278,8 @@ var UseToolTool = class UseToolTool {
|
|
|
786
1278
|
const matchingServers = [];
|
|
787
1279
|
const results = await Promise.all(clients.map(async (client$1) => {
|
|
788
1280
|
try {
|
|
789
|
-
if (client$1.toolBlacklist && client$1.toolBlacklist.includes(
|
|
790
|
-
if ((await client$1.listTools()).some((t) => t.name ===
|
|
1281
|
+
if (client$1.toolBlacklist && client$1.toolBlacklist.includes(actualToolName)) return null;
|
|
1282
|
+
if ((await client$1.listTools()).some((t) => t.name === actualToolName)) return client$1.serverName;
|
|
791
1283
|
} catch (error) {
|
|
792
1284
|
console.error(`Failed to list tools from ${client$1.serverName}:`, error);
|
|
793
1285
|
}
|
|
@@ -797,14 +1289,14 @@ var UseToolTool = class UseToolTool {
|
|
|
797
1289
|
if (matchingServers.length === 0) return {
|
|
798
1290
|
content: [{
|
|
799
1291
|
type: "text",
|
|
800
|
-
text: `Tool "${
|
|
1292
|
+
text: `Tool "${actualToolName}" not found on any connected server. Use describe_tools to see available tools.`
|
|
801
1293
|
}],
|
|
802
1294
|
isError: true
|
|
803
1295
|
};
|
|
804
1296
|
if (matchingServers.length > 1) return {
|
|
805
1297
|
content: [{
|
|
806
1298
|
type: "text",
|
|
807
|
-
text: `
|
|
1299
|
+
text: `Tool "${actualToolName}" found on multiple servers. Use prefixed format to specify: ${matchingServers.map((s) => `${s}__${actualToolName}`).join(", ")}`
|
|
808
1300
|
}],
|
|
809
1301
|
isError: true
|
|
810
1302
|
};
|
|
@@ -818,12 +1310,12 @@ var UseToolTool = class UseToolTool {
|
|
|
818
1310
|
isError: true
|
|
819
1311
|
};
|
|
820
1312
|
try {
|
|
821
|
-
return await client.callTool(
|
|
1313
|
+
return await client.callTool(actualToolName, toolArgs);
|
|
822
1314
|
} catch (error) {
|
|
823
1315
|
return {
|
|
824
1316
|
content: [{
|
|
825
1317
|
type: "text",
|
|
826
|
-
text: `Failed to call tool "${
|
|
1318
|
+
text: `Failed to call tool "${actualToolName}": ${error instanceof Error ? error.message : "Unknown error"}`
|
|
827
1319
|
}],
|
|
828
1320
|
isError: true
|
|
829
1321
|
};
|
|
@@ -862,7 +1354,10 @@ async function createServer(options) {
|
|
|
862
1354
|
}, { capabilities: { tools: {} } });
|
|
863
1355
|
const clientManager = new McpClientManagerService();
|
|
864
1356
|
if (options?.configFilePath) try {
|
|
865
|
-
const config = await new ConfigFetcherService({
|
|
1357
|
+
const config = await new ConfigFetcherService({
|
|
1358
|
+
configFilePath: options.configFilePath,
|
|
1359
|
+
useCache: !options.noCache
|
|
1360
|
+
}).fetchConfiguration(options.noCache || false);
|
|
866
1361
|
const connectionPromises = Object.entries(config.mcpServers).map(async ([serverName, serverConfig]) => {
|
|
867
1362
|
try {
|
|
868
1363
|
await clientManager.connectToServer(serverName, serverConfig);
|
|
@@ -1015,14 +1510,14 @@ var SseTransportHandler = class {
|
|
|
1015
1510
|
}
|
|
1016
1511
|
}
|
|
1017
1512
|
async start() {
|
|
1018
|
-
return new Promise((resolve, reject) => {
|
|
1513
|
+
return new Promise((resolve$1, reject) => {
|
|
1019
1514
|
try {
|
|
1020
1515
|
this.server = this.app.listen(this.config.port, this.config.host, () => {
|
|
1021
1516
|
console.error(`@agiflowai/one-mcp MCP server started with SSE transport on http://${this.config.host}:${this.config.port}`);
|
|
1022
1517
|
console.error(`SSE endpoint: http://${this.config.host}:${this.config.port}/sse`);
|
|
1023
1518
|
console.error(`Messages endpoint: http://${this.config.host}:${this.config.port}/messages`);
|
|
1024
1519
|
console.error(`Health check: http://${this.config.host}:${this.config.port}/health`);
|
|
1025
|
-
resolve();
|
|
1520
|
+
resolve$1();
|
|
1026
1521
|
});
|
|
1027
1522
|
this.server.on("error", (error) => {
|
|
1028
1523
|
reject(error);
|
|
@@ -1033,17 +1528,17 @@ var SseTransportHandler = class {
|
|
|
1033
1528
|
});
|
|
1034
1529
|
}
|
|
1035
1530
|
async stop() {
|
|
1036
|
-
return new Promise((resolve, reject) => {
|
|
1531
|
+
return new Promise((resolve$1, reject) => {
|
|
1037
1532
|
if (this.server) {
|
|
1038
1533
|
this.sessionManager.clear();
|
|
1039
1534
|
this.server.close((err) => {
|
|
1040
1535
|
if (err) reject(err);
|
|
1041
1536
|
else {
|
|
1042
1537
|
this.server = null;
|
|
1043
|
-
resolve();
|
|
1538
|
+
resolve$1();
|
|
1044
1539
|
}
|
|
1045
1540
|
});
|
|
1046
|
-
} else resolve();
|
|
1541
|
+
} else resolve$1();
|
|
1047
1542
|
});
|
|
1048
1543
|
}
|
|
1049
1544
|
getPort() {
|
|
@@ -1195,12 +1690,12 @@ var HttpTransportHandler = class {
|
|
|
1195
1690
|
this.sessionManager.deleteSession(sessionId);
|
|
1196
1691
|
}
|
|
1197
1692
|
async start() {
|
|
1198
|
-
return new Promise((resolve, reject) => {
|
|
1693
|
+
return new Promise((resolve$1, reject) => {
|
|
1199
1694
|
try {
|
|
1200
1695
|
this.server = this.app.listen(this.config.port, this.config.host, () => {
|
|
1201
1696
|
console.error(`@agiflowai/one-mcp MCP server started on http://${this.config.host}:${this.config.port}/mcp`);
|
|
1202
1697
|
console.error(`Health check: http://${this.config.host}:${this.config.port}/health`);
|
|
1203
|
-
resolve();
|
|
1698
|
+
resolve$1();
|
|
1204
1699
|
});
|
|
1205
1700
|
this.server.on("error", (error) => {
|
|
1206
1701
|
reject(error);
|
|
@@ -1211,17 +1706,17 @@ var HttpTransportHandler = class {
|
|
|
1211
1706
|
});
|
|
1212
1707
|
}
|
|
1213
1708
|
async stop() {
|
|
1214
|
-
return new Promise((resolve, reject) => {
|
|
1709
|
+
return new Promise((resolve$1, reject) => {
|
|
1215
1710
|
if (this.server) {
|
|
1216
1711
|
this.sessionManager.clear();
|
|
1217
1712
|
this.server.close((err) => {
|
|
1218
1713
|
if (err) reject(err);
|
|
1219
1714
|
else {
|
|
1220
1715
|
this.server = null;
|
|
1221
|
-
resolve();
|
|
1716
|
+
resolve$1();
|
|
1222
1717
|
}
|
|
1223
1718
|
});
|
|
1224
|
-
} else resolve();
|
|
1719
|
+
} else resolve$1();
|
|
1225
1720
|
});
|
|
1226
1721
|
}
|
|
1227
1722
|
getPort() {
|
|
@@ -1274,4 +1769,10 @@ Object.defineProperty(exports, 'createServer', {
|
|
|
1274
1769
|
get: function () {
|
|
1275
1770
|
return createServer;
|
|
1276
1771
|
}
|
|
1772
|
+
});
|
|
1773
|
+
Object.defineProperty(exports, 'findConfigFile', {
|
|
1774
|
+
enumerable: true,
|
|
1775
|
+
get: function () {
|
|
1776
|
+
return findConfigFile;
|
|
1777
|
+
}
|
|
1277
1778
|
});
|