@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
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
2
|
import { CallToolRequestSchema, ListToolsRequestSchema, isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
3
|
-
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { mkdir, readFile, readdir, unlink, writeFile } from "node:fs/promises";
|
|
4
4
|
import { existsSync } from "node:fs";
|
|
5
5
|
import yaml from "js-yaml";
|
|
6
6
|
import { z } from "zod";
|
|
7
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
8
|
+
import { join, resolve } from "node:path";
|
|
9
|
+
import { tmpdir } from "node:os";
|
|
7
10
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
8
11
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
9
12
|
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
10
13
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
11
14
|
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
12
15
|
import express from "express";
|
|
13
|
-
import { randomUUID } from "node:crypto";
|
|
14
16
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
15
17
|
|
|
16
18
|
//#region src/utils/mcpConfigSchema.ts
|
|
@@ -80,6 +82,95 @@ function interpolateEnvVarsInObject(obj) {
|
|
|
80
82
|
return obj;
|
|
81
83
|
}
|
|
82
84
|
/**
|
|
85
|
+
* Private IP range patterns for SSRF protection
|
|
86
|
+
* Covers both IPv4 and IPv6 loopback, private, and link-local ranges
|
|
87
|
+
*/
|
|
88
|
+
const PRIVATE_IP_PATTERNS = [
|
|
89
|
+
/^127\./,
|
|
90
|
+
/^10\./,
|
|
91
|
+
/^172\.(1[6-9]|2\d|3[01])\./,
|
|
92
|
+
/^192\.168\./,
|
|
93
|
+
/^169\.254\./,
|
|
94
|
+
/^0\./,
|
|
95
|
+
/^224\./,
|
|
96
|
+
/^240\./,
|
|
97
|
+
/^localhost$/i,
|
|
98
|
+
/^.*\.localhost$/i,
|
|
99
|
+
/^\[::\]/,
|
|
100
|
+
/^\[::1\]/,
|
|
101
|
+
/^\[0:0:0:0:0:0:0:1\]/,
|
|
102
|
+
/^\[0{1,4}:0{1,4}:0{1,4}:0{1,4}:0{1,4}:0{1,4}:0{1,4}:1\]/i,
|
|
103
|
+
/^\[fe80:/i,
|
|
104
|
+
/^\[fc00:/i,
|
|
105
|
+
/^\[fd00:/i,
|
|
106
|
+
/^\[::ffff:127\./i,
|
|
107
|
+
/^\[::ffff:7f[0-9a-f]{2}:/i,
|
|
108
|
+
/^\[::ffff:10\./i,
|
|
109
|
+
/^\[::ffff:a[0-9a-f]{2}:/i,
|
|
110
|
+
/^\[::ffff:172\.(1[6-9]|2\d|3[01])\./i,
|
|
111
|
+
/^\[::ffff:ac1[0-9a-f]:/i,
|
|
112
|
+
/^\[::ffff:192\.168\./i,
|
|
113
|
+
/^\[::ffff:c0a8:/i,
|
|
114
|
+
/^\[::ffff:169\.254\./i,
|
|
115
|
+
/^\[::ffff:a9fe:/i,
|
|
116
|
+
/^\[::ffff:0\./i,
|
|
117
|
+
/^\[::127\./i,
|
|
118
|
+
/^\[::7f[0-9a-f]{2}:/i,
|
|
119
|
+
/^\[::10\./i,
|
|
120
|
+
/^\[::a[0-9a-f]{2}:/i,
|
|
121
|
+
/^\[::192\.168\./i,
|
|
122
|
+
/^\[::c0a8:/i
|
|
123
|
+
];
|
|
124
|
+
/**
|
|
125
|
+
* Validate URL for SSRF protection
|
|
126
|
+
*
|
|
127
|
+
* @param url - The URL to validate (after env var interpolation)
|
|
128
|
+
* @param security - Security settings
|
|
129
|
+
* @throws Error if URL is unsafe
|
|
130
|
+
*/
|
|
131
|
+
function validateUrlSecurity(url, security) {
|
|
132
|
+
const allowPrivateIPs = security?.allowPrivateIPs ?? false;
|
|
133
|
+
const enforceHttps = security?.enforceHttps ?? true;
|
|
134
|
+
let parsedUrl;
|
|
135
|
+
try {
|
|
136
|
+
parsedUrl = new URL(url);
|
|
137
|
+
} catch (error) {
|
|
138
|
+
throw new Error(`Invalid URL format: ${url}`);
|
|
139
|
+
}
|
|
140
|
+
const protocol = parsedUrl.protocol.replace(":", "");
|
|
141
|
+
if (enforceHttps && protocol !== "https") throw new Error(`HTTPS is required for security. URL uses '${protocol}://'. Set security.enforceHttps: false to allow HTTP.`);
|
|
142
|
+
if (protocol !== "http" && protocol !== "https") throw new Error(`Invalid URL protocol '${protocol}://'. Only http:// and https:// are allowed.`);
|
|
143
|
+
if (!allowPrivateIPs) {
|
|
144
|
+
const hostname = parsedUrl.hostname.toLowerCase();
|
|
145
|
+
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.`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Validate a remote config source against its validation rules
|
|
150
|
+
*
|
|
151
|
+
* @param source - Remote config source with validation rules
|
|
152
|
+
* @throws Error if validation fails
|
|
153
|
+
*/
|
|
154
|
+
function validateRemoteConfigSource(source) {
|
|
155
|
+
const interpolatedUrl = interpolateEnvVars(source.url);
|
|
156
|
+
validateUrlSecurity(interpolatedUrl, source.security);
|
|
157
|
+
if (!source.validation) return;
|
|
158
|
+
if (source.validation.url) {
|
|
159
|
+
if (!new RegExp(source.validation.url).test(interpolatedUrl)) throw new Error(`Remote config URL "${interpolatedUrl}" does not match validation pattern: ${source.validation.url}`);
|
|
160
|
+
}
|
|
161
|
+
if (source.validation.headers && Object.keys(source.validation.headers).length > 0) {
|
|
162
|
+
if (!source.headers) {
|
|
163
|
+
const requiredHeaders = Object.keys(source.validation.headers);
|
|
164
|
+
throw new Error(`Remote config is missing required headers: ${requiredHeaders.join(", ")}`);
|
|
165
|
+
}
|
|
166
|
+
for (const [headerName, pattern] of Object.entries(source.validation.headers)) {
|
|
167
|
+
if (!(headerName in source.headers)) throw new Error(`Remote config is missing required header: ${headerName}`);
|
|
168
|
+
const interpolatedHeaderValue = interpolateEnvVars(source.headers[headerName]);
|
|
169
|
+
if (!new RegExp(pattern).test(interpolatedHeaderValue)) throw new Error(`Remote config header "${headerName}" value "${interpolatedHeaderValue}" does not match validation pattern: ${pattern}`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
83
174
|
* Claude Code / Claude Desktop standard MCP config format
|
|
84
175
|
* This is the format users write in their config files
|
|
85
176
|
*/
|
|
@@ -105,10 +196,32 @@ const ClaudeCodeHttpServerSchema = z.object({
|
|
|
105
196
|
config: AdditionalConfigSchema
|
|
106
197
|
});
|
|
107
198
|
const ClaudeCodeServerConfigSchema = z.union([ClaudeCodeStdioServerSchema, ClaudeCodeHttpServerSchema]);
|
|
199
|
+
const RemoteConfigValidationSchema = z.object({
|
|
200
|
+
url: z.string().optional(),
|
|
201
|
+
headers: z.record(z.string(), z.string()).optional()
|
|
202
|
+
}).optional();
|
|
203
|
+
const RemoteConfigSecuritySchema = z.object({
|
|
204
|
+
allowPrivateIPs: z.boolean().optional(),
|
|
205
|
+
enforceHttps: z.boolean().optional()
|
|
206
|
+
}).optional();
|
|
207
|
+
const RemoteConfigSourceSchema = z.object({
|
|
208
|
+
url: z.string(),
|
|
209
|
+
headers: z.record(z.string(), z.string()).optional(),
|
|
210
|
+
validation: RemoteConfigValidationSchema,
|
|
211
|
+
security: RemoteConfigSecuritySchema,
|
|
212
|
+
mergeStrategy: z.enum([
|
|
213
|
+
"local-priority",
|
|
214
|
+
"remote-priority",
|
|
215
|
+
"merge-deep"
|
|
216
|
+
]).optional()
|
|
217
|
+
});
|
|
108
218
|
/**
|
|
109
219
|
* Full Claude Code MCP configuration schema
|
|
110
220
|
*/
|
|
111
|
-
const ClaudeCodeMcpConfigSchema = z.object({
|
|
221
|
+
const ClaudeCodeMcpConfigSchema = z.object({
|
|
222
|
+
mcpServers: z.record(z.string(), ClaudeCodeServerConfigSchema),
|
|
223
|
+
remoteConfigs: z.array(RemoteConfigSourceSchema).optional()
|
|
224
|
+
});
|
|
112
225
|
/**
|
|
113
226
|
* Internal MCP config format
|
|
114
227
|
* This is the normalized format used internally by the proxy
|
|
@@ -217,6 +330,226 @@ function parseMcpConfig(rawConfig) {
|
|
|
217
330
|
return InternalMcpConfigSchema.parse(internalConfig);
|
|
218
331
|
}
|
|
219
332
|
|
|
333
|
+
//#endregion
|
|
334
|
+
//#region src/services/RemoteConfigCacheService.ts
|
|
335
|
+
/**
|
|
336
|
+
* RemoteConfigCacheService
|
|
337
|
+
*
|
|
338
|
+
* DESIGN PATTERNS:
|
|
339
|
+
* - Service pattern for cache management
|
|
340
|
+
* - Single responsibility principle
|
|
341
|
+
* - File-based caching with TTL support
|
|
342
|
+
*
|
|
343
|
+
* CODING STANDARDS:
|
|
344
|
+
* - Use async/await for asynchronous operations
|
|
345
|
+
* - Handle file system errors gracefully
|
|
346
|
+
* - Keep cache organized by URL hash
|
|
347
|
+
* - Implement automatic cache expiration
|
|
348
|
+
*
|
|
349
|
+
* AVOID:
|
|
350
|
+
* - Storing sensitive data in cache (headers with tokens)
|
|
351
|
+
* - Unbounded cache growth
|
|
352
|
+
* - Missing error handling for file operations
|
|
353
|
+
*/
|
|
354
|
+
/**
|
|
355
|
+
* Service for caching remote MCP configurations
|
|
356
|
+
*/
|
|
357
|
+
var RemoteConfigCacheService = class {
|
|
358
|
+
cacheDir;
|
|
359
|
+
cacheTTL;
|
|
360
|
+
readEnabled;
|
|
361
|
+
writeEnabled;
|
|
362
|
+
constructor(options) {
|
|
363
|
+
this.cacheDir = join(tmpdir(), "one-mcp-cache", "remote-configs");
|
|
364
|
+
this.cacheTTL = options?.ttl || 3600 * 1e3;
|
|
365
|
+
this.readEnabled = options?.readEnabled !== void 0 ? options.readEnabled : true;
|
|
366
|
+
this.writeEnabled = options?.writeEnabled !== void 0 ? options.writeEnabled : true;
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Generate a hash key from remote config URL
|
|
370
|
+
* Only uses URL for hashing to avoid caching credentials in the key
|
|
371
|
+
*/
|
|
372
|
+
generateCacheKey(url) {
|
|
373
|
+
return createHash("sha256").update(url).digest("hex");
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Get the cache file path for a given cache key
|
|
377
|
+
*/
|
|
378
|
+
getCacheFilePath(cacheKey) {
|
|
379
|
+
return join(this.cacheDir, `${cacheKey}.json`);
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Initialize cache directory
|
|
383
|
+
* Uses mkdir with recursive option which handles existing directories gracefully
|
|
384
|
+
* (no TOCTOU race condition from existsSync check)
|
|
385
|
+
*/
|
|
386
|
+
async ensureCacheDir() {
|
|
387
|
+
try {
|
|
388
|
+
await mkdir(this.cacheDir, { recursive: true });
|
|
389
|
+
} catch (error) {
|
|
390
|
+
if (error?.code !== "EEXIST") throw error;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Get cached data for a remote config URL
|
|
395
|
+
*/
|
|
396
|
+
async get(url) {
|
|
397
|
+
if (!this.readEnabled) return null;
|
|
398
|
+
try {
|
|
399
|
+
await this.ensureCacheDir();
|
|
400
|
+
const cacheKey = this.generateCacheKey(url);
|
|
401
|
+
const cacheFilePath = this.getCacheFilePath(cacheKey);
|
|
402
|
+
if (!existsSync(cacheFilePath)) return null;
|
|
403
|
+
const cacheContent = await readFile(cacheFilePath, "utf-8");
|
|
404
|
+
const cacheEntry = JSON.parse(cacheContent);
|
|
405
|
+
const now = Date.now();
|
|
406
|
+
if (now > cacheEntry.expiresAt) {
|
|
407
|
+
await unlink(cacheFilePath).catch(() => {});
|
|
408
|
+
return null;
|
|
409
|
+
}
|
|
410
|
+
const expiresInSeconds = Math.round((cacheEntry.expiresAt - now) / 1e3);
|
|
411
|
+
console.error(`Remote config cache hit for ${url} (expires in ${expiresInSeconds}s)`);
|
|
412
|
+
return cacheEntry.data;
|
|
413
|
+
} catch (error) {
|
|
414
|
+
console.error(`Failed to read remote config cache for ${url}:`, error);
|
|
415
|
+
return null;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Set cached data for a remote config URL
|
|
420
|
+
*/
|
|
421
|
+
async set(url, data) {
|
|
422
|
+
if (!this.writeEnabled) return;
|
|
423
|
+
try {
|
|
424
|
+
await this.ensureCacheDir();
|
|
425
|
+
const cacheKey = this.generateCacheKey(url);
|
|
426
|
+
const cacheFilePath = this.getCacheFilePath(cacheKey);
|
|
427
|
+
const now = Date.now();
|
|
428
|
+
const cacheEntry = {
|
|
429
|
+
data,
|
|
430
|
+
timestamp: now,
|
|
431
|
+
expiresAt: now + this.cacheTTL,
|
|
432
|
+
url
|
|
433
|
+
};
|
|
434
|
+
await writeFile(cacheFilePath, JSON.stringify(cacheEntry, null, 2), "utf-8");
|
|
435
|
+
console.error(`Cached remote config for ${url} (TTL: ${Math.round(this.cacheTTL / 1e3)}s)`);
|
|
436
|
+
} catch (error) {
|
|
437
|
+
console.error(`Failed to write remote config cache for ${url}:`, error);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Clear cache for a specific URL
|
|
442
|
+
*/
|
|
443
|
+
async clear(url) {
|
|
444
|
+
try {
|
|
445
|
+
const cacheKey = this.generateCacheKey(url);
|
|
446
|
+
const cacheFilePath = this.getCacheFilePath(cacheKey);
|
|
447
|
+
if (existsSync(cacheFilePath)) {
|
|
448
|
+
await unlink(cacheFilePath);
|
|
449
|
+
console.error(`Cleared remote config cache for ${url}`);
|
|
450
|
+
}
|
|
451
|
+
} catch (error) {
|
|
452
|
+
console.error(`Failed to clear remote config cache for ${url}:`, error);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Clear all cached remote configs
|
|
457
|
+
*/
|
|
458
|
+
async clearAll() {
|
|
459
|
+
try {
|
|
460
|
+
if (!existsSync(this.cacheDir)) return;
|
|
461
|
+
const files = await readdir(this.cacheDir);
|
|
462
|
+
const deletePromises = files.filter((file) => file.endsWith(".json")).map((file) => unlink(join(this.cacheDir, file)).catch(() => {}));
|
|
463
|
+
await Promise.all(deletePromises);
|
|
464
|
+
console.error(`Cleared all remote config cache entries (${files.length} files)`);
|
|
465
|
+
} catch (error) {
|
|
466
|
+
console.error("Failed to clear all remote config cache:", error);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* Clean up expired cache entries
|
|
471
|
+
*/
|
|
472
|
+
async cleanExpired() {
|
|
473
|
+
try {
|
|
474
|
+
if (!existsSync(this.cacheDir)) return;
|
|
475
|
+
const now = Date.now();
|
|
476
|
+
const files = await readdir(this.cacheDir);
|
|
477
|
+
let expiredCount = 0;
|
|
478
|
+
for (const file of files) {
|
|
479
|
+
if (!file.endsWith(".json")) continue;
|
|
480
|
+
const filePath = join(this.cacheDir, file);
|
|
481
|
+
try {
|
|
482
|
+
const content = await readFile(filePath, "utf-8");
|
|
483
|
+
if (now > JSON.parse(content).expiresAt) {
|
|
484
|
+
await unlink(filePath);
|
|
485
|
+
expiredCount++;
|
|
486
|
+
}
|
|
487
|
+
} catch (error) {
|
|
488
|
+
await unlink(filePath).catch(() => {});
|
|
489
|
+
expiredCount++;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
if (expiredCount > 0) console.error(`Cleaned up ${expiredCount} expired remote config cache entries`);
|
|
493
|
+
} catch (error) {
|
|
494
|
+
console.error("Failed to clean expired remote config cache:", error);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Get cache statistics
|
|
499
|
+
*/
|
|
500
|
+
async getStats() {
|
|
501
|
+
try {
|
|
502
|
+
if (!existsSync(this.cacheDir)) return {
|
|
503
|
+
totalEntries: 0,
|
|
504
|
+
totalSize: 0
|
|
505
|
+
};
|
|
506
|
+
const jsonFiles = (await readdir(this.cacheDir)).filter((file) => file.endsWith(".json"));
|
|
507
|
+
let totalSize = 0;
|
|
508
|
+
for (const file of jsonFiles) {
|
|
509
|
+
const filePath = join(this.cacheDir, file);
|
|
510
|
+
try {
|
|
511
|
+
const content = await readFile(filePath, "utf-8");
|
|
512
|
+
totalSize += Buffer.byteLength(content, "utf-8");
|
|
513
|
+
} catch {}
|
|
514
|
+
}
|
|
515
|
+
return {
|
|
516
|
+
totalEntries: jsonFiles.length,
|
|
517
|
+
totalSize
|
|
518
|
+
};
|
|
519
|
+
} catch (error) {
|
|
520
|
+
console.error("Failed to get remote config cache stats:", error);
|
|
521
|
+
return {
|
|
522
|
+
totalEntries: 0,
|
|
523
|
+
totalSize: 0
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* Check if read from cache is enabled
|
|
529
|
+
*/
|
|
530
|
+
isReadEnabled() {
|
|
531
|
+
return this.readEnabled;
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* Check if write to cache is enabled
|
|
535
|
+
*/
|
|
536
|
+
isWriteEnabled() {
|
|
537
|
+
return this.writeEnabled;
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* Set read enabled state
|
|
541
|
+
*/
|
|
542
|
+
setReadEnabled(enabled) {
|
|
543
|
+
this.readEnabled = enabled;
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* Set write enabled state
|
|
547
|
+
*/
|
|
548
|
+
setWriteEnabled(enabled) {
|
|
549
|
+
this.writeEnabled = enabled;
|
|
550
|
+
}
|
|
551
|
+
};
|
|
552
|
+
|
|
220
553
|
//#endregion
|
|
221
554
|
//#region src/services/ConfigFetcherService.ts
|
|
222
555
|
/**
|
|
@@ -238,35 +571,62 @@ function parseMcpConfig(rawConfig) {
|
|
|
238
571
|
* - Direct tool implementation (services should be tool-agnostic)
|
|
239
572
|
*/
|
|
240
573
|
/**
|
|
241
|
-
* Service for fetching and caching MCP server configurations from local file
|
|
574
|
+
* Service for fetching and caching MCP server configurations from local file and remote sources
|
|
575
|
+
* Supports merging multiple remote configs with local config
|
|
242
576
|
*/
|
|
243
577
|
var ConfigFetcherService = class {
|
|
244
578
|
configFilePath;
|
|
245
579
|
cacheTtlMs;
|
|
246
580
|
cachedConfig = null;
|
|
247
581
|
lastFetchTime = 0;
|
|
582
|
+
remoteConfigCache;
|
|
248
583
|
constructor(options) {
|
|
249
584
|
this.configFilePath = options.configFilePath;
|
|
250
585
|
this.cacheTtlMs = options.cacheTtlMs || 6e4;
|
|
586
|
+
const useCache = options.useCache !== void 0 ? options.useCache : true;
|
|
587
|
+
this.remoteConfigCache = new RemoteConfigCacheService({
|
|
588
|
+
ttl: options.remoteCacheTtlMs || 3600 * 1e3,
|
|
589
|
+
readEnabled: useCache,
|
|
590
|
+
writeEnabled: true
|
|
591
|
+
});
|
|
251
592
|
if (!this.configFilePath) throw new Error("configFilePath must be provided");
|
|
252
593
|
}
|
|
253
594
|
/**
|
|
254
|
-
* Fetch MCP configuration from local file with caching
|
|
595
|
+
* Fetch MCP configuration from local file and remote sources with caching
|
|
596
|
+
* Merges remote configs with local config based on merge strategy
|
|
255
597
|
* @param forceRefresh - Force reload from source, bypassing cache
|
|
256
598
|
*/
|
|
257
599
|
async fetchConfiguration(forceRefresh = false) {
|
|
258
600
|
const now = Date.now();
|
|
259
601
|
if (!forceRefresh && this.cachedConfig && now - this.lastFetchTime < this.cacheTtlMs) return this.cachedConfig;
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
|
|
602
|
+
const localConfigData = await this.loadRawConfigFromFile();
|
|
603
|
+
const remoteConfigSources = localConfigData.remoteConfigs || [];
|
|
604
|
+
let mergedConfig = await this.parseConfig(localConfigData);
|
|
605
|
+
const remoteConfigPromises = remoteConfigSources.map(async (remoteSource) => {
|
|
606
|
+
try {
|
|
607
|
+
validateRemoteConfigSource(remoteSource);
|
|
608
|
+
return {
|
|
609
|
+
config: await this.loadFromUrl(remoteSource),
|
|
610
|
+
mergeStrategy: remoteSource.mergeStrategy || "local-priority",
|
|
611
|
+
url: remoteSource.url
|
|
612
|
+
};
|
|
613
|
+
} catch (error) {
|
|
614
|
+
if (error instanceof Error) console.error(`Failed to fetch remote config from ${remoteSource.url}: ${error.message}`);
|
|
615
|
+
return null;
|
|
616
|
+
}
|
|
617
|
+
});
|
|
618
|
+
const remoteConfigResults = await Promise.all(remoteConfigPromises);
|
|
619
|
+
for (const result of remoteConfigResults) if (result !== null) mergedConfig = this.mergeConfigurations(mergedConfig, result.config, result.mergeStrategy);
|
|
620
|
+
if (!mergedConfig.mcpServers || typeof mergedConfig.mcpServers !== "object") throw new Error("Invalid MCP configuration: missing or invalid mcpServers");
|
|
621
|
+
this.cachedConfig = mergedConfig;
|
|
263
622
|
this.lastFetchTime = now;
|
|
264
|
-
return
|
|
623
|
+
return mergedConfig;
|
|
265
624
|
}
|
|
266
625
|
/**
|
|
267
|
-
* Load configuration from a local file (supports JSON and YAML)
|
|
626
|
+
* Load raw configuration data from a local file (supports JSON and YAML)
|
|
627
|
+
* Returns unparsed config data to allow access to remoteConfigs
|
|
268
628
|
*/
|
|
269
|
-
async
|
|
629
|
+
async loadRawConfigFromFile() {
|
|
270
630
|
if (!this.configFilePath) throw new Error("No config file path provided");
|
|
271
631
|
if (!existsSync(this.configFilePath)) throw new Error(`Config file not found: ${this.configFilePath}`);
|
|
272
632
|
try {
|
|
@@ -274,13 +634,117 @@ var ConfigFetcherService = class {
|
|
|
274
634
|
let rawConfig;
|
|
275
635
|
if (this.configFilePath.endsWith(".yaml") || this.configFilePath.endsWith(".yml")) rawConfig = yaml.load(content);
|
|
276
636
|
else rawConfig = JSON.parse(content);
|
|
277
|
-
return
|
|
637
|
+
return rawConfig;
|
|
278
638
|
} catch (error) {
|
|
279
639
|
if (error instanceof Error) throw new Error(`Failed to load config file: ${error.message}`);
|
|
280
640
|
throw new Error("Failed to load config file: Unknown error");
|
|
281
641
|
}
|
|
282
642
|
}
|
|
283
643
|
/**
|
|
644
|
+
* Parse raw config data using Zod schema
|
|
645
|
+
* Filters out remoteConfigs to avoid including them in the final config
|
|
646
|
+
*/
|
|
647
|
+
async parseConfig(rawConfig) {
|
|
648
|
+
try {
|
|
649
|
+
const { remoteConfigs, ...configWithoutRemote } = rawConfig;
|
|
650
|
+
return parseMcpConfig(configWithoutRemote);
|
|
651
|
+
} catch (error) {
|
|
652
|
+
if (error instanceof Error) throw new Error(`Failed to parse config: ${error.message}`);
|
|
653
|
+
throw new Error("Failed to parse config: Unknown error");
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
/**
|
|
657
|
+
* Load configuration from a remote URL with caching
|
|
658
|
+
*
|
|
659
|
+
* SECURITY NOTE: This method fetches remote configs based on URLs from the local config file.
|
|
660
|
+
* This is intentional and safe because:
|
|
661
|
+
* 1. URLs are user-controlled via their local config file (not external input)
|
|
662
|
+
* 2. SSRF protection validates URLs before fetching (blocks private IPs, enforces HTTPS)
|
|
663
|
+
* 3. Users explicitly opt-in to remote configs in their local configuration
|
|
664
|
+
* 4. This enables centralized config management (intended feature, not a vulnerability)
|
|
665
|
+
*
|
|
666
|
+
* CodeQL alert "file-access-to-http" is a false positive here - we're not leaking
|
|
667
|
+
* file contents to arbitrary URLs, we're fetching configs from user-specified sources.
|
|
668
|
+
*/
|
|
669
|
+
async loadFromUrl(source) {
|
|
670
|
+
try {
|
|
671
|
+
const interpolatedUrl = this.interpolateEnvVars(source.url);
|
|
672
|
+
const cachedConfig = await this.remoteConfigCache.get(interpolatedUrl);
|
|
673
|
+
if (cachedConfig) return cachedConfig;
|
|
674
|
+
const interpolatedHeaders = source.headers ? Object.fromEntries(Object.entries(source.headers).map(([key, value]) => [key, this.interpolateEnvVars(value)])) : {};
|
|
675
|
+
const response = await fetch(interpolatedUrl, { headers: interpolatedHeaders });
|
|
676
|
+
if (!response.ok) throw new Error(`Failed to fetch remote config: ${response.status} ${response.statusText}`);
|
|
677
|
+
const config = parseMcpConfig(await response.json());
|
|
678
|
+
await this.remoteConfigCache.set(interpolatedUrl, config);
|
|
679
|
+
return config;
|
|
680
|
+
} catch (error) {
|
|
681
|
+
if (error instanceof Error) throw new Error(`Failed to fetch remote config from ${source.url}: ${error.message}`);
|
|
682
|
+
throw new Error(`Failed to fetch remote config from ${source.url}: Unknown error`);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
/**
|
|
686
|
+
* Interpolate environment variables in a string
|
|
687
|
+
* Supports ${VAR_NAME} syntax
|
|
688
|
+
*/
|
|
689
|
+
interpolateEnvVars(value) {
|
|
690
|
+
return value.replace(/\$\{([^}]+)\}/g, (_, varName) => {
|
|
691
|
+
const envValue = process.env[varName];
|
|
692
|
+
if (envValue === void 0) {
|
|
693
|
+
console.warn(`Environment variable ${varName} is not defined, keeping placeholder`);
|
|
694
|
+
return `\${${varName}}`;
|
|
695
|
+
}
|
|
696
|
+
return envValue;
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* Merge two MCP configurations based on the specified merge strategy
|
|
701
|
+
* @param localConfig Configuration loaded from local file
|
|
702
|
+
* @param remoteConfig Configuration loaded from remote URL
|
|
703
|
+
* @param mergeStrategy Strategy for merging configs
|
|
704
|
+
* @returns Merged configuration
|
|
705
|
+
*/
|
|
706
|
+
mergeConfigurations(localConfig, remoteConfig, mergeStrategy) {
|
|
707
|
+
switch (mergeStrategy) {
|
|
708
|
+
case "local-priority": return { mcpServers: {
|
|
709
|
+
...remoteConfig.mcpServers,
|
|
710
|
+
...localConfig.mcpServers
|
|
711
|
+
} };
|
|
712
|
+
case "remote-priority": return { mcpServers: {
|
|
713
|
+
...localConfig.mcpServers,
|
|
714
|
+
...remoteConfig.mcpServers
|
|
715
|
+
} };
|
|
716
|
+
case "merge-deep": {
|
|
717
|
+
const merged = { ...remoteConfig.mcpServers };
|
|
718
|
+
for (const [serverName, localServerConfig] of Object.entries(localConfig.mcpServers)) if (merged[serverName]) {
|
|
719
|
+
const remoteServer = merged[serverName];
|
|
720
|
+
const mergedConfig = {
|
|
721
|
+
...remoteServer.config,
|
|
722
|
+
...localServerConfig.config
|
|
723
|
+
};
|
|
724
|
+
const remoteEnv = "env" in remoteServer.config ? remoteServer.config.env : void 0;
|
|
725
|
+
const localEnv = "env" in localServerConfig.config ? localServerConfig.config.env : void 0;
|
|
726
|
+
if (remoteEnv || localEnv) mergedConfig.env = {
|
|
727
|
+
...remoteEnv || {},
|
|
728
|
+
...localEnv || {}
|
|
729
|
+
};
|
|
730
|
+
const remoteHeaders = "headers" in remoteServer.config ? remoteServer.config.headers : void 0;
|
|
731
|
+
const localHeaders = "headers" in localServerConfig.config ? localServerConfig.config.headers : void 0;
|
|
732
|
+
if (remoteHeaders || localHeaders) mergedConfig.headers = {
|
|
733
|
+
...remoteHeaders || {},
|
|
734
|
+
...localHeaders || {}
|
|
735
|
+
};
|
|
736
|
+
merged[serverName] = {
|
|
737
|
+
...remoteServer,
|
|
738
|
+
...localServerConfig,
|
|
739
|
+
config: mergedConfig
|
|
740
|
+
};
|
|
741
|
+
} else merged[serverName] = localServerConfig;
|
|
742
|
+
return { mcpServers: merged };
|
|
743
|
+
}
|
|
744
|
+
default: throw new Error(`Unknown merge strategy: ${mergeStrategy}`);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
/**
|
|
284
748
|
* Clear the cached configuration
|
|
285
749
|
*/
|
|
286
750
|
clearCache() {
|
|
@@ -487,6 +951,75 @@ var McpClientManagerService = class {
|
|
|
487
951
|
}
|
|
488
952
|
};
|
|
489
953
|
|
|
954
|
+
//#endregion
|
|
955
|
+
//#region src/utils/findConfigFile.ts
|
|
956
|
+
/**
|
|
957
|
+
* Config File Finder Utility
|
|
958
|
+
*
|
|
959
|
+
* DESIGN PATTERNS:
|
|
960
|
+
* - Utility function pattern for reusable logic
|
|
961
|
+
* - Fail-fast pattern with early returns
|
|
962
|
+
* - Environment variable configuration pattern
|
|
963
|
+
*
|
|
964
|
+
* CODING STANDARDS:
|
|
965
|
+
* - Use sync filesystem operations for config discovery (performance)
|
|
966
|
+
* - Check PROJECT_PATH environment variable first
|
|
967
|
+
* - Fall back to current working directory
|
|
968
|
+
* - Support both .yaml and .json extensions
|
|
969
|
+
* - Return null if no config file is found
|
|
970
|
+
*
|
|
971
|
+
* AVOID:
|
|
972
|
+
* - Throwing errors (return null instead for optional config)
|
|
973
|
+
* - Hardcoded file names without extension variants
|
|
974
|
+
* - Ignoring environment variables
|
|
975
|
+
*/
|
|
976
|
+
/**
|
|
977
|
+
* Find MCP configuration file by checking PROJECT_PATH first, then cwd
|
|
978
|
+
* Looks for both mcp-config.yaml and mcp-config.json
|
|
979
|
+
*
|
|
980
|
+
* @returns Absolute path to config file, or null if not found
|
|
981
|
+
*/
|
|
982
|
+
function findConfigFile() {
|
|
983
|
+
const configFileNames = [
|
|
984
|
+
"mcp-config.yaml",
|
|
985
|
+
"mcp-config.yml",
|
|
986
|
+
"mcp-config.json"
|
|
987
|
+
];
|
|
988
|
+
const projectPath = process.env.PROJECT_PATH;
|
|
989
|
+
if (projectPath) for (const fileName of configFileNames) {
|
|
990
|
+
const configPath = resolve(projectPath, fileName);
|
|
991
|
+
if (existsSync(configPath)) return configPath;
|
|
992
|
+
}
|
|
993
|
+
const cwd = process.cwd();
|
|
994
|
+
for (const fileName of configFileNames) {
|
|
995
|
+
const configPath = join(cwd, fileName);
|
|
996
|
+
if (existsSync(configPath)) return configPath;
|
|
997
|
+
}
|
|
998
|
+
return null;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
//#endregion
|
|
1002
|
+
//#region src/utils/parseToolName.ts
|
|
1003
|
+
/**
|
|
1004
|
+
* Parse tool name to extract server and actual tool name
|
|
1005
|
+
* Supports both plain tool names and prefixed format: {serverName}__{toolName}
|
|
1006
|
+
*
|
|
1007
|
+
* @param toolName - The tool name to parse (e.g., "my_tool" or "server__my_tool")
|
|
1008
|
+
* @returns Parsed result with optional serverName and actualToolName
|
|
1009
|
+
*
|
|
1010
|
+
* @example
|
|
1011
|
+
* parseToolName("my_tool") // { actualToolName: "my_tool" }
|
|
1012
|
+
* parseToolName("server__my_tool") // { serverName: "server", actualToolName: "my_tool" }
|
|
1013
|
+
*/
|
|
1014
|
+
function parseToolName(toolName) {
|
|
1015
|
+
const separatorIndex = toolName.indexOf("__");
|
|
1016
|
+
if (separatorIndex > 0) return {
|
|
1017
|
+
serverName: toolName.substring(0, separatorIndex),
|
|
1018
|
+
actualToolName: toolName.substring(separatorIndex + 2)
|
|
1019
|
+
};
|
|
1020
|
+
return { actualToolName: toolName };
|
|
1021
|
+
}
|
|
1022
|
+
|
|
490
1023
|
//#endregion
|
|
491
1024
|
//#region src/tools/DescribeToolsTool.ts
|
|
492
1025
|
var DescribeToolsTool = class DescribeToolsTool {
|
|
@@ -497,46 +1030,50 @@ var DescribeToolsTool = class DescribeToolsTool {
|
|
|
497
1030
|
}
|
|
498
1031
|
async getDefinition() {
|
|
499
1032
|
const clients = this.clientManager.getAllClients();
|
|
500
|
-
const
|
|
1033
|
+
const toolToServers = /* @__PURE__ */ new Map();
|
|
1034
|
+
const serverToolsMap = /* @__PURE__ */ new Map();
|
|
1035
|
+
await Promise.all(clients.map(async (client) => {
|
|
501
1036
|
try {
|
|
502
1037
|
const tools = await client.listTools();
|
|
503
1038
|
const blacklist = new Set(client.toolBlacklist || []);
|
|
504
1039
|
const filteredTools = tools.filter((t) => !blacklist.has(t.name));
|
|
505
|
-
|
|
506
|
-
const
|
|
507
|
-
|
|
1040
|
+
serverToolsMap.set(client.serverName, filteredTools);
|
|
1041
|
+
for (const tool of filteredTools) {
|
|
1042
|
+
if (!toolToServers.has(tool.name)) toolToServers.set(tool.name, []);
|
|
1043
|
+
toolToServers.get(tool.name).push(client.serverName);
|
|
1044
|
+
}
|
|
508
1045
|
} catch (error) {
|
|
509
1046
|
console.error(`Failed to list tools from ${client.serverName}:`, error);
|
|
510
|
-
|
|
511
|
-
return `\n\n**Server: ${client.serverName}**${instructionLine}\n`;
|
|
1047
|
+
serverToolsMap.set(client.serverName, []);
|
|
512
1048
|
}
|
|
513
1049
|
}));
|
|
1050
|
+
const serverDescriptions = clients.map((client) => {
|
|
1051
|
+
const tools = serverToolsMap.get(client.serverName) || [];
|
|
1052
|
+
const formatToolName = (toolName) => {
|
|
1053
|
+
return (toolToServers.get(toolName) || []).length > 1 ? `${client.serverName}__${toolName}` : toolName;
|
|
1054
|
+
};
|
|
1055
|
+
const toolList = client.omitToolDescription ? tools.map((t) => formatToolName(t.name)).join(", ") : tools.map((t) => `${formatToolName(t.name)}: """${t.description || "No description"}"""`).join("\n");
|
|
1056
|
+
const instructionLine = client.serverInstruction ? `\n"""${client.serverInstruction}"""` : "";
|
|
1057
|
+
return `\n\n### Server: ${client.serverName}${instructionLine}\n\n- Available tools:\n${toolList || "No tools available"}`;
|
|
1058
|
+
});
|
|
514
1059
|
return {
|
|
515
1060
|
name: DescribeToolsTool.TOOL_NAME,
|
|
516
|
-
description:
|
|
517
|
-
|
|
518
|
-
## Available MCP Servers:${serverDescriptions.join("")}
|
|
1061
|
+
description: `## Available MCP Servers:${serverDescriptions.join("")}
|
|
519
1062
|
|
|
520
1063
|
## Usage:
|
|
521
|
-
|
|
1064
|
+
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:
|
|
522
1065
|
- Arguments schema needed to pass to the tool use
|
|
523
1066
|
- Description about each tool
|
|
524
1067
|
|
|
525
1068
|
This tool is optimized for batch queries - you can request multiple tools at once for better performance.`,
|
|
526
1069
|
inputSchema: {
|
|
527
1070
|
type: "object",
|
|
528
|
-
properties: {
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
},
|
|
535
|
-
serverName: {
|
|
536
|
-
type: "string",
|
|
537
|
-
description: "Optional server name to search within. If not specified, searches all servers."
|
|
538
|
-
}
|
|
539
|
-
},
|
|
1071
|
+
properties: { toolNames: {
|
|
1072
|
+
type: "array",
|
|
1073
|
+
items: { type: "string" },
|
|
1074
|
+
description: "List of tool names to get detailed information about",
|
|
1075
|
+
minItems: 1
|
|
1076
|
+
} },
|
|
540
1077
|
required: ["toolNames"],
|
|
541
1078
|
additionalProperties: false
|
|
542
1079
|
}
|
|
@@ -544,7 +1081,7 @@ This tool is optimized for batch queries - you can request multiple tools at onc
|
|
|
544
1081
|
}
|
|
545
1082
|
async execute(input) {
|
|
546
1083
|
try {
|
|
547
|
-
const { toolNames
|
|
1084
|
+
const { toolNames } = input;
|
|
548
1085
|
const clients = this.clientManager.getAllClients();
|
|
549
1086
|
if (!toolNames || toolNames.length === 0) return {
|
|
550
1087
|
content: [{
|
|
@@ -553,127 +1090,85 @@ This tool is optimized for batch queries - you can request multiple tools at onc
|
|
|
553
1090
|
}],
|
|
554
1091
|
isError: true
|
|
555
1092
|
};
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
content: [{
|
|
560
|
-
type: "text",
|
|
561
|
-
text: `Server "${serverName}" not found. Available servers: ${clients.map((c) => c.serverName).join(", ")}`
|
|
562
|
-
}],
|
|
563
|
-
isError: true
|
|
564
|
-
};
|
|
565
|
-
const tools = await client.listTools();
|
|
566
|
-
const blacklist = new Set(client.toolBlacklist || []);
|
|
567
|
-
const filteredTools = tools.filter((t) => !blacklist.has(t.name));
|
|
568
|
-
const foundTools$1 = [];
|
|
569
|
-
const notFoundTools$1 = [];
|
|
570
|
-
for (const toolName of toolNames) {
|
|
571
|
-
if (blacklist.has(toolName)) {
|
|
572
|
-
notFoundTools$1.push(toolName);
|
|
573
|
-
continue;
|
|
574
|
-
}
|
|
575
|
-
const tool = filteredTools.find((t) => t.name === toolName);
|
|
576
|
-
if (tool) foundTools$1.push({
|
|
577
|
-
server: serverName,
|
|
578
|
-
tool: {
|
|
579
|
-
name: tool.name,
|
|
580
|
-
description: tool.description,
|
|
581
|
-
inputSchema: tool.inputSchema
|
|
582
|
-
}
|
|
583
|
-
});
|
|
584
|
-
else notFoundTools$1.push(toolName);
|
|
585
|
-
}
|
|
586
|
-
if (foundTools$1.length === 0) return {
|
|
587
|
-
content: [{
|
|
588
|
-
type: "text",
|
|
589
|
-
text: `None of the requested tools found on server "${serverName}".\nRequested: ${toolNames.join(", ")}\nAvailable tools: ${tools.map((t) => t.name).join(", ")}`
|
|
590
|
-
}],
|
|
591
|
-
isError: true
|
|
592
|
-
};
|
|
593
|
-
const result$1 = { tools: foundTools$1 };
|
|
594
|
-
if (notFoundTools$1.length > 0) {
|
|
595
|
-
result$1.notFound = notFoundTools$1;
|
|
596
|
-
result$1.warning = `Some tools were not found on server "${serverName}": ${notFoundTools$1.join(", ")}`;
|
|
597
|
-
}
|
|
598
|
-
return { content: [{
|
|
599
|
-
type: "text",
|
|
600
|
-
text: JSON.stringify(result$1, null, 2)
|
|
601
|
-
}] };
|
|
602
|
-
}
|
|
603
|
-
const foundTools = [];
|
|
604
|
-
const notFoundTools = [...toolNames];
|
|
605
|
-
const toolMatches = /* @__PURE__ */ new Map();
|
|
606
|
-
const results = await Promise.all(clients.map(async (client) => {
|
|
1093
|
+
const serverToolsMap = /* @__PURE__ */ new Map();
|
|
1094
|
+
const toolToServers = /* @__PURE__ */ new Map();
|
|
1095
|
+
await Promise.all(clients.map(async (client) => {
|
|
607
1096
|
try {
|
|
608
1097
|
const tools = await client.listTools();
|
|
609
1098
|
const blacklist = new Set(client.toolBlacklist || []);
|
|
610
1099
|
const filteredTools = tools.filter((t) => !blacklist.has(t.name));
|
|
611
|
-
|
|
612
|
-
for (const
|
|
613
|
-
if (
|
|
614
|
-
|
|
615
|
-
if (tool) matches.push({
|
|
616
|
-
toolName,
|
|
617
|
-
server: client.serverName,
|
|
618
|
-
tool
|
|
619
|
-
});
|
|
1100
|
+
serverToolsMap.set(client.serverName, filteredTools);
|
|
1101
|
+
for (const tool of filteredTools) {
|
|
1102
|
+
if (!toolToServers.has(tool.name)) toolToServers.set(tool.name, []);
|
|
1103
|
+
toolToServers.get(tool.name).push(client.serverName);
|
|
620
1104
|
}
|
|
621
|
-
return matches;
|
|
622
1105
|
} catch (error) {
|
|
623
1106
|
console.error(`Failed to list tools from ${client.serverName}:`, error);
|
|
624
|
-
|
|
1107
|
+
serverToolsMap.set(client.serverName, []);
|
|
625
1108
|
}
|
|
626
1109
|
}));
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
foundTools.push({
|
|
641
|
-
server: match.server,
|
|
1110
|
+
const foundTools = [];
|
|
1111
|
+
const notFoundTools = [];
|
|
1112
|
+
for (const requestedToolName of toolNames) {
|
|
1113
|
+
const { serverName, actualToolName } = parseToolName(requestedToolName);
|
|
1114
|
+
if (serverName) {
|
|
1115
|
+
const serverTools = serverToolsMap.get(serverName);
|
|
1116
|
+
if (!serverTools) {
|
|
1117
|
+
notFoundTools.push(requestedToolName);
|
|
1118
|
+
continue;
|
|
1119
|
+
}
|
|
1120
|
+
const tool = serverTools.find((t) => t.name === actualToolName);
|
|
1121
|
+
if (tool) foundTools.push({
|
|
1122
|
+
server: serverName,
|
|
642
1123
|
tool: {
|
|
643
|
-
name:
|
|
644
|
-
description:
|
|
645
|
-
inputSchema:
|
|
1124
|
+
name: tool.name,
|
|
1125
|
+
description: tool.description,
|
|
1126
|
+
inputSchema: tool.inputSchema
|
|
646
1127
|
}
|
|
647
1128
|
});
|
|
648
|
-
|
|
649
|
-
if (idx > -1) notFoundTools.splice(idx, 1);
|
|
1129
|
+
else notFoundTools.push(requestedToolName);
|
|
650
1130
|
} else {
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
if (
|
|
1131
|
+
const servers = toolToServers.get(actualToolName);
|
|
1132
|
+
if (!servers || servers.length === 0) {
|
|
1133
|
+
notFoundTools.push(requestedToolName);
|
|
1134
|
+
continue;
|
|
1135
|
+
}
|
|
1136
|
+
if (servers.length === 1) {
|
|
1137
|
+
const server = servers[0];
|
|
1138
|
+
const tool = serverToolsMap.get(server).find((t) => t.name === actualToolName);
|
|
1139
|
+
foundTools.push({
|
|
1140
|
+
server,
|
|
1141
|
+
tool: {
|
|
1142
|
+
name: tool.name,
|
|
1143
|
+
description: tool.description,
|
|
1144
|
+
inputSchema: tool.inputSchema
|
|
1145
|
+
}
|
|
1146
|
+
});
|
|
1147
|
+
} else for (const server of servers) {
|
|
1148
|
+
const tool = serverToolsMap.get(server).find((t) => t.name === actualToolName);
|
|
1149
|
+
foundTools.push({
|
|
1150
|
+
server,
|
|
1151
|
+
tool: {
|
|
1152
|
+
name: tool.name,
|
|
1153
|
+
description: tool.description,
|
|
1154
|
+
inputSchema: tool.inputSchema
|
|
1155
|
+
}
|
|
1156
|
+
});
|
|
1157
|
+
}
|
|
657
1158
|
}
|
|
658
1159
|
}
|
|
659
|
-
if (foundTools.length === 0
|
|
1160
|
+
if (foundTools.length === 0) return {
|
|
660
1161
|
content: [{
|
|
661
1162
|
type: "text",
|
|
662
|
-
text: `None of the requested tools found on any connected server.\nRequested: ${toolNames.join(", ")}\nUse describe_tools
|
|
1163
|
+
text: `None of the requested tools found on any connected server.\nRequested: ${toolNames.join(", ")}\nUse describe_tools to see available tools.`
|
|
663
1164
|
}],
|
|
664
1165
|
isError: true
|
|
665
1166
|
};
|
|
666
1167
|
const result = { tools: foundTools };
|
|
667
|
-
if (notFoundTools.length > 0)
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
message: `Tool "${item.toolName}" found on multiple servers: ${item.servers.join(", ")}. Please specify serverName to disambiguate.`
|
|
672
|
-
}));
|
|
673
|
-
const warnings = [];
|
|
674
|
-
if (notFoundTools.length > 0) warnings.push(`Tools not found: ${notFoundTools.join(", ")}`);
|
|
675
|
-
if (ambiguousTools.length > 0) warnings.push(`Ambiguous tools (specify serverName): ${ambiguousTools.map((t) => t.toolName).join(", ")}`);
|
|
676
|
-
if (warnings.length > 0) result.warnings = warnings;
|
|
1168
|
+
if (notFoundTools.length > 0) {
|
|
1169
|
+
result.notFound = notFoundTools;
|
|
1170
|
+
result.warnings = [`Tools not found: ${notFoundTools.join(", ")}`];
|
|
1171
|
+
}
|
|
677
1172
|
return { content: [{
|
|
678
1173
|
type: "text",
|
|
679
1174
|
text: JSON.stringify(result, null, 2)
|
|
@@ -715,10 +1210,6 @@ var UseToolTool = class UseToolTool {
|
|
|
715
1210
|
toolArgs: {
|
|
716
1211
|
type: "object",
|
|
717
1212
|
description: "Arguments to pass to the tool, as discovered from describe_tools"
|
|
718
|
-
},
|
|
719
|
-
serverName: {
|
|
720
|
-
type: "string",
|
|
721
|
-
description: "Optional server name to disambiguate when multiple servers have the same tool"
|
|
722
1213
|
}
|
|
723
1214
|
},
|
|
724
1215
|
required: ["toolName"],
|
|
@@ -728,8 +1219,9 @@ var UseToolTool = class UseToolTool {
|
|
|
728
1219
|
}
|
|
729
1220
|
async execute(input) {
|
|
730
1221
|
try {
|
|
731
|
-
const { toolName, toolArgs = {}
|
|
1222
|
+
const { toolName: inputToolName, toolArgs = {} } = input;
|
|
732
1223
|
const clients = this.clientManager.getAllClients();
|
|
1224
|
+
const { serverName, actualToolName } = parseToolName(inputToolName);
|
|
733
1225
|
if (serverName) {
|
|
734
1226
|
const client$1 = this.clientManager.getClient(serverName);
|
|
735
1227
|
if (!client$1) return {
|
|
@@ -739,20 +1231,20 @@ var UseToolTool = class UseToolTool {
|
|
|
739
1231
|
}],
|
|
740
1232
|
isError: true
|
|
741
1233
|
};
|
|
742
|
-
if (client$1.toolBlacklist && client$1.toolBlacklist.includes(
|
|
1234
|
+
if (client$1.toolBlacklist && client$1.toolBlacklist.includes(actualToolName)) return {
|
|
743
1235
|
content: [{
|
|
744
1236
|
type: "text",
|
|
745
|
-
text: `Tool "${
|
|
1237
|
+
text: `Tool "${actualToolName}" is blacklisted on server "${serverName}" and cannot be executed.`
|
|
746
1238
|
}],
|
|
747
1239
|
isError: true
|
|
748
1240
|
};
|
|
749
1241
|
try {
|
|
750
|
-
return await client$1.callTool(
|
|
1242
|
+
return await client$1.callTool(actualToolName, toolArgs);
|
|
751
1243
|
} catch (error) {
|
|
752
1244
|
return {
|
|
753
1245
|
content: [{
|
|
754
1246
|
type: "text",
|
|
755
|
-
text: `Failed to call tool "${
|
|
1247
|
+
text: `Failed to call tool "${actualToolName}" on server "${serverName}": ${error instanceof Error ? error.message : "Unknown error"}`
|
|
756
1248
|
}],
|
|
757
1249
|
isError: true
|
|
758
1250
|
};
|
|
@@ -761,8 +1253,8 @@ var UseToolTool = class UseToolTool {
|
|
|
761
1253
|
const matchingServers = [];
|
|
762
1254
|
const results = await Promise.all(clients.map(async (client$1) => {
|
|
763
1255
|
try {
|
|
764
|
-
if (client$1.toolBlacklist && client$1.toolBlacklist.includes(
|
|
765
|
-
if ((await client$1.listTools()).some((t) => t.name ===
|
|
1256
|
+
if (client$1.toolBlacklist && client$1.toolBlacklist.includes(actualToolName)) return null;
|
|
1257
|
+
if ((await client$1.listTools()).some((t) => t.name === actualToolName)) return client$1.serverName;
|
|
766
1258
|
} catch (error) {
|
|
767
1259
|
console.error(`Failed to list tools from ${client$1.serverName}:`, error);
|
|
768
1260
|
}
|
|
@@ -772,14 +1264,14 @@ var UseToolTool = class UseToolTool {
|
|
|
772
1264
|
if (matchingServers.length === 0) return {
|
|
773
1265
|
content: [{
|
|
774
1266
|
type: "text",
|
|
775
|
-
text: `Tool "${
|
|
1267
|
+
text: `Tool "${actualToolName}" not found on any connected server. Use describe_tools to see available tools.`
|
|
776
1268
|
}],
|
|
777
1269
|
isError: true
|
|
778
1270
|
};
|
|
779
1271
|
if (matchingServers.length > 1) return {
|
|
780
1272
|
content: [{
|
|
781
1273
|
type: "text",
|
|
782
|
-
text: `
|
|
1274
|
+
text: `Tool "${actualToolName}" found on multiple servers. Use prefixed format to specify: ${matchingServers.map((s) => `${s}__${actualToolName}`).join(", ")}`
|
|
783
1275
|
}],
|
|
784
1276
|
isError: true
|
|
785
1277
|
};
|
|
@@ -793,12 +1285,12 @@ var UseToolTool = class UseToolTool {
|
|
|
793
1285
|
isError: true
|
|
794
1286
|
};
|
|
795
1287
|
try {
|
|
796
|
-
return await client.callTool(
|
|
1288
|
+
return await client.callTool(actualToolName, toolArgs);
|
|
797
1289
|
} catch (error) {
|
|
798
1290
|
return {
|
|
799
1291
|
content: [{
|
|
800
1292
|
type: "text",
|
|
801
|
-
text: `Failed to call tool "${
|
|
1293
|
+
text: `Failed to call tool "${actualToolName}": ${error instanceof Error ? error.message : "Unknown error"}`
|
|
802
1294
|
}],
|
|
803
1295
|
isError: true
|
|
804
1296
|
};
|
|
@@ -837,7 +1329,10 @@ async function createServer(options) {
|
|
|
837
1329
|
}, { capabilities: { tools: {} } });
|
|
838
1330
|
const clientManager = new McpClientManagerService();
|
|
839
1331
|
if (options?.configFilePath) try {
|
|
840
|
-
const config = await new ConfigFetcherService({
|
|
1332
|
+
const config = await new ConfigFetcherService({
|
|
1333
|
+
configFilePath: options.configFilePath,
|
|
1334
|
+
useCache: !options.noCache
|
|
1335
|
+
}).fetchConfiguration(options.noCache || false);
|
|
841
1336
|
const connectionPromises = Object.entries(config.mcpServers).map(async ([serverName, serverConfig]) => {
|
|
842
1337
|
try {
|
|
843
1338
|
await clientManager.connectToServer(serverName, serverConfig);
|
|
@@ -990,14 +1485,14 @@ var SseTransportHandler = class {
|
|
|
990
1485
|
}
|
|
991
1486
|
}
|
|
992
1487
|
async start() {
|
|
993
|
-
return new Promise((resolve, reject) => {
|
|
1488
|
+
return new Promise((resolve$1, reject) => {
|
|
994
1489
|
try {
|
|
995
1490
|
this.server = this.app.listen(this.config.port, this.config.host, () => {
|
|
996
1491
|
console.error(`@agiflowai/one-mcp MCP server started with SSE transport on http://${this.config.host}:${this.config.port}`);
|
|
997
1492
|
console.error(`SSE endpoint: http://${this.config.host}:${this.config.port}/sse`);
|
|
998
1493
|
console.error(`Messages endpoint: http://${this.config.host}:${this.config.port}/messages`);
|
|
999
1494
|
console.error(`Health check: http://${this.config.host}:${this.config.port}/health`);
|
|
1000
|
-
resolve();
|
|
1495
|
+
resolve$1();
|
|
1001
1496
|
});
|
|
1002
1497
|
this.server.on("error", (error) => {
|
|
1003
1498
|
reject(error);
|
|
@@ -1008,17 +1503,17 @@ var SseTransportHandler = class {
|
|
|
1008
1503
|
});
|
|
1009
1504
|
}
|
|
1010
1505
|
async stop() {
|
|
1011
|
-
return new Promise((resolve, reject) => {
|
|
1506
|
+
return new Promise((resolve$1, reject) => {
|
|
1012
1507
|
if (this.server) {
|
|
1013
1508
|
this.sessionManager.clear();
|
|
1014
1509
|
this.server.close((err) => {
|
|
1015
1510
|
if (err) reject(err);
|
|
1016
1511
|
else {
|
|
1017
1512
|
this.server = null;
|
|
1018
|
-
resolve();
|
|
1513
|
+
resolve$1();
|
|
1019
1514
|
}
|
|
1020
1515
|
});
|
|
1021
|
-
} else resolve();
|
|
1516
|
+
} else resolve$1();
|
|
1022
1517
|
});
|
|
1023
1518
|
}
|
|
1024
1519
|
getPort() {
|
|
@@ -1170,12 +1665,12 @@ var HttpTransportHandler = class {
|
|
|
1170
1665
|
this.sessionManager.deleteSession(sessionId);
|
|
1171
1666
|
}
|
|
1172
1667
|
async start() {
|
|
1173
|
-
return new Promise((resolve, reject) => {
|
|
1668
|
+
return new Promise((resolve$1, reject) => {
|
|
1174
1669
|
try {
|
|
1175
1670
|
this.server = this.app.listen(this.config.port, this.config.host, () => {
|
|
1176
1671
|
console.error(`@agiflowai/one-mcp MCP server started on http://${this.config.host}:${this.config.port}/mcp`);
|
|
1177
1672
|
console.error(`Health check: http://${this.config.host}:${this.config.port}/health`);
|
|
1178
|
-
resolve();
|
|
1673
|
+
resolve$1();
|
|
1179
1674
|
});
|
|
1180
1675
|
this.server.on("error", (error) => {
|
|
1181
1676
|
reject(error);
|
|
@@ -1186,17 +1681,17 @@ var HttpTransportHandler = class {
|
|
|
1186
1681
|
});
|
|
1187
1682
|
}
|
|
1188
1683
|
async stop() {
|
|
1189
|
-
return new Promise((resolve, reject) => {
|
|
1684
|
+
return new Promise((resolve$1, reject) => {
|
|
1190
1685
|
if (this.server) {
|
|
1191
1686
|
this.sessionManager.clear();
|
|
1192
1687
|
this.server.close((err) => {
|
|
1193
1688
|
if (err) reject(err);
|
|
1194
1689
|
else {
|
|
1195
1690
|
this.server = null;
|
|
1196
|
-
resolve();
|
|
1691
|
+
resolve$1();
|
|
1197
1692
|
}
|
|
1198
1693
|
});
|
|
1199
|
-
} else resolve();
|
|
1694
|
+
} else resolve$1();
|
|
1200
1695
|
});
|
|
1201
1696
|
}
|
|
1202
1697
|
getPort() {
|
|
@@ -1208,4 +1703,4 @@ var HttpTransportHandler = class {
|
|
|
1208
1703
|
};
|
|
1209
1704
|
|
|
1210
1705
|
//#endregion
|
|
1211
|
-
export {
|
|
1706
|
+
export { findConfigFile as a, createServer as i, SseTransportHandler as n, McpClientManagerService as o, StdioTransportHandler as r, ConfigFetcherService as s, HttpTransportHandler as t };
|