@skilder-ai/runtime 0.7.3 → 0.7.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +361 -109
- package/dist/index.js.map +4 -4
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -138392,6 +138392,7 @@ var McpServerStatus = /* @__PURE__ */ ((McpServerStatus2) => {
|
|
|
138392
138392
|
McpServerStatus2["Connected"] = "CONNECTED";
|
|
138393
138393
|
McpServerStatus2["Disconnected"] = "DISCONNECTED";
|
|
138394
138394
|
McpServerStatus2["Error"] = "ERROR";
|
|
138395
|
+
McpServerStatus2["Idle"] = "IDLE";
|
|
138395
138396
|
McpServerStatus2["Pending"] = "PENDING";
|
|
138396
138397
|
McpServerStatus2["Starting"] = "STARTING";
|
|
138397
138398
|
return McpServerStatus2;
|
|
@@ -138461,6 +138462,34 @@ var WorkspaceRole = /* @__PURE__ */ ((WorkspaceRole2) => {
|
|
|
138461
138462
|
return WorkspaceRole2;
|
|
138462
138463
|
})(WorkspaceRole || {});
|
|
138463
138464
|
|
|
138465
|
+
// ../common/src/utils/parse-script-args.ts
|
|
138466
|
+
function parseScriptArgs(argsString) {
|
|
138467
|
+
const args = [];
|
|
138468
|
+
let current = "";
|
|
138469
|
+
let inQuotes = false;
|
|
138470
|
+
let quoteChar = "";
|
|
138471
|
+
for (const char of argsString) {
|
|
138472
|
+
if ((char === '"' || char === "'") && !inQuotes) {
|
|
138473
|
+
inQuotes = true;
|
|
138474
|
+
quoteChar = char;
|
|
138475
|
+
} else if (char === quoteChar && inQuotes) {
|
|
138476
|
+
inQuotes = false;
|
|
138477
|
+
quoteChar = "";
|
|
138478
|
+
} else if (char === " " && !inQuotes) {
|
|
138479
|
+
if (current) {
|
|
138480
|
+
args.push(current);
|
|
138481
|
+
current = "";
|
|
138482
|
+
}
|
|
138483
|
+
} else {
|
|
138484
|
+
current += char;
|
|
138485
|
+
}
|
|
138486
|
+
}
|
|
138487
|
+
if (current) {
|
|
138488
|
+
args.push(current);
|
|
138489
|
+
}
|
|
138490
|
+
return args;
|
|
138491
|
+
}
|
|
138492
|
+
|
|
138464
138493
|
// ../common/src/related-content-detector.ts
|
|
138465
138494
|
function escapeRegex2(str) {
|
|
138466
138495
|
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -144820,39 +144849,9 @@ For other resources, use the \`learn\` tool instead.`
|
|
|
144820
144849
|
};
|
|
144821
144850
|
}
|
|
144822
144851
|
const script = scriptEntry;
|
|
144823
|
-
const args = lookupArgs ?
|
|
144852
|
+
const args = lookupArgs ? parseScriptArgs(lookupArgs) : [];
|
|
144824
144853
|
return await this.routeScriptToRuntime(script, skill, args);
|
|
144825
144854
|
}
|
|
144826
|
-
/**
|
|
144827
|
-
* Parse script arguments from the path string.
|
|
144828
|
-
* Arguments can be space-separated, with quoted strings supported.
|
|
144829
|
-
*/
|
|
144830
|
-
parseScriptArgs(argsString) {
|
|
144831
|
-
const args = [];
|
|
144832
|
-
let current = "";
|
|
144833
|
-
let inQuotes = false;
|
|
144834
|
-
let quoteChar = "";
|
|
144835
|
-
for (const char of argsString) {
|
|
144836
|
-
if ((char === '"' || char === "'") && !inQuotes) {
|
|
144837
|
-
inQuotes = true;
|
|
144838
|
-
quoteChar = char;
|
|
144839
|
-
} else if (char === quoteChar && inQuotes) {
|
|
144840
|
-
inQuotes = false;
|
|
144841
|
-
quoteChar = "";
|
|
144842
|
-
} else if (char === " " && !inQuotes) {
|
|
144843
|
-
if (current) {
|
|
144844
|
-
args.push(current);
|
|
144845
|
-
current = "";
|
|
144846
|
-
}
|
|
144847
|
-
} else {
|
|
144848
|
-
current += char;
|
|
144849
|
-
}
|
|
144850
|
-
}
|
|
144851
|
-
if (current) {
|
|
144852
|
-
args.push(current);
|
|
144853
|
-
}
|
|
144854
|
-
return args;
|
|
144855
|
-
}
|
|
144856
144855
|
/**
|
|
144857
144856
|
* Route script execution to a runtime via NATS.
|
|
144858
144857
|
* Scripts always execute on the runtime linked to the script.
|
|
@@ -145178,34 +145177,68 @@ This file can be used with other file I/O tools that accept \`@skilder-file:/\`
|
|
|
145178
145177
|
const lines = [];
|
|
145179
145178
|
lines.push(`# Skill: ${skill.name}`);
|
|
145180
145179
|
lines.push("");
|
|
145180
|
+
lines.push(...this.formatSkillDetails(skill));
|
|
145181
|
+
return {
|
|
145182
|
+
response: { content: [{ type: "text", text: lines.join("\n") }] },
|
|
145183
|
+
skillIds: [skill.id]
|
|
145184
|
+
};
|
|
145185
|
+
}
|
|
145186
|
+
/**
|
|
145187
|
+
* Format full details for a single skill including description, instructions, tool schemas,
|
|
145188
|
+
* and referenced resources. Used by both handleLearnSkill and handleGetHat to ensure DRY
|
|
145189
|
+
* consistency when rendering skill content.
|
|
145190
|
+
*
|
|
145191
|
+
* @param skill - The skill catalog entry to format
|
|
145192
|
+
* @param options.headingLevel - Heading level for tools section (2 for standalone, 4 for within-hat)
|
|
145193
|
+
* @param options.renderedToolNames - Set of tool names already rendered (for deduplication across skills in a hat)
|
|
145194
|
+
*/
|
|
145195
|
+
formatSkillDetails(skill, options) {
|
|
145196
|
+
const headingLevel = options?.headingLevel ?? 2;
|
|
145197
|
+
const heading = "#".repeat(headingLevel);
|
|
145198
|
+
const renderedToolNames = options?.renderedToolNames;
|
|
145199
|
+
const firstSkillForTool = options?.firstSkillForTool;
|
|
145200
|
+
const lines = [];
|
|
145181
145201
|
if (skill.description) {
|
|
145182
145202
|
lines.push(skill.description);
|
|
145183
145203
|
lines.push("");
|
|
145184
145204
|
}
|
|
145185
145205
|
if (skill.instructions) {
|
|
145186
|
-
lines.push(
|
|
145206
|
+
lines.push(`${heading} Instructions`);
|
|
145187
145207
|
lines.push(skill.instructions);
|
|
145188
145208
|
lines.push("");
|
|
145189
145209
|
}
|
|
145190
145210
|
if (skill.toolNames.length > 0) {
|
|
145191
|
-
lines.push(
|
|
145211
|
+
lines.push(`${heading} Available Tools`);
|
|
145192
145212
|
lines.push("Use `call_tool` with the tool name and input to execute these tools:");
|
|
145193
145213
|
lines.push("");
|
|
145194
145214
|
for (const toolName of skill.toolNames) {
|
|
145195
|
-
const
|
|
145196
|
-
if (
|
|
145197
|
-
|
|
145198
|
-
lines.push(
|
|
145199
|
-
lines.push("");
|
|
145200
|
-
lines.push("**Input Schema:**");
|
|
145201
|
-
lines.push("```json");
|
|
145202
|
-
lines.push(JSON.stringify(tool2.inputSchema, null, 2));
|
|
145203
|
-
lines.push("```");
|
|
145215
|
+
const subHeading = "#".repeat(headingLevel + 1);
|
|
145216
|
+
if (renderedToolNames?.has(toolName)) {
|
|
145217
|
+
const firstSkill = firstSkillForTool?.get(toolName);
|
|
145218
|
+
lines.push(`${subHeading} ${toolName}`);
|
|
145219
|
+
lines.push(firstSkill ? `(schema shown above under ${firstSkill})` : "(schema shown above)");
|
|
145204
145220
|
lines.push("");
|
|
145205
145221
|
} else {
|
|
145206
|
-
|
|
145207
|
-
|
|
145208
|
-
|
|
145222
|
+
if (renderedToolNames) {
|
|
145223
|
+
renderedToolNames.add(toolName);
|
|
145224
|
+
firstSkillForTool?.set(toolName, skill.name);
|
|
145225
|
+
}
|
|
145226
|
+
const tool2 = this.tools.getValue()?.find((t4) => t4.name === toolName);
|
|
145227
|
+
if (tool2) {
|
|
145228
|
+
lines.push(`${subHeading} ${tool2.name}`);
|
|
145229
|
+
lines.push(tool2.description || "No description");
|
|
145230
|
+
lines.push("");
|
|
145231
|
+
lines.push("**Input Schema:**");
|
|
145232
|
+
lines.push("```json");
|
|
145233
|
+
lines.push(JSON.stringify(tool2.inputSchema, null, 2));
|
|
145234
|
+
lines.push("```");
|
|
145235
|
+
lines.push("");
|
|
145236
|
+
} else {
|
|
145237
|
+
this.logger.warn({ toolName, skillId: skill.id, toolsInitialized: this.tools.getValue() != null }, `Tool "${toolName}" referenced by skill "${skill.name}" not found in tools list`);
|
|
145238
|
+
lines.push(`${subHeading} ${toolName}`);
|
|
145239
|
+
lines.push("Tool definition not available");
|
|
145240
|
+
lines.push("");
|
|
145241
|
+
}
|
|
145209
145242
|
}
|
|
145210
145243
|
}
|
|
145211
145244
|
lines.push('Call these tools using: `call_tool(tool_name="<name>", tool_input={...})`');
|
|
@@ -145221,10 +145254,7 @@ This file can be used with other file I/O tools that accept \`@skilder-file:/\`
|
|
|
145221
145254
|
} catch (error48) {
|
|
145222
145255
|
this.logger.warn({ error: error48, skillId: skill.id }, "Failed to detect related mentions in skill instructions");
|
|
145223
145256
|
}
|
|
145224
|
-
return
|
|
145225
|
-
response: { content: [{ type: "text", text: lines.join("\n") }] },
|
|
145226
|
-
skillIds: [skill.id]
|
|
145227
|
-
};
|
|
145257
|
+
return lines;
|
|
145228
145258
|
}
|
|
145229
145259
|
/**
|
|
145230
145260
|
* Handle feedback_skill tool call.
|
|
@@ -145496,27 +145526,19 @@ ${JSON.stringify(toolConfig.inputSchema, null, 2)}`
|
|
|
145496
145526
|
}
|
|
145497
145527
|
this.session.learnedHatIds.add(hat.id);
|
|
145498
145528
|
const skills = [];
|
|
145499
|
-
const allToolNames = /* @__PURE__ */ new Set();
|
|
145500
145529
|
const missingSkillIds = [];
|
|
145501
145530
|
for (const skillId of hat.skillIds) {
|
|
145502
145531
|
const skill = this.catalog.skills.find((s) => s.id === skillId);
|
|
145503
145532
|
if (skill) {
|
|
145504
145533
|
this.session.learnedSkillIds.add(skill.id);
|
|
145505
|
-
skills.push(
|
|
145506
|
-
name: skill.name,
|
|
145507
|
-
description: skill.description,
|
|
145508
|
-
instructions: skill.instructions,
|
|
145509
|
-
toolNames: skill.toolNames
|
|
145510
|
-
});
|
|
145511
|
-
for (const toolName of skill.toolNames) {
|
|
145512
|
-
allToolNames.add(toolName);
|
|
145513
|
-
}
|
|
145534
|
+
skills.push(skill);
|
|
145514
145535
|
} else {
|
|
145515
|
-
this.logger.warn(
|
|
145536
|
+
this.logger.warn({ skillId, hatId: hat.id, hatName: hat.name }, "Skill referenced in hat not found in catalog");
|
|
145516
145537
|
missingSkillIds.push(skillId);
|
|
145517
145538
|
}
|
|
145518
145539
|
}
|
|
145519
|
-
|
|
145540
|
+
const totalToolCount = new Set(skills.flatMap((s) => s.toolNames)).size;
|
|
145541
|
+
this.logger.info(`User ${this.identity.id} retrieved hat "${hat.name}" with ${skills.length} skills and ${totalToolCount} tools`);
|
|
145520
145542
|
const lines = [];
|
|
145521
145543
|
lines.push(`# Hat: ${hat.name}`);
|
|
145522
145544
|
lines.push("");
|
|
@@ -145532,43 +145554,18 @@ ${JSON.stringify(toolConfig.inputSchema, null, 2)}`
|
|
|
145532
145554
|
if (skills.length > 0) {
|
|
145533
145555
|
lines.push("## Skills Learned");
|
|
145534
145556
|
lines.push("");
|
|
145557
|
+
const renderedToolNames = /* @__PURE__ */ new Set();
|
|
145558
|
+
const firstSkillForTool = /* @__PURE__ */ new Map();
|
|
145535
145559
|
for (const skill of skills) {
|
|
145536
145560
|
lines.push(`### ${skill.name}`);
|
|
145537
|
-
|
|
145538
|
-
lines.push(skill.description);
|
|
145539
|
-
}
|
|
145540
|
-
if (skill.instructions) {
|
|
145541
|
-
lines.push("");
|
|
145542
|
-
lines.push("**Instructions:**");
|
|
145543
|
-
lines.push(skill.instructions);
|
|
145544
|
-
}
|
|
145545
|
-
if (skill.toolNames.length > 0) {
|
|
145546
|
-
lines.push("");
|
|
145547
|
-
lines.push(`**Tools:** ${skill.toolNames.join(", ")}`);
|
|
145548
|
-
}
|
|
145561
|
+
lines.push(...this.formatSkillDetails(skill, { headingLevel: 4, renderedToolNames, firstSkillForTool }));
|
|
145549
145562
|
lines.push("");
|
|
145550
145563
|
}
|
|
145551
145564
|
} else {
|
|
145552
145565
|
lines.push("This hat contains no skills.");
|
|
145553
145566
|
lines.push("");
|
|
145554
145567
|
}
|
|
145555
|
-
if (allToolNames.size > 0) {
|
|
145556
|
-
lines.push("## All Available Tools");
|
|
145557
|
-
lines.push("");
|
|
145558
|
-
lines.push("The following tools are now available:");
|
|
145559
|
-
for (const toolName of allToolNames) {
|
|
145560
|
-
const tool2 = this.tools.getValue()?.find((t4) => t4.name === toolName);
|
|
145561
|
-
if (tool2) {
|
|
145562
|
-
lines.push(`- **${tool2.name}**: ${tool2.description || "No description"}`);
|
|
145563
|
-
} else {
|
|
145564
|
-
lines.push(`- **${toolName}**`);
|
|
145565
|
-
}
|
|
145566
|
-
}
|
|
145567
|
-
lines.push("");
|
|
145568
|
-
lines.push("You can now use these tools to help the user.");
|
|
145569
|
-
}
|
|
145570
145568
|
if (missingSkillIds.length > 0) {
|
|
145571
|
-
lines.push("");
|
|
145572
145569
|
lines.push("## Warning");
|
|
145573
145570
|
lines.push("");
|
|
145574
145571
|
lines.push(`Some skills referenced by this hat could not be loaded: ${missingSkillIds.join(", ")}`);
|
|
@@ -152628,10 +152625,29 @@ var ToolService = class ToolService2 extends Service {
|
|
|
152628
152625
|
this.rxSubscriptions = [];
|
|
152629
152626
|
this.sdkBundleContent = null;
|
|
152630
152627
|
this.sdkBundleLoadPromise = null;
|
|
152628
|
+
this.dormantConfigs = /* @__PURE__ */ new Map();
|
|
152629
|
+
this.idleTimers = /* @__PURE__ */ new Map();
|
|
152630
|
+
this.startingServers = /* @__PURE__ */ new Map();
|
|
152631
|
+
this.stoppingServers = /* @__PURE__ */ new Map();
|
|
152632
|
+
this.activationFailures = /* @__PURE__ */ new Map();
|
|
152633
|
+
this.maxActivationRetries = 3;
|
|
152634
|
+
this.activationCooldownMs = 6e4;
|
|
152635
|
+
this.lazyStartEnabled = true;
|
|
152636
|
+
this.idleTimeoutMs = 3e5;
|
|
152631
152637
|
this.logger = this.loggerService.getLogger(this.name);
|
|
152632
152638
|
}
|
|
152633
152639
|
async initialize() {
|
|
152634
152640
|
this.logger.info("Starting");
|
|
152641
|
+
const lazyStartRaw = (process.env.MCP_LAZY_START ?? "true").toLowerCase();
|
|
152642
|
+
this.lazyStartEnabled = !["false", "0", "no", "off"].includes(lazyStartRaw);
|
|
152643
|
+
const parsedTimeout = parseInt(process.env.MCP_IDLE_TIMEOUT_MS ?? "300000", 10);
|
|
152644
|
+
if (Number.isNaN(parsedTimeout) || parsedTimeout <= 0) {
|
|
152645
|
+
this.logger.warn({ rawValue: process.env.MCP_IDLE_TIMEOUT_MS }, "Invalid MCP_IDLE_TIMEOUT_MS value, falling back to default 300000ms");
|
|
152646
|
+
this.idleTimeoutMs = 3e5;
|
|
152647
|
+
} else {
|
|
152648
|
+
this.idleTimeoutMs = parsedTimeout;
|
|
152649
|
+
}
|
|
152650
|
+
this.logger.info({ lazyStart: this.lazyStartEnabled, idleTimeoutMs: this.idleTimeoutMs }, "Lazy-start configuration");
|
|
152635
152651
|
await this.authService.waitForStarted();
|
|
152636
152652
|
await this.natsService.waitForStarted();
|
|
152637
152653
|
await this.healthService.waitForStarted();
|
|
@@ -152666,7 +152682,7 @@ var ToolService = class ToolService2 extends Service {
|
|
|
152666
152682
|
this.natsService.publish(message);
|
|
152667
152683
|
this.logger.debug(`Published MCP server status: ${mcpServerId} -> ${status}${error48 ? ` (${error48})` : ""}`);
|
|
152668
152684
|
} catch (publishError) {
|
|
152669
|
-
this.logger.error(
|
|
152685
|
+
this.logger.error({ mcpServerId, status, err: publishError }, "Failed to publish MCP server status");
|
|
152670
152686
|
}
|
|
152671
152687
|
}
|
|
152672
152688
|
async shutdown() {
|
|
@@ -152697,19 +152713,25 @@ var ToolService = class ToolService2 extends Service {
|
|
|
152697
152713
|
this.logger.debug(`Processing MCP Servers update with ${msg.data.mcpServers.length} MCP Servers`);
|
|
152698
152714
|
this.roots = msg.data.roots;
|
|
152699
152715
|
const mcpServerIds = msg.data.mcpServers.map((mcpServer) => mcpServer.id);
|
|
152700
|
-
const
|
|
152701
|
-
|
|
152716
|
+
const runningToStop = Array.from(this.mcpServers.keys()).filter((id) => !mcpServerIds.includes(id));
|
|
152717
|
+
const dormantToRemove = Array.from(this.dormantConfigs.keys()).filter((id) => !mcpServerIds.includes(id));
|
|
152718
|
+
for (const mcpServerId of runningToStop) {
|
|
152702
152719
|
const service = this.mcpServers.get(mcpServerId);
|
|
152703
152720
|
await this.stopMCPServer({ id: mcpServerId, name: service.getName() });
|
|
152704
152721
|
}
|
|
152722
|
+
for (const mcpServerId of dormantToRemove) {
|
|
152723
|
+
this.removeDormantServer(mcpServerId);
|
|
152724
|
+
}
|
|
152705
152725
|
for (const mcpServer of msg.data.mcpServers) {
|
|
152706
|
-
await this.
|
|
152707
|
-
this.logger.error(
|
|
152726
|
+
await this.registerMCPServer(mcpServer).catch(async (error48) => {
|
|
152727
|
+
this.logger.error({ mcpServerId: mcpServer.id, mcpServerName: mcpServer.name, err: error48 }, "Failed to register MCP Server");
|
|
152728
|
+
this.publishMCPServerStatus(mcpServer.workspace?.id, mcpServer.id, "ERROR", error48 instanceof Error ? error48.message : String(error48));
|
|
152708
152729
|
const service = this.mcpServers.get(mcpServer.id);
|
|
152709
152730
|
if (service) {
|
|
152710
152731
|
await this.stopService(service);
|
|
152711
152732
|
}
|
|
152712
152733
|
this.mcpServers.delete(mcpServer.id);
|
|
152734
|
+
this.removeDormantServer(mcpServer.id);
|
|
152713
152735
|
});
|
|
152714
152736
|
}
|
|
152715
152737
|
}
|
|
@@ -152737,6 +152759,13 @@ var ToolService = class ToolService2 extends Service {
|
|
|
152737
152759
|
}
|
|
152738
152760
|
this.cacheSubscriptions = [];
|
|
152739
152761
|
}
|
|
152762
|
+
if (this.startingServers.size > 0 || this.stoppingServers.size > 0) {
|
|
152763
|
+
this.logger.debug({ starting: this.startingServers.size, stopping: this.stoppingServers.size }, "Waiting for in-flight MCP server activations/stops to settle");
|
|
152764
|
+
await Promise.allSettled([
|
|
152765
|
+
...Array.from(this.startingServers.values()),
|
|
152766
|
+
...Array.from(this.stoppingServers.values())
|
|
152767
|
+
]);
|
|
152768
|
+
}
|
|
152740
152769
|
const serverIds = Array.from(this.mcpServers.keys());
|
|
152741
152770
|
for (const mcpServerId of serverIds) {
|
|
152742
152771
|
const service = this.mcpServers.get(mcpServerId);
|
|
@@ -152744,20 +152773,76 @@ var ToolService = class ToolService2 extends Service {
|
|
|
152744
152773
|
continue;
|
|
152745
152774
|
await this.stopMCPServer({ id: mcpServerId, name: service.getName() });
|
|
152746
152775
|
}
|
|
152776
|
+
for (const mcpServerId of Array.from(this.dormantConfigs.keys())) {
|
|
152777
|
+
this.removeDormantServer(mcpServerId);
|
|
152778
|
+
}
|
|
152779
|
+
for (const timer of this.idleTimers.values()) {
|
|
152780
|
+
clearTimeout(timer);
|
|
152781
|
+
}
|
|
152782
|
+
this.idleTimers.clear();
|
|
152747
152783
|
}
|
|
152748
152784
|
/**
|
|
152749
|
-
*
|
|
152750
|
-
*
|
|
152785
|
+
* Register an MCP Server. For STDIO servers with lazy-start enabled, the server is registered
|
|
152786
|
+
* as dormant (config stored, NATS tool subscriptions active, no subprocess spawned).
|
|
152787
|
+
* For non-STDIO servers or when lazy-start is disabled, the server is activated immediately.
|
|
152751
152788
|
*/
|
|
152752
|
-
async
|
|
152789
|
+
async registerMCPServer(mcpServer) {
|
|
152753
152790
|
const roots = this.getRoots();
|
|
152754
|
-
const
|
|
152755
|
-
|
|
152756
|
-
|
|
152757
|
-
|
|
152758
|
-
this.ensureToolsSubscribed(mcpServer.id,
|
|
152791
|
+
const newSignature = getConfigSignature(mcpServer, roots);
|
|
152792
|
+
const existingRunning = this.mcpServers.get(mcpServer.id);
|
|
152793
|
+
if (existingRunning && getConfigSignature(existingRunning.getConfig(), existingRunning.getRoots()) === newSignature) {
|
|
152794
|
+
this.logger.debug(`MCPServer ${mcpServer.name} already running -> skipping`);
|
|
152795
|
+
this.ensureToolsSubscribed(mcpServer.id, mcpServer.tools ?? []);
|
|
152759
152796
|
return;
|
|
152760
152797
|
}
|
|
152798
|
+
const existingDormant = this.dormantConfigs.get(mcpServer.id);
|
|
152799
|
+
if (existingDormant && existingDormant.configSignature === newSignature) {
|
|
152800
|
+
this.logger.debug(`MCPServer ${mcpServer.name} already dormant with same config -> skipping`);
|
|
152801
|
+
this.ensureToolsSubscribed(mcpServer.id, mcpServer.tools ?? []);
|
|
152802
|
+
return;
|
|
152803
|
+
}
|
|
152804
|
+
if (existingRunning) {
|
|
152805
|
+
this.logger.debug(`MCPServer ${mcpServer.name} config changed -> stopping`);
|
|
152806
|
+
await this.stopMCPServer(mcpServer);
|
|
152807
|
+
}
|
|
152808
|
+
if (existingDormant) {
|
|
152809
|
+
this.dormantConfigs.delete(mcpServer.id);
|
|
152810
|
+
}
|
|
152811
|
+
this.activationFailures.delete(mcpServer.id);
|
|
152812
|
+
const isStdio = mcpServer.transport === dgraph_resolvers_types_exports.McpTransportType.Stdio;
|
|
152813
|
+
const hasKnownTools = (mcpServer.tools?.length ?? 0) > 0;
|
|
152814
|
+
if (this.lazyStartEnabled && isStdio && hasKnownTools) {
|
|
152815
|
+
this.logger.info(`Registering MCPServer ${mcpServer.name} as dormant (lazy-start)`);
|
|
152816
|
+
this.dormantConfigs.set(mcpServer.id, { config: mcpServer, configSignature: newSignature });
|
|
152817
|
+
if (mcpServer.workspace?.id) {
|
|
152818
|
+
this.mcpServerWorkspaces.set(mcpServer.id, mcpServer.workspace.id);
|
|
152819
|
+
}
|
|
152820
|
+
this.ensureToolsSubscribed(mcpServer.id, mcpServer.tools ?? []);
|
|
152821
|
+
this.publishMCPServerStatus(mcpServer.workspace?.id, mcpServer.id, "IDLE");
|
|
152822
|
+
return;
|
|
152823
|
+
}
|
|
152824
|
+
if (this.lazyStartEnabled && isStdio && !hasKnownTools) {
|
|
152825
|
+
this.logger.info(`MCPServer ${mcpServer.name} has no known tools -> activating eagerly for tool discovery`);
|
|
152826
|
+
}
|
|
152827
|
+
await this.activateMCPServer(mcpServer);
|
|
152828
|
+
}
|
|
152829
|
+
/**
|
|
152830
|
+
* Activate an MCP Server: spawn the subprocess, connect, and start observing tools.
|
|
152831
|
+
* Can be called eagerly (from registerMCPServer) or on-demand (from ensureServerActive).
|
|
152832
|
+
*/
|
|
152833
|
+
async activateMCPServer(mcpServerOrId) {
|
|
152834
|
+
let mcpServer;
|
|
152835
|
+
let isDormantActivation = false;
|
|
152836
|
+
if (typeof mcpServerOrId === "string") {
|
|
152837
|
+
const dormant = this.dormantConfigs.get(mcpServerOrId);
|
|
152838
|
+
if (!dormant)
|
|
152839
|
+
throw new Error(`No dormant config found for MCP Server ${mcpServerOrId}`);
|
|
152840
|
+
mcpServer = dormant.config;
|
|
152841
|
+
isDormantActivation = true;
|
|
152842
|
+
} else {
|
|
152843
|
+
mcpServer = mcpServerOrId;
|
|
152844
|
+
}
|
|
152845
|
+
const roots = this.getRoots();
|
|
152761
152846
|
let authProvider;
|
|
152762
152847
|
if (mcpServer.oauthProvider === "MCP_OAUTH") {
|
|
152763
152848
|
const identity = this.authService.getIdentity();
|
|
@@ -152790,7 +152875,7 @@ var ToolService = class ToolService2 extends Service {
|
|
|
152790
152875
|
}
|
|
152791
152876
|
}
|
|
152792
152877
|
const mcpServerService = this.toolServerServiceFactory(mcpServer, roots, authProvider);
|
|
152793
|
-
this.logger.info(`
|
|
152878
|
+
this.logger.info(`Activating MCPServer: ${mcpServer.name} with ${mcpServer.tools?.length ?? 0} tools, and roots: ${JSON.stringify(roots)}`);
|
|
152794
152879
|
if (this.mcpServers.has(mcpServer.id)) {
|
|
152795
152880
|
this.logger.debug(`MCPServer ${mcpServer.name} already running -> shutting down`);
|
|
152796
152881
|
await this.stopMCPServer(mcpServer);
|
|
@@ -152808,11 +152893,32 @@ var ToolService = class ToolService2 extends Service {
|
|
|
152808
152893
|
this.mcpServerWorkspaces.delete(mcpServer.id);
|
|
152809
152894
|
throw error48;
|
|
152810
152895
|
}
|
|
152896
|
+
if (isDormantActivation) {
|
|
152897
|
+
this.dormantConfigs.delete(mcpServer.id);
|
|
152898
|
+
}
|
|
152811
152899
|
let hasPublishedConnected = false;
|
|
152812
152900
|
mcpServerService.observeTools().subscribe({
|
|
152813
152901
|
error: (err) => {
|
|
152814
|
-
this.logger.error(
|
|
152902
|
+
this.logger.error({ mcpServerId: mcpServer.id, err }, "Tools observable error for MCP Server");
|
|
152815
152903
|
this.publishMCPServerStatus(mcpServer.workspace?.id, mcpServer.id, "ERROR", err instanceof Error ? err.message : String(err));
|
|
152904
|
+
this.mcpServers.delete(mcpServer.id);
|
|
152905
|
+
this.mcpTools.delete(mcpServer.id);
|
|
152906
|
+
const idleTimer = this.idleTimers.get(mcpServer.id);
|
|
152907
|
+
if (idleTimer) {
|
|
152908
|
+
clearTimeout(idleTimer);
|
|
152909
|
+
this.idleTimers.delete(mcpServer.id);
|
|
152910
|
+
}
|
|
152911
|
+
const stopPromise = this.stopService(mcpServerService).catch((stopErr) => this.logger.error({ mcpServerId: mcpServer.id, err: stopErr }, "Failed to stop errored MCP server")).then(() => {
|
|
152912
|
+
this.stoppingServers.delete(mcpServer.id);
|
|
152913
|
+
if (this.lazyStartEnabled && mcpServer.transport === dgraph_resolvers_types_exports.McpTransportType.Stdio) {
|
|
152914
|
+
this.dormantConfigs.set(mcpServer.id, {
|
|
152915
|
+
config: mcpServer,
|
|
152916
|
+
configSignature: getConfigSignature(mcpServer, roots)
|
|
152917
|
+
});
|
|
152918
|
+
this.publishMCPServerStatus(mcpServer.workspace?.id, mcpServer.id, "IDLE");
|
|
152919
|
+
}
|
|
152920
|
+
});
|
|
152921
|
+
this.stoppingServers.set(mcpServer.id, stopPromise);
|
|
152816
152922
|
},
|
|
152817
152923
|
next: (tools2) => {
|
|
152818
152924
|
this.logger.debug(`Updating tools for MCP Server ${mcpServer.id} with ${tools2.length} tools`);
|
|
@@ -152833,13 +152939,119 @@ var ToolService = class ToolService2 extends Service {
|
|
|
152833
152939
|
});
|
|
152834
152940
|
this.natsService.publish(message);
|
|
152835
152941
|
} catch (publishError) {
|
|
152836
|
-
this.logger.
|
|
152942
|
+
this.logger.error({ mcpServerId: mcpServer.id, err: publishError }, "Failed to publish discovered tools \u2014 backend may show stale tool list");
|
|
152837
152943
|
}
|
|
152838
152944
|
}
|
|
152839
152945
|
});
|
|
152840
152946
|
const tools = mcpServer.tools ?? [];
|
|
152841
152947
|
this.ensureToolsSubscribed(mcpServer.id, tools);
|
|
152842
|
-
this.
|
|
152948
|
+
if (this.lazyStartEnabled && mcpServer.transport === dgraph_resolvers_types_exports.McpTransportType.Stdio) {
|
|
152949
|
+
this.resetIdleTimer(mcpServer.id);
|
|
152950
|
+
}
|
|
152951
|
+
this.logger.info(`MCPServer ${mcpServer.name} activated`);
|
|
152952
|
+
return mcpServerService;
|
|
152953
|
+
}
|
|
152954
|
+
/**
|
|
152955
|
+
* Ensure a dormant server is activated, deduplicating concurrent activation requests.
|
|
152956
|
+
* Waits for any in-flight stop to complete before re-activating.
|
|
152957
|
+
*/
|
|
152958
|
+
async ensureServerActive(mcpServerId) {
|
|
152959
|
+
const existing = this.startingServers.get(mcpServerId);
|
|
152960
|
+
if (existing)
|
|
152961
|
+
return existing;
|
|
152962
|
+
const stopping = this.stoppingServers.get(mcpServerId);
|
|
152963
|
+
if (stopping) {
|
|
152964
|
+
await stopping;
|
|
152965
|
+
}
|
|
152966
|
+
const promise2 = this.activateMCPServer(mcpServerId);
|
|
152967
|
+
this.startingServers.set(mcpServerId, promise2);
|
|
152968
|
+
try {
|
|
152969
|
+
const service = await promise2;
|
|
152970
|
+
if (!this.mcpServers.has(mcpServerId)) {
|
|
152971
|
+
throw new Error(`MCP server ${mcpServerId} was deregistered during activation`);
|
|
152972
|
+
}
|
|
152973
|
+
return service;
|
|
152974
|
+
} finally {
|
|
152975
|
+
this.startingServers.delete(mcpServerId);
|
|
152976
|
+
}
|
|
152977
|
+
}
|
|
152978
|
+
/**
|
|
152979
|
+
* Reset the idle timer for a lazy-started server.
|
|
152980
|
+
*/
|
|
152981
|
+
resetIdleTimer(mcpServerId) {
|
|
152982
|
+
const existing = this.idleTimers.get(mcpServerId);
|
|
152983
|
+
if (existing)
|
|
152984
|
+
clearTimeout(existing);
|
|
152985
|
+
const timer = setTimeout(() => {
|
|
152986
|
+
this.idleShutdown(mcpServerId).catch((err) => this.logger.error({ mcpServerId, err }, "Idle shutdown failed"));
|
|
152987
|
+
}, this.idleTimeoutMs);
|
|
152988
|
+
this.idleTimers.set(mcpServerId, timer);
|
|
152989
|
+
}
|
|
152990
|
+
/**
|
|
152991
|
+
* Shut down an idle MCP server and move it back to dormant state.
|
|
152992
|
+
* NATS tool subscriptions and mcpTools are kept alive for on-demand re-activation.
|
|
152993
|
+
*/
|
|
152994
|
+
async idleShutdown(mcpServerId) {
|
|
152995
|
+
if (this.startingServers.has(mcpServerId)) {
|
|
152996
|
+
this.resetIdleTimer(mcpServerId);
|
|
152997
|
+
return;
|
|
152998
|
+
}
|
|
152999
|
+
const server = this.mcpServers.get(mcpServerId);
|
|
153000
|
+
if (!server) {
|
|
153001
|
+
this.logger.debug({ mcpServerId }, "Idle timer fired for server not in mcpServers map, ignoring");
|
|
153002
|
+
this.idleTimers.delete(mcpServerId);
|
|
153003
|
+
return;
|
|
153004
|
+
}
|
|
153005
|
+
const config2 = server.getConfig();
|
|
153006
|
+
const roots = server.getRoots();
|
|
153007
|
+
this.logger.info(`Idle shutdown for MCP server ${config2.name}`);
|
|
153008
|
+
try {
|
|
153009
|
+
await this.stopService(server);
|
|
153010
|
+
} catch (err) {
|
|
153011
|
+
this.logger.error({ mcpServerId, err }, "Error stopping service during idle shutdown");
|
|
153012
|
+
this.mcpServers.delete(mcpServerId);
|
|
153013
|
+
this.idleTimers.delete(mcpServerId);
|
|
153014
|
+
this.dormantConfigs.set(mcpServerId, {
|
|
153015
|
+
config: config2,
|
|
153016
|
+
configSignature: getConfigSignature(config2, roots)
|
|
153017
|
+
});
|
|
153018
|
+
const workspaceId2 = this.mcpServerWorkspaces.get(mcpServerId);
|
|
153019
|
+
this.publishMCPServerStatus(workspaceId2, mcpServerId, "IDLE");
|
|
153020
|
+
return;
|
|
153021
|
+
}
|
|
153022
|
+
this.mcpServers.delete(mcpServerId);
|
|
153023
|
+
this.dormantConfigs.set(mcpServerId, {
|
|
153024
|
+
config: config2,
|
|
153025
|
+
configSignature: getConfigSignature(config2, roots)
|
|
153026
|
+
});
|
|
153027
|
+
const workspaceId = this.mcpServerWorkspaces.get(mcpServerId);
|
|
153028
|
+
this.publishMCPServerStatus(workspaceId, mcpServerId, "IDLE");
|
|
153029
|
+
this.idleTimers.delete(mcpServerId);
|
|
153030
|
+
}
|
|
153031
|
+
/**
|
|
153032
|
+
* Remove a dormant server completely: clean up NATS tool subscriptions, mcpTools, and config.
|
|
153033
|
+
*/
|
|
153034
|
+
removeDormantServer(mcpServerId) {
|
|
153035
|
+
this.dormantConfigs.delete(mcpServerId);
|
|
153036
|
+
this.activationFailures.delete(mcpServerId);
|
|
153037
|
+
const serverToolSubs = this.toolSubscriptions.get(mcpServerId);
|
|
153038
|
+
if (serverToolSubs) {
|
|
153039
|
+
for (const [toolId, subscription] of serverToolSubs.entries()) {
|
|
153040
|
+
try {
|
|
153041
|
+
subscription.unsubscribe();
|
|
153042
|
+
} catch (error48) {
|
|
153043
|
+
this.logger.warn({ toolId, err: error48 }, "Failed to unsubscribe dormant tool");
|
|
153044
|
+
}
|
|
153045
|
+
}
|
|
153046
|
+
this.toolSubscriptions.delete(mcpServerId);
|
|
153047
|
+
}
|
|
153048
|
+
this.mcpTools.delete(mcpServerId);
|
|
153049
|
+
this.mcpServerWorkspaces.delete(mcpServerId);
|
|
153050
|
+
const timer = this.idleTimers.get(mcpServerId);
|
|
153051
|
+
if (timer) {
|
|
153052
|
+
clearTimeout(timer);
|
|
153053
|
+
this.idleTimers.delete(mcpServerId);
|
|
153054
|
+
}
|
|
152843
153055
|
}
|
|
152844
153056
|
// Subscribe to a tool and return the subscription
|
|
152845
153057
|
subscribeToTool(tool2) {
|
|
@@ -152901,11 +153113,45 @@ var ToolService = class ToolService2 extends Service {
|
|
|
152901
153113
|
continue;
|
|
152902
153114
|
}
|
|
152903
153115
|
this.logger.debug(`Found tool ${tool2.name} in MCP Server ${mcpServerId}`);
|
|
152904
|
-
|
|
153116
|
+
let mcpServer = this.mcpServers.get(mcpServerId);
|
|
153117
|
+
if (!mcpServer && this.dormantConfigs.has(mcpServerId)) {
|
|
153118
|
+
const failures = this.activationFailures.get(mcpServerId);
|
|
153119
|
+
if (failures && failures.count >= this.maxActivationRetries) {
|
|
153120
|
+
const elapsed = Date.now() - failures.lastAttempt;
|
|
153121
|
+
if (elapsed < this.activationCooldownMs) {
|
|
153122
|
+
const retryInSec = Math.ceil((this.activationCooldownMs - elapsed) / 1e3);
|
|
153123
|
+
msg.respond(new ErrorResponse({
|
|
153124
|
+
error: `MCP server failed to start after ${failures.count} attempts. Will retry in ${retryInSec}s.`
|
|
153125
|
+
}));
|
|
153126
|
+
toolCalled = true;
|
|
153127
|
+
continue;
|
|
153128
|
+
}
|
|
153129
|
+
this.activationFailures.delete(mcpServerId);
|
|
153130
|
+
}
|
|
153131
|
+
this.logger.info(`Activating dormant MCP server ${mcpServerId} for tool call`);
|
|
153132
|
+
try {
|
|
153133
|
+
mcpServer = await this.ensureServerActive(mcpServerId);
|
|
153134
|
+
this.activationFailures.delete(mcpServerId);
|
|
153135
|
+
} catch (activationError) {
|
|
153136
|
+
const prev = this.activationFailures.get(mcpServerId);
|
|
153137
|
+
this.activationFailures.set(mcpServerId, {
|
|
153138
|
+
count: (prev?.count ?? 0) + 1,
|
|
153139
|
+
lastAttempt: Date.now()
|
|
153140
|
+
});
|
|
153141
|
+
this.logger.error({ mcpServerId, err: activationError }, "Failed to activate dormant MCP server");
|
|
153142
|
+
const userMessage = activationError instanceof Error && activationError.message.includes("No dormant config") ? "MCP server is being reconfigured, please retry" : `MCP server activation failed: ${activationError instanceof Error ? activationError.message : String(activationError)}`;
|
|
153143
|
+
msg.respond(new ErrorResponse({ error: userMessage }));
|
|
153144
|
+
toolCalled = true;
|
|
153145
|
+
continue;
|
|
153146
|
+
}
|
|
153147
|
+
}
|
|
152905
153148
|
if (!mcpServer) {
|
|
152906
153149
|
this.logger.warn(`MCP Server ${mcpServerId} not found`);
|
|
152907
153150
|
continue;
|
|
152908
153151
|
}
|
|
153152
|
+
if (this.idleTimers.has(mcpServerId)) {
|
|
153153
|
+
this.resetIdleTimer(mcpServerId);
|
|
153154
|
+
}
|
|
152909
153155
|
let _meta;
|
|
152910
153156
|
const mcpServerConfig = mcpServer.getConfig();
|
|
152911
153157
|
if (mcpServerConfig.oauthProvider && userId) {
|
|
@@ -153027,7 +153273,7 @@ var ToolService = class ToolService2 extends Service {
|
|
|
153027
153273
|
executedBy
|
|
153028
153274
|
}));
|
|
153029
153275
|
} catch (callError) {
|
|
153030
|
-
this.logger.error(
|
|
153276
|
+
this.logger.error({ mcpServerId, toolName: tool2.name, err: callError }, "Tool call failed");
|
|
153031
153277
|
msg.respond(new ErrorResponse({
|
|
153032
153278
|
error: `Tool call failed: ${callError instanceof Error ? callError.message : String(callError)}`
|
|
153033
153279
|
}));
|
|
@@ -153049,7 +153295,8 @@ var ToolService = class ToolService2 extends Service {
|
|
|
153049
153295
|
*/
|
|
153050
153296
|
async handleScriptExecution(msg, executedBy) {
|
|
153051
153297
|
const { scriptId, scriptName, workspaceId, arguments: toolArgs } = msg.data;
|
|
153052
|
-
const
|
|
153298
|
+
const rawArgs = toolArgs?.args;
|
|
153299
|
+
const args = Array.isArray(rawArgs) ? rawArgs : typeof rawArgs === "string" && rawArgs.trim() ? parseScriptArgs(rawArgs) : [];
|
|
153053
153300
|
this.logger.info({ scriptId, scriptName, argsCount: args.length }, "Handling script execution request");
|
|
153054
153301
|
const fetchRequest = SkillFetchScriptRequest.create({
|
|
153055
153302
|
scriptId,
|
|
@@ -153313,6 +153560,11 @@ ${errorOutput}`;
|
|
|
153313
153560
|
await this.stopService(service);
|
|
153314
153561
|
}
|
|
153315
153562
|
this.mcpServers.delete(mcpServer.id);
|
|
153563
|
+
const idleTimer = this.idleTimers.get(mcpServer.id);
|
|
153564
|
+
if (idleTimer) {
|
|
153565
|
+
clearTimeout(idleTimer);
|
|
153566
|
+
this.idleTimers.delete(mcpServer.id);
|
|
153567
|
+
}
|
|
153316
153568
|
const workspaceId = this.mcpServerWorkspaces.get(mcpServer.id);
|
|
153317
153569
|
this.publishMCPServerStatus(workspaceId, mcpServer.id, "DISCONNECTED");
|
|
153318
153570
|
this.mcpServerWorkspaces.delete(mcpServer.id);
|