@agiflowai/one-mcp 0.3.13 → 0.3.15
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/README.md +9 -3
- package/dist/cli.cjs +579 -241
- package/dist/cli.mjs +541 -203
- package/dist/index.cjs +18 -12
- package/dist/index.d.cts +334 -23
- package/dist/index.d.mts +335 -23
- package/dist/index.mjs +2 -2
- package/dist/{http-_ThlSpST.mjs → src-CH93aUm2.mjs} +805 -167
- package/dist/{http-SFQFxDCq.cjs → src-CWShQS8u.cjs} +828 -166
- package/package.json +2 -2
|
@@ -40,11 +40,13 @@ let __modelcontextprotocol_sdk_client_stdio_js = require("@modelcontextprotocol/
|
|
|
40
40
|
let __modelcontextprotocol_sdk_client_sse_js = require("@modelcontextprotocol/sdk/client/sse.js");
|
|
41
41
|
let __modelcontextprotocol_sdk_client_streamableHttp_js = require("@modelcontextprotocol/sdk/client/streamableHttp.js");
|
|
42
42
|
let liquidjs = require("liquidjs");
|
|
43
|
-
let
|
|
44
|
-
let
|
|
43
|
+
let node_events = require("node:events");
|
|
44
|
+
let node_util = require("node:util");
|
|
45
|
+
let __modelcontextprotocol_sdk_server_streamableHttp_js = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
|
|
45
46
|
let express = require("express");
|
|
46
47
|
express = __toESM(express);
|
|
47
|
-
let
|
|
48
|
+
let __modelcontextprotocol_sdk_server_sse_js = require("@modelcontextprotocol/sdk/server/sse.js");
|
|
49
|
+
let __modelcontextprotocol_sdk_server_stdio_js = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
48
50
|
|
|
49
51
|
//#region src/utils/mcpConfigSchema.ts
|
|
50
52
|
/**
|
|
@@ -1166,7 +1168,7 @@ const DEFAULT_SERVER_ID = "unknown";
|
|
|
1166
1168
|
function isYamlPath(filePath) {
|
|
1167
1169
|
return filePath.endsWith(".yaml") || filePath.endsWith(".yml");
|
|
1168
1170
|
}
|
|
1169
|
-
function toErrorMessage(error) {
|
|
1171
|
+
function toErrorMessage$3(error) {
|
|
1170
1172
|
return error instanceof Error ? error.message : String(error);
|
|
1171
1173
|
}
|
|
1172
1174
|
function sanitizeConfigPathForFilename(configFilePath) {
|
|
@@ -1357,7 +1359,7 @@ var DefinitionsCacheService = class {
|
|
|
1357
1359
|
} catch (error) {
|
|
1358
1360
|
failures.push({
|
|
1359
1361
|
serverName: client.serverName,
|
|
1360
|
-
error: toErrorMessage(error)
|
|
1362
|
+
error: toErrorMessage$3(error)
|
|
1361
1363
|
});
|
|
1362
1364
|
return null;
|
|
1363
1365
|
}
|
|
@@ -1395,7 +1397,7 @@ var DefinitionsCacheService = class {
|
|
|
1395
1397
|
arguments: prompt.arguments?.map((arg) => ({ ...arg }))
|
|
1396
1398
|
}));
|
|
1397
1399
|
} catch (error) {
|
|
1398
|
-
console.error(`${LOG_PREFIX_SKILL_DETECTION} Failed to list prompts from ${client.serverName}: ${toErrorMessage(error)}`);
|
|
1400
|
+
console.error(`${LOG_PREFIX_SKILL_DETECTION} Failed to list prompts from ${client.serverName}: ${toErrorMessage$3(error)}`);
|
|
1399
1401
|
return [];
|
|
1400
1402
|
}
|
|
1401
1403
|
}
|
|
@@ -1408,7 +1410,7 @@ var DefinitionsCacheService = class {
|
|
|
1408
1410
|
mimeType: resource.mimeType
|
|
1409
1411
|
}));
|
|
1410
1412
|
} catch (error) {
|
|
1411
|
-
console.error(`${LOG_PREFIX_CAPABILITY_DISCOVERY} Failed to list resources from ${client.serverName}: ${toErrorMessage(error)}`);
|
|
1413
|
+
console.error(`${LOG_PREFIX_CAPABILITY_DISCOVERY} Failed to list resources from ${client.serverName}: ${toErrorMessage$3(error)}`);
|
|
1412
1414
|
return [];
|
|
1413
1415
|
}
|
|
1414
1416
|
}
|
|
@@ -1437,7 +1439,7 @@ var DefinitionsCacheService = class {
|
|
|
1437
1439
|
autoDetected: true
|
|
1438
1440
|
};
|
|
1439
1441
|
} catch (error) {
|
|
1440
|
-
console.error(`${LOG_PREFIX_SKILL_DETECTION} Failed to fetch prompt '${prompt.name}' from ${client.serverName}: ${toErrorMessage(error)}`);
|
|
1442
|
+
console.error(`${LOG_PREFIX_SKILL_DETECTION} Failed to fetch prompt '${prompt.name}' from ${client.serverName}: ${toErrorMessage$3(error)}`);
|
|
1441
1443
|
return null;
|
|
1442
1444
|
}
|
|
1443
1445
|
}));
|
|
@@ -2806,7 +2808,7 @@ IMPORTANT: Only use tools discovered from describe_tools with id="${this.serverI
|
|
|
2806
2808
|
|
|
2807
2809
|
//#endregion
|
|
2808
2810
|
//#region package.json
|
|
2809
|
-
var version = "0.3.
|
|
2811
|
+
var version = "0.3.14";
|
|
2810
2812
|
|
|
2811
2813
|
//#endregion
|
|
2812
2814
|
//#region src/server/index.ts
|
|
@@ -3126,28 +3128,341 @@ async function createServer(options) {
|
|
|
3126
3128
|
}
|
|
3127
3129
|
|
|
3128
3130
|
//#endregion
|
|
3129
|
-
//#region src/
|
|
3131
|
+
//#region src/types/index.ts
|
|
3130
3132
|
/**
|
|
3131
|
-
*
|
|
3132
|
-
* Used for command-line and direct integrations
|
|
3133
|
+
* Transport mode constants
|
|
3133
3134
|
*/
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3135
|
+
const TRANSPORT_MODE = {
|
|
3136
|
+
STDIO: "stdio",
|
|
3137
|
+
HTTP: "http",
|
|
3138
|
+
SSE: "sse"
|
|
3139
|
+
};
|
|
3140
|
+
|
|
3141
|
+
//#endregion
|
|
3142
|
+
//#region src/transports/http.ts
|
|
3143
|
+
/**
|
|
3144
|
+
* HTTP Transport Handler
|
|
3145
|
+
*
|
|
3146
|
+
* DESIGN PATTERNS:
|
|
3147
|
+
* - Transport handler pattern implementing TransportHandler interface
|
|
3148
|
+
* - Session management for stateful connections
|
|
3149
|
+
* - Streamable HTTP protocol (2025-03-26) with resumability support
|
|
3150
|
+
* - Factory pattern for creating MCP server instances per session
|
|
3151
|
+
*
|
|
3152
|
+
* CODING STANDARDS:
|
|
3153
|
+
* - Use async/await for all asynchronous operations
|
|
3154
|
+
* - Implement proper session lifecycle management
|
|
3155
|
+
* - Handle errors gracefully with appropriate HTTP status codes
|
|
3156
|
+
* - Provide health check endpoint for monitoring
|
|
3157
|
+
* - Clean up resources on shutdown
|
|
3158
|
+
*
|
|
3159
|
+
* AVOID:
|
|
3160
|
+
* - Sharing MCP server instances across sessions (use factory pattern)
|
|
3161
|
+
* - Forgetting to clean up sessions on disconnect
|
|
3162
|
+
* - Missing error handling for request processing
|
|
3163
|
+
* - Hardcoded configuration (use TransportConfig)
|
|
3164
|
+
*/
|
|
3165
|
+
/**
|
|
3166
|
+
* HTTP session manager
|
|
3167
|
+
*/
|
|
3168
|
+
var HttpFullSessionManager = class {
|
|
3169
|
+
sessions = /* @__PURE__ */ new Map();
|
|
3170
|
+
getSession(sessionId) {
|
|
3171
|
+
return this.sessions.get(sessionId);
|
|
3172
|
+
}
|
|
3173
|
+
setSession(sessionId, transport, server) {
|
|
3174
|
+
this.sessions.set(sessionId, {
|
|
3175
|
+
transport,
|
|
3176
|
+
server
|
|
3177
|
+
});
|
|
3178
|
+
}
|
|
3179
|
+
async deleteSession(sessionId) {
|
|
3180
|
+
const session = this.sessions.get(sessionId);
|
|
3181
|
+
if (session) try {
|
|
3182
|
+
await session.server.close();
|
|
3183
|
+
} catch (error) {
|
|
3184
|
+
throw new Error(`Failed to close MCP server for session '${sessionId}': ${toErrorMessage$2(error)}`);
|
|
3185
|
+
}
|
|
3186
|
+
this.sessions.delete(sessionId);
|
|
3187
|
+
}
|
|
3188
|
+
hasSession(sessionId) {
|
|
3189
|
+
return this.sessions.has(sessionId);
|
|
3190
|
+
}
|
|
3191
|
+
async clear() {
|
|
3192
|
+
try {
|
|
3193
|
+
await Promise.all(Array.from(this.sessions.values()).map(async (session) => {
|
|
3194
|
+
await session.server.close();
|
|
3195
|
+
}));
|
|
3196
|
+
} catch (error) {
|
|
3197
|
+
throw new Error(`Failed to clear sessions: ${toErrorMessage$2(error)}`);
|
|
3198
|
+
}
|
|
3199
|
+
this.sessions.clear();
|
|
3200
|
+
}
|
|
3201
|
+
};
|
|
3202
|
+
function toErrorMessage$2(error) {
|
|
3203
|
+
return error instanceof Error ? error.message : String(error);
|
|
3204
|
+
}
|
|
3205
|
+
const ADMIN_RATE_LIMIT_WINDOW_MS = 6e4;
|
|
3206
|
+
const ADMIN_RATE_LIMIT_MAX_REQUESTS = 5;
|
|
3207
|
+
/**
|
|
3208
|
+
* Simple in-memory rate limiter for the admin shutdown endpoint.
|
|
3209
|
+
* Tracks request timestamps per IP within a sliding window.
|
|
3210
|
+
*/
|
|
3211
|
+
var AdminRateLimiter = class {
|
|
3212
|
+
requests = /* @__PURE__ */ new Map();
|
|
3213
|
+
isAllowed(ip) {
|
|
3214
|
+
const now = Date.now();
|
|
3215
|
+
const windowStart = now - ADMIN_RATE_LIMIT_WINDOW_MS;
|
|
3216
|
+
const timestamps = (this.requests.get(ip) ?? []).filter((t) => t > windowStart);
|
|
3217
|
+
if (timestamps.length >= ADMIN_RATE_LIMIT_MAX_REQUESTS) {
|
|
3218
|
+
this.requests.set(ip, timestamps);
|
|
3219
|
+
return false;
|
|
3220
|
+
}
|
|
3221
|
+
timestamps.push(now);
|
|
3222
|
+
this.requests.set(ip, timestamps);
|
|
3223
|
+
return true;
|
|
3224
|
+
}
|
|
3225
|
+
};
|
|
3226
|
+
/**
|
|
3227
|
+
* HTTP transport handler using Streamable HTTP (protocol version 2025-03-26)
|
|
3228
|
+
* Provides stateful session management with resumability support
|
|
3229
|
+
*/
|
|
3230
|
+
var HttpTransportHandler = class {
|
|
3231
|
+
serverFactory;
|
|
3232
|
+
app;
|
|
3233
|
+
server = null;
|
|
3234
|
+
sessionManager;
|
|
3235
|
+
config;
|
|
3236
|
+
adminOptions;
|
|
3237
|
+
adminRateLimiter = new AdminRateLimiter();
|
|
3238
|
+
constructor(serverFactory, config, adminOptions) {
|
|
3239
|
+
this.serverFactory = serverFactory;
|
|
3240
|
+
this.app = (0, express.default)();
|
|
3241
|
+
this.sessionManager = new HttpFullSessionManager();
|
|
3242
|
+
this.config = {
|
|
3243
|
+
mode: config.mode,
|
|
3244
|
+
port: config.port ?? 3e3,
|
|
3245
|
+
host: config.host ?? "localhost"
|
|
3246
|
+
};
|
|
3247
|
+
this.adminOptions = adminOptions;
|
|
3248
|
+
this.setupMiddleware();
|
|
3249
|
+
this.setupRoutes();
|
|
3250
|
+
}
|
|
3251
|
+
setupMiddleware() {
|
|
3252
|
+
this.app.use(express.default.json());
|
|
3253
|
+
}
|
|
3254
|
+
setupRoutes() {
|
|
3255
|
+
this.app.post("/mcp", async (req, res) => {
|
|
3256
|
+
try {
|
|
3257
|
+
await this.handlePostRequest(req, res);
|
|
3258
|
+
} catch (error) {
|
|
3259
|
+
console.error(`Failed to handle MCP POST request: ${toErrorMessage$2(error)}`);
|
|
3260
|
+
res.status(500).json({
|
|
3261
|
+
jsonrpc: "2.0",
|
|
3262
|
+
error: {
|
|
3263
|
+
code: -32603,
|
|
3264
|
+
message: "Failed to handle MCP POST request."
|
|
3265
|
+
},
|
|
3266
|
+
id: null
|
|
3267
|
+
});
|
|
3268
|
+
}
|
|
3269
|
+
});
|
|
3270
|
+
this.app.get("/mcp", async (req, res) => {
|
|
3271
|
+
try {
|
|
3272
|
+
await this.handleGetRequest(req, res);
|
|
3273
|
+
} catch (error) {
|
|
3274
|
+
console.error(`Failed to handle MCP GET request: ${toErrorMessage$2(error)}`);
|
|
3275
|
+
res.status(500).send("Failed to handle MCP GET request.");
|
|
3276
|
+
}
|
|
3277
|
+
});
|
|
3278
|
+
this.app.delete("/mcp", async (req, res) => {
|
|
3279
|
+
try {
|
|
3280
|
+
await this.handleDeleteRequest(req, res);
|
|
3281
|
+
} catch (error) {
|
|
3282
|
+
console.error(`Failed to handle MCP DELETE request: ${toErrorMessage$2(error)}`);
|
|
3283
|
+
res.status(500).send("Failed to handle MCP DELETE request.");
|
|
3284
|
+
}
|
|
3285
|
+
});
|
|
3286
|
+
this.app.get("/health", (_req, res) => {
|
|
3287
|
+
const payload = {
|
|
3288
|
+
status: "ok",
|
|
3289
|
+
transport: "http",
|
|
3290
|
+
serverId: this.adminOptions?.serverId
|
|
3291
|
+
};
|
|
3292
|
+
res.json(payload);
|
|
3293
|
+
});
|
|
3294
|
+
this.app.post("/admin/shutdown", async (req, res) => {
|
|
3295
|
+
try {
|
|
3296
|
+
const clientIp = req.ip ?? req.socket.remoteAddress ?? "unknown";
|
|
3297
|
+
if (!this.adminRateLimiter.isAllowed(clientIp)) {
|
|
3298
|
+
const payload = {
|
|
3299
|
+
ok: false,
|
|
3300
|
+
message: "Too many shutdown requests. Try again later.",
|
|
3301
|
+
serverId: this.adminOptions?.serverId
|
|
3302
|
+
};
|
|
3303
|
+
res.status(429).json(payload);
|
|
3304
|
+
return;
|
|
3305
|
+
}
|
|
3306
|
+
await this.handleAdminShutdownRequest(req, res);
|
|
3307
|
+
} catch (error) {
|
|
3308
|
+
console.error(`Failed to process shutdown request: ${toErrorMessage$2(error)}`);
|
|
3309
|
+
const payload = {
|
|
3310
|
+
ok: false,
|
|
3311
|
+
message: "Failed to process shutdown request.",
|
|
3312
|
+
serverId: this.adminOptions?.serverId
|
|
3313
|
+
};
|
|
3314
|
+
res.status(500).json(payload);
|
|
3315
|
+
}
|
|
3316
|
+
});
|
|
3317
|
+
}
|
|
3318
|
+
isAuthorizedShutdownRequest(req) {
|
|
3319
|
+
const expectedToken = this.adminOptions?.shutdownToken;
|
|
3320
|
+
if (!expectedToken) return false;
|
|
3321
|
+
const authHeader = req.headers.authorization;
|
|
3322
|
+
if (typeof authHeader === "string" && authHeader.startsWith("Bearer ")) return authHeader.slice(7) === expectedToken;
|
|
3323
|
+
const tokenHeader = req.headers["x-one-mcp-shutdown-token"];
|
|
3324
|
+
return typeof tokenHeader === "string" && tokenHeader === expectedToken;
|
|
3325
|
+
}
|
|
3326
|
+
async handleAdminShutdownRequest(req, res) {
|
|
3327
|
+
try {
|
|
3328
|
+
if (!this.adminOptions?.onShutdownRequested) {
|
|
3329
|
+
const payload$1 = {
|
|
3330
|
+
ok: false,
|
|
3331
|
+
message: "Shutdown endpoint is not enabled for this server instance.",
|
|
3332
|
+
serverId: this.adminOptions?.serverId
|
|
3333
|
+
};
|
|
3334
|
+
res.status(404).json(payload$1);
|
|
3335
|
+
return;
|
|
3336
|
+
}
|
|
3337
|
+
if (!this.isAuthorizedShutdownRequest(req)) {
|
|
3338
|
+
const payload$1 = {
|
|
3339
|
+
ok: false,
|
|
3340
|
+
message: "Unauthorized shutdown request: invalid or missing shutdown token.",
|
|
3341
|
+
serverId: this.adminOptions?.serverId
|
|
3342
|
+
};
|
|
3343
|
+
res.status(401).json(payload$1);
|
|
3344
|
+
return;
|
|
3345
|
+
}
|
|
3346
|
+
const payload = {
|
|
3347
|
+
ok: true,
|
|
3348
|
+
message: "Shutdown request accepted. Stopping server gracefully.",
|
|
3349
|
+
serverId: this.adminOptions?.serverId
|
|
3350
|
+
};
|
|
3351
|
+
res.json(payload);
|
|
3352
|
+
await this.adminOptions.onShutdownRequested();
|
|
3353
|
+
} catch (error) {
|
|
3354
|
+
throw new Error(`Failed to handle admin shutdown request: ${toErrorMessage$2(error)}`);
|
|
3355
|
+
}
|
|
3356
|
+
}
|
|
3357
|
+
async handlePostRequest(req, res) {
|
|
3358
|
+
const sessionId = req.headers["mcp-session-id"];
|
|
3359
|
+
let transport;
|
|
3360
|
+
if (sessionId && this.sessionManager.hasSession(sessionId)) transport = this.sessionManager.getSession(sessionId).transport;
|
|
3361
|
+
else if (!sessionId && (0, __modelcontextprotocol_sdk_types_js.isInitializeRequest)(req.body)) {
|
|
3362
|
+
const mcpServer = await this.serverFactory();
|
|
3363
|
+
transport = new __modelcontextprotocol_sdk_server_streamableHttp_js.StreamableHTTPServerTransport({
|
|
3364
|
+
sessionIdGenerator: () => (0, node_crypto.randomUUID)(),
|
|
3365
|
+
enableJsonResponse: true,
|
|
3366
|
+
onsessioninitialized: (initializedSessionId) => {
|
|
3367
|
+
this.sessionManager.setSession(initializedSessionId, transport, mcpServer);
|
|
3368
|
+
}
|
|
3369
|
+
});
|
|
3370
|
+
transport.onclose = async () => {
|
|
3371
|
+
if (transport.sessionId) try {
|
|
3372
|
+
await this.sessionManager.deleteSession(transport.sessionId);
|
|
3373
|
+
} catch (error) {
|
|
3374
|
+
console.error(`Failed to clean up session '${transport.sessionId}': ${toErrorMessage$2(error)}`);
|
|
3375
|
+
}
|
|
3376
|
+
};
|
|
3377
|
+
try {
|
|
3378
|
+
await mcpServer.connect(transport);
|
|
3379
|
+
} catch (error) {
|
|
3380
|
+
throw new Error(`Failed to connect MCP server transport for initialization request: ${toErrorMessage$2(error)}`);
|
|
3381
|
+
}
|
|
3382
|
+
} else {
|
|
3383
|
+
res.status(400).json({
|
|
3384
|
+
jsonrpc: "2.0",
|
|
3385
|
+
error: {
|
|
3386
|
+
code: -32e3,
|
|
3387
|
+
message: sessionId === void 0 ? "Bad Request: missing session ID and request body is not an initialize request." : `Bad Request: unknown session ID '${sessionId}'.`
|
|
3388
|
+
},
|
|
3389
|
+
id: null
|
|
3390
|
+
});
|
|
3391
|
+
return;
|
|
3392
|
+
}
|
|
3393
|
+
try {
|
|
3394
|
+
await transport.handleRequest(req, res, req.body);
|
|
3395
|
+
} catch (error) {
|
|
3396
|
+
throw new Error(`Failed handling MCP transport request: ${toErrorMessage$2(error)}`);
|
|
3397
|
+
}
|
|
3398
|
+
}
|
|
3399
|
+
async handleGetRequest(req, res) {
|
|
3400
|
+
const sessionId = req.headers["mcp-session-id"];
|
|
3401
|
+
if (!sessionId || !this.sessionManager.hasSession(sessionId)) {
|
|
3402
|
+
res.status(400).send("Invalid or missing session ID");
|
|
3403
|
+
return;
|
|
3404
|
+
}
|
|
3405
|
+
const session = this.sessionManager.getSession(sessionId);
|
|
3406
|
+
try {
|
|
3407
|
+
await session.transport.handleRequest(req, res);
|
|
3408
|
+
} catch (error) {
|
|
3409
|
+
throw new Error(`Failed handling MCP GET request for session '${sessionId}': ${toErrorMessage$2(error)}`);
|
|
3410
|
+
}
|
|
3411
|
+
}
|
|
3412
|
+
async handleDeleteRequest(req, res) {
|
|
3413
|
+
const sessionId = req.headers["mcp-session-id"];
|
|
3414
|
+
if (!sessionId || !this.sessionManager.hasSession(sessionId)) {
|
|
3415
|
+
res.status(400).send("Invalid or missing session ID");
|
|
3416
|
+
return;
|
|
3417
|
+
}
|
|
3418
|
+
const session = this.sessionManager.getSession(sessionId);
|
|
3419
|
+
try {
|
|
3420
|
+
await session.transport.handleRequest(req, res);
|
|
3421
|
+
} catch (error) {
|
|
3422
|
+
throw new Error(`Failed handling MCP DELETE request for session '${sessionId}': ${toErrorMessage$2(error)}`);
|
|
3423
|
+
}
|
|
3424
|
+
await this.sessionManager.deleteSession(sessionId);
|
|
3139
3425
|
}
|
|
3140
3426
|
async start() {
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3427
|
+
try {
|
|
3428
|
+
const server = this.app.listen(this.config.port, this.config.host);
|
|
3429
|
+
this.server = server;
|
|
3430
|
+
const listeningPromise = (async () => {
|
|
3431
|
+
await (0, node_events.once)(server, "listening");
|
|
3432
|
+
})();
|
|
3433
|
+
const errorPromise = (async () => {
|
|
3434
|
+
const [error] = await (0, node_events.once)(server, "error");
|
|
3435
|
+
throw error instanceof Error ? error : new Error(String(error));
|
|
3436
|
+
})();
|
|
3437
|
+
await Promise.race([listeningPromise, errorPromise]);
|
|
3438
|
+
console.error(`@agiflowai/one-mcp MCP server started on http://${this.config.host}:${this.config.port}/mcp`);
|
|
3439
|
+
console.error(`Health check: http://${this.config.host}:${this.config.port}/health`);
|
|
3440
|
+
} catch (error) {
|
|
3441
|
+
this.server = null;
|
|
3442
|
+
throw new Error(`Failed to start HTTP transport: ${toErrorMessage$2(error)}`);
|
|
3443
|
+
}
|
|
3144
3444
|
}
|
|
3145
3445
|
async stop() {
|
|
3146
|
-
if (this.
|
|
3147
|
-
|
|
3148
|
-
this.
|
|
3446
|
+
if (!this.server) return;
|
|
3447
|
+
try {
|
|
3448
|
+
await this.sessionManager.clear();
|
|
3449
|
+
} catch (error) {
|
|
3450
|
+
throw new Error(`Failed to clear sessions during HTTP transport stop: ${toErrorMessage$2(error)}`);
|
|
3451
|
+
}
|
|
3452
|
+
const closeServer = (0, node_util.promisify)(this.server.close.bind(this.server));
|
|
3453
|
+
try {
|
|
3454
|
+
await closeServer();
|
|
3455
|
+
this.server = null;
|
|
3456
|
+
} catch (error) {
|
|
3457
|
+
throw new Error(`Failed to stop HTTP transport: ${toErrorMessage$2(error)}`);
|
|
3149
3458
|
}
|
|
3150
3459
|
}
|
|
3460
|
+
getPort() {
|
|
3461
|
+
return this.config.port;
|
|
3462
|
+
}
|
|
3463
|
+
getHost() {
|
|
3464
|
+
return this.config.host;
|
|
3465
|
+
}
|
|
3151
3466
|
};
|
|
3152
3467
|
|
|
3153
3468
|
//#endregion
|
|
@@ -3293,180 +3608,503 @@ var SseTransportHandler = class {
|
|
|
3293
3608
|
};
|
|
3294
3609
|
|
|
3295
3610
|
//#endregion
|
|
3296
|
-
//#region src/transports/
|
|
3611
|
+
//#region src/transports/stdio.ts
|
|
3297
3612
|
/**
|
|
3298
|
-
*
|
|
3613
|
+
* Stdio transport handler for MCP server
|
|
3614
|
+
* Used for command-line and direct integrations
|
|
3615
|
+
*/
|
|
3616
|
+
var StdioTransportHandler = class {
|
|
3617
|
+
server;
|
|
3618
|
+
transport = null;
|
|
3619
|
+
constructor(server) {
|
|
3620
|
+
this.server = server;
|
|
3621
|
+
}
|
|
3622
|
+
async start() {
|
|
3623
|
+
this.transport = new __modelcontextprotocol_sdk_server_stdio_js.StdioServerTransport();
|
|
3624
|
+
await this.server.connect(this.transport);
|
|
3625
|
+
console.error("@agiflowai/one-mcp MCP server started on stdio");
|
|
3626
|
+
}
|
|
3627
|
+
async stop() {
|
|
3628
|
+
if (this.transport) {
|
|
3629
|
+
await this.transport.close();
|
|
3630
|
+
this.transport = null;
|
|
3631
|
+
}
|
|
3632
|
+
}
|
|
3633
|
+
};
|
|
3634
|
+
|
|
3635
|
+
//#endregion
|
|
3636
|
+
//#region src/transports/stdio-http.ts
|
|
3637
|
+
/**
|
|
3638
|
+
* STDIO-HTTP Proxy Transport
|
|
3299
3639
|
*
|
|
3300
3640
|
* DESIGN PATTERNS:
|
|
3301
3641
|
* - Transport handler pattern implementing TransportHandler interface
|
|
3302
|
-
* -
|
|
3303
|
-
* -
|
|
3304
|
-
* - Factory pattern for creating MCP server instances per session
|
|
3642
|
+
* - STDIO transport with MCP request forwarding to HTTP backend
|
|
3643
|
+
* - Graceful cleanup with error isolation
|
|
3305
3644
|
*
|
|
3306
3645
|
* CODING STANDARDS:
|
|
3307
|
-
* - Use
|
|
3308
|
-
* -
|
|
3309
|
-
* -
|
|
3310
|
-
* - Provide health check endpoint for monitoring
|
|
3311
|
-
* - Clean up resources on shutdown
|
|
3646
|
+
* - Use StdioServerTransport for stdio communication
|
|
3647
|
+
* - Reuse a single StreamableHTTP client connection
|
|
3648
|
+
* - Wrap async operations with try-catch and descriptive errors
|
|
3312
3649
|
*
|
|
3313
3650
|
* AVOID:
|
|
3314
|
-
* -
|
|
3315
|
-
* -
|
|
3316
|
-
* -
|
|
3317
|
-
* - Hardcoded configuration (use TransportConfig)
|
|
3651
|
+
* - Starting HTTP server lifecycle in this transport entry point
|
|
3652
|
+
* - Recreating HTTP client per request
|
|
3653
|
+
* - Swallowing cleanup failures silently
|
|
3318
3654
|
*/
|
|
3319
3655
|
/**
|
|
3320
|
-
* HTTP
|
|
3656
|
+
* Transport that serves MCP over stdio and forwards MCP requests to an HTTP endpoint.
|
|
3321
3657
|
*/
|
|
3322
|
-
var
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3658
|
+
var StdioHttpTransportHandler = class {
|
|
3659
|
+
endpoint;
|
|
3660
|
+
stdioProxyServer = null;
|
|
3661
|
+
stdioTransport = null;
|
|
3662
|
+
httpClient = null;
|
|
3663
|
+
constructor(config) {
|
|
3664
|
+
this.endpoint = config.endpoint;
|
|
3326
3665
|
}
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3666
|
+
async start() {
|
|
3667
|
+
try {
|
|
3668
|
+
const httpClientTransport = new __modelcontextprotocol_sdk_client_streamableHttp_js.StreamableHTTPClientTransport(this.endpoint);
|
|
3669
|
+
const client = new __modelcontextprotocol_sdk_client_index_js.Client({
|
|
3670
|
+
name: "@agiflowai/one-mcp-stdio-http-proxy",
|
|
3671
|
+
version: "0.1.0"
|
|
3672
|
+
}, { capabilities: {} });
|
|
3673
|
+
await client.connect(httpClientTransport);
|
|
3674
|
+
this.httpClient = client;
|
|
3675
|
+
this.stdioProxyServer = this.createProxyServer(client);
|
|
3676
|
+
this.stdioTransport = new __modelcontextprotocol_sdk_server_stdio_js.StdioServerTransport();
|
|
3677
|
+
await this.stdioProxyServer.connect(this.stdioTransport);
|
|
3678
|
+
console.error(`@agiflowai/one-mcp MCP stdio proxy connected to ${this.endpoint.toString()}`);
|
|
3679
|
+
} catch (error) {
|
|
3680
|
+
await this.stop();
|
|
3681
|
+
throw new Error(`Failed to start stdio-http proxy transport: ${error instanceof Error ? error.message : String(error)}`);
|
|
3682
|
+
}
|
|
3337
3683
|
}
|
|
3338
|
-
|
|
3339
|
-
|
|
3684
|
+
async stop() {
|
|
3685
|
+
const stdioTransport = this.stdioTransport;
|
|
3686
|
+
const stdioProxyServer = this.stdioProxyServer;
|
|
3687
|
+
const httpClient = this.httpClient;
|
|
3688
|
+
this.stdioTransport = null;
|
|
3689
|
+
this.stdioProxyServer = null;
|
|
3690
|
+
this.httpClient = null;
|
|
3691
|
+
const cleanupErrors = [];
|
|
3692
|
+
await Promise.all([
|
|
3693
|
+
(async () => {
|
|
3694
|
+
try {
|
|
3695
|
+
if (stdioTransport) await stdioTransport.close();
|
|
3696
|
+
} catch (error) {
|
|
3697
|
+
cleanupErrors.push(`failed closing stdio transport: ${error instanceof Error ? error.message : String(error)}`);
|
|
3698
|
+
}
|
|
3699
|
+
})(),
|
|
3700
|
+
(async () => {
|
|
3701
|
+
try {
|
|
3702
|
+
if (stdioProxyServer) await stdioProxyServer.close();
|
|
3703
|
+
} catch (error) {
|
|
3704
|
+
cleanupErrors.push(`failed closing stdio proxy server: ${error instanceof Error ? error.message : String(error)}`);
|
|
3705
|
+
}
|
|
3706
|
+
})(),
|
|
3707
|
+
(async () => {
|
|
3708
|
+
try {
|
|
3709
|
+
if (httpClient) await httpClient.close();
|
|
3710
|
+
} catch (error) {
|
|
3711
|
+
cleanupErrors.push(`failed closing http client: ${error instanceof Error ? error.message : String(error)}`);
|
|
3712
|
+
}
|
|
3713
|
+
})()
|
|
3714
|
+
]);
|
|
3715
|
+
if (cleanupErrors.length > 0) throw new Error(`Failed to stop stdio-http proxy transport: ${cleanupErrors.join("; ")}`);
|
|
3340
3716
|
}
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3717
|
+
createProxyServer(client) {
|
|
3718
|
+
const proxyServer = new __modelcontextprotocol_sdk_server_index_js.Server({
|
|
3719
|
+
name: "@agiflowai/one-mcp-stdio-http-proxy",
|
|
3720
|
+
version: "0.1.0"
|
|
3721
|
+
}, { capabilities: {
|
|
3722
|
+
tools: {},
|
|
3723
|
+
resources: {},
|
|
3724
|
+
prompts: {}
|
|
3725
|
+
} });
|
|
3726
|
+
proxyServer.setRequestHandler(__modelcontextprotocol_sdk_types_js.ListToolsRequestSchema, async () => {
|
|
3727
|
+
try {
|
|
3728
|
+
return await client.listTools();
|
|
3729
|
+
} catch (error) {
|
|
3730
|
+
throw new Error(`Failed forwarding tools/list to HTTP backend: ${error instanceof Error ? error.message : String(error)}`);
|
|
3731
|
+
}
|
|
3732
|
+
});
|
|
3733
|
+
proxyServer.setRequestHandler(__modelcontextprotocol_sdk_types_js.CallToolRequestSchema, async (request) => {
|
|
3734
|
+
try {
|
|
3735
|
+
return await client.callTool({
|
|
3736
|
+
name: request.params.name,
|
|
3737
|
+
arguments: request.params.arguments
|
|
3738
|
+
});
|
|
3739
|
+
} catch (error) {
|
|
3740
|
+
throw new Error(`Failed forwarding tools/call (${request.params.name}) to HTTP backend: ${error instanceof Error ? error.message : String(error)}`);
|
|
3741
|
+
}
|
|
3742
|
+
});
|
|
3743
|
+
proxyServer.setRequestHandler(__modelcontextprotocol_sdk_types_js.ListResourcesRequestSchema, async () => {
|
|
3744
|
+
try {
|
|
3745
|
+
return await client.listResources();
|
|
3746
|
+
} catch (error) {
|
|
3747
|
+
throw new Error(`Failed forwarding resources/list to HTTP backend: ${error instanceof Error ? error.message : String(error)}`);
|
|
3748
|
+
}
|
|
3749
|
+
});
|
|
3750
|
+
proxyServer.setRequestHandler(__modelcontextprotocol_sdk_types_js.ReadResourceRequestSchema, async (request) => {
|
|
3751
|
+
try {
|
|
3752
|
+
return await client.readResource({ uri: request.params.uri });
|
|
3753
|
+
} catch (error) {
|
|
3754
|
+
throw new Error(`Failed forwarding resources/read (${request.params.uri}) to HTTP backend: ${error instanceof Error ? error.message : String(error)}`);
|
|
3755
|
+
}
|
|
3756
|
+
});
|
|
3757
|
+
proxyServer.setRequestHandler(__modelcontextprotocol_sdk_types_js.ListPromptsRequestSchema, async () => {
|
|
3758
|
+
try {
|
|
3759
|
+
return await client.listPrompts();
|
|
3760
|
+
} catch (error) {
|
|
3761
|
+
throw new Error(`Failed forwarding prompts/list to HTTP backend: ${error instanceof Error ? error.message : String(error)}`);
|
|
3762
|
+
}
|
|
3763
|
+
});
|
|
3764
|
+
proxyServer.setRequestHandler(__modelcontextprotocol_sdk_types_js.GetPromptRequestSchema, async (request) => {
|
|
3765
|
+
try {
|
|
3766
|
+
return await client.getPrompt({
|
|
3767
|
+
name: request.params.name,
|
|
3768
|
+
arguments: request.params.arguments
|
|
3769
|
+
});
|
|
3770
|
+
} catch (error) {
|
|
3771
|
+
throw new Error(`Failed forwarding prompts/get (${request.params.name}) to HTTP backend: ${error instanceof Error ? error.message : String(error)}`);
|
|
3772
|
+
}
|
|
3773
|
+
});
|
|
3774
|
+
return proxyServer;
|
|
3344
3775
|
}
|
|
3345
3776
|
};
|
|
3777
|
+
|
|
3778
|
+
//#endregion
|
|
3779
|
+
//#region src/services/RuntimeStateService.ts
|
|
3346
3780
|
/**
|
|
3347
|
-
*
|
|
3348
|
-
*
|
|
3781
|
+
* RuntimeStateService
|
|
3782
|
+
*
|
|
3783
|
+
* Persists runtime metadata for HTTP one-mcp instances so external commands
|
|
3784
|
+
* (for example `one-mcp stop`) can discover and target the correct server.
|
|
3349
3785
|
*/
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3786
|
+
const RUNTIME_DIR_NAME = "runtimes";
|
|
3787
|
+
const RUNTIME_FILE_SUFFIX = ".runtime.json";
|
|
3788
|
+
function isObject(value) {
|
|
3789
|
+
return typeof value === "object" && value !== null;
|
|
3790
|
+
}
|
|
3791
|
+
function isRuntimeStateRecord(value) {
|
|
3792
|
+
if (!isObject(value)) return false;
|
|
3793
|
+
return typeof value.serverId === "string" && typeof value.host === "string" && typeof value.port === "number" && value.transport === "http" && typeof value.shutdownToken === "string" && typeof value.startedAt === "string" && typeof value.pid === "number" && (value.configPath === void 0 || typeof value.configPath === "string");
|
|
3794
|
+
}
|
|
3795
|
+
function toErrorMessage$1(error) {
|
|
3796
|
+
return error instanceof Error ? error.message : String(error);
|
|
3797
|
+
}
|
|
3798
|
+
/**
|
|
3799
|
+
* Runtime state persistence implementation.
|
|
3800
|
+
*/
|
|
3801
|
+
var RuntimeStateService = class RuntimeStateService {
|
|
3802
|
+
runtimeDir;
|
|
3803
|
+
constructor(runtimeDir) {
|
|
3804
|
+
this.runtimeDir = runtimeDir ?? RuntimeStateService.getDefaultRuntimeDir();
|
|
3367
3805
|
}
|
|
3368
|
-
|
|
3369
|
-
|
|
3806
|
+
/**
|
|
3807
|
+
* Resolve default runtime directory under the user's home cache path.
|
|
3808
|
+
* @returns Absolute runtime directory path
|
|
3809
|
+
*/
|
|
3810
|
+
static getDefaultRuntimeDir() {
|
|
3811
|
+
return (0, node_path.join)((0, node_os.homedir)(), ".aicode-toolkit", "one-mcp", RUNTIME_DIR_NAME);
|
|
3370
3812
|
}
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
});
|
|
3378
|
-
this.app.delete("/mcp", async (req, res) => {
|
|
3379
|
-
await this.handleDeleteRequest(req, res);
|
|
3380
|
-
});
|
|
3381
|
-
this.app.get("/health", (_req, res) => {
|
|
3382
|
-
res.json({
|
|
3383
|
-
status: "ok",
|
|
3384
|
-
transport: "http"
|
|
3385
|
-
});
|
|
3386
|
-
});
|
|
3813
|
+
/**
|
|
3814
|
+
* Build runtime state file path for a given server ID.
|
|
3815
|
+
* @param serverId - Target one-mcp server identifier
|
|
3816
|
+
* @returns Absolute runtime file path
|
|
3817
|
+
*/
|
|
3818
|
+
getRecordPath(serverId) {
|
|
3819
|
+
return (0, node_path.join)(this.runtimeDir, `${serverId}${RUNTIME_FILE_SUFFIX}`);
|
|
3387
3820
|
}
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3821
|
+
/**
|
|
3822
|
+
* Persist a runtime state record.
|
|
3823
|
+
* @param record - Runtime metadata to persist
|
|
3824
|
+
* @returns Promise that resolves when write completes
|
|
3825
|
+
*/
|
|
3826
|
+
async write(record) {
|
|
3827
|
+
await (0, node_fs_promises.mkdir)(this.runtimeDir, { recursive: true });
|
|
3828
|
+
await (0, node_fs_promises.writeFile)(this.getRecordPath(record.serverId), JSON.stringify(record, null, 2), "utf-8");
|
|
3829
|
+
}
|
|
3830
|
+
/**
|
|
3831
|
+
* Read a runtime state record by server ID.
|
|
3832
|
+
* @param serverId - Target one-mcp server identifier
|
|
3833
|
+
* @returns Matching runtime record, or null when no record exists
|
|
3834
|
+
*/
|
|
3835
|
+
async read(serverId) {
|
|
3836
|
+
const filePath = this.getRecordPath(serverId);
|
|
3837
|
+
try {
|
|
3838
|
+
const content = await (0, node_fs_promises.readFile)(filePath, "utf-8");
|
|
3839
|
+
const parsed = JSON.parse(content);
|
|
3840
|
+
return isRuntimeStateRecord(parsed) ? parsed : null;
|
|
3841
|
+
} catch (error) {
|
|
3842
|
+
if (isObject(error) && "code" in error && error.code === "ENOENT") return null;
|
|
3843
|
+
throw new Error(`Failed to read runtime state for server '${serverId}' from '${filePath}': ${toErrorMessage$1(error)}`);
|
|
3844
|
+
}
|
|
3845
|
+
}
|
|
3846
|
+
/**
|
|
3847
|
+
* List all persisted runtime records.
|
|
3848
|
+
* @returns Array of runtime records
|
|
3849
|
+
*/
|
|
3850
|
+
async list() {
|
|
3851
|
+
try {
|
|
3852
|
+
const files = (await (0, node_fs_promises.readdir)(this.runtimeDir, { withFileTypes: true })).filter((entry) => entry.isFile() && entry.name.endsWith(RUNTIME_FILE_SUFFIX));
|
|
3853
|
+
return (await Promise.all(files.map(async (file) => {
|
|
3854
|
+
try {
|
|
3855
|
+
const content = await (0, node_fs_promises.readFile)((0, node_path.join)(this.runtimeDir, file.name), "utf-8");
|
|
3856
|
+
const parsed = JSON.parse(content);
|
|
3857
|
+
return isRuntimeStateRecord(parsed) ? parsed : null;
|
|
3858
|
+
} catch {
|
|
3859
|
+
return null;
|
|
3399
3860
|
}
|
|
3400
|
-
});
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
};
|
|
3404
|
-
await mcpServer.connect(transport);
|
|
3405
|
-
} else {
|
|
3406
|
-
res.status(400).json({
|
|
3407
|
-
jsonrpc: "2.0",
|
|
3408
|
-
error: {
|
|
3409
|
-
code: -32e3,
|
|
3410
|
-
message: "Bad Request: No valid session ID provided"
|
|
3411
|
-
},
|
|
3412
|
-
id: null
|
|
3413
|
-
});
|
|
3414
|
-
return;
|
|
3861
|
+
}))).filter((record) => record !== null);
|
|
3862
|
+
} catch (error) {
|
|
3863
|
+
if (isObject(error) && "code" in error && error.code === "ENOENT") return [];
|
|
3864
|
+
throw new Error(`Failed to list runtime states from '${this.runtimeDir}': ${toErrorMessage$1(error)}`);
|
|
3415
3865
|
}
|
|
3416
|
-
await transport.handleRequest(req, res, req.body);
|
|
3417
3866
|
}
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3867
|
+
/**
|
|
3868
|
+
* Remove a runtime state record by server ID.
|
|
3869
|
+
* @param serverId - Target one-mcp server identifier
|
|
3870
|
+
* @returns Promise that resolves when delete completes
|
|
3871
|
+
*/
|
|
3872
|
+
async remove(serverId) {
|
|
3873
|
+
await (0, node_fs_promises.rm)(this.getRecordPath(serverId), { force: true });
|
|
3874
|
+
}
|
|
3875
|
+
};
|
|
3876
|
+
|
|
3877
|
+
//#endregion
|
|
3878
|
+
//#region src/services/StopServerService/constants.ts
|
|
3879
|
+
/**
|
|
3880
|
+
* StopServerService constants.
|
|
3881
|
+
*/
|
|
3882
|
+
/** Maximum time in milliseconds to wait for a shutdown to complete. */
|
|
3883
|
+
const DEFAULT_STOP_TIMEOUT_MS = 5e3;
|
|
3884
|
+
/** Minimum timeout in milliseconds for individual health check requests. */
|
|
3885
|
+
const HEALTH_REQUEST_TIMEOUT_FLOOR_MS = 250;
|
|
3886
|
+
/** Delay in milliseconds between shutdown polling attempts. */
|
|
3887
|
+
const SHUTDOWN_POLL_INTERVAL_MS = 200;
|
|
3888
|
+
/** Path for the runtime health check endpoint. */
|
|
3889
|
+
const HEALTH_CHECK_PATH = "/health";
|
|
3890
|
+
/** Path for the authenticated admin shutdown endpoint. */
|
|
3891
|
+
const ADMIN_SHUTDOWN_PATH = "/admin/shutdown";
|
|
3892
|
+
/** HTTP GET method identifier. */
|
|
3893
|
+
const HTTP_METHOD_GET = "GET";
|
|
3894
|
+
/** HTTP POST method identifier. */
|
|
3895
|
+
const HTTP_METHOD_POST = "POST";
|
|
3896
|
+
/** HTTP header name for bearer token authorization. */
|
|
3897
|
+
const AUTHORIZATION_HEADER_NAME = "Authorization";
|
|
3898
|
+
/** Prefix for bearer token values in the Authorization header. */
|
|
3899
|
+
const BEARER_TOKEN_PREFIX = "Bearer ";
|
|
3900
|
+
/** HTTP protocol scheme prefix for URL construction. */
|
|
3901
|
+
const HTTP_PROTOCOL = "http://";
|
|
3902
|
+
/** Separator between host and port in URL construction. */
|
|
3903
|
+
const URL_PORT_SEPARATOR = ":";
|
|
3904
|
+
/** Loopback hostname. */
|
|
3905
|
+
const LOOPBACK_HOST_LOCALHOST = "localhost";
|
|
3906
|
+
/** IPv4 loopback address. */
|
|
3907
|
+
const LOOPBACK_HOST_IPV4 = "127.0.0.1";
|
|
3908
|
+
/** IPv6 loopback address. */
|
|
3909
|
+
const LOOPBACK_HOST_IPV6 = "::1";
|
|
3910
|
+
/** Hosts that are safe to send admin requests to (loopback only). */
|
|
3911
|
+
const ALLOWED_HOSTS = new Set([
|
|
3912
|
+
LOOPBACK_HOST_LOCALHOST,
|
|
3913
|
+
LOOPBACK_HOST_IPV4,
|
|
3914
|
+
LOOPBACK_HOST_IPV6
|
|
3915
|
+
]);
|
|
3916
|
+
/** Expected status value in a healthy runtime response. */
|
|
3917
|
+
const HEALTH_STATUS_OK = "ok";
|
|
3918
|
+
/** Expected transport value in a healthy runtime response. */
|
|
3919
|
+
const HEALTH_TRANSPORT_HTTP = "http";
|
|
3920
|
+
/** Property key for status field in health responses. */
|
|
3921
|
+
const KEY_STATUS = "status";
|
|
3922
|
+
/** Property key for transport field in health responses. */
|
|
3923
|
+
const KEY_TRANSPORT = "transport";
|
|
3924
|
+
/** Property key for serverId field in runtime responses. */
|
|
3925
|
+
const KEY_SERVER_ID = "serverId";
|
|
3926
|
+
/** Property key for ok field in shutdown responses. */
|
|
3927
|
+
const KEY_OK = "ok";
|
|
3928
|
+
/** Property key for message field in shutdown responses. */
|
|
3929
|
+
const KEY_MESSAGE = "message";
|
|
3930
|
+
|
|
3931
|
+
//#endregion
|
|
3932
|
+
//#region src/services/StopServerService/types.ts
|
|
3933
|
+
/**
|
|
3934
|
+
* Safely cast a non-null object to a string-keyed record for property access.
|
|
3935
|
+
* @param value - Object value already verified as non-null
|
|
3936
|
+
* @returns The same value typed as a record
|
|
3937
|
+
*/
|
|
3938
|
+
function toRecord(value) {
|
|
3939
|
+
return value;
|
|
3940
|
+
}
|
|
3941
|
+
/**
|
|
3942
|
+
* Type guard for health responses.
|
|
3943
|
+
* @param value - Candidate payload to validate
|
|
3944
|
+
* @returns True when payload matches health response shape
|
|
3945
|
+
*/
|
|
3946
|
+
function isHealthResponse(value) {
|
|
3947
|
+
if (typeof value !== "object" || value === null) return false;
|
|
3948
|
+
const record = toRecord(value);
|
|
3949
|
+
return KEY_STATUS in record && record[KEY_STATUS] === HEALTH_STATUS_OK && KEY_TRANSPORT in record && record[KEY_TRANSPORT] === HEALTH_TRANSPORT_HTTP && (!(KEY_SERVER_ID in record) || record[KEY_SERVER_ID] === void 0 || typeof record[KEY_SERVER_ID] === "string");
|
|
3950
|
+
}
|
|
3951
|
+
/**
|
|
3952
|
+
* Type guard for shutdown responses.
|
|
3953
|
+
* @param value - Candidate payload to validate
|
|
3954
|
+
* @returns True when payload matches shutdown response shape
|
|
3955
|
+
*/
|
|
3956
|
+
function isShutdownResponse(value) {
|
|
3957
|
+
if (typeof value !== "object" || value === null) return false;
|
|
3958
|
+
const record = toRecord(value);
|
|
3959
|
+
return KEY_OK in record && typeof record[KEY_OK] === "boolean" && KEY_MESSAGE in record && typeof record[KEY_MESSAGE] === "string" && (!(KEY_SERVER_ID in record) || record[KEY_SERVER_ID] === void 0 || typeof record[KEY_SERVER_ID] === "string");
|
|
3960
|
+
}
|
|
3961
|
+
|
|
3962
|
+
//#endregion
|
|
3963
|
+
//#region src/services/StopServerService/StopServerService.ts
|
|
3964
|
+
/**
|
|
3965
|
+
* Format runtime endpoint URL after validating the host is a loopback address.
|
|
3966
|
+
* Rejects non-loopback hosts to prevent SSRF via tampered runtime state files.
|
|
3967
|
+
* @param runtime - Runtime record to format
|
|
3968
|
+
* @param path - Request path to append
|
|
3969
|
+
* @returns Full runtime URL
|
|
3970
|
+
*/
|
|
3971
|
+
function buildRuntimeUrl(runtime, path) {
|
|
3972
|
+
if (!ALLOWED_HOSTS.has(runtime.host)) throw new Error(`Refusing to connect to non-loopback host '${runtime.host}'. Only ${Array.from(ALLOWED_HOSTS).join(", ")} are allowed.`);
|
|
3973
|
+
return `${HTTP_PROTOCOL}${runtime.host}${URL_PORT_SEPARATOR}${runtime.port}${path}`;
|
|
3974
|
+
}
|
|
3975
|
+
function toErrorMessage(error) {
|
|
3976
|
+
return error instanceof Error ? error.message : String(error);
|
|
3977
|
+
}
|
|
3978
|
+
function sleep(delayMs) {
|
|
3979
|
+
return new Promise((resolve$2) => {
|
|
3980
|
+
setTimeout(resolve$2, delayMs);
|
|
3981
|
+
});
|
|
3982
|
+
}
|
|
3983
|
+
/**
|
|
3984
|
+
* Service for resolving runtime targets and stopping them safely.
|
|
3985
|
+
*/
|
|
3986
|
+
var StopServerService = class {
|
|
3987
|
+
runtimeStateService;
|
|
3988
|
+
constructor(runtimeStateService = new RuntimeStateService()) {
|
|
3989
|
+
this.runtimeStateService = runtimeStateService;
|
|
3990
|
+
}
|
|
3991
|
+
/**
|
|
3992
|
+
* Resolve a target runtime and stop it cooperatively.
|
|
3993
|
+
* @param request - Stop request options
|
|
3994
|
+
* @returns Stop result payload
|
|
3995
|
+
*/
|
|
3996
|
+
async stop(request) {
|
|
3997
|
+
const timeoutMs = request.timeoutMs ?? DEFAULT_STOP_TIMEOUT_MS;
|
|
3998
|
+
const runtime = await this.resolveRuntime(request);
|
|
3999
|
+
const health = await this.fetchHealth(runtime, timeoutMs);
|
|
4000
|
+
if (!health.reachable) {
|
|
4001
|
+
await this.runtimeStateService.remove(runtime.serverId);
|
|
4002
|
+
throw new Error(`Runtime '${runtime.serverId}' is not reachable at http://${runtime.host}:${runtime.port}. Removed stale runtime record.`);
|
|
3423
4003
|
}
|
|
3424
|
-
|
|
4004
|
+
if (!request.force && health.payload?.serverId && health.payload.serverId !== runtime.serverId) throw new Error(`Refusing to stop runtime at http://${runtime.host}:${runtime.port}: expected server ID '${runtime.serverId}' but health endpoint reported '${health.payload.serverId}'. Use --force to override.`);
|
|
4005
|
+
const shutdownToken = request.token ?? runtime.shutdownToken;
|
|
4006
|
+
if (!shutdownToken) throw new Error(`No shutdown token available for runtime '${runtime.serverId}'.`);
|
|
4007
|
+
const shutdownResponse = await this.requestShutdown(runtime, shutdownToken, timeoutMs);
|
|
4008
|
+
await this.waitForShutdown(runtime, timeoutMs);
|
|
4009
|
+
await this.runtimeStateService.remove(runtime.serverId);
|
|
4010
|
+
return {
|
|
4011
|
+
ok: true,
|
|
4012
|
+
serverId: runtime.serverId,
|
|
4013
|
+
host: runtime.host,
|
|
4014
|
+
port: runtime.port,
|
|
4015
|
+
message: shutdownResponse.message
|
|
4016
|
+
};
|
|
3425
4017
|
}
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
4018
|
+
/**
|
|
4019
|
+
* Resolve a runtime record from explicit ID or a unique host/port pair.
|
|
4020
|
+
* @param request - Stop request options
|
|
4021
|
+
* @returns Matching runtime record
|
|
4022
|
+
*/
|
|
4023
|
+
async resolveRuntime(request) {
|
|
4024
|
+
if (request.serverId) {
|
|
4025
|
+
const runtime = await this.runtimeStateService.read(request.serverId);
|
|
4026
|
+
if (!runtime) throw new Error(`No runtime record found for server ID '${request.serverId}'. Start the server with 'one-mcp mcp-serve --type http' first.`);
|
|
4027
|
+
return runtime;
|
|
3431
4028
|
}
|
|
3432
|
-
|
|
3433
|
-
this.
|
|
4029
|
+
if (request.host === void 0 || request.port === void 0) throw new Error("Provide --id or both --host and --port to select a runtime.");
|
|
4030
|
+
const matches = (await this.runtimeStateService.list()).filter((runtime) => runtime.host === request.host && runtime.port === request.port);
|
|
4031
|
+
if (matches.length === 0) throw new Error(`No runtime record found for http://${request.host}:${request.port}. Start the server with 'one-mcp mcp-serve --type http' first.`);
|
|
4032
|
+
if (matches.length > 1) throw new Error(`Multiple runtime records match http://${request.host}:${request.port}. Retry with --id to avoid stopping the wrong server.`);
|
|
4033
|
+
return matches[0];
|
|
3434
4034
|
}
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
4035
|
+
/**
|
|
4036
|
+
* Read the runtime health payload.
|
|
4037
|
+
* @param runtime - Runtime to query
|
|
4038
|
+
* @param timeoutMs - Request timeout in milliseconds
|
|
4039
|
+
* @returns Reachability status and optional payload
|
|
4040
|
+
*/
|
|
4041
|
+
async fetchHealth(runtime, timeoutMs) {
|
|
4042
|
+
try {
|
|
4043
|
+
const response = await this.fetchWithTimeout(buildRuntimeUrl(runtime, HEALTH_CHECK_PATH), { method: HTTP_METHOD_GET }, timeoutMs);
|
|
4044
|
+
if (!response.ok) return { reachable: false };
|
|
4045
|
+
const payload = await response.json();
|
|
4046
|
+
if (!isHealthResponse(payload)) throw new Error("Received invalid health response payload.");
|
|
4047
|
+
return {
|
|
4048
|
+
reachable: true,
|
|
4049
|
+
payload
|
|
4050
|
+
};
|
|
4051
|
+
} catch {
|
|
4052
|
+
return { reachable: false };
|
|
4053
|
+
}
|
|
3450
4054
|
}
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
4055
|
+
/**
|
|
4056
|
+
* Send authenticated shutdown request to the admin endpoint.
|
|
4057
|
+
* @param runtime - Runtime to stop
|
|
4058
|
+
* @param shutdownToken - Bearer token for the admin endpoint
|
|
4059
|
+
* @param timeoutMs - Request timeout in milliseconds
|
|
4060
|
+
* @returns Parsed shutdown response payload
|
|
4061
|
+
*/
|
|
4062
|
+
async requestShutdown(runtime, shutdownToken, timeoutMs) {
|
|
4063
|
+
const response = await this.fetchWithTimeout(buildRuntimeUrl(runtime, ADMIN_SHUTDOWN_PATH), {
|
|
4064
|
+
method: HTTP_METHOD_POST,
|
|
4065
|
+
headers: { [AUTHORIZATION_HEADER_NAME]: `${BEARER_TOKEN_PREFIX}${shutdownToken}` }
|
|
4066
|
+
}, timeoutMs);
|
|
4067
|
+
const payload = await response.json();
|
|
4068
|
+
if (!isShutdownResponse(payload)) throw new Error("Received invalid shutdown response payload.");
|
|
4069
|
+
if (!response.ok || !payload.ok) throw new Error(payload.message);
|
|
4070
|
+
return payload;
|
|
3464
4071
|
}
|
|
3465
|
-
|
|
3466
|
-
|
|
4072
|
+
/**
|
|
4073
|
+
* Poll until the target runtime is no longer reachable.
|
|
4074
|
+
* @param runtime - Runtime expected to stop
|
|
4075
|
+
* @param timeoutMs - Maximum wait time in milliseconds
|
|
4076
|
+
* @returns Promise that resolves when shutdown is observed
|
|
4077
|
+
*/
|
|
4078
|
+
async waitForShutdown(runtime, timeoutMs) {
|
|
4079
|
+
const deadline = Date.now() + timeoutMs;
|
|
4080
|
+
while (Date.now() < deadline) {
|
|
4081
|
+
if (!(await this.fetchHealth(runtime, Math.max(HEALTH_REQUEST_TIMEOUT_FLOOR_MS, deadline - Date.now()))).reachable) return;
|
|
4082
|
+
await sleep(SHUTDOWN_POLL_INTERVAL_MS);
|
|
4083
|
+
}
|
|
4084
|
+
throw new Error(`Timed out waiting for runtime '${runtime.serverId}' to stop at http://${runtime.host}:${runtime.port}.`);
|
|
3467
4085
|
}
|
|
3468
|
-
|
|
3469
|
-
|
|
4086
|
+
/**
|
|
4087
|
+
* Perform a fetch with an abort timeout.
|
|
4088
|
+
* @param url - Target URL
|
|
4089
|
+
* @param init - Fetch options
|
|
4090
|
+
* @param timeoutMs - Timeout in milliseconds
|
|
4091
|
+
* @returns Fetch response
|
|
4092
|
+
*/
|
|
4093
|
+
async fetchWithTimeout(url, init, timeoutMs) {
|
|
4094
|
+
const controller = new AbortController();
|
|
4095
|
+
const timeoutId = setTimeout(() => {
|
|
4096
|
+
controller.abort();
|
|
4097
|
+
}, timeoutMs);
|
|
4098
|
+
try {
|
|
4099
|
+
return await fetch(url, {
|
|
4100
|
+
...init,
|
|
4101
|
+
signal: controller.signal
|
|
4102
|
+
});
|
|
4103
|
+
} catch (error) {
|
|
4104
|
+
throw new Error(`Request to '${url}' failed: ${toErrorMessage(error)}`);
|
|
4105
|
+
} finally {
|
|
4106
|
+
clearTimeout(timeoutId);
|
|
4107
|
+
}
|
|
3470
4108
|
}
|
|
3471
4109
|
};
|
|
3472
4110
|
|
|
@@ -3501,6 +4139,12 @@ Object.defineProperty(exports, 'McpClientManagerService', {
|
|
|
3501
4139
|
return McpClientManagerService;
|
|
3502
4140
|
}
|
|
3503
4141
|
});
|
|
4142
|
+
Object.defineProperty(exports, 'RuntimeStateService', {
|
|
4143
|
+
enumerable: true,
|
|
4144
|
+
get: function () {
|
|
4145
|
+
return RuntimeStateService;
|
|
4146
|
+
}
|
|
4147
|
+
});
|
|
3504
4148
|
Object.defineProperty(exports, 'SearchListToolsTool', {
|
|
3505
4149
|
enumerable: true,
|
|
3506
4150
|
get: function () {
|
|
@@ -3519,12 +4163,30 @@ Object.defineProperty(exports, 'SseTransportHandler', {
|
|
|
3519
4163
|
return SseTransportHandler;
|
|
3520
4164
|
}
|
|
3521
4165
|
});
|
|
4166
|
+
Object.defineProperty(exports, 'StdioHttpTransportHandler', {
|
|
4167
|
+
enumerable: true,
|
|
4168
|
+
get: function () {
|
|
4169
|
+
return StdioHttpTransportHandler;
|
|
4170
|
+
}
|
|
4171
|
+
});
|
|
3522
4172
|
Object.defineProperty(exports, 'StdioTransportHandler', {
|
|
3523
4173
|
enumerable: true,
|
|
3524
4174
|
get: function () {
|
|
3525
4175
|
return StdioTransportHandler;
|
|
3526
4176
|
}
|
|
3527
4177
|
});
|
|
4178
|
+
Object.defineProperty(exports, 'StopServerService', {
|
|
4179
|
+
enumerable: true,
|
|
4180
|
+
get: function () {
|
|
4181
|
+
return StopServerService;
|
|
4182
|
+
}
|
|
4183
|
+
});
|
|
4184
|
+
Object.defineProperty(exports, 'TRANSPORT_MODE', {
|
|
4185
|
+
enumerable: true,
|
|
4186
|
+
get: function () {
|
|
4187
|
+
return TRANSPORT_MODE;
|
|
4188
|
+
}
|
|
4189
|
+
});
|
|
3528
4190
|
Object.defineProperty(exports, 'UseToolTool', {
|
|
3529
4191
|
enumerable: true,
|
|
3530
4192
|
get: function () {
|