@agiflowai/one-mcp 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/README.md +500 -0
- package/dist/cli.cjs +497 -0
- package/dist/cli.d.cts +1 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +498 -0
- package/dist/http-B1EDyxR_.mjs +1211 -0
- package/dist/http-B5WVqLzz.cjs +1277 -0
- package/dist/index.cjs +6 -0
- package/dist/index.d.cts +172 -0
- package/dist/index.d.mts +172 -0
- package/dist/index.mjs +3 -0
- package/package.json +57 -0
|
@@ -0,0 +1,1277 @@
|
|
|
1
|
+
//#region rolldown:runtime
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __copyProps = (to, from, except, desc) => {
|
|
9
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
10
|
+
key = keys[i];
|
|
11
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
12
|
+
get: ((k) => from[k]).bind(null, key),
|
|
13
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
19
|
+
value: mod,
|
|
20
|
+
enumerable: true
|
|
21
|
+
}) : target, mod));
|
|
22
|
+
|
|
23
|
+
//#endregion
|
|
24
|
+
let __modelcontextprotocol_sdk_server_index_js = require("@modelcontextprotocol/sdk/server/index.js");
|
|
25
|
+
let __modelcontextprotocol_sdk_types_js = require("@modelcontextprotocol/sdk/types.js");
|
|
26
|
+
let node_fs_promises = require("node:fs/promises");
|
|
27
|
+
let node_fs = require("node:fs");
|
|
28
|
+
let js_yaml = require("js-yaml");
|
|
29
|
+
js_yaml = __toESM(js_yaml);
|
|
30
|
+
let zod = require("zod");
|
|
31
|
+
let __modelcontextprotocol_sdk_client_index_js = require("@modelcontextprotocol/sdk/client/index.js");
|
|
32
|
+
let __modelcontextprotocol_sdk_client_stdio_js = require("@modelcontextprotocol/sdk/client/stdio.js");
|
|
33
|
+
let __modelcontextprotocol_sdk_client_sse_js = require("@modelcontextprotocol/sdk/client/sse.js");
|
|
34
|
+
let __modelcontextprotocol_sdk_server_stdio_js = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
35
|
+
let __modelcontextprotocol_sdk_server_sse_js = require("@modelcontextprotocol/sdk/server/sse.js");
|
|
36
|
+
let express = require("express");
|
|
37
|
+
express = __toESM(express);
|
|
38
|
+
let node_crypto = require("node:crypto");
|
|
39
|
+
let __modelcontextprotocol_sdk_server_streamableHttp_js = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
|
|
40
|
+
|
|
41
|
+
//#region src/utils/mcpConfigSchema.ts
|
|
42
|
+
/**
|
|
43
|
+
* mcpConfigSchema Utilities
|
|
44
|
+
*
|
|
45
|
+
* DESIGN PATTERNS:
|
|
46
|
+
* - Schema-based validation using Zod
|
|
47
|
+
* - Pure functions with no side effects
|
|
48
|
+
* - Type inference from schemas
|
|
49
|
+
* - Transformation from Claude Code format to internal format
|
|
50
|
+
*
|
|
51
|
+
* CODING STANDARDS:
|
|
52
|
+
* - Export individual functions and schemas
|
|
53
|
+
* - Use descriptive function names with verbs
|
|
54
|
+
* - Add JSDoc comments for complex logic
|
|
55
|
+
* - Keep functions small and focused
|
|
56
|
+
*
|
|
57
|
+
* AVOID:
|
|
58
|
+
* - Side effects (mutating external state)
|
|
59
|
+
* - Stateful logic (use services for state)
|
|
60
|
+
* - Loosely typed configs (use Zod for runtime safety)
|
|
61
|
+
*/
|
|
62
|
+
/**
|
|
63
|
+
* Interpolate environment variables in a string
|
|
64
|
+
* Supports ${VAR_NAME} syntax
|
|
65
|
+
*
|
|
66
|
+
* This function replaces environment variable placeholders with their actual values.
|
|
67
|
+
* If an environment variable is not defined, the placeholder is kept as-is and a warning is logged.
|
|
68
|
+
*
|
|
69
|
+
* Examples:
|
|
70
|
+
* - "${HOME}/data" → "/Users/username/data"
|
|
71
|
+
* - "Bearer ${API_KEY}" → "Bearer sk-abc123xyz"
|
|
72
|
+
* - "${DATABASE_URL}/api" → "postgres://localhost:5432/mydb/api"
|
|
73
|
+
*
|
|
74
|
+
* Supported locations for environment variable interpolation:
|
|
75
|
+
* - Stdio config: command, args, env values
|
|
76
|
+
* - HTTP/SSE config: url, header values
|
|
77
|
+
*
|
|
78
|
+
* @param value - String that may contain environment variable references
|
|
79
|
+
* @returns String with environment variables replaced
|
|
80
|
+
*/
|
|
81
|
+
function interpolateEnvVars(value) {
|
|
82
|
+
return value.replace(/\$\{([^}]+)\}/g, (_, varName) => {
|
|
83
|
+
const envValue = process.env[varName];
|
|
84
|
+
if (envValue === void 0) {
|
|
85
|
+
console.warn(`Environment variable ${varName} is not defined, keeping placeholder`);
|
|
86
|
+
return `\${${varName}}`;
|
|
87
|
+
}
|
|
88
|
+
return envValue;
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Recursively interpolate environment variables in an object
|
|
93
|
+
*
|
|
94
|
+
* @param obj - Object that may contain environment variable references
|
|
95
|
+
* @returns Object with environment variables replaced
|
|
96
|
+
*/
|
|
97
|
+
function interpolateEnvVarsInObject(obj) {
|
|
98
|
+
if (typeof obj === "string") return interpolateEnvVars(obj);
|
|
99
|
+
if (Array.isArray(obj)) return obj.map((item) => interpolateEnvVarsInObject(item));
|
|
100
|
+
if (obj !== null && typeof obj === "object") {
|
|
101
|
+
const result = {};
|
|
102
|
+
for (const [key, value] of Object.entries(obj)) result[key] = interpolateEnvVarsInObject(value);
|
|
103
|
+
return result;
|
|
104
|
+
}
|
|
105
|
+
return obj;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Claude Code / Claude Desktop standard MCP config format
|
|
109
|
+
* This is the format users write in their config files
|
|
110
|
+
*/
|
|
111
|
+
const AdditionalConfigSchema = zod.z.object({
|
|
112
|
+
instruction: zod.z.string().optional(),
|
|
113
|
+
toolBlacklist: zod.z.array(zod.z.string()).optional(),
|
|
114
|
+
omitToolDescription: zod.z.boolean().optional()
|
|
115
|
+
}).optional();
|
|
116
|
+
const ClaudeCodeStdioServerSchema = zod.z.object({
|
|
117
|
+
command: zod.z.string(),
|
|
118
|
+
args: zod.z.array(zod.z.string()).optional(),
|
|
119
|
+
env: zod.z.record(zod.z.string(), zod.z.string()).optional(),
|
|
120
|
+
disabled: zod.z.boolean().optional(),
|
|
121
|
+
instruction: zod.z.string().optional(),
|
|
122
|
+
config: AdditionalConfigSchema
|
|
123
|
+
});
|
|
124
|
+
const ClaudeCodeHttpServerSchema = zod.z.object({
|
|
125
|
+
url: zod.z.string().url(),
|
|
126
|
+
headers: zod.z.record(zod.z.string(), zod.z.string()).optional(),
|
|
127
|
+
type: zod.z.enum(["http", "sse"]).optional(),
|
|
128
|
+
disabled: zod.z.boolean().optional(),
|
|
129
|
+
instruction: zod.z.string().optional(),
|
|
130
|
+
config: AdditionalConfigSchema
|
|
131
|
+
});
|
|
132
|
+
const ClaudeCodeServerConfigSchema = zod.z.union([ClaudeCodeStdioServerSchema, ClaudeCodeHttpServerSchema]);
|
|
133
|
+
/**
|
|
134
|
+
* Full Claude Code MCP configuration schema
|
|
135
|
+
*/
|
|
136
|
+
const ClaudeCodeMcpConfigSchema = zod.z.object({ mcpServers: zod.z.record(zod.z.string(), ClaudeCodeServerConfigSchema) });
|
|
137
|
+
/**
|
|
138
|
+
* Internal MCP config format
|
|
139
|
+
* This is the normalized format used internally by the proxy
|
|
140
|
+
*/
|
|
141
|
+
const McpStdioConfigSchema = zod.z.object({
|
|
142
|
+
command: zod.z.string(),
|
|
143
|
+
args: zod.z.array(zod.z.string()).optional(),
|
|
144
|
+
env: zod.z.record(zod.z.string(), zod.z.string()).optional()
|
|
145
|
+
});
|
|
146
|
+
const McpHttpConfigSchema = zod.z.object({
|
|
147
|
+
url: zod.z.string().url(),
|
|
148
|
+
headers: zod.z.record(zod.z.string(), zod.z.string()).optional()
|
|
149
|
+
});
|
|
150
|
+
const McpSseConfigSchema = zod.z.object({
|
|
151
|
+
url: zod.z.string().url(),
|
|
152
|
+
headers: zod.z.record(zod.z.string(), zod.z.string()).optional()
|
|
153
|
+
});
|
|
154
|
+
const McpServerConfigSchema = zod.z.discriminatedUnion("transport", [
|
|
155
|
+
zod.z.object({
|
|
156
|
+
name: zod.z.string(),
|
|
157
|
+
instruction: zod.z.string().optional(),
|
|
158
|
+
toolBlacklist: zod.z.array(zod.z.string()).optional(),
|
|
159
|
+
omitToolDescription: zod.z.boolean().optional(),
|
|
160
|
+
transport: zod.z.literal("stdio"),
|
|
161
|
+
config: McpStdioConfigSchema
|
|
162
|
+
}),
|
|
163
|
+
zod.z.object({
|
|
164
|
+
name: zod.z.string(),
|
|
165
|
+
instruction: zod.z.string().optional(),
|
|
166
|
+
toolBlacklist: zod.z.array(zod.z.string()).optional(),
|
|
167
|
+
omitToolDescription: zod.z.boolean().optional(),
|
|
168
|
+
transport: zod.z.literal("http"),
|
|
169
|
+
config: McpHttpConfigSchema
|
|
170
|
+
}),
|
|
171
|
+
zod.z.object({
|
|
172
|
+
name: zod.z.string(),
|
|
173
|
+
instruction: zod.z.string().optional(),
|
|
174
|
+
toolBlacklist: zod.z.array(zod.z.string()).optional(),
|
|
175
|
+
omitToolDescription: zod.z.boolean().optional(),
|
|
176
|
+
transport: zod.z.literal("sse"),
|
|
177
|
+
config: McpSseConfigSchema
|
|
178
|
+
})
|
|
179
|
+
]);
|
|
180
|
+
/**
|
|
181
|
+
* Full internal MCP configuration schema
|
|
182
|
+
*/
|
|
183
|
+
const InternalMcpConfigSchema = zod.z.object({ mcpServers: zod.z.record(zod.z.string(), McpServerConfigSchema) });
|
|
184
|
+
/**
|
|
185
|
+
* Transform Claude Code config to internal format
|
|
186
|
+
* Converts standard Claude Code MCP configuration to normalized internal format
|
|
187
|
+
*
|
|
188
|
+
* @param claudeConfig - Claude Code format configuration
|
|
189
|
+
* @returns Internal format configuration
|
|
190
|
+
*/
|
|
191
|
+
function transformClaudeCodeConfig(claudeConfig) {
|
|
192
|
+
const transformedServers = {};
|
|
193
|
+
for (const [serverName, serverConfig] of Object.entries(claudeConfig.mcpServers)) {
|
|
194
|
+
if ("disabled" in serverConfig && serverConfig.disabled === true) continue;
|
|
195
|
+
if ("command" in serverConfig) {
|
|
196
|
+
const stdioConfig = serverConfig;
|
|
197
|
+
const interpolatedCommand = interpolateEnvVars(stdioConfig.command);
|
|
198
|
+
const interpolatedArgs = stdioConfig.args?.map((arg) => interpolateEnvVars(arg));
|
|
199
|
+
const interpolatedEnv = stdioConfig.env ? interpolateEnvVarsInObject(stdioConfig.env) : void 0;
|
|
200
|
+
transformedServers[serverName] = {
|
|
201
|
+
name: serverName,
|
|
202
|
+
instruction: stdioConfig.instruction || stdioConfig.config?.instruction,
|
|
203
|
+
toolBlacklist: stdioConfig.config?.toolBlacklist,
|
|
204
|
+
omitToolDescription: stdioConfig.config?.omitToolDescription,
|
|
205
|
+
transport: "stdio",
|
|
206
|
+
config: {
|
|
207
|
+
command: interpolatedCommand,
|
|
208
|
+
args: interpolatedArgs,
|
|
209
|
+
env: interpolatedEnv
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
} else if ("url" in serverConfig) {
|
|
213
|
+
const httpConfig = serverConfig;
|
|
214
|
+
const transport = httpConfig.type === "sse" ? "sse" : "http";
|
|
215
|
+
const interpolatedUrl = interpolateEnvVars(httpConfig.url);
|
|
216
|
+
const interpolatedHeaders = httpConfig.headers ? interpolateEnvVarsInObject(httpConfig.headers) : void 0;
|
|
217
|
+
transformedServers[serverName] = {
|
|
218
|
+
name: serverName,
|
|
219
|
+
instruction: httpConfig.instruction || httpConfig.config?.instruction,
|
|
220
|
+
toolBlacklist: httpConfig.config?.toolBlacklist,
|
|
221
|
+
omitToolDescription: httpConfig.config?.omitToolDescription,
|
|
222
|
+
transport,
|
|
223
|
+
config: {
|
|
224
|
+
url: interpolatedUrl,
|
|
225
|
+
headers: interpolatedHeaders
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return { mcpServers: transformedServers };
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Parse and validate MCP config from raw JSON
|
|
234
|
+
* Validates against Claude Code format, transforms to internal format, and validates result
|
|
235
|
+
*
|
|
236
|
+
* @param rawConfig - Raw JSON configuration object
|
|
237
|
+
* @returns Validated and transformed internal configuration
|
|
238
|
+
* @throws ZodError if validation fails
|
|
239
|
+
*/
|
|
240
|
+
function parseMcpConfig(rawConfig) {
|
|
241
|
+
const internalConfig = transformClaudeCodeConfig(ClaudeCodeMcpConfigSchema.parse(rawConfig));
|
|
242
|
+
return InternalMcpConfigSchema.parse(internalConfig);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
//#endregion
|
|
246
|
+
//#region src/services/ConfigFetcherService.ts
|
|
247
|
+
/**
|
|
248
|
+
* ConfigFetcherService
|
|
249
|
+
*
|
|
250
|
+
* DESIGN PATTERNS:
|
|
251
|
+
* - Service pattern for business logic encapsulation
|
|
252
|
+
* - Single responsibility principle
|
|
253
|
+
* - Caching pattern for performance optimization
|
|
254
|
+
*
|
|
255
|
+
* CODING STANDARDS:
|
|
256
|
+
* - Use async/await for asynchronous operations
|
|
257
|
+
* - Throw descriptive errors for error cases
|
|
258
|
+
* - Keep methods focused and well-named
|
|
259
|
+
* - Document complex logic with comments
|
|
260
|
+
*
|
|
261
|
+
* AVOID:
|
|
262
|
+
* - Mixing concerns (keep focused on single domain)
|
|
263
|
+
* - Direct tool implementation (services should be tool-agnostic)
|
|
264
|
+
*/
|
|
265
|
+
/**
|
|
266
|
+
* Service for fetching and caching MCP server configurations from local file
|
|
267
|
+
*/
|
|
268
|
+
var ConfigFetcherService = class {
|
|
269
|
+
configFilePath;
|
|
270
|
+
cacheTtlMs;
|
|
271
|
+
cachedConfig = null;
|
|
272
|
+
lastFetchTime = 0;
|
|
273
|
+
constructor(options) {
|
|
274
|
+
this.configFilePath = options.configFilePath;
|
|
275
|
+
this.cacheTtlMs = options.cacheTtlMs || 6e4;
|
|
276
|
+
if (!this.configFilePath) throw new Error("configFilePath must be provided");
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Fetch MCP configuration from local file with caching
|
|
280
|
+
* @param forceRefresh - Force reload from source, bypassing cache
|
|
281
|
+
*/
|
|
282
|
+
async fetchConfiguration(forceRefresh = false) {
|
|
283
|
+
const now = Date.now();
|
|
284
|
+
if (!forceRefresh && this.cachedConfig && now - this.lastFetchTime < this.cacheTtlMs) return this.cachedConfig;
|
|
285
|
+
const config = await this.loadFromFile();
|
|
286
|
+
if (!config.mcpServers || typeof config.mcpServers !== "object") throw new Error("Invalid MCP configuration: missing or invalid mcpServers");
|
|
287
|
+
this.cachedConfig = config;
|
|
288
|
+
this.lastFetchTime = now;
|
|
289
|
+
return config;
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Load configuration from a local file (supports JSON and YAML)
|
|
293
|
+
*/
|
|
294
|
+
async loadFromFile() {
|
|
295
|
+
if (!this.configFilePath) throw new Error("No config file path provided");
|
|
296
|
+
if (!(0, node_fs.existsSync)(this.configFilePath)) throw new Error(`Config file not found: ${this.configFilePath}`);
|
|
297
|
+
try {
|
|
298
|
+
const content = await (0, node_fs_promises.readFile)(this.configFilePath, "utf-8");
|
|
299
|
+
let rawConfig;
|
|
300
|
+
if (this.configFilePath.endsWith(".yaml") || this.configFilePath.endsWith(".yml")) rawConfig = js_yaml.default.load(content);
|
|
301
|
+
else rawConfig = JSON.parse(content);
|
|
302
|
+
return parseMcpConfig(rawConfig);
|
|
303
|
+
} catch (error) {
|
|
304
|
+
if (error instanceof Error) throw new Error(`Failed to load config file: ${error.message}`);
|
|
305
|
+
throw new Error("Failed to load config file: Unknown error");
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Clear the cached configuration
|
|
310
|
+
*/
|
|
311
|
+
clearCache() {
|
|
312
|
+
this.cachedConfig = null;
|
|
313
|
+
this.lastFetchTime = 0;
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Check if cache is valid
|
|
317
|
+
*/
|
|
318
|
+
isCacheValid() {
|
|
319
|
+
const now = Date.now();
|
|
320
|
+
return this.cachedConfig !== null && now - this.lastFetchTime < this.cacheTtlMs;
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
//#endregion
|
|
325
|
+
//#region src/services/McpClientManagerService.ts
|
|
326
|
+
/**
|
|
327
|
+
* MCP Client wrapper for managing individual server connections
|
|
328
|
+
*/
|
|
329
|
+
var McpClient = class {
|
|
330
|
+
serverName;
|
|
331
|
+
serverInstruction;
|
|
332
|
+
toolBlacklist;
|
|
333
|
+
omitToolDescription;
|
|
334
|
+
transport;
|
|
335
|
+
client;
|
|
336
|
+
childProcess;
|
|
337
|
+
connected = false;
|
|
338
|
+
constructor(serverName, transport, client, config) {
|
|
339
|
+
this.serverName = serverName;
|
|
340
|
+
this.serverInstruction = config.instruction;
|
|
341
|
+
this.toolBlacklist = config.toolBlacklist;
|
|
342
|
+
this.omitToolDescription = config.omitToolDescription;
|
|
343
|
+
this.transport = transport;
|
|
344
|
+
this.client = client;
|
|
345
|
+
}
|
|
346
|
+
setChildProcess(process$1) {
|
|
347
|
+
this.childProcess = process$1;
|
|
348
|
+
}
|
|
349
|
+
setConnected(connected) {
|
|
350
|
+
this.connected = connected;
|
|
351
|
+
}
|
|
352
|
+
async listTools() {
|
|
353
|
+
if (!this.connected) throw new Error(`Client for ${this.serverName} is not connected`);
|
|
354
|
+
return (await this.client.listTools()).tools;
|
|
355
|
+
}
|
|
356
|
+
async listResources() {
|
|
357
|
+
if (!this.connected) throw new Error(`Client for ${this.serverName} is not connected`);
|
|
358
|
+
return (await this.client.listResources()).resources;
|
|
359
|
+
}
|
|
360
|
+
async listPrompts() {
|
|
361
|
+
if (!this.connected) throw new Error(`Client for ${this.serverName} is not connected`);
|
|
362
|
+
return (await this.client.listPrompts()).prompts;
|
|
363
|
+
}
|
|
364
|
+
async callTool(name, args) {
|
|
365
|
+
if (!this.connected) throw new Error(`Client for ${this.serverName} is not connected`);
|
|
366
|
+
return await this.client.callTool({
|
|
367
|
+
name,
|
|
368
|
+
arguments: args
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
async readResource(uri) {
|
|
372
|
+
if (!this.connected) throw new Error(`Client for ${this.serverName} is not connected`);
|
|
373
|
+
return await this.client.readResource({ uri });
|
|
374
|
+
}
|
|
375
|
+
async getPrompt(name, args) {
|
|
376
|
+
if (!this.connected) throw new Error(`Client for ${this.serverName} is not connected`);
|
|
377
|
+
return await this.client.getPrompt({
|
|
378
|
+
name,
|
|
379
|
+
arguments: args
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
async close() {
|
|
383
|
+
if (this.childProcess) this.childProcess.kill();
|
|
384
|
+
await this.client.close();
|
|
385
|
+
this.connected = false;
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
/**
|
|
389
|
+
* Service for managing MCP client connections to remote servers
|
|
390
|
+
*/
|
|
391
|
+
var McpClientManagerService = class {
|
|
392
|
+
clients = /* @__PURE__ */ new Map();
|
|
393
|
+
constructor() {
|
|
394
|
+
process.on("exit", () => {
|
|
395
|
+
this.cleanupOnExit();
|
|
396
|
+
});
|
|
397
|
+
process.on("SIGINT", () => {
|
|
398
|
+
this.cleanupOnExit();
|
|
399
|
+
process.exit(0);
|
|
400
|
+
});
|
|
401
|
+
process.on("SIGTERM", () => {
|
|
402
|
+
this.cleanupOnExit();
|
|
403
|
+
process.exit(0);
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Cleanup all resources on exit (child processes)
|
|
408
|
+
*/
|
|
409
|
+
cleanupOnExit() {
|
|
410
|
+
for (const [serverName, client] of this.clients) try {
|
|
411
|
+
const childProcess = client["childProcess"];
|
|
412
|
+
if (childProcess && !childProcess.killed) {
|
|
413
|
+
console.error(`Killing stdio MCP server: ${serverName} (PID: ${childProcess.pid})`);
|
|
414
|
+
childProcess.kill("SIGTERM");
|
|
415
|
+
setTimeout(() => {
|
|
416
|
+
if (!childProcess.killed) {
|
|
417
|
+
console.error(`Force killing stdio MCP server: ${serverName} (PID: ${childProcess.pid})`);
|
|
418
|
+
childProcess.kill("SIGKILL");
|
|
419
|
+
}
|
|
420
|
+
}, 1e3);
|
|
421
|
+
}
|
|
422
|
+
} catch (error) {
|
|
423
|
+
console.error(`Failed to kill child process for ${serverName}:`, error);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Connect to an MCP server based on its configuration with timeout
|
|
428
|
+
*/
|
|
429
|
+
async connectToServer(serverName, config, timeoutMs = 1e4) {
|
|
430
|
+
if (this.clients.has(serverName)) throw new Error(`Client for ${serverName} is already connected`);
|
|
431
|
+
const client = new __modelcontextprotocol_sdk_client_index_js.Client({
|
|
432
|
+
name: `@agiflowai/one-mcp-client`,
|
|
433
|
+
version: "0.1.0"
|
|
434
|
+
}, { capabilities: {} });
|
|
435
|
+
const mcpClient = new McpClient(serverName, config.transport, client, {
|
|
436
|
+
instruction: config.instruction,
|
|
437
|
+
toolBlacklist: config.toolBlacklist,
|
|
438
|
+
omitToolDescription: config.omitToolDescription
|
|
439
|
+
});
|
|
440
|
+
try {
|
|
441
|
+
await Promise.race([this.performConnection(mcpClient, config), new Promise((_, reject) => setTimeout(() => reject(/* @__PURE__ */ new Error(`Connection timeout after ${timeoutMs}ms`)), timeoutMs))]);
|
|
442
|
+
mcpClient.setConnected(true);
|
|
443
|
+
if (!mcpClient.serverInstruction) try {
|
|
444
|
+
const serverInstruction = mcpClient["client"].getInstructions();
|
|
445
|
+
if (serverInstruction) mcpClient.serverInstruction = serverInstruction;
|
|
446
|
+
} catch (error) {
|
|
447
|
+
console.error(`Failed to get server instruction from ${serverName}:`, error);
|
|
448
|
+
}
|
|
449
|
+
this.clients.set(serverName, mcpClient);
|
|
450
|
+
} catch (error) {
|
|
451
|
+
await mcpClient.close();
|
|
452
|
+
throw error;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Perform the actual connection to MCP server
|
|
457
|
+
*/
|
|
458
|
+
async performConnection(mcpClient, config) {
|
|
459
|
+
if (config.transport === "stdio") await this.connectStdioClient(mcpClient, config.config);
|
|
460
|
+
else if (config.transport === "sse") await this.connectSseClient(mcpClient, config.config);
|
|
461
|
+
else throw new Error(`Unsupported transport type: ${config.transport}`);
|
|
462
|
+
}
|
|
463
|
+
async connectStdioClient(mcpClient, config) {
|
|
464
|
+
const transport = new __modelcontextprotocol_sdk_client_stdio_js.StdioClientTransport({
|
|
465
|
+
command: config.command,
|
|
466
|
+
args: config.args,
|
|
467
|
+
env: config.env
|
|
468
|
+
});
|
|
469
|
+
await mcpClient["client"].connect(transport);
|
|
470
|
+
const childProcess = transport["_process"];
|
|
471
|
+
if (childProcess) mcpClient.setChildProcess(childProcess);
|
|
472
|
+
}
|
|
473
|
+
async connectSseClient(mcpClient, config) {
|
|
474
|
+
const transport = new __modelcontextprotocol_sdk_client_sse_js.SSEClientTransport(new URL(config.url));
|
|
475
|
+
await mcpClient["client"].connect(transport);
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* Get a connected client by server name
|
|
479
|
+
*/
|
|
480
|
+
getClient(serverName) {
|
|
481
|
+
return this.clients.get(serverName);
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* Get all connected clients
|
|
485
|
+
*/
|
|
486
|
+
getAllClients() {
|
|
487
|
+
return Array.from(this.clients.values());
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Disconnect from a specific server
|
|
491
|
+
*/
|
|
492
|
+
async disconnectServer(serverName) {
|
|
493
|
+
const client = this.clients.get(serverName);
|
|
494
|
+
if (client) {
|
|
495
|
+
await client.close();
|
|
496
|
+
this.clients.delete(serverName);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Disconnect from all servers
|
|
501
|
+
*/
|
|
502
|
+
async disconnectAll() {
|
|
503
|
+
const disconnectPromises = Array.from(this.clients.values()).map((client) => client.close());
|
|
504
|
+
await Promise.all(disconnectPromises);
|
|
505
|
+
this.clients.clear();
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Check if a server is connected
|
|
509
|
+
*/
|
|
510
|
+
isConnected(serverName) {
|
|
511
|
+
return this.clients.has(serverName);
|
|
512
|
+
}
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
//#endregion
|
|
516
|
+
//#region src/tools/DescribeToolsTool.ts
|
|
517
|
+
var DescribeToolsTool = class DescribeToolsTool {
|
|
518
|
+
static TOOL_NAME = "describe_tools";
|
|
519
|
+
clientManager;
|
|
520
|
+
constructor(clientManager) {
|
|
521
|
+
this.clientManager = clientManager;
|
|
522
|
+
}
|
|
523
|
+
async getDefinition() {
|
|
524
|
+
const clients = this.clientManager.getAllClients();
|
|
525
|
+
const serverDescriptions = await Promise.all(clients.map(async (client) => {
|
|
526
|
+
try {
|
|
527
|
+
const tools = await client.listTools();
|
|
528
|
+
const blacklist = new Set(client.toolBlacklist || []);
|
|
529
|
+
const filteredTools = tools.filter((t) => !blacklist.has(t.name));
|
|
530
|
+
const toolList = client.omitToolDescription ? filteredTools.map((t) => t.name).join(", ") : filteredTools.map((t) => `${t.name}: ${t.description || "No description"}`).join("\n");
|
|
531
|
+
const instructionLine = client.serverInstruction ? `\n- Description: ${client.serverInstruction}` : "";
|
|
532
|
+
return `\n\n### Server: ${client.serverName}${instructionLine}\n- Available tools:${toolList ? "\n" + toolList : ""}`;
|
|
533
|
+
} catch (error) {
|
|
534
|
+
console.error(`Failed to list tools from ${client.serverName}:`, error);
|
|
535
|
+
const instructionLine = client.serverInstruction ? `\n- Description: ${client.serverInstruction}` : "";
|
|
536
|
+
return `\n\n**Server: ${client.serverName}**${instructionLine}\n`;
|
|
537
|
+
}
|
|
538
|
+
}));
|
|
539
|
+
return {
|
|
540
|
+
name: DescribeToolsTool.TOOL_NAME,
|
|
541
|
+
description: `Learn how to use multiple MCP tools before using them. Below are supported tools and capabilities.
|
|
542
|
+
|
|
543
|
+
## Available MCP Servers:${serverDescriptions.join("")}
|
|
544
|
+
|
|
545
|
+
## Usage:
|
|
546
|
+
You MUST call this tool with a list of tool names to learn how to use them properly before use_tool; this includes:
|
|
547
|
+
- Arguments schema needed to pass to the tool use
|
|
548
|
+
- Description about each tool
|
|
549
|
+
|
|
550
|
+
This tool is optimized for batch queries - you can request multiple tools at once for better performance.`,
|
|
551
|
+
inputSchema: {
|
|
552
|
+
type: "object",
|
|
553
|
+
properties: {
|
|
554
|
+
toolNames: {
|
|
555
|
+
type: "array",
|
|
556
|
+
items: { type: "string" },
|
|
557
|
+
description: "List of tool names to get detailed information about",
|
|
558
|
+
minItems: 1
|
|
559
|
+
},
|
|
560
|
+
serverName: {
|
|
561
|
+
type: "string",
|
|
562
|
+
description: "Optional server name to search within. If not specified, searches all servers."
|
|
563
|
+
}
|
|
564
|
+
},
|
|
565
|
+
required: ["toolNames"],
|
|
566
|
+
additionalProperties: false
|
|
567
|
+
}
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
async execute(input) {
|
|
571
|
+
try {
|
|
572
|
+
const { toolNames, serverName } = input;
|
|
573
|
+
const clients = this.clientManager.getAllClients();
|
|
574
|
+
if (!toolNames || toolNames.length === 0) return {
|
|
575
|
+
content: [{
|
|
576
|
+
type: "text",
|
|
577
|
+
text: "No tool names provided. Please specify at least one tool name."
|
|
578
|
+
}],
|
|
579
|
+
isError: true
|
|
580
|
+
};
|
|
581
|
+
if (serverName) {
|
|
582
|
+
const client = this.clientManager.getClient(serverName);
|
|
583
|
+
if (!client) return {
|
|
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) => {
|
|
632
|
+
try {
|
|
633
|
+
const tools = await client.listTools();
|
|
634
|
+
const blacklist = new Set(client.toolBlacklist || []);
|
|
635
|
+
const filteredTools = tools.filter((t) => !blacklist.has(t.name));
|
|
636
|
+
const matches = [];
|
|
637
|
+
for (const toolName of toolNames) {
|
|
638
|
+
if (blacklist.has(toolName)) continue;
|
|
639
|
+
const tool = filteredTools.find((t) => t.name === toolName);
|
|
640
|
+
if (tool) matches.push({
|
|
641
|
+
toolName,
|
|
642
|
+
server: client.serverName,
|
|
643
|
+
tool
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
return matches;
|
|
647
|
+
} catch (error) {
|
|
648
|
+
console.error(`Failed to list tools from ${client.serverName}:`, error);
|
|
649
|
+
return [];
|
|
650
|
+
}
|
|
651
|
+
}));
|
|
652
|
+
for (const matches of results) for (const match of matches) {
|
|
653
|
+
if (!toolMatches.has(match.toolName)) toolMatches.set(match.toolName, []);
|
|
654
|
+
toolMatches.get(match.toolName).push({
|
|
655
|
+
server: match.server,
|
|
656
|
+
tool: match.tool
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
const ambiguousTools = [];
|
|
660
|
+
for (const toolName of toolNames) {
|
|
661
|
+
const matches = toolMatches.get(toolName);
|
|
662
|
+
if (!matches || matches.length === 0) continue;
|
|
663
|
+
if (matches.length === 1) {
|
|
664
|
+
const match = matches[0];
|
|
665
|
+
foundTools.push({
|
|
666
|
+
server: match.server,
|
|
667
|
+
tool: {
|
|
668
|
+
name: match.tool.name,
|
|
669
|
+
description: match.tool.description,
|
|
670
|
+
inputSchema: match.tool.inputSchema
|
|
671
|
+
}
|
|
672
|
+
});
|
|
673
|
+
const idx = notFoundTools.indexOf(toolName);
|
|
674
|
+
if (idx > -1) notFoundTools.splice(idx, 1);
|
|
675
|
+
} else {
|
|
676
|
+
ambiguousTools.push({
|
|
677
|
+
toolName,
|
|
678
|
+
servers: matches.map((m) => m.server)
|
|
679
|
+
});
|
|
680
|
+
const idx = notFoundTools.indexOf(toolName);
|
|
681
|
+
if (idx > -1) notFoundTools.splice(idx, 1);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
if (foundTools.length === 0 && ambiguousTools.length === 0) return {
|
|
685
|
+
content: [{
|
|
686
|
+
type: "text",
|
|
687
|
+
text: `None of the requested tools found on any connected server.\nRequested: ${toolNames.join(", ")}\nUse describe_tools without arguments to see available servers.`
|
|
688
|
+
}],
|
|
689
|
+
isError: true
|
|
690
|
+
};
|
|
691
|
+
const result = { tools: foundTools };
|
|
692
|
+
if (notFoundTools.length > 0) result.notFound = notFoundTools;
|
|
693
|
+
if (ambiguousTools.length > 0) result.ambiguous = ambiguousTools.map((item) => ({
|
|
694
|
+
toolName: item.toolName,
|
|
695
|
+
servers: item.servers,
|
|
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;
|
|
702
|
+
return { content: [{
|
|
703
|
+
type: "text",
|
|
704
|
+
text: JSON.stringify(result, null, 2)
|
|
705
|
+
}] };
|
|
706
|
+
} catch (error) {
|
|
707
|
+
return {
|
|
708
|
+
content: [{
|
|
709
|
+
type: "text",
|
|
710
|
+
text: `Error describing tools: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
711
|
+
}],
|
|
712
|
+
isError: true
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
};
|
|
717
|
+
|
|
718
|
+
//#endregion
|
|
719
|
+
//#region src/tools/UseToolTool.ts
|
|
720
|
+
var UseToolTool = class UseToolTool {
|
|
721
|
+
static TOOL_NAME = "use_tool";
|
|
722
|
+
clientManager;
|
|
723
|
+
constructor(clientManager) {
|
|
724
|
+
this.clientManager = clientManager;
|
|
725
|
+
}
|
|
726
|
+
getDefinition() {
|
|
727
|
+
return {
|
|
728
|
+
name: UseToolTool.TOOL_NAME,
|
|
729
|
+
description: `Execute an MCP tool with provided arguments. You MUST call describe_tools first to discover the tool's correct arguments. Then to use tool:
|
|
730
|
+
- Provide toolName and toolArgs based on the schema
|
|
731
|
+
- If multiple servers provide the same tool, specify serverName
|
|
732
|
+
`,
|
|
733
|
+
inputSchema: {
|
|
734
|
+
type: "object",
|
|
735
|
+
properties: {
|
|
736
|
+
toolName: {
|
|
737
|
+
type: "string",
|
|
738
|
+
description: "Name of the tool to execute"
|
|
739
|
+
},
|
|
740
|
+
toolArgs: {
|
|
741
|
+
type: "object",
|
|
742
|
+
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
|
+
}
|
|
748
|
+
},
|
|
749
|
+
required: ["toolName"],
|
|
750
|
+
additionalProperties: false
|
|
751
|
+
}
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
async execute(input) {
|
|
755
|
+
try {
|
|
756
|
+
const { toolName, toolArgs = {}, serverName } = input;
|
|
757
|
+
const clients = this.clientManager.getAllClients();
|
|
758
|
+
if (serverName) {
|
|
759
|
+
const client$1 = this.clientManager.getClient(serverName);
|
|
760
|
+
if (!client$1) return {
|
|
761
|
+
content: [{
|
|
762
|
+
type: "text",
|
|
763
|
+
text: `Server "${serverName}" not found. Available servers: ${clients.map((c) => c.serverName).join(", ")}`
|
|
764
|
+
}],
|
|
765
|
+
isError: true
|
|
766
|
+
};
|
|
767
|
+
if (client$1.toolBlacklist && client$1.toolBlacklist.includes(toolName)) return {
|
|
768
|
+
content: [{
|
|
769
|
+
type: "text",
|
|
770
|
+
text: `Tool "${toolName}" is blacklisted on server "${serverName}" and cannot be executed.`
|
|
771
|
+
}],
|
|
772
|
+
isError: true
|
|
773
|
+
};
|
|
774
|
+
try {
|
|
775
|
+
return await client$1.callTool(toolName, toolArgs);
|
|
776
|
+
} catch (error) {
|
|
777
|
+
return {
|
|
778
|
+
content: [{
|
|
779
|
+
type: "text",
|
|
780
|
+
text: `Failed to call tool "${toolName}" on server "${serverName}": ${error instanceof Error ? error.message : "Unknown error"}`
|
|
781
|
+
}],
|
|
782
|
+
isError: true
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
const matchingServers = [];
|
|
787
|
+
const results = await Promise.all(clients.map(async (client$1) => {
|
|
788
|
+
try {
|
|
789
|
+
if (client$1.toolBlacklist && client$1.toolBlacklist.includes(toolName)) return null;
|
|
790
|
+
if ((await client$1.listTools()).some((t) => t.name === toolName)) return client$1.serverName;
|
|
791
|
+
} catch (error) {
|
|
792
|
+
console.error(`Failed to list tools from ${client$1.serverName}:`, error);
|
|
793
|
+
}
|
|
794
|
+
return null;
|
|
795
|
+
}));
|
|
796
|
+
matchingServers.push(...results.filter((r) => r !== null));
|
|
797
|
+
if (matchingServers.length === 0) return {
|
|
798
|
+
content: [{
|
|
799
|
+
type: "text",
|
|
800
|
+
text: `Tool "${toolName}" not found on any connected server. Use describe_tools to see available tools.`
|
|
801
|
+
}],
|
|
802
|
+
isError: true
|
|
803
|
+
};
|
|
804
|
+
if (matchingServers.length > 1) return {
|
|
805
|
+
content: [{
|
|
806
|
+
type: "text",
|
|
807
|
+
text: `Multiple servers provide tool "${toolName}". Please specify serverName. Available servers: ${matchingServers.join(", ")}`
|
|
808
|
+
}],
|
|
809
|
+
isError: true
|
|
810
|
+
};
|
|
811
|
+
const targetServerName = matchingServers[0];
|
|
812
|
+
const client = this.clientManager.getClient(targetServerName);
|
|
813
|
+
if (!client) return {
|
|
814
|
+
content: [{
|
|
815
|
+
type: "text",
|
|
816
|
+
text: `Internal error: Server "${targetServerName}" was found but is not connected`
|
|
817
|
+
}],
|
|
818
|
+
isError: true
|
|
819
|
+
};
|
|
820
|
+
try {
|
|
821
|
+
return await client.callTool(toolName, toolArgs);
|
|
822
|
+
} catch (error) {
|
|
823
|
+
return {
|
|
824
|
+
content: [{
|
|
825
|
+
type: "text",
|
|
826
|
+
text: `Failed to call tool "${toolName}": ${error instanceof Error ? error.message : "Unknown error"}`
|
|
827
|
+
}],
|
|
828
|
+
isError: true
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
} catch (error) {
|
|
832
|
+
return {
|
|
833
|
+
content: [{
|
|
834
|
+
type: "text",
|
|
835
|
+
text: `Error executing tool: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
836
|
+
}],
|
|
837
|
+
isError: true
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
};
|
|
842
|
+
|
|
843
|
+
//#endregion
|
|
844
|
+
//#region src/server/index.ts
|
|
845
|
+
/**
|
|
846
|
+
* MCP Server Setup
|
|
847
|
+
*
|
|
848
|
+
* DESIGN PATTERNS:
|
|
849
|
+
* - Factory pattern for server creation
|
|
850
|
+
* - Tool registration pattern
|
|
851
|
+
* - Dependency injection for services
|
|
852
|
+
*
|
|
853
|
+
* CODING STANDARDS:
|
|
854
|
+
* - Register all tools, resources, and prompts here
|
|
855
|
+
* - Keep server setup modular and extensible
|
|
856
|
+
* - Import tools from ../tools/ and register them in the handlers
|
|
857
|
+
*/
|
|
858
|
+
async function createServer(options) {
|
|
859
|
+
const server = new __modelcontextprotocol_sdk_server_index_js.Server({
|
|
860
|
+
name: "@agiflowai/one-mcp",
|
|
861
|
+
version: "0.1.0"
|
|
862
|
+
}, { capabilities: { tools: {} } });
|
|
863
|
+
const clientManager = new McpClientManagerService();
|
|
864
|
+
if (options?.configFilePath) try {
|
|
865
|
+
const config = await new ConfigFetcherService({ configFilePath: options.configFilePath }).fetchConfiguration(options.noCache || false);
|
|
866
|
+
const connectionPromises = Object.entries(config.mcpServers).map(async ([serverName, serverConfig]) => {
|
|
867
|
+
try {
|
|
868
|
+
await clientManager.connectToServer(serverName, serverConfig);
|
|
869
|
+
console.error(`Connected to MCP server: ${serverName}`);
|
|
870
|
+
} catch (error) {
|
|
871
|
+
console.error(`Failed to connect to ${serverName}:`, error);
|
|
872
|
+
}
|
|
873
|
+
});
|
|
874
|
+
await Promise.all(connectionPromises);
|
|
875
|
+
} catch (error) {
|
|
876
|
+
console.error("Failed to load MCP configuration:", error);
|
|
877
|
+
}
|
|
878
|
+
const describeTools = new DescribeToolsTool(clientManager);
|
|
879
|
+
const useTool = new UseToolTool(clientManager);
|
|
880
|
+
server.setRequestHandler(__modelcontextprotocol_sdk_types_js.ListToolsRequestSchema, async () => ({ tools: [await describeTools.getDefinition(), useTool.getDefinition()] }));
|
|
881
|
+
server.setRequestHandler(__modelcontextprotocol_sdk_types_js.CallToolRequestSchema, async (request) => {
|
|
882
|
+
const { name, arguments: args } = request.params;
|
|
883
|
+
if (name === DescribeToolsTool.TOOL_NAME) return await describeTools.execute(args);
|
|
884
|
+
if (name === UseToolTool.TOOL_NAME) return await useTool.execute(args);
|
|
885
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
886
|
+
});
|
|
887
|
+
return server;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
//#endregion
|
|
891
|
+
//#region src/transports/stdio.ts
|
|
892
|
+
/**
|
|
893
|
+
* Stdio transport handler for MCP server
|
|
894
|
+
* Used for command-line and direct integrations
|
|
895
|
+
*/
|
|
896
|
+
var StdioTransportHandler = class {
|
|
897
|
+
server;
|
|
898
|
+
transport = null;
|
|
899
|
+
constructor(server) {
|
|
900
|
+
this.server = server;
|
|
901
|
+
}
|
|
902
|
+
async start() {
|
|
903
|
+
this.transport = new __modelcontextprotocol_sdk_server_stdio_js.StdioServerTransport();
|
|
904
|
+
await this.server.connect(this.transport);
|
|
905
|
+
console.error("@agiflowai/one-mcp MCP server started on stdio");
|
|
906
|
+
}
|
|
907
|
+
async stop() {
|
|
908
|
+
if (this.transport) {
|
|
909
|
+
await this.transport.close();
|
|
910
|
+
this.transport = null;
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
};
|
|
914
|
+
|
|
915
|
+
//#endregion
|
|
916
|
+
//#region src/transports/sse.ts
|
|
917
|
+
/**
|
|
918
|
+
* Session manager for SSE transports
|
|
919
|
+
*/
|
|
920
|
+
var SseSessionManager = class {
|
|
921
|
+
sessions = /* @__PURE__ */ new Map();
|
|
922
|
+
getSession(sessionId) {
|
|
923
|
+
return this.sessions.get(sessionId)?.transport;
|
|
924
|
+
}
|
|
925
|
+
setSession(sessionId, transport, server) {
|
|
926
|
+
this.sessions.set(sessionId, {
|
|
927
|
+
transport,
|
|
928
|
+
server
|
|
929
|
+
});
|
|
930
|
+
}
|
|
931
|
+
deleteSession(sessionId) {
|
|
932
|
+
const session = this.sessions.get(sessionId);
|
|
933
|
+
if (session) session.server.close();
|
|
934
|
+
this.sessions.delete(sessionId);
|
|
935
|
+
}
|
|
936
|
+
hasSession(sessionId) {
|
|
937
|
+
return this.sessions.has(sessionId);
|
|
938
|
+
}
|
|
939
|
+
clear() {
|
|
940
|
+
for (const session of this.sessions.values()) session.server.close();
|
|
941
|
+
this.sessions.clear();
|
|
942
|
+
}
|
|
943
|
+
};
|
|
944
|
+
/**
|
|
945
|
+
* SSE (Server-Sent Events) transport handler
|
|
946
|
+
* Legacy transport for backwards compatibility (protocol version 2024-11-05)
|
|
947
|
+
* Uses separate endpoints: /sse for SSE stream (GET) and /messages for client messages (POST)
|
|
948
|
+
*/
|
|
949
|
+
var SseTransportHandler = class {
|
|
950
|
+
serverFactory;
|
|
951
|
+
app;
|
|
952
|
+
server = null;
|
|
953
|
+
sessionManager;
|
|
954
|
+
config;
|
|
955
|
+
constructor(serverFactory, config) {
|
|
956
|
+
this.serverFactory = typeof serverFactory === "function" ? serverFactory : () => serverFactory;
|
|
957
|
+
this.app = (0, express.default)();
|
|
958
|
+
this.sessionManager = new SseSessionManager();
|
|
959
|
+
this.config = {
|
|
960
|
+
mode: config.mode,
|
|
961
|
+
port: config.port ?? 3e3,
|
|
962
|
+
host: config.host ?? "localhost"
|
|
963
|
+
};
|
|
964
|
+
this.setupMiddleware();
|
|
965
|
+
this.setupRoutes();
|
|
966
|
+
}
|
|
967
|
+
setupMiddleware() {
|
|
968
|
+
this.app.use(express.default.json());
|
|
969
|
+
}
|
|
970
|
+
setupRoutes() {
|
|
971
|
+
this.app.get("/sse", async (req, res) => {
|
|
972
|
+
await this.handleSseConnection(req, res);
|
|
973
|
+
});
|
|
974
|
+
this.app.post("/messages", async (req, res) => {
|
|
975
|
+
await this.handlePostMessage(req, res);
|
|
976
|
+
});
|
|
977
|
+
this.app.get("/health", (_req, res) => {
|
|
978
|
+
res.json({
|
|
979
|
+
status: "ok",
|
|
980
|
+
transport: "sse"
|
|
981
|
+
});
|
|
982
|
+
});
|
|
983
|
+
}
|
|
984
|
+
async handleSseConnection(_req, res) {
|
|
985
|
+
try {
|
|
986
|
+
const mcpServer = this.serverFactory();
|
|
987
|
+
const transport = new __modelcontextprotocol_sdk_server_sse_js.SSEServerTransport("/messages", res);
|
|
988
|
+
this.sessionManager.setSession(transport.sessionId, transport, mcpServer);
|
|
989
|
+
res.on("close", () => {
|
|
990
|
+
this.sessionManager.deleteSession(transport.sessionId);
|
|
991
|
+
});
|
|
992
|
+
await mcpServer.connect(transport);
|
|
993
|
+
console.error(`SSE session established: ${transport.sessionId}`);
|
|
994
|
+
} catch (error) {
|
|
995
|
+
console.error("Error handling SSE connection:", error);
|
|
996
|
+
if (!res.headersSent) res.status(500).send("Internal Server Error");
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
async handlePostMessage(req, res) {
|
|
1000
|
+
const sessionId = req.query.sessionId;
|
|
1001
|
+
if (!sessionId) {
|
|
1002
|
+
res.status(400).send("Missing sessionId query parameter");
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
const transport = this.sessionManager.getSession(sessionId);
|
|
1006
|
+
if (!transport) {
|
|
1007
|
+
res.status(404).send("No transport found for sessionId");
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
1010
|
+
try {
|
|
1011
|
+
await transport.handlePostMessage(req, res, req.body);
|
|
1012
|
+
} catch (error) {
|
|
1013
|
+
console.error("Error handling post message:", error);
|
|
1014
|
+
if (!res.headersSent) res.status(500).send("Internal Server Error");
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
async start() {
|
|
1018
|
+
return new Promise((resolve, reject) => {
|
|
1019
|
+
try {
|
|
1020
|
+
this.server = this.app.listen(this.config.port, this.config.host, () => {
|
|
1021
|
+
console.error(`@agiflowai/one-mcp MCP server started with SSE transport on http://${this.config.host}:${this.config.port}`);
|
|
1022
|
+
console.error(`SSE endpoint: http://${this.config.host}:${this.config.port}/sse`);
|
|
1023
|
+
console.error(`Messages endpoint: http://${this.config.host}:${this.config.port}/messages`);
|
|
1024
|
+
console.error(`Health check: http://${this.config.host}:${this.config.port}/health`);
|
|
1025
|
+
resolve();
|
|
1026
|
+
});
|
|
1027
|
+
this.server.on("error", (error) => {
|
|
1028
|
+
reject(error);
|
|
1029
|
+
});
|
|
1030
|
+
} catch (error) {
|
|
1031
|
+
reject(error);
|
|
1032
|
+
}
|
|
1033
|
+
});
|
|
1034
|
+
}
|
|
1035
|
+
async stop() {
|
|
1036
|
+
return new Promise((resolve, reject) => {
|
|
1037
|
+
if (this.server) {
|
|
1038
|
+
this.sessionManager.clear();
|
|
1039
|
+
this.server.close((err) => {
|
|
1040
|
+
if (err) reject(err);
|
|
1041
|
+
else {
|
|
1042
|
+
this.server = null;
|
|
1043
|
+
resolve();
|
|
1044
|
+
}
|
|
1045
|
+
});
|
|
1046
|
+
} else resolve();
|
|
1047
|
+
});
|
|
1048
|
+
}
|
|
1049
|
+
getPort() {
|
|
1050
|
+
return this.config.port;
|
|
1051
|
+
}
|
|
1052
|
+
getHost() {
|
|
1053
|
+
return this.config.host;
|
|
1054
|
+
}
|
|
1055
|
+
};
|
|
1056
|
+
|
|
1057
|
+
//#endregion
|
|
1058
|
+
//#region src/transports/http.ts
|
|
1059
|
+
/**
|
|
1060
|
+
* HTTP Transport Handler
|
|
1061
|
+
*
|
|
1062
|
+
* DESIGN PATTERNS:
|
|
1063
|
+
* - Transport handler pattern implementing TransportHandler interface
|
|
1064
|
+
* - Session management for stateful connections
|
|
1065
|
+
* - Streamable HTTP protocol (2025-03-26) with resumability support
|
|
1066
|
+
* - Factory pattern for creating MCP server instances per session
|
|
1067
|
+
*
|
|
1068
|
+
* CODING STANDARDS:
|
|
1069
|
+
* - Use async/await for all asynchronous operations
|
|
1070
|
+
* - Implement proper session lifecycle management
|
|
1071
|
+
* - Handle errors gracefully with appropriate HTTP status codes
|
|
1072
|
+
* - Provide health check endpoint for monitoring
|
|
1073
|
+
* - Clean up resources on shutdown
|
|
1074
|
+
*
|
|
1075
|
+
* AVOID:
|
|
1076
|
+
* - Sharing MCP server instances across sessions (use factory pattern)
|
|
1077
|
+
* - Forgetting to clean up sessions on disconnect
|
|
1078
|
+
* - Missing error handling for request processing
|
|
1079
|
+
* - Hardcoded configuration (use TransportConfig)
|
|
1080
|
+
*/
|
|
1081
|
+
/**
|
|
1082
|
+
* HTTP session manager
|
|
1083
|
+
*/
|
|
1084
|
+
var HttpFullSessionManager = class {
|
|
1085
|
+
sessions = /* @__PURE__ */ new Map();
|
|
1086
|
+
getSession(sessionId) {
|
|
1087
|
+
return this.sessions.get(sessionId);
|
|
1088
|
+
}
|
|
1089
|
+
setSession(sessionId, transport, server) {
|
|
1090
|
+
this.sessions.set(sessionId, {
|
|
1091
|
+
transport,
|
|
1092
|
+
server
|
|
1093
|
+
});
|
|
1094
|
+
}
|
|
1095
|
+
deleteSession(sessionId) {
|
|
1096
|
+
const session = this.sessions.get(sessionId);
|
|
1097
|
+
if (session) session.server.close();
|
|
1098
|
+
this.sessions.delete(sessionId);
|
|
1099
|
+
}
|
|
1100
|
+
hasSession(sessionId) {
|
|
1101
|
+
return this.sessions.has(sessionId);
|
|
1102
|
+
}
|
|
1103
|
+
clear() {
|
|
1104
|
+
for (const session of this.sessions.values()) session.server.close();
|
|
1105
|
+
this.sessions.clear();
|
|
1106
|
+
}
|
|
1107
|
+
};
|
|
1108
|
+
/**
|
|
1109
|
+
* HTTP transport handler using Streamable HTTP (protocol version 2025-03-26)
|
|
1110
|
+
* Provides stateful session management with resumability support
|
|
1111
|
+
*/
|
|
1112
|
+
var HttpTransportHandler = class {
|
|
1113
|
+
serverFactory;
|
|
1114
|
+
app;
|
|
1115
|
+
server = null;
|
|
1116
|
+
sessionManager;
|
|
1117
|
+
config;
|
|
1118
|
+
constructor(serverFactory, config) {
|
|
1119
|
+
this.serverFactory = typeof serverFactory === "function" ? serverFactory : () => serverFactory;
|
|
1120
|
+
this.app = (0, express.default)();
|
|
1121
|
+
this.sessionManager = new HttpFullSessionManager();
|
|
1122
|
+
this.config = {
|
|
1123
|
+
mode: config.mode,
|
|
1124
|
+
port: config.port ?? 3e3,
|
|
1125
|
+
host: config.host ?? "localhost"
|
|
1126
|
+
};
|
|
1127
|
+
this.setupMiddleware();
|
|
1128
|
+
this.setupRoutes();
|
|
1129
|
+
}
|
|
1130
|
+
setupMiddleware() {
|
|
1131
|
+
this.app.use(express.default.json());
|
|
1132
|
+
}
|
|
1133
|
+
setupRoutes() {
|
|
1134
|
+
this.app.post("/mcp", async (req, res) => {
|
|
1135
|
+
await this.handlePostRequest(req, res);
|
|
1136
|
+
});
|
|
1137
|
+
this.app.get("/mcp", async (req, res) => {
|
|
1138
|
+
await this.handleGetRequest(req, res);
|
|
1139
|
+
});
|
|
1140
|
+
this.app.delete("/mcp", async (req, res) => {
|
|
1141
|
+
await this.handleDeleteRequest(req, res);
|
|
1142
|
+
});
|
|
1143
|
+
this.app.get("/health", (_req, res) => {
|
|
1144
|
+
res.json({
|
|
1145
|
+
status: "ok",
|
|
1146
|
+
transport: "http"
|
|
1147
|
+
});
|
|
1148
|
+
});
|
|
1149
|
+
}
|
|
1150
|
+
async handlePostRequest(req, res) {
|
|
1151
|
+
const sessionId = req.headers["mcp-session-id"];
|
|
1152
|
+
let transport;
|
|
1153
|
+
if (sessionId && this.sessionManager.hasSession(sessionId)) transport = this.sessionManager.getSession(sessionId).transport;
|
|
1154
|
+
else if (!sessionId && (0, __modelcontextprotocol_sdk_types_js.isInitializeRequest)(req.body)) {
|
|
1155
|
+
const mcpServer = this.serverFactory();
|
|
1156
|
+
transport = new __modelcontextprotocol_sdk_server_streamableHttp_js.StreamableHTTPServerTransport({
|
|
1157
|
+
sessionIdGenerator: () => (0, node_crypto.randomUUID)(),
|
|
1158
|
+
enableJsonResponse: true,
|
|
1159
|
+
onsessioninitialized: (sessionId$1) => {
|
|
1160
|
+
this.sessionManager.setSession(sessionId$1, transport, mcpServer);
|
|
1161
|
+
}
|
|
1162
|
+
});
|
|
1163
|
+
transport.onclose = () => {
|
|
1164
|
+
if (transport.sessionId) this.sessionManager.deleteSession(transport.sessionId);
|
|
1165
|
+
};
|
|
1166
|
+
await mcpServer.connect(transport);
|
|
1167
|
+
} else {
|
|
1168
|
+
res.status(400).json({
|
|
1169
|
+
jsonrpc: "2.0",
|
|
1170
|
+
error: {
|
|
1171
|
+
code: -32e3,
|
|
1172
|
+
message: "Bad Request: No valid session ID provided"
|
|
1173
|
+
},
|
|
1174
|
+
id: null
|
|
1175
|
+
});
|
|
1176
|
+
return;
|
|
1177
|
+
}
|
|
1178
|
+
await transport.handleRequest(req, res, req.body);
|
|
1179
|
+
}
|
|
1180
|
+
async handleGetRequest(req, res) {
|
|
1181
|
+
const sessionId = req.headers["mcp-session-id"];
|
|
1182
|
+
if (!sessionId || !this.sessionManager.hasSession(sessionId)) {
|
|
1183
|
+
res.status(400).send("Invalid or missing session ID");
|
|
1184
|
+
return;
|
|
1185
|
+
}
|
|
1186
|
+
await this.sessionManager.getSession(sessionId).transport.handleRequest(req, res);
|
|
1187
|
+
}
|
|
1188
|
+
async handleDeleteRequest(req, res) {
|
|
1189
|
+
const sessionId = req.headers["mcp-session-id"];
|
|
1190
|
+
if (!sessionId || !this.sessionManager.hasSession(sessionId)) {
|
|
1191
|
+
res.status(400).send("Invalid or missing session ID");
|
|
1192
|
+
return;
|
|
1193
|
+
}
|
|
1194
|
+
await this.sessionManager.getSession(sessionId).transport.handleRequest(req, res);
|
|
1195
|
+
this.sessionManager.deleteSession(sessionId);
|
|
1196
|
+
}
|
|
1197
|
+
async start() {
|
|
1198
|
+
return new Promise((resolve, reject) => {
|
|
1199
|
+
try {
|
|
1200
|
+
this.server = this.app.listen(this.config.port, this.config.host, () => {
|
|
1201
|
+
console.error(`@agiflowai/one-mcp MCP server started on http://${this.config.host}:${this.config.port}/mcp`);
|
|
1202
|
+
console.error(`Health check: http://${this.config.host}:${this.config.port}/health`);
|
|
1203
|
+
resolve();
|
|
1204
|
+
});
|
|
1205
|
+
this.server.on("error", (error) => {
|
|
1206
|
+
reject(error);
|
|
1207
|
+
});
|
|
1208
|
+
} catch (error) {
|
|
1209
|
+
reject(error);
|
|
1210
|
+
}
|
|
1211
|
+
});
|
|
1212
|
+
}
|
|
1213
|
+
async stop() {
|
|
1214
|
+
return new Promise((resolve, reject) => {
|
|
1215
|
+
if (this.server) {
|
|
1216
|
+
this.sessionManager.clear();
|
|
1217
|
+
this.server.close((err) => {
|
|
1218
|
+
if (err) reject(err);
|
|
1219
|
+
else {
|
|
1220
|
+
this.server = null;
|
|
1221
|
+
resolve();
|
|
1222
|
+
}
|
|
1223
|
+
});
|
|
1224
|
+
} else resolve();
|
|
1225
|
+
});
|
|
1226
|
+
}
|
|
1227
|
+
getPort() {
|
|
1228
|
+
return this.config.port;
|
|
1229
|
+
}
|
|
1230
|
+
getHost() {
|
|
1231
|
+
return this.config.host;
|
|
1232
|
+
}
|
|
1233
|
+
};
|
|
1234
|
+
|
|
1235
|
+
//#endregion
|
|
1236
|
+
Object.defineProperty(exports, 'ConfigFetcherService', {
|
|
1237
|
+
enumerable: true,
|
|
1238
|
+
get: function () {
|
|
1239
|
+
return ConfigFetcherService;
|
|
1240
|
+
}
|
|
1241
|
+
});
|
|
1242
|
+
Object.defineProperty(exports, 'HttpTransportHandler', {
|
|
1243
|
+
enumerable: true,
|
|
1244
|
+
get: function () {
|
|
1245
|
+
return HttpTransportHandler;
|
|
1246
|
+
}
|
|
1247
|
+
});
|
|
1248
|
+
Object.defineProperty(exports, 'McpClientManagerService', {
|
|
1249
|
+
enumerable: true,
|
|
1250
|
+
get: function () {
|
|
1251
|
+
return McpClientManagerService;
|
|
1252
|
+
}
|
|
1253
|
+
});
|
|
1254
|
+
Object.defineProperty(exports, 'SseTransportHandler', {
|
|
1255
|
+
enumerable: true,
|
|
1256
|
+
get: function () {
|
|
1257
|
+
return SseTransportHandler;
|
|
1258
|
+
}
|
|
1259
|
+
});
|
|
1260
|
+
Object.defineProperty(exports, 'StdioTransportHandler', {
|
|
1261
|
+
enumerable: true,
|
|
1262
|
+
get: function () {
|
|
1263
|
+
return StdioTransportHandler;
|
|
1264
|
+
}
|
|
1265
|
+
});
|
|
1266
|
+
Object.defineProperty(exports, '__toESM', {
|
|
1267
|
+
enumerable: true,
|
|
1268
|
+
get: function () {
|
|
1269
|
+
return __toESM;
|
|
1270
|
+
}
|
|
1271
|
+
});
|
|
1272
|
+
Object.defineProperty(exports, 'createServer', {
|
|
1273
|
+
enumerable: true,
|
|
1274
|
+
get: function () {
|
|
1275
|
+
return createServer;
|
|
1276
|
+
}
|
|
1277
|
+
});
|