@agiflowai/one-mcp 0.2.6 → 0.2.8
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 +87 -5
- package/dist/cli.cjs +564 -52
- package/dist/cli.mjs +565 -53
- package/dist/{http-xSfxBa8A.cjs → http-BzrxGEr-.cjs} +669 -286
- package/dist/{http-D9BDXhHn.mjs → http-DeUYygKb.mjs} +672 -289
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +8 -0
- package/dist/index.d.mts +8 -0
- package/dist/index.mjs +1 -1
- package/package.json +2 -2
|
@@ -38,6 +38,7 @@ let node_os = require("node:os");
|
|
|
38
38
|
let __modelcontextprotocol_sdk_client_index_js = require("@modelcontextprotocol/sdk/client/index.js");
|
|
39
39
|
let __modelcontextprotocol_sdk_client_stdio_js = require("@modelcontextprotocol/sdk/client/stdio.js");
|
|
40
40
|
let __modelcontextprotocol_sdk_client_sse_js = require("@modelcontextprotocol/sdk/client/sse.js");
|
|
41
|
+
let __modelcontextprotocol_sdk_client_streamableHttp_js = require("@modelcontextprotocol/sdk/client/streamableHttp.js");
|
|
41
42
|
let liquidjs = require("liquidjs");
|
|
42
43
|
let __modelcontextprotocol_sdk_server_stdio_js = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
43
44
|
let __modelcontextprotocol_sdk_server_sse_js = require("@modelcontextprotocol/sdk/server/sse.js");
|
|
@@ -230,6 +231,7 @@ const ClaudeCodeStdioServerSchema = zod.z.object({
|
|
|
230
231
|
env: zod.z.record(zod.z.string(), zod.z.string()).optional(),
|
|
231
232
|
disabled: zod.z.boolean().optional(),
|
|
232
233
|
instruction: zod.z.string().optional(),
|
|
234
|
+
timeout: zod.z.number().positive().optional(),
|
|
233
235
|
config: AdditionalConfigSchema
|
|
234
236
|
});
|
|
235
237
|
const ClaudeCodeHttpServerSchema = zod.z.object({
|
|
@@ -238,6 +240,7 @@ const ClaudeCodeHttpServerSchema = zod.z.object({
|
|
|
238
240
|
type: zod.z.enum(["http", "sse"]).optional(),
|
|
239
241
|
disabled: zod.z.boolean().optional(),
|
|
240
242
|
instruction: zod.z.string().optional(),
|
|
243
|
+
timeout: zod.z.number().positive().optional(),
|
|
241
244
|
config: AdditionalConfigSchema
|
|
242
245
|
});
|
|
243
246
|
const ClaudeCodeServerConfigSchema = zod.z.union([ClaudeCodeStdioServerSchema, ClaudeCodeHttpServerSchema]);
|
|
@@ -268,6 +271,7 @@ const SkillsConfigSchema = zod.z.object({ paths: zod.z.array(zod.z.string()) });
|
|
|
268
271
|
* Full Claude Code MCP configuration schema
|
|
269
272
|
*/
|
|
270
273
|
const ClaudeCodeMcpConfigSchema = zod.z.object({
|
|
274
|
+
id: zod.z.string().optional(),
|
|
271
275
|
mcpServers: zod.z.record(zod.z.string(), ClaudeCodeServerConfigSchema),
|
|
272
276
|
remoteConfigs: zod.z.array(RemoteConfigSourceSchema).optional(),
|
|
273
277
|
skills: SkillsConfigSchema.optional()
|
|
@@ -308,6 +312,7 @@ const McpServerConfigSchema = zod.z.discriminatedUnion("transport", [
|
|
|
308
312
|
toolBlacklist: zod.z.array(zod.z.string()).optional(),
|
|
309
313
|
omitToolDescription: zod.z.boolean().optional(),
|
|
310
314
|
prompts: zod.z.record(zod.z.string(), InternalPromptConfigSchema).optional(),
|
|
315
|
+
timeout: zod.z.number().positive().optional(),
|
|
311
316
|
transport: zod.z.literal("stdio"),
|
|
312
317
|
config: McpStdioConfigSchema
|
|
313
318
|
}),
|
|
@@ -317,6 +322,7 @@ const McpServerConfigSchema = zod.z.discriminatedUnion("transport", [
|
|
|
317
322
|
toolBlacklist: zod.z.array(zod.z.string()).optional(),
|
|
318
323
|
omitToolDescription: zod.z.boolean().optional(),
|
|
319
324
|
prompts: zod.z.record(zod.z.string(), InternalPromptConfigSchema).optional(),
|
|
325
|
+
timeout: zod.z.number().positive().optional(),
|
|
320
326
|
transport: zod.z.literal("http"),
|
|
321
327
|
config: McpHttpConfigSchema
|
|
322
328
|
}),
|
|
@@ -326,6 +332,7 @@ const McpServerConfigSchema = zod.z.discriminatedUnion("transport", [
|
|
|
326
332
|
toolBlacklist: zod.z.array(zod.z.string()).optional(),
|
|
327
333
|
omitToolDescription: zod.z.boolean().optional(),
|
|
328
334
|
prompts: zod.z.record(zod.z.string(), InternalPromptConfigSchema).optional(),
|
|
335
|
+
timeout: zod.z.number().positive().optional(),
|
|
329
336
|
transport: zod.z.literal("sse"),
|
|
330
337
|
config: McpSseConfigSchema
|
|
331
338
|
})
|
|
@@ -334,6 +341,7 @@ const McpServerConfigSchema = zod.z.discriminatedUnion("transport", [
|
|
|
334
341
|
* Full internal MCP configuration schema
|
|
335
342
|
*/
|
|
336
343
|
const InternalMcpConfigSchema = zod.z.object({
|
|
344
|
+
id: zod.z.string().optional(),
|
|
337
345
|
mcpServers: zod.z.record(zod.z.string(), McpServerConfigSchema),
|
|
338
346
|
skills: SkillsConfigSchema.optional()
|
|
339
347
|
});
|
|
@@ -359,6 +367,7 @@ function transformClaudeCodeConfig(claudeConfig) {
|
|
|
359
367
|
toolBlacklist: stdioConfig.config?.toolBlacklist,
|
|
360
368
|
omitToolDescription: stdioConfig.config?.omitToolDescription,
|
|
361
369
|
prompts: stdioConfig.config?.prompts,
|
|
370
|
+
timeout: stdioConfig.timeout,
|
|
362
371
|
transport: "stdio",
|
|
363
372
|
config: {
|
|
364
373
|
command: interpolatedCommand,
|
|
@@ -377,6 +386,7 @@ function transformClaudeCodeConfig(claudeConfig) {
|
|
|
377
386
|
toolBlacklist: httpConfig.config?.toolBlacklist,
|
|
378
387
|
omitToolDescription: httpConfig.config?.omitToolDescription,
|
|
379
388
|
prompts: httpConfig.config?.prompts,
|
|
389
|
+
timeout: httpConfig.timeout,
|
|
380
390
|
transport,
|
|
381
391
|
config: {
|
|
382
392
|
url: interpolatedUrl,
|
|
@@ -386,6 +396,7 @@ function transformClaudeCodeConfig(claudeConfig) {
|
|
|
386
396
|
}
|
|
387
397
|
}
|
|
388
398
|
return {
|
|
399
|
+
id: claudeConfig.id,
|
|
389
400
|
mcpServers: transformedServers,
|
|
390
401
|
skills: claudeConfig.skills
|
|
391
402
|
};
|
|
@@ -546,22 +557,21 @@ var RemoteConfigCacheService = class {
|
|
|
546
557
|
try {
|
|
547
558
|
if (!(0, node_fs.existsSync)(this.cacheDir)) return;
|
|
548
559
|
const now = Date.now();
|
|
549
|
-
const
|
|
550
|
-
|
|
551
|
-
for (const file of files) {
|
|
552
|
-
if (!file.endsWith(".json")) continue;
|
|
560
|
+
const jsonFiles = (await (0, node_fs_promises.readdir)(this.cacheDir)).filter((file) => file.endsWith(".json"));
|
|
561
|
+
const expiredCount = (await Promise.all(jsonFiles.map(async (file) => {
|
|
553
562
|
const filePath = (0, node_path.join)(this.cacheDir, file);
|
|
554
563
|
try {
|
|
555
564
|
const content = await (0, node_fs_promises.readFile)(filePath, "utf-8");
|
|
556
565
|
if (now > JSON.parse(content).expiresAt) {
|
|
557
566
|
await (0, node_fs_promises.unlink)(filePath);
|
|
558
|
-
|
|
567
|
+
return true;
|
|
559
568
|
}
|
|
569
|
+
return false;
|
|
560
570
|
} catch (error) {
|
|
561
571
|
await (0, node_fs_promises.unlink)(filePath).catch(() => {});
|
|
562
|
-
|
|
572
|
+
return true;
|
|
563
573
|
}
|
|
564
|
-
}
|
|
574
|
+
}))).filter(Boolean).length;
|
|
565
575
|
if (expiredCount > 0) console.error(`Cleaned up ${expiredCount} expired remote config cache entries`);
|
|
566
576
|
} catch (error) {
|
|
567
577
|
console.error("Failed to clean expired remote config cache:", error);
|
|
@@ -577,14 +587,15 @@ var RemoteConfigCacheService = class {
|
|
|
577
587
|
totalSize: 0
|
|
578
588
|
};
|
|
579
589
|
const jsonFiles = (await (0, node_fs_promises.readdir)(this.cacheDir)).filter((file) => file.endsWith(".json"));
|
|
580
|
-
|
|
581
|
-
for (const file of jsonFiles) {
|
|
590
|
+
const totalSize = (await Promise.all(jsonFiles.map(async (file) => {
|
|
582
591
|
const filePath = (0, node_path.join)(this.cacheDir, file);
|
|
583
592
|
try {
|
|
584
593
|
const content = await (0, node_fs_promises.readFile)(filePath, "utf-8");
|
|
585
|
-
|
|
586
|
-
} catch {
|
|
587
|
-
|
|
594
|
+
return Buffer.byteLength(content, "utf-8");
|
|
595
|
+
} catch {
|
|
596
|
+
return 0;
|
|
597
|
+
}
|
|
598
|
+
}))).reduce((sum, size) => sum + size, 0);
|
|
588
599
|
return {
|
|
589
600
|
totalEntries: jsonFiles.length,
|
|
590
601
|
totalSize
|
|
@@ -835,6 +846,8 @@ var ConfigFetcherService = class {
|
|
|
835
846
|
|
|
836
847
|
//#endregion
|
|
837
848
|
//#region src/services/McpClientManagerService.ts
|
|
849
|
+
/** Default connection timeout in milliseconds (30 seconds) */
|
|
850
|
+
const DEFAULT_CONNECTION_TIMEOUT_MS = 3e4;
|
|
838
851
|
/**
|
|
839
852
|
* MCP Client wrapper for managing individual server connections
|
|
840
853
|
* This is an internal class used by McpClientManagerService
|
|
@@ -940,8 +953,10 @@ var McpClientManagerService = class {
|
|
|
940
953
|
}
|
|
941
954
|
/**
|
|
942
955
|
* Connect to an MCP server based on its configuration with timeout
|
|
956
|
+
* Uses the timeout from server config, falling back to default (30s)
|
|
943
957
|
*/
|
|
944
|
-
async connectToServer(serverName, config
|
|
958
|
+
async connectToServer(serverName, config) {
|
|
959
|
+
const timeoutMs = config.timeout ?? DEFAULT_CONNECTION_TIMEOUT_MS;
|
|
945
960
|
if (this.clients.has(serverName)) throw new Error(`Client for ${serverName} is already connected`);
|
|
946
961
|
const client = new __modelcontextprotocol_sdk_client_index_js.Client({
|
|
947
962
|
name: `@agiflowai/one-mcp-client`,
|
|
@@ -973,6 +988,7 @@ var McpClientManagerService = class {
|
|
|
973
988
|
*/
|
|
974
989
|
async performConnection(mcpClient, config) {
|
|
975
990
|
if (config.transport === "stdio") await this.connectStdioClient(mcpClient, config.config);
|
|
991
|
+
else if (config.transport === "http") await this.connectHttpClient(mcpClient, config.config);
|
|
976
992
|
else if (config.transport === "sse") await this.connectSseClient(mcpClient, config.config);
|
|
977
993
|
else throw new Error(`Unsupported transport type: ${config.transport}`);
|
|
978
994
|
}
|
|
@@ -986,6 +1002,10 @@ var McpClientManagerService = class {
|
|
|
986
1002
|
const childProcess = transport["_process"];
|
|
987
1003
|
if (childProcess) mcpClient.setChildProcess(childProcess);
|
|
988
1004
|
}
|
|
1005
|
+
async connectHttpClient(mcpClient, config) {
|
|
1006
|
+
const transport = new __modelcontextprotocol_sdk_client_streamableHttp_js.StreamableHTTPClientTransport(new URL(config.url), { requestInit: config.headers ? { headers: config.headers } : void 0 });
|
|
1007
|
+
await mcpClient["client"].connect(transport);
|
|
1008
|
+
}
|
|
989
1009
|
async connectSseClient(mcpClient, config) {
|
|
990
1010
|
const transport = new __modelcontextprotocol_sdk_client_sse_js.SSEClientTransport(new URL(config.url));
|
|
991
1011
|
await mcpClient["client"].connect(transport);
|
|
@@ -1028,6 +1048,274 @@ var McpClientManagerService = class {
|
|
|
1028
1048
|
}
|
|
1029
1049
|
};
|
|
1030
1050
|
|
|
1051
|
+
//#endregion
|
|
1052
|
+
//#region src/utils/findConfigFile.ts
|
|
1053
|
+
/**
|
|
1054
|
+
* Config File Finder Utility
|
|
1055
|
+
*
|
|
1056
|
+
* DESIGN PATTERNS:
|
|
1057
|
+
* - Utility function pattern for reusable logic
|
|
1058
|
+
* - Fail-fast pattern with early returns
|
|
1059
|
+
* - Environment variable configuration pattern
|
|
1060
|
+
*
|
|
1061
|
+
* CODING STANDARDS:
|
|
1062
|
+
* - Use sync filesystem operations for config discovery (performance)
|
|
1063
|
+
* - Check PROJECT_PATH environment variable first
|
|
1064
|
+
* - Fall back to current working directory
|
|
1065
|
+
* - Support both .yaml and .json extensions
|
|
1066
|
+
* - Return null if no config file is found
|
|
1067
|
+
*
|
|
1068
|
+
* AVOID:
|
|
1069
|
+
* - Throwing errors (return null instead for optional config)
|
|
1070
|
+
* - Hardcoded file names without extension variants
|
|
1071
|
+
* - Ignoring environment variables
|
|
1072
|
+
*/
|
|
1073
|
+
/**
|
|
1074
|
+
* Find MCP configuration file by checking PROJECT_PATH first, then cwd
|
|
1075
|
+
* Looks for both mcp-config.yaml and mcp-config.json
|
|
1076
|
+
*
|
|
1077
|
+
* @returns Absolute path to config file, or null if not found
|
|
1078
|
+
*/
|
|
1079
|
+
function findConfigFile() {
|
|
1080
|
+
const configFileNames = [
|
|
1081
|
+
"mcp-config.yaml",
|
|
1082
|
+
"mcp-config.yml",
|
|
1083
|
+
"mcp-config.json"
|
|
1084
|
+
];
|
|
1085
|
+
const projectPath = process.env.PROJECT_PATH;
|
|
1086
|
+
if (projectPath) for (const fileName of configFileNames) {
|
|
1087
|
+
const configPath = (0, node_path.resolve)(projectPath, fileName);
|
|
1088
|
+
if ((0, node_fs.existsSync)(configPath)) return configPath;
|
|
1089
|
+
}
|
|
1090
|
+
const cwd = process.cwd();
|
|
1091
|
+
for (const fileName of configFileNames) {
|
|
1092
|
+
const configPath = (0, node_path.join)(cwd, fileName);
|
|
1093
|
+
if ((0, node_fs.existsSync)(configPath)) return configPath;
|
|
1094
|
+
}
|
|
1095
|
+
return null;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
//#endregion
|
|
1099
|
+
//#region src/utils/parseToolName.ts
|
|
1100
|
+
/**
|
|
1101
|
+
* Parse tool name to extract server and actual tool name
|
|
1102
|
+
* Supports both plain tool names and prefixed format: {serverName}__{toolName}
|
|
1103
|
+
*
|
|
1104
|
+
* @param toolName - The tool name to parse (e.g., "my_tool" or "server__my_tool")
|
|
1105
|
+
* @returns Parsed result with optional serverName and actualToolName
|
|
1106
|
+
*
|
|
1107
|
+
* @example
|
|
1108
|
+
* parseToolName("my_tool") // { actualToolName: "my_tool" }
|
|
1109
|
+
* parseToolName("server__my_tool") // { serverName: "server", actualToolName: "my_tool" }
|
|
1110
|
+
*/
|
|
1111
|
+
function parseToolName(toolName) {
|
|
1112
|
+
const separatorIndex = toolName.indexOf("__");
|
|
1113
|
+
if (separatorIndex > 0) return {
|
|
1114
|
+
serverName: toolName.substring(0, separatorIndex),
|
|
1115
|
+
actualToolName: toolName.substring(separatorIndex + 2)
|
|
1116
|
+
};
|
|
1117
|
+
return { actualToolName: toolName };
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
//#endregion
|
|
1121
|
+
//#region src/utils/parseFrontMatter.ts
|
|
1122
|
+
/**
|
|
1123
|
+
* Parses YAML front matter from a string content.
|
|
1124
|
+
* Front matter must be at the start of the content, delimited by `---`.
|
|
1125
|
+
*
|
|
1126
|
+
* Supports:
|
|
1127
|
+
* - Simple key: value pairs
|
|
1128
|
+
* - Literal block scalar (|) for multi-line preserving newlines
|
|
1129
|
+
* - Folded block scalar (>) for multi-line folding to single line
|
|
1130
|
+
*
|
|
1131
|
+
* @param content - The content string that may contain front matter
|
|
1132
|
+
* @returns Object with parsed front matter (or null) and remaining content
|
|
1133
|
+
*
|
|
1134
|
+
* @example
|
|
1135
|
+
* const result = parseFrontMatter(`---
|
|
1136
|
+
* name: my-skill
|
|
1137
|
+
* description: A skill description
|
|
1138
|
+
* ---
|
|
1139
|
+
* The actual content here`);
|
|
1140
|
+
* // result.frontMatter = { name: 'my-skill', description: 'A skill description' }
|
|
1141
|
+
* // result.content = 'The actual content here'
|
|
1142
|
+
*
|
|
1143
|
+
* @example
|
|
1144
|
+
* // Multi-line with literal block scalar
|
|
1145
|
+
* const result = parseFrontMatter(`---
|
|
1146
|
+
* name: my-skill
|
|
1147
|
+
* description: |
|
|
1148
|
+
* Line 1
|
|
1149
|
+
* Line 2
|
|
1150
|
+
* ---
|
|
1151
|
+
* Content`);
|
|
1152
|
+
* // result.frontMatter.description = 'Line 1\nLine 2'
|
|
1153
|
+
*/
|
|
1154
|
+
function parseFrontMatter(content) {
|
|
1155
|
+
const trimmedContent = content.trimStart();
|
|
1156
|
+
if (!trimmedContent.startsWith("---")) return {
|
|
1157
|
+
frontMatter: null,
|
|
1158
|
+
content
|
|
1159
|
+
};
|
|
1160
|
+
const endDelimiterIndex = trimmedContent.indexOf("\n---", 3);
|
|
1161
|
+
if (endDelimiterIndex === -1) return {
|
|
1162
|
+
frontMatter: null,
|
|
1163
|
+
content
|
|
1164
|
+
};
|
|
1165
|
+
const yamlContent = trimmedContent.slice(4, endDelimiterIndex).trim();
|
|
1166
|
+
if (!yamlContent) return {
|
|
1167
|
+
frontMatter: null,
|
|
1168
|
+
content
|
|
1169
|
+
};
|
|
1170
|
+
const frontMatter = {};
|
|
1171
|
+
const lines = yamlContent.split("\n");
|
|
1172
|
+
let currentKey = null;
|
|
1173
|
+
let currentValue = [];
|
|
1174
|
+
let multiLineMode = null;
|
|
1175
|
+
let baseIndent = 0;
|
|
1176
|
+
const saveCurrentKey = () => {
|
|
1177
|
+
if (currentKey && currentValue.length > 0) if (multiLineMode === "literal") frontMatter[currentKey] = currentValue.join("\n").trimEnd();
|
|
1178
|
+
else if (multiLineMode === "folded") frontMatter[currentKey] = currentValue.join(" ").trim();
|
|
1179
|
+
else frontMatter[currentKey] = currentValue.join("").trim();
|
|
1180
|
+
currentKey = null;
|
|
1181
|
+
currentValue = [];
|
|
1182
|
+
multiLineMode = null;
|
|
1183
|
+
baseIndent = 0;
|
|
1184
|
+
};
|
|
1185
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1186
|
+
const line = lines[i];
|
|
1187
|
+
const trimmedLine = line.trim();
|
|
1188
|
+
const colonIndex = line.indexOf(":");
|
|
1189
|
+
if (colonIndex !== -1 && !line.startsWith(" ") && !line.startsWith(" ")) {
|
|
1190
|
+
saveCurrentKey();
|
|
1191
|
+
const key = line.slice(0, colonIndex).trim();
|
|
1192
|
+
let value = line.slice(colonIndex + 1).trim();
|
|
1193
|
+
if (value === "|" || value === "|-") {
|
|
1194
|
+
currentKey = key;
|
|
1195
|
+
multiLineMode = "literal";
|
|
1196
|
+
if (i + 1 < lines.length) {
|
|
1197
|
+
const match = lines[i + 1].match(/^(\s+)/);
|
|
1198
|
+
baseIndent = match ? match[1].length : 2;
|
|
1199
|
+
}
|
|
1200
|
+
} else if (value === ">" || value === ">-") {
|
|
1201
|
+
currentKey = key;
|
|
1202
|
+
multiLineMode = "folded";
|
|
1203
|
+
if (i + 1 < lines.length) {
|
|
1204
|
+
const match = lines[i + 1].match(/^(\s+)/);
|
|
1205
|
+
baseIndent = match ? match[1].length : 2;
|
|
1206
|
+
}
|
|
1207
|
+
} else {
|
|
1208
|
+
if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
|
|
1209
|
+
if (key && value) frontMatter[key] = value;
|
|
1210
|
+
}
|
|
1211
|
+
} else if (multiLineMode && currentKey) {
|
|
1212
|
+
const lineIndent = line.match(/^(\s*)/)?.[1].length || 0;
|
|
1213
|
+
if (trimmedLine === "") currentValue.push("");
|
|
1214
|
+
else if (lineIndent >= baseIndent) {
|
|
1215
|
+
const unindentedLine = line.slice(baseIndent);
|
|
1216
|
+
currentValue.push(unindentedLine);
|
|
1217
|
+
} else saveCurrentKey();
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
saveCurrentKey();
|
|
1221
|
+
return {
|
|
1222
|
+
frontMatter,
|
|
1223
|
+
content: trimmedContent.slice(endDelimiterIndex + 4).trimStart()
|
|
1224
|
+
};
|
|
1225
|
+
}
|
|
1226
|
+
/**
|
|
1227
|
+
* Checks if parsed front matter contains valid skill metadata.
|
|
1228
|
+
* A valid skill front matter must have both `name` and `description` fields.
|
|
1229
|
+
*
|
|
1230
|
+
* @param frontMatter - The parsed front matter object
|
|
1231
|
+
* @returns True if front matter contains valid skill metadata
|
|
1232
|
+
*/
|
|
1233
|
+
function isValidSkillFrontMatter(frontMatter) {
|
|
1234
|
+
return frontMatter !== null && typeof frontMatter.name === "string" && frontMatter.name.length > 0 && typeof frontMatter.description === "string" && frontMatter.description.length > 0;
|
|
1235
|
+
}
|
|
1236
|
+
/**
|
|
1237
|
+
* Extracts skill front matter from content if present and valid.
|
|
1238
|
+
*
|
|
1239
|
+
* @param content - The content string that may contain skill front matter
|
|
1240
|
+
* @returns Object with skill metadata and content, or null if no valid skill front matter
|
|
1241
|
+
*/
|
|
1242
|
+
function extractSkillFrontMatter(content) {
|
|
1243
|
+
const { frontMatter, content: remainingContent } = parseFrontMatter(content);
|
|
1244
|
+
if (frontMatter && isValidSkillFrontMatter(frontMatter)) return {
|
|
1245
|
+
skill: {
|
|
1246
|
+
name: frontMatter.name,
|
|
1247
|
+
description: frontMatter.description
|
|
1248
|
+
},
|
|
1249
|
+
content: remainingContent
|
|
1250
|
+
};
|
|
1251
|
+
return null;
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
//#endregion
|
|
1255
|
+
//#region src/utils/generateServerId.ts
|
|
1256
|
+
/**
|
|
1257
|
+
* generateServerId Utilities
|
|
1258
|
+
*
|
|
1259
|
+
* DESIGN PATTERNS:
|
|
1260
|
+
* - Pure functions with no side effects
|
|
1261
|
+
* - Single responsibility per function
|
|
1262
|
+
* - Functional programming approach
|
|
1263
|
+
*
|
|
1264
|
+
* CODING STANDARDS:
|
|
1265
|
+
* - Export individual functions, not classes
|
|
1266
|
+
* - Use descriptive function names with verbs
|
|
1267
|
+
* - Add JSDoc comments for complex logic
|
|
1268
|
+
* - Keep functions small and focused
|
|
1269
|
+
*
|
|
1270
|
+
* AVOID:
|
|
1271
|
+
* - Side effects (mutating external state)
|
|
1272
|
+
* - Stateful logic (use services for state)
|
|
1273
|
+
* - Complex external dependencies
|
|
1274
|
+
*/
|
|
1275
|
+
/**
|
|
1276
|
+
* Character set for generating human-readable IDs.
|
|
1277
|
+
* Excludes confusing characters: 0, O, 1, l, I
|
|
1278
|
+
*/
|
|
1279
|
+
const CHARSET = "23456789abcdefghjkmnpqrstuvwxyz";
|
|
1280
|
+
/**
|
|
1281
|
+
* Default length for generated server IDs (6 characters)
|
|
1282
|
+
*/
|
|
1283
|
+
const DEFAULT_ID_LENGTH = 6;
|
|
1284
|
+
/**
|
|
1285
|
+
* Generate a short, human-readable server ID.
|
|
1286
|
+
*
|
|
1287
|
+
* Uses Node.js crypto.randomBytes for cryptographically secure randomness
|
|
1288
|
+
* with rejection sampling to avoid modulo bias.
|
|
1289
|
+
*
|
|
1290
|
+
* The generated ID:
|
|
1291
|
+
* - Is 6 characters long by default
|
|
1292
|
+
* - Uses only lowercase alphanumeric characters
|
|
1293
|
+
* - Excludes confusing characters (0, O, 1, l, I)
|
|
1294
|
+
*
|
|
1295
|
+
* @param length - Length of the ID to generate (default: 6)
|
|
1296
|
+
* @returns A random, human-readable ID
|
|
1297
|
+
*
|
|
1298
|
+
* @example
|
|
1299
|
+
* generateServerId() // "abc234"
|
|
1300
|
+
* generateServerId(4) // "x7mn"
|
|
1301
|
+
*/
|
|
1302
|
+
function generateServerId(length = DEFAULT_ID_LENGTH) {
|
|
1303
|
+
const charsetLength = 31;
|
|
1304
|
+
const maxUnbiased = Math.floor(256 / charsetLength) * charsetLength - 1;
|
|
1305
|
+
let result = "";
|
|
1306
|
+
let remaining = length;
|
|
1307
|
+
while (remaining > 0) {
|
|
1308
|
+
const bytes = (0, node_crypto.randomBytes)(remaining);
|
|
1309
|
+
for (let i = 0; i < bytes.length && remaining > 0; i++) {
|
|
1310
|
+
const byte = bytes[i];
|
|
1311
|
+
if (byte > maxUnbiased) continue;
|
|
1312
|
+
result += CHARSET[byte % charsetLength];
|
|
1313
|
+
remaining--;
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
return result;
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1031
1319
|
//#endregion
|
|
1032
1320
|
//#region src/services/SkillService.ts
|
|
1033
1321
|
/**
|
|
@@ -1098,14 +1386,21 @@ var SkillService = class {
|
|
|
1098
1386
|
skillPaths;
|
|
1099
1387
|
cachedSkills = null;
|
|
1100
1388
|
skillsByName = null;
|
|
1389
|
+
/** Active file watchers for skill directories */
|
|
1390
|
+
watchers = [];
|
|
1391
|
+
/** Callback invoked when cache is invalidated due to file changes */
|
|
1392
|
+
onCacheInvalidated;
|
|
1101
1393
|
/**
|
|
1102
1394
|
* Creates a new SkillService instance
|
|
1103
1395
|
* @param cwd - Current working directory for resolving relative paths
|
|
1104
1396
|
* @param skillPaths - Array of paths to skills directories
|
|
1397
|
+
* @param options - Optional configuration
|
|
1398
|
+
* @param options.onCacheInvalidated - Callback invoked when cache is invalidated due to file changes
|
|
1105
1399
|
*/
|
|
1106
|
-
constructor(cwd, skillPaths) {
|
|
1400
|
+
constructor(cwd, skillPaths, options) {
|
|
1107
1401
|
this.cwd = cwd;
|
|
1108
1402
|
this.skillPaths = skillPaths;
|
|
1403
|
+
this.onCacheInvalidated = options?.onCacheInvalidated;
|
|
1109
1404
|
}
|
|
1110
1405
|
/**
|
|
1111
1406
|
* Get all available skills from configured directories.
|
|
@@ -1121,13 +1416,13 @@ var SkillService = class {
|
|
|
1121
1416
|
if (this.cachedSkills !== null) return this.cachedSkills;
|
|
1122
1417
|
const skills = [];
|
|
1123
1418
|
const loadedSkillNames = /* @__PURE__ */ new Set();
|
|
1124
|
-
|
|
1419
|
+
const allDirSkills = await Promise.all(this.skillPaths.map(async (skillPath) => {
|
|
1125
1420
|
const skillsDir = (0, node_path.isAbsolute)(skillPath) ? skillPath : (0, node_path.join)(this.cwd, skillPath);
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1421
|
+
return this.loadSkillsFromDirectory(skillsDir, "project");
|
|
1422
|
+
}));
|
|
1423
|
+
for (const dirSkills of allDirSkills) for (const skill of dirSkills) if (!loadedSkillNames.has(skill.name)) {
|
|
1424
|
+
skills.push(skill);
|
|
1425
|
+
loadedSkillNames.add(skill.name);
|
|
1131
1426
|
}
|
|
1132
1427
|
this.cachedSkills = skills;
|
|
1133
1428
|
this.skillsByName = new Map(skills.map((skill) => [skill.name, skill]));
|
|
@@ -1151,6 +1446,60 @@ var SkillService = class {
|
|
|
1151
1446
|
this.skillsByName = null;
|
|
1152
1447
|
}
|
|
1153
1448
|
/**
|
|
1449
|
+
* Starts watching skill directories for changes to SKILL.md files.
|
|
1450
|
+
* When changes are detected, the cache is automatically invalidated.
|
|
1451
|
+
*
|
|
1452
|
+
* Uses Node.js fs.watch with recursive option for efficient directory monitoring.
|
|
1453
|
+
* Only invalidates cache when SKILL.md files are modified.
|
|
1454
|
+
*
|
|
1455
|
+
* @example
|
|
1456
|
+
* const skillService = new SkillService(cwd, skillPaths, {
|
|
1457
|
+
* onCacheInvalidated: () => console.log('Skills cache invalidated')
|
|
1458
|
+
* });
|
|
1459
|
+
* await skillService.startWatching();
|
|
1460
|
+
*/
|
|
1461
|
+
async startWatching() {
|
|
1462
|
+
this.stopWatching();
|
|
1463
|
+
const existenceChecks = await Promise.all(this.skillPaths.map(async (skillPath) => {
|
|
1464
|
+
const skillsDir = (0, node_path.isAbsolute)(skillPath) ? skillPath : (0, node_path.join)(this.cwd, skillPath);
|
|
1465
|
+
return {
|
|
1466
|
+
skillsDir,
|
|
1467
|
+
exists: await pathExists(skillsDir)
|
|
1468
|
+
};
|
|
1469
|
+
}));
|
|
1470
|
+
for (const { skillsDir, exists } of existenceChecks) {
|
|
1471
|
+
if (!exists) continue;
|
|
1472
|
+
const abortController = new AbortController();
|
|
1473
|
+
this.watchers.push(abortController);
|
|
1474
|
+
this.watchDirectory(skillsDir, abortController.signal).catch((error) => {
|
|
1475
|
+
if (error?.name !== "AbortError") console.error(`[skill-watcher] Error watching ${skillsDir}: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
1476
|
+
});
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
/**
|
|
1480
|
+
* Stops all active file watchers.
|
|
1481
|
+
* Should be called when the service is being disposed.
|
|
1482
|
+
*/
|
|
1483
|
+
stopWatching() {
|
|
1484
|
+
for (const controller of this.watchers) controller.abort();
|
|
1485
|
+
this.watchers = [];
|
|
1486
|
+
}
|
|
1487
|
+
/**
|
|
1488
|
+
* Watches a directory for changes to SKILL.md files.
|
|
1489
|
+
* @param dirPath - Directory path to watch
|
|
1490
|
+
* @param signal - AbortSignal to stop watching
|
|
1491
|
+
*/
|
|
1492
|
+
async watchDirectory(dirPath, signal) {
|
|
1493
|
+
const watcher = (0, node_fs_promises.watch)(dirPath, {
|
|
1494
|
+
recursive: true,
|
|
1495
|
+
signal
|
|
1496
|
+
});
|
|
1497
|
+
for await (const event of watcher) if (event.filename && event.filename.endsWith("SKILL.md")) {
|
|
1498
|
+
this.clearCache();
|
|
1499
|
+
this.onCacheInvalidated?.();
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
/**
|
|
1154
1503
|
* Load skills from a directory.
|
|
1155
1504
|
* Supports both flat structure (SKILL.md) and nested structure (name/SKILL.md).
|
|
1156
1505
|
*
|
|
@@ -1177,38 +1526,54 @@ var SkillService = class {
|
|
|
1177
1526
|
} catch (error) {
|
|
1178
1527
|
throw new SkillLoadError(`Failed to read skills directory: ${error instanceof Error ? error.message : "Unknown error"}`, dirPath, error instanceof Error ? error : void 0);
|
|
1179
1528
|
}
|
|
1180
|
-
|
|
1529
|
+
const entryStats = await Promise.all(entries.map(async (entry) => {
|
|
1181
1530
|
const entryPath = (0, node_path.join)(dirPath, entry);
|
|
1182
|
-
let entryStat;
|
|
1183
1531
|
try {
|
|
1184
|
-
|
|
1532
|
+
return {
|
|
1533
|
+
entry,
|
|
1534
|
+
entryPath,
|
|
1535
|
+
stat: await (0, node_fs_promises.stat)(entryPath),
|
|
1536
|
+
error: null
|
|
1537
|
+
};
|
|
1185
1538
|
} catch (error) {
|
|
1186
1539
|
console.warn(`Skipping entry ${entryPath}: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
1187
|
-
|
|
1540
|
+
return {
|
|
1541
|
+
entry,
|
|
1542
|
+
entryPath,
|
|
1543
|
+
stat: null,
|
|
1544
|
+
error
|
|
1545
|
+
};
|
|
1188
1546
|
}
|
|
1547
|
+
}));
|
|
1548
|
+
const skillFilesToLoad = [];
|
|
1549
|
+
for (const { entry, entryPath, stat: entryStat } of entryStats) {
|
|
1550
|
+
if (!entryStat) continue;
|
|
1189
1551
|
if (entryStat.isDirectory()) {
|
|
1190
1552
|
const skillFilePath = (0, node_path.join)(entryPath, "SKILL.md");
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
if (
|
|
1553
|
+
skillFilesToLoad.push({
|
|
1554
|
+
filePath: skillFilePath,
|
|
1555
|
+
isRootLevel: false
|
|
1556
|
+
});
|
|
1557
|
+
} else if (entry === "SKILL.md") skillFilesToLoad.push({
|
|
1558
|
+
filePath: entryPath,
|
|
1559
|
+
isRootLevel: true
|
|
1560
|
+
});
|
|
1561
|
+
}
|
|
1562
|
+
const loadResults = await Promise.all(skillFilesToLoad.map(async ({ filePath, isRootLevel }) => {
|
|
1563
|
+
try {
|
|
1564
|
+
if (!isRootLevel && !await pathExists(filePath)) return null;
|
|
1565
|
+
return await this.loadSkillFile(filePath, location);
|
|
1203
1566
|
} catch (error) {
|
|
1204
|
-
console.warn(`Skipping skill at ${
|
|
1205
|
-
|
|
1567
|
+
console.warn(`Skipping skill at ${filePath}: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
1568
|
+
return null;
|
|
1206
1569
|
}
|
|
1207
|
-
}
|
|
1570
|
+
}));
|
|
1571
|
+
for (const skill of loadResults) if (skill) skills.push(skill);
|
|
1208
1572
|
return skills;
|
|
1209
1573
|
}
|
|
1210
1574
|
/**
|
|
1211
1575
|
* Load a single skill file and parse its frontmatter.
|
|
1576
|
+
* Supports multi-line YAML values using literal (|) and folded (>) block scalars.
|
|
1212
1577
|
*
|
|
1213
1578
|
* @param filePath - Path to the SKILL.md file
|
|
1214
1579
|
* @param location - Whether this is a 'project' or 'user' skill
|
|
@@ -1222,150 +1587,67 @@ var SkillService = class {
|
|
|
1222
1587
|
* // Returns null if frontmatter is missing name or description
|
|
1223
1588
|
*/
|
|
1224
1589
|
async loadSkillFile(filePath, location) {
|
|
1225
|
-
let
|
|
1590
|
+
let fileContent;
|
|
1226
1591
|
try {
|
|
1227
|
-
|
|
1592
|
+
fileContent = await (0, node_fs_promises.readFile)(filePath, "utf-8");
|
|
1228
1593
|
} catch (error) {
|
|
1229
1594
|
throw new SkillLoadError(`Failed to read skill file: ${error instanceof Error ? error.message : "Unknown error"}`, filePath, error instanceof Error ? error : void 0);
|
|
1230
1595
|
}
|
|
1231
|
-
const {
|
|
1232
|
-
if (!
|
|
1596
|
+
const { frontMatter, content } = parseFrontMatter(fileContent);
|
|
1597
|
+
if (!frontMatter || !frontMatter.name || !frontMatter.description) return null;
|
|
1233
1598
|
return {
|
|
1234
|
-
name:
|
|
1235
|
-
description:
|
|
1599
|
+
name: frontMatter.name,
|
|
1600
|
+
description: frontMatter.description,
|
|
1236
1601
|
location,
|
|
1237
|
-
content
|
|
1602
|
+
content,
|
|
1238
1603
|
basePath: (0, node_path.dirname)(filePath)
|
|
1239
1604
|
};
|
|
1240
1605
|
}
|
|
1241
|
-
/**
|
|
1242
|
-
* Parse YAML frontmatter from markdown content.
|
|
1243
|
-
* Frontmatter is delimited by --- at start and end.
|
|
1244
|
-
*
|
|
1245
|
-
* @param content - Full markdown content with frontmatter
|
|
1246
|
-
* @returns Parsed metadata and body content
|
|
1247
|
-
*
|
|
1248
|
-
* @example
|
|
1249
|
-
* // Input content:
|
|
1250
|
-
* // ---
|
|
1251
|
-
* // name: my-skill
|
|
1252
|
-
* // description: A sample skill
|
|
1253
|
-
* // ---
|
|
1254
|
-
* // # Skill Content
|
|
1255
|
-
* // This is the skill body.
|
|
1256
|
-
*
|
|
1257
|
-
* const result = parseFrontmatter(content);
|
|
1258
|
-
* // result.metadata = { name: 'my-skill', description: 'A sample skill' }
|
|
1259
|
-
* // result.body = '# Skill Content\nThis is the skill body.'
|
|
1260
|
-
*/
|
|
1261
|
-
parseFrontmatter(content) {
|
|
1262
|
-
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
|
|
1263
|
-
if (!match) return {
|
|
1264
|
-
metadata: {},
|
|
1265
|
-
body: content
|
|
1266
|
-
};
|
|
1267
|
-
const [, frontmatter, body] = match;
|
|
1268
|
-
const metadata = {};
|
|
1269
|
-
const lines = frontmatter.split("\n");
|
|
1270
|
-
for (const line of lines) {
|
|
1271
|
-
const colonIndex = line.indexOf(":");
|
|
1272
|
-
if (colonIndex > 0) {
|
|
1273
|
-
const key = line.slice(0, colonIndex).trim();
|
|
1274
|
-
let value = line.slice(colonIndex + 1).trim();
|
|
1275
|
-
if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
|
|
1276
|
-
if (key === "name" || key === "description" || key === "license") metadata[key] = value;
|
|
1277
|
-
}
|
|
1278
|
-
}
|
|
1279
|
-
return {
|
|
1280
|
-
metadata,
|
|
1281
|
-
body: body.trim()
|
|
1282
|
-
};
|
|
1283
|
-
}
|
|
1284
1606
|
};
|
|
1285
1607
|
|
|
1286
1608
|
//#endregion
|
|
1287
|
-
//#region src/
|
|
1609
|
+
//#region src/constants/index.ts
|
|
1288
1610
|
/**
|
|
1289
|
-
*
|
|
1290
|
-
*
|
|
1291
|
-
* DESIGN PATTERNS:
|
|
1292
|
-
* - Utility function pattern for reusable logic
|
|
1293
|
-
* - Fail-fast pattern with early returns
|
|
1294
|
-
* - Environment variable configuration pattern
|
|
1295
|
-
*
|
|
1296
|
-
* CODING STANDARDS:
|
|
1297
|
-
* - Use sync filesystem operations for config discovery (performance)
|
|
1298
|
-
* - Check PROJECT_PATH environment variable first
|
|
1299
|
-
* - Fall back to current working directory
|
|
1300
|
-
* - Support both .yaml and .json extensions
|
|
1301
|
-
* - Return null if no config file is found
|
|
1302
|
-
*
|
|
1303
|
-
* AVOID:
|
|
1304
|
-
* - Throwing errors (return null instead for optional config)
|
|
1305
|
-
* - Hardcoded file names without extension variants
|
|
1306
|
-
* - Ignoring environment variables
|
|
1611
|
+
* Shared constants for one-mcp package
|
|
1307
1612
|
*/
|
|
1308
1613
|
/**
|
|
1309
|
-
*
|
|
1310
|
-
*
|
|
1311
|
-
*
|
|
1312
|
-
* @returns Absolute path to config file, or null if not found
|
|
1614
|
+
* Prefix added to skill names when they clash with MCP tool names.
|
|
1615
|
+
* This ensures skills can be uniquely identified even when a tool has the same name.
|
|
1313
1616
|
*/
|
|
1314
|
-
|
|
1315
|
-
const configFileNames = [
|
|
1316
|
-
"mcp-config.yaml",
|
|
1317
|
-
"mcp-config.yml",
|
|
1318
|
-
"mcp-config.json"
|
|
1319
|
-
];
|
|
1320
|
-
const projectPath = process.env.PROJECT_PATH;
|
|
1321
|
-
if (projectPath) for (const fileName of configFileNames) {
|
|
1322
|
-
const configPath = (0, node_path.resolve)(projectPath, fileName);
|
|
1323
|
-
if ((0, node_fs.existsSync)(configPath)) return configPath;
|
|
1324
|
-
}
|
|
1325
|
-
const cwd = process.cwd();
|
|
1326
|
-
for (const fileName of configFileNames) {
|
|
1327
|
-
const configPath = (0, node_path.join)(cwd, fileName);
|
|
1328
|
-
if ((0, node_fs.existsSync)(configPath)) return configPath;
|
|
1329
|
-
}
|
|
1330
|
-
return null;
|
|
1331
|
-
}
|
|
1332
|
-
|
|
1333
|
-
//#endregion
|
|
1334
|
-
//#region src/utils/parseToolName.ts
|
|
1617
|
+
const SKILL_PREFIX = "skill__";
|
|
1335
1618
|
/**
|
|
1336
|
-
*
|
|
1337
|
-
*
|
|
1338
|
-
*
|
|
1339
|
-
* @param toolName - The tool name to parse (e.g., "my_tool" or "server__my_tool")
|
|
1340
|
-
* @returns Parsed result with optional serverName and actualToolName
|
|
1341
|
-
*
|
|
1342
|
-
* @example
|
|
1343
|
-
* parseToolName("my_tool") // { actualToolName: "my_tool" }
|
|
1344
|
-
* parseToolName("server__my_tool") // { serverName: "server", actualToolName: "my_tool" }
|
|
1619
|
+
* Log prefix for skill detection messages.
|
|
1620
|
+
* Used to easily filter skill detection logs in stderr output.
|
|
1345
1621
|
*/
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
var skills_description_default = "{% if skills.size > 0 %}\n<skills>\n<instructions>\nWhen users ask you to perform tasks, check if any of the available skills below can help complete the task more effectively. Skills provide specialized capabilities and domain knowledge.\n\nHow to use skills:\n- Invoke skills using this tool with the skill name only (no arguments)\n- When you invoke a skill, you will see <command-message>The \"{name}\" skill is loading</command-message>\n- The skill's prompt will expand and provide detailed instructions on how to complete the task\n- Examples:\n - `skill: \"pdf\"` - invoke the pdf skill\n - `skill: \"xlsx\"` - invoke the xlsx skill\n - `skill: \"ms-office-suite:pdf\"` - invoke using fully qualified name\n\nImportant:\n- Only use skills listed in <available_skills> below\n- Do not invoke a skill that is already running\n- Do not use this tool for built-in CLI commands (like /help, /clear, etc.)\n</instructions>\n\n<available_skills>\n{% for skill in skills -%}\n<item><name>{{ skill.displayName }}</name><description>{{ skill.description }}</description></item>\n{% endfor -%}\n</available_skills>\n</skills>\n{% endif %}\n";
|
|
1622
|
+
const LOG_PREFIX_SKILL_DETECTION = "[skill-detection]";
|
|
1623
|
+
/**
|
|
1624
|
+
* Prefix for prompt-based skill locations.
|
|
1625
|
+
* Format: "prompt:{serverName}:{promptName}"
|
|
1626
|
+
*/
|
|
1627
|
+
const PROMPT_LOCATION_PREFIX = "prompt:";
|
|
1628
|
+
/**
|
|
1629
|
+
* Default server ID used when no ID is provided via CLI or config.
|
|
1630
|
+
* This fallback is used when auto-generation also fails.
|
|
1631
|
+
*/
|
|
1632
|
+
const DEFAULT_SERVER_ID = "unknown";
|
|
1358
1633
|
|
|
1359
1634
|
//#endregion
|
|
1360
|
-
//#region src/templates/
|
|
1361
|
-
var
|
|
1635
|
+
//#region src/templates/toolkit-description.liquid?raw
|
|
1636
|
+
var toolkit_description_default = "<toolkit id=\"{{ serverId }}\">\n<instruction>\nBefore you use any capabilities below, you MUST call this tool with a list of names to learn how to use them properly; this includes:\n- For tools: Arguments schema needed to pass to use_tool\n- For skills: Detailed instructions that will expand when invoked (Prefer to be explored first when relevant)\n\nThis tool is optimized for batch queries - you can request multiple capabilities at once for better performance.\n\nHow to invoke:\n- For MCP tools: Use use_tool with toolName and toolArgs based on the schema\n- For skills: Use this tool with the skill name to get expanded instructions\n</instruction>\n\n<available_capabilities>\n{% for server in servers -%}\n<group name=\"{{ server.name }}\">\n{% if server.instruction -%}\n<group_instruction>{{ server.instruction }}</group_instruction>\n{% endif -%}\n{% if server.omitToolDescription -%}\n{% for toolName in server.toolNames -%}\n<item name=\"{{ toolName }}\"></item>\n{% endfor -%}\n{% else -%}\n{% for tool in server.tools -%}\n<item name=\"{{ tool.displayName }}\"><description>{{ tool.description | default: \"No description\" }}</description></item>\n{% endfor -%}\n{% endif -%}\n</group>\n{% endfor -%}\n{% if skills.size > 0 -%}\n<group name=\"skills\">\n{% for skill in skills -%}\n<item name=\"{{ skill.displayName }}\"><description>{{ skill.description }}</description></item>\n{% endfor -%}\n</group>\n{% endif -%}\n</available_capabilities>\n</toolkit>\n";
|
|
1362
1637
|
|
|
1363
1638
|
//#endregion
|
|
1364
1639
|
//#region src/tools/DescribeToolsTool.ts
|
|
1365
1640
|
/**
|
|
1366
|
-
*
|
|
1641
|
+
* Formats skill instructions with the loading command message prefix.
|
|
1642
|
+
* This message is used by Claude Code to indicate that a skill is being loaded.
|
|
1643
|
+
*
|
|
1644
|
+
* @param name - The skill name
|
|
1645
|
+
* @param instructions - The raw skill instructions/content
|
|
1646
|
+
* @returns Formatted instructions with command message prefix
|
|
1367
1647
|
*/
|
|
1368
|
-
|
|
1648
|
+
function formatSkillInstructions(name, instructions) {
|
|
1649
|
+
return `<command-message>The "${name}" skill is loading</command-message>\n${instructions}`;
|
|
1650
|
+
}
|
|
1369
1651
|
/**
|
|
1370
1652
|
* DescribeToolsTool provides progressive disclosure of MCP tools and skills.
|
|
1371
1653
|
*
|
|
@@ -1388,22 +1670,93 @@ var DescribeToolsTool = class DescribeToolsTool {
|
|
|
1388
1670
|
clientManager;
|
|
1389
1671
|
skillService;
|
|
1390
1672
|
liquid = new liquidjs.Liquid();
|
|
1673
|
+
/** Cache for auto-detected skills from prompt front-matter */
|
|
1674
|
+
autoDetectedSkillsCache = null;
|
|
1675
|
+
/** Unique server identifier for this one-mcp instance */
|
|
1676
|
+
serverId;
|
|
1391
1677
|
/**
|
|
1392
1678
|
* Creates a new DescribeToolsTool instance
|
|
1393
1679
|
* @param clientManager - The MCP client manager for accessing remote servers
|
|
1394
1680
|
* @param skillService - Optional skill service for loading skills
|
|
1681
|
+
* @param serverId - Unique server identifier for this one-mcp instance
|
|
1395
1682
|
*/
|
|
1396
|
-
constructor(clientManager, skillService) {
|
|
1683
|
+
constructor(clientManager, skillService, serverId) {
|
|
1397
1684
|
this.clientManager = clientManager;
|
|
1398
1685
|
this.skillService = skillService;
|
|
1686
|
+
this.serverId = serverId || DEFAULT_SERVER_ID;
|
|
1687
|
+
}
|
|
1688
|
+
/**
|
|
1689
|
+
* Clears the cached auto-detected skills from prompt front-matter.
|
|
1690
|
+
* Use this when prompt configurations may have changed or when
|
|
1691
|
+
* the skill service cache is invalidated.
|
|
1692
|
+
*/
|
|
1693
|
+
clearAutoDetectedSkillsCache() {
|
|
1694
|
+
this.autoDetectedSkillsCache = null;
|
|
1695
|
+
}
|
|
1696
|
+
/**
|
|
1697
|
+
* Detects and caches skills from prompt front-matter across all connected MCP servers.
|
|
1698
|
+
* Fetches all prompts and checks their content for YAML front-matter with name/description.
|
|
1699
|
+
* Results are cached to avoid repeated fetches.
|
|
1700
|
+
*
|
|
1701
|
+
* Error Handling Strategy:
|
|
1702
|
+
* - Errors are logged to stderr but do not fail the overall detection process
|
|
1703
|
+
* - This ensures partial results are returned even if some servers/prompts fail
|
|
1704
|
+
* - Common failure reasons: server temporarily unavailable, prompt requires arguments,
|
|
1705
|
+
* network timeout, or server doesn't support listPrompts
|
|
1706
|
+
* - Errors are prefixed with [skill-detection] for easy filtering in logs
|
|
1707
|
+
*
|
|
1708
|
+
* @returns Array of auto-detected skills from prompt front-matter
|
|
1709
|
+
*/
|
|
1710
|
+
async detectSkillsFromPromptFrontMatter() {
|
|
1711
|
+
if (this.autoDetectedSkillsCache !== null) return this.autoDetectedSkillsCache;
|
|
1712
|
+
const clients = this.clientManager.getAllClients();
|
|
1713
|
+
let listPromptsFailures = 0;
|
|
1714
|
+
let fetchPromptFailures = 0;
|
|
1715
|
+
const autoDetectedSkills = (await Promise.all(clients.map(async (client) => {
|
|
1716
|
+
const detectedSkills = [];
|
|
1717
|
+
const configuredPromptNames = new Set(client.prompts ? Object.keys(client.prompts) : []);
|
|
1718
|
+
try {
|
|
1719
|
+
const prompts = await client.listPrompts();
|
|
1720
|
+
if (!prompts || prompts.length === 0) return detectedSkills;
|
|
1721
|
+
const promptResults = await Promise.all(prompts.map(async (promptInfo) => {
|
|
1722
|
+
if (configuredPromptNames.has(promptInfo.name)) return null;
|
|
1723
|
+
try {
|
|
1724
|
+
const skillExtraction = extractSkillFrontMatter(((await client.getPrompt(promptInfo.name)).messages || []).map((m) => {
|
|
1725
|
+
const content = m.content;
|
|
1726
|
+
if (typeof content === "string") return content;
|
|
1727
|
+
if (content && typeof content === "object" && "text" in content) return String(content.text);
|
|
1728
|
+
return "";
|
|
1729
|
+
}).join("\n"));
|
|
1730
|
+
if (skillExtraction) return {
|
|
1731
|
+
serverName: client.serverName,
|
|
1732
|
+
promptName: promptInfo.name,
|
|
1733
|
+
skill: skillExtraction.skill
|
|
1734
|
+
};
|
|
1735
|
+
return null;
|
|
1736
|
+
} catch (error) {
|
|
1737
|
+
fetchPromptFailures++;
|
|
1738
|
+
console.error(`${LOG_PREFIX_SKILL_DETECTION} Failed to fetch prompt '${promptInfo.name}' from ${client.serverName}: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
1739
|
+
return null;
|
|
1740
|
+
}
|
|
1741
|
+
}));
|
|
1742
|
+
for (const result of promptResults) if (result) detectedSkills.push(result);
|
|
1743
|
+
} catch (error) {
|
|
1744
|
+
listPromptsFailures++;
|
|
1745
|
+
console.error(`${LOG_PREFIX_SKILL_DETECTION} Failed to list prompts from ${client.serverName}: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
1746
|
+
}
|
|
1747
|
+
return detectedSkills;
|
|
1748
|
+
}))).flat();
|
|
1749
|
+
if (listPromptsFailures > 0 || fetchPromptFailures > 0) console.error(`${LOG_PREFIX_SKILL_DETECTION} Completed with ${listPromptsFailures} server failure(s) and ${fetchPromptFailures} prompt failure(s). Detected ${autoDetectedSkills.length} skill(s).`);
|
|
1750
|
+
this.autoDetectedSkillsCache = autoDetectedSkills;
|
|
1751
|
+
return autoDetectedSkills;
|
|
1399
1752
|
}
|
|
1400
1753
|
/**
|
|
1401
1754
|
* Collects skills derived from prompt configurations across all connected MCP servers.
|
|
1402
|
-
*
|
|
1755
|
+
* Includes both explicitly configured prompts and auto-detected skills from front-matter.
|
|
1403
1756
|
*
|
|
1404
1757
|
* @returns Array of skill template data derived from prompts
|
|
1405
1758
|
*/
|
|
1406
|
-
collectPromptSkills() {
|
|
1759
|
+
async collectPromptSkills() {
|
|
1407
1760
|
const clients = this.clientManager.getAllClients();
|
|
1408
1761
|
const promptSkills = [];
|
|
1409
1762
|
for (const client of clients) {
|
|
@@ -1414,16 +1767,22 @@ var DescribeToolsTool = class DescribeToolsTool {
|
|
|
1414
1767
|
description: promptConfig.skill.description
|
|
1415
1768
|
});
|
|
1416
1769
|
}
|
|
1770
|
+
const autoDetectedSkills = await this.detectSkillsFromPromptFrontMatter();
|
|
1771
|
+
for (const autoSkill of autoDetectedSkills) promptSkills.push({
|
|
1772
|
+
name: autoSkill.skill.name,
|
|
1773
|
+
displayName: autoSkill.skill.name,
|
|
1774
|
+
description: autoSkill.skill.description
|
|
1775
|
+
});
|
|
1417
1776
|
return promptSkills;
|
|
1418
1777
|
}
|
|
1419
1778
|
/**
|
|
1420
1779
|
* Finds a prompt-based skill by name from all connected MCP servers.
|
|
1421
|
-
*
|
|
1780
|
+
* Searches both explicitly configured prompts and auto-detected skills from front-matter.
|
|
1422
1781
|
*
|
|
1423
1782
|
* @param skillName - The skill name to search for
|
|
1424
1783
|
* @returns Object with serverName, promptName, and skill config, or undefined if not found
|
|
1425
1784
|
*/
|
|
1426
|
-
findPromptSkill(skillName) {
|
|
1785
|
+
async findPromptSkill(skillName) {
|
|
1427
1786
|
if (!skillName) return void 0;
|
|
1428
1787
|
const clients = this.clientManager.getAllClients();
|
|
1429
1788
|
for (const client of clients) {
|
|
@@ -1434,16 +1793,24 @@ var DescribeToolsTool = class DescribeToolsTool {
|
|
|
1434
1793
|
skill: promptConfig.skill
|
|
1435
1794
|
};
|
|
1436
1795
|
}
|
|
1796
|
+
const autoDetectedSkills = await this.detectSkillsFromPromptFrontMatter();
|
|
1797
|
+
for (const autoSkill of autoDetectedSkills) if (autoSkill.skill.name === skillName) return {
|
|
1798
|
+
serverName: autoSkill.serverName,
|
|
1799
|
+
promptName: autoSkill.promptName,
|
|
1800
|
+
skill: autoSkill.skill,
|
|
1801
|
+
autoDetected: true
|
|
1802
|
+
};
|
|
1437
1803
|
}
|
|
1438
1804
|
/**
|
|
1439
1805
|
* Retrieves skill content from a prompt-based skill configuration.
|
|
1440
1806
|
* Fetches the prompt from the MCP server and extracts text content.
|
|
1807
|
+
* Handles both explicitly configured prompts and auto-detected skills from front-matter.
|
|
1441
1808
|
*
|
|
1442
1809
|
* @param skillName - The skill name being requested
|
|
1443
1810
|
* @returns SkillDescription if found and successfully fetched, undefined otherwise
|
|
1444
1811
|
*/
|
|
1445
1812
|
async getPromptSkillContent(skillName) {
|
|
1446
|
-
const promptSkill = this.findPromptSkill(skillName);
|
|
1813
|
+
const promptSkill = await this.findPromptSkill(skillName);
|
|
1447
1814
|
if (!promptSkill) return void 0;
|
|
1448
1815
|
const client = this.clientManager.getClient(promptSkill.serverName);
|
|
1449
1816
|
if (!client) {
|
|
@@ -1451,7 +1818,7 @@ var DescribeToolsTool = class DescribeToolsTool {
|
|
|
1451
1818
|
return;
|
|
1452
1819
|
}
|
|
1453
1820
|
try {
|
|
1454
|
-
const
|
|
1821
|
+
const rawInstructions = (await client.getPrompt(promptSkill.promptName)).messages?.map((m) => {
|
|
1455
1822
|
const content = m.content;
|
|
1456
1823
|
if (typeof content === "string") return content;
|
|
1457
1824
|
if (content && typeof content === "object" && "text" in content) return String(content.text);
|
|
@@ -1459,8 +1826,8 @@ var DescribeToolsTool = class DescribeToolsTool {
|
|
|
1459
1826
|
}).join("\n") || "";
|
|
1460
1827
|
return {
|
|
1461
1828
|
name: promptSkill.skill.name,
|
|
1462
|
-
location: promptSkill.skill.folder ||
|
|
1463
|
-
instructions
|
|
1829
|
+
location: promptSkill.skill.folder || `${PROMPT_LOCATION_PREFIX}${promptSkill.serverName}/${promptSkill.promptName}`,
|
|
1830
|
+
instructions: formatSkillInstructions(promptSkill.skill.name, rawInstructions)
|
|
1464
1831
|
};
|
|
1465
1832
|
} catch (error) {
|
|
1466
1833
|
console.error(`Failed to get prompt-based skill '${skillName}': ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
@@ -1468,49 +1835,18 @@ var DescribeToolsTool = class DescribeToolsTool {
|
|
|
1468
1835
|
}
|
|
1469
1836
|
}
|
|
1470
1837
|
/**
|
|
1471
|
-
* Builds the
|
|
1838
|
+
* Builds the combined toolkit description using a single Liquid template.
|
|
1472
1839
|
*
|
|
1473
|
-
*
|
|
1474
|
-
*
|
|
1475
|
-
* prefixed with skill__ when their name clashes with an MCP tool or another skill.
|
|
1476
|
-
*
|
|
1477
|
-
* @param mcpToolNames - Set of MCP tool names to check for clashes
|
|
1478
|
-
* @returns Rendered skills section string with available skills list
|
|
1479
|
-
*/
|
|
1480
|
-
async buildSkillsSection(mcpToolNames) {
|
|
1481
|
-
const rawSkills = this.skillService ? await this.skillService.getSkills() : [];
|
|
1482
|
-
const promptSkills = this.collectPromptSkills();
|
|
1483
|
-
const allSkillsData = [...rawSkills.map((skill) => ({
|
|
1484
|
-
name: skill.name,
|
|
1485
|
-
displayName: skill.name,
|
|
1486
|
-
description: skill.description
|
|
1487
|
-
})), ...promptSkills];
|
|
1488
|
-
const skillNameCounts = /* @__PURE__ */ new Map();
|
|
1489
|
-
for (const skill of allSkillsData) skillNameCounts.set(skill.name, (skillNameCounts.get(skill.name) || 0) + 1);
|
|
1490
|
-
const skills = allSkillsData.map((skill) => {
|
|
1491
|
-
const clashesWithMcpTool = mcpToolNames.has(skill.name);
|
|
1492
|
-
const clashesWithOtherSkill = (skillNameCounts.get(skill.name) || 0) > 1;
|
|
1493
|
-
const needsPrefix = clashesWithMcpTool || clashesWithOtherSkill;
|
|
1494
|
-
return {
|
|
1495
|
-
name: skill.name,
|
|
1496
|
-
displayName: needsPrefix ? `${SKILL_PREFIX$1}${skill.name}` : skill.name,
|
|
1497
|
-
description: skill.description
|
|
1498
|
-
};
|
|
1499
|
-
});
|
|
1500
|
-
return this.liquid.parseAndRender(skills_description_default, { skills });
|
|
1501
|
-
}
|
|
1502
|
-
/**
|
|
1503
|
-
* Builds the MCP servers section of the tool description using a Liquid template.
|
|
1504
|
-
*
|
|
1505
|
-
* Collects all tools from connected MCP servers, detects name clashes,
|
|
1506
|
-
* and renders them using the mcp-servers-description.liquid template.
|
|
1840
|
+
* Collects all tools from connected MCP servers and all skills, then renders
|
|
1841
|
+
* them together using the toolkit-description.liquid template.
|
|
1507
1842
|
*
|
|
1508
1843
|
* Tool names are prefixed with serverName__ when the same tool exists
|
|
1509
|
-
* on multiple servers
|
|
1844
|
+
* on multiple servers. Skill names are prefixed with skill__ when they
|
|
1845
|
+
* clash with MCP tools or other skills.
|
|
1510
1846
|
*
|
|
1511
|
-
* @returns Object with rendered
|
|
1847
|
+
* @returns Object with rendered description and set of all tool names
|
|
1512
1848
|
*/
|
|
1513
|
-
async
|
|
1849
|
+
async buildToolkitDescription() {
|
|
1514
1850
|
const clients = this.clientManager.getAllClients();
|
|
1515
1851
|
const toolToServers = /* @__PURE__ */ new Map();
|
|
1516
1852
|
const serverToolsMap = /* @__PURE__ */ new Map();
|
|
@@ -1531,9 +1867,6 @@ var DescribeToolsTool = class DescribeToolsTool {
|
|
|
1531
1867
|
}));
|
|
1532
1868
|
/**
|
|
1533
1869
|
* Formats tool name with server prefix if the tool exists on multiple servers
|
|
1534
|
-
* @param toolName - The original tool name
|
|
1535
|
-
* @param serverName - The server providing this tool
|
|
1536
|
-
* @returns Tool name prefixed with serverName__ if clashing, otherwise plain name
|
|
1537
1870
|
*/
|
|
1538
1871
|
const formatToolName = (toolName, serverName) => {
|
|
1539
1872
|
return (toolToServers.get(toolName) || []).length > 1 ? `${serverName}__${toolName}` : toolName;
|
|
@@ -1553,31 +1886,57 @@ var DescribeToolsTool = class DescribeToolsTool {
|
|
|
1553
1886
|
toolNames: formattedTools.map((t) => t.displayName)
|
|
1554
1887
|
};
|
|
1555
1888
|
});
|
|
1889
|
+
const rawSkills = this.skillService ? await this.skillService.getSkills() : [];
|
|
1890
|
+
const promptSkills = await this.collectPromptSkills();
|
|
1891
|
+
const seenSkillNames = /* @__PURE__ */ new Set();
|
|
1892
|
+
const allSkillsData = [];
|
|
1893
|
+
for (const skill of rawSkills) if (!seenSkillNames.has(skill.name)) {
|
|
1894
|
+
seenSkillNames.add(skill.name);
|
|
1895
|
+
allSkillsData.push({
|
|
1896
|
+
name: skill.name,
|
|
1897
|
+
displayName: skill.name,
|
|
1898
|
+
description: skill.description
|
|
1899
|
+
});
|
|
1900
|
+
}
|
|
1901
|
+
for (const skill of promptSkills) if (!seenSkillNames.has(skill.name)) {
|
|
1902
|
+
seenSkillNames.add(skill.name);
|
|
1903
|
+
allSkillsData.push(skill);
|
|
1904
|
+
}
|
|
1905
|
+
const skills = allSkillsData.map((skill) => {
|
|
1906
|
+
const clashesWithMcpTool = allToolNames.has(skill.name);
|
|
1907
|
+
return {
|
|
1908
|
+
name: skill.name,
|
|
1909
|
+
displayName: clashesWithMcpTool ? `${SKILL_PREFIX}${skill.name}` : skill.name,
|
|
1910
|
+
description: skill.description
|
|
1911
|
+
};
|
|
1912
|
+
});
|
|
1556
1913
|
return {
|
|
1557
|
-
content: await this.liquid.parseAndRender(
|
|
1914
|
+
content: await this.liquid.parseAndRender(toolkit_description_default, {
|
|
1915
|
+
servers,
|
|
1916
|
+
skills,
|
|
1917
|
+
serverId: this.serverId
|
|
1918
|
+
}),
|
|
1558
1919
|
toolNames: allToolNames
|
|
1559
1920
|
};
|
|
1560
1921
|
}
|
|
1561
1922
|
/**
|
|
1562
|
-
* Gets the tool definition including available
|
|
1923
|
+
* Gets the tool definition including available tools and skills in a unified format.
|
|
1563
1924
|
*
|
|
1564
1925
|
* The definition includes:
|
|
1565
|
-
* -
|
|
1566
|
-
* -
|
|
1567
|
-
* -
|
|
1926
|
+
* - All MCP tools from connected servers
|
|
1927
|
+
* - All available skills (file-based and prompt-based)
|
|
1928
|
+
* - Unified instructions for querying capability details
|
|
1568
1929
|
*
|
|
1569
|
-
* Tool names are prefixed with serverName__ when
|
|
1570
|
-
*
|
|
1930
|
+
* Tool names are prefixed with serverName__ when clashing.
|
|
1931
|
+
* Skill names are prefixed with skill__ when clashing.
|
|
1571
1932
|
*
|
|
1572
1933
|
* @returns Tool definition with description and input schema
|
|
1573
1934
|
*/
|
|
1574
1935
|
async getDefinition() {
|
|
1575
|
-
const
|
|
1576
|
-
const skillsSection = await this.buildSkillsSection(serversResult.toolNames);
|
|
1936
|
+
const { content } = await this.buildToolkitDescription();
|
|
1577
1937
|
return {
|
|
1578
1938
|
name: DescribeToolsTool.TOOL_NAME,
|
|
1579
|
-
description:
|
|
1580
|
-
${skillsSection}`,
|
|
1939
|
+
description: content,
|
|
1581
1940
|
inputSchema: {
|
|
1582
1941
|
type: "object",
|
|
1583
1942
|
properties: { toolNames: {
|
|
@@ -1637,40 +1996,42 @@ ${skillsSection}`,
|
|
|
1637
1996
|
serverToolsMap.set(client.serverName, []);
|
|
1638
1997
|
}
|
|
1639
1998
|
}));
|
|
1640
|
-
const
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1999
|
+
const lookupResults = await Promise.all(toolNames.map(async (requestedName) => {
|
|
2000
|
+
const result$1 = {
|
|
2001
|
+
tools: [],
|
|
2002
|
+
skills: [],
|
|
2003
|
+
notFound: null
|
|
2004
|
+
};
|
|
2005
|
+
if (requestedName.startsWith(SKILL_PREFIX)) {
|
|
2006
|
+
const skillName = requestedName.slice(SKILL_PREFIX.length);
|
|
1646
2007
|
if (this.skillService) {
|
|
1647
2008
|
const skill = await this.skillService.getSkill(skillName);
|
|
1648
2009
|
if (skill) {
|
|
1649
|
-
|
|
2010
|
+
result$1.skills.push({
|
|
1650
2011
|
name: skill.name,
|
|
1651
2012
|
location: skill.basePath,
|
|
1652
|
-
instructions: skill.content
|
|
2013
|
+
instructions: formatSkillInstructions(skill.name, skill.content)
|
|
1653
2014
|
});
|
|
1654
|
-
|
|
2015
|
+
return result$1;
|
|
1655
2016
|
}
|
|
1656
2017
|
}
|
|
1657
2018
|
const promptSkillContent = await this.getPromptSkillContent(skillName);
|
|
1658
2019
|
if (promptSkillContent) {
|
|
1659
|
-
|
|
1660
|
-
|
|
2020
|
+
result$1.skills.push(promptSkillContent);
|
|
2021
|
+
return result$1;
|
|
1661
2022
|
}
|
|
1662
|
-
|
|
1663
|
-
|
|
2023
|
+
result$1.notFound = requestedName;
|
|
2024
|
+
return result$1;
|
|
1664
2025
|
}
|
|
1665
2026
|
const { serverName, actualToolName } = parseToolName(requestedName);
|
|
1666
2027
|
if (serverName) {
|
|
1667
2028
|
const serverTools = serverToolsMap.get(serverName);
|
|
1668
2029
|
if (!serverTools) {
|
|
1669
|
-
|
|
1670
|
-
|
|
2030
|
+
result$1.notFound = requestedName;
|
|
2031
|
+
return result$1;
|
|
1671
2032
|
}
|
|
1672
2033
|
const tool = serverTools.find((t) => t.name === actualToolName);
|
|
1673
|
-
if (tool)
|
|
2034
|
+
if (tool) result$1.tools.push({
|
|
1674
2035
|
server: serverName,
|
|
1675
2036
|
tool: {
|
|
1676
2037
|
name: tool.name,
|
|
@@ -1678,52 +2039,61 @@ ${skillsSection}`,
|
|
|
1678
2039
|
inputSchema: tool.inputSchema
|
|
1679
2040
|
}
|
|
1680
2041
|
});
|
|
1681
|
-
else
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
}
|
|
1696
|
-
const promptSkillContent = await this.getPromptSkillContent(actualToolName);
|
|
1697
|
-
if (promptSkillContent) {
|
|
1698
|
-
foundSkills.push(promptSkillContent);
|
|
1699
|
-
continue;
|
|
2042
|
+
else result$1.notFound = requestedName;
|
|
2043
|
+
return result$1;
|
|
2044
|
+
}
|
|
2045
|
+
const servers = toolToServers.get(actualToolName);
|
|
2046
|
+
if (!servers || servers.length === 0) {
|
|
2047
|
+
if (this.skillService) {
|
|
2048
|
+
const skill = await this.skillService.getSkill(actualToolName);
|
|
2049
|
+
if (skill) {
|
|
2050
|
+
result$1.skills.push({
|
|
2051
|
+
name: skill.name,
|
|
2052
|
+
location: skill.basePath,
|
|
2053
|
+
instructions: formatSkillInstructions(skill.name, skill.content)
|
|
2054
|
+
});
|
|
2055
|
+
return result$1;
|
|
1700
2056
|
}
|
|
1701
|
-
notFoundItems.push(requestedName);
|
|
1702
|
-
continue;
|
|
1703
2057
|
}
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
server,
|
|
1709
|
-
tool: {
|
|
1710
|
-
name: tool.name,
|
|
1711
|
-
description: tool.description,
|
|
1712
|
-
inputSchema: tool.inputSchema
|
|
1713
|
-
}
|
|
1714
|
-
});
|
|
1715
|
-
} else for (const server of servers) {
|
|
1716
|
-
const tool = serverToolsMap.get(server).find((t) => t.name === actualToolName);
|
|
1717
|
-
foundTools.push({
|
|
1718
|
-
server,
|
|
1719
|
-
tool: {
|
|
1720
|
-
name: tool.name,
|
|
1721
|
-
description: tool.description,
|
|
1722
|
-
inputSchema: tool.inputSchema
|
|
1723
|
-
}
|
|
1724
|
-
});
|
|
2058
|
+
const promptSkillContent = await this.getPromptSkillContent(actualToolName);
|
|
2059
|
+
if (promptSkillContent) {
|
|
2060
|
+
result$1.skills.push(promptSkillContent);
|
|
2061
|
+
return result$1;
|
|
1725
2062
|
}
|
|
2063
|
+
result$1.notFound = requestedName;
|
|
2064
|
+
return result$1;
|
|
1726
2065
|
}
|
|
2066
|
+
if (servers.length === 1) {
|
|
2067
|
+
const server = servers[0];
|
|
2068
|
+
const tool = serverToolsMap.get(server).find((t) => t.name === actualToolName);
|
|
2069
|
+
result$1.tools.push({
|
|
2070
|
+
server,
|
|
2071
|
+
tool: {
|
|
2072
|
+
name: tool.name,
|
|
2073
|
+
description: tool.description,
|
|
2074
|
+
inputSchema: tool.inputSchema
|
|
2075
|
+
}
|
|
2076
|
+
});
|
|
2077
|
+
} else for (const server of servers) {
|
|
2078
|
+
const tool = serverToolsMap.get(server).find((t) => t.name === actualToolName);
|
|
2079
|
+
result$1.tools.push({
|
|
2080
|
+
server,
|
|
2081
|
+
tool: {
|
|
2082
|
+
name: tool.name,
|
|
2083
|
+
description: tool.description,
|
|
2084
|
+
inputSchema: tool.inputSchema
|
|
2085
|
+
}
|
|
2086
|
+
});
|
|
2087
|
+
}
|
|
2088
|
+
return result$1;
|
|
2089
|
+
}));
|
|
2090
|
+
const foundTools = [];
|
|
2091
|
+
const foundSkills = [];
|
|
2092
|
+
const notFoundItems = [];
|
|
2093
|
+
for (const result$1 of lookupResults) {
|
|
2094
|
+
foundTools.push(...result$1.tools);
|
|
2095
|
+
foundSkills.push(...result$1.skills);
|
|
2096
|
+
if (result$1.notFound) notFoundItems.push(result$1.notFound);
|
|
1727
2097
|
}
|
|
1728
2098
|
if (foundTools.length === 0 && foundSkills.length === 0) return {
|
|
1729
2099
|
content: [{
|
|
@@ -1766,10 +2136,6 @@ ${skillsSection}`,
|
|
|
1766
2136
|
//#endregion
|
|
1767
2137
|
//#region src/tools/UseToolTool.ts
|
|
1768
2138
|
/**
|
|
1769
|
-
* Prefix used to identify skill invocations (e.g., skill__pdf)
|
|
1770
|
-
*/
|
|
1771
|
-
const SKILL_PREFIX = "skill__";
|
|
1772
|
-
/**
|
|
1773
2139
|
* UseToolTool executes MCP tools and skills with proper error handling.
|
|
1774
2140
|
*
|
|
1775
2141
|
* This tool supports three invocation patterns:
|
|
@@ -1786,14 +2152,18 @@ var UseToolTool = class UseToolTool {
|
|
|
1786
2152
|
static TOOL_NAME = "use_tool";
|
|
1787
2153
|
clientManager;
|
|
1788
2154
|
skillService;
|
|
2155
|
+
/** Unique server identifier for this one-mcp instance */
|
|
2156
|
+
serverId;
|
|
1789
2157
|
/**
|
|
1790
2158
|
* Creates a new UseToolTool instance
|
|
1791
2159
|
* @param clientManager - The MCP client manager for accessing remote servers
|
|
1792
2160
|
* @param skillService - Optional skill service for loading and executing skills
|
|
2161
|
+
* @param serverId - Unique server identifier for this one-mcp instance
|
|
1793
2162
|
*/
|
|
1794
|
-
constructor(clientManager, skillService) {
|
|
2163
|
+
constructor(clientManager, skillService, serverId) {
|
|
1795
2164
|
this.clientManager = clientManager;
|
|
1796
2165
|
this.skillService = skillService;
|
|
2166
|
+
this.serverId = serverId || DEFAULT_SERVER_ID;
|
|
1797
2167
|
}
|
|
1798
2168
|
/**
|
|
1799
2169
|
* Returns the MCP tool definition with name, description, and input schema.
|
|
@@ -1809,6 +2179,8 @@ var UseToolTool = class UseToolTool {
|
|
|
1809
2179
|
description: `Execute an MCP tool (NOT Skill) with provided arguments. You MUST call describe_tools first to discover the tool's correct arguments. Then to use tool:
|
|
1810
2180
|
- Provide toolName and toolArgs based on the schema
|
|
1811
2181
|
- If multiple servers provide the same tool, specify serverName
|
|
2182
|
+
|
|
2183
|
+
IMPORTANT: Only use tools discovered from describe_tools with id="${this.serverId}".
|
|
1812
2184
|
`,
|
|
1813
2185
|
inputSchema: {
|
|
1814
2186
|
type: "object",
|
|
@@ -1895,7 +2267,7 @@ var UseToolTool = class UseToolTool {
|
|
|
1895
2267
|
try {
|
|
1896
2268
|
const { toolName: inputToolName, toolArgs = {} } = input;
|
|
1897
2269
|
if (inputToolName.startsWith(SKILL_PREFIX)) {
|
|
1898
|
-
const skillName = inputToolName.slice(
|
|
2270
|
+
const skillName = inputToolName.slice(SKILL_PREFIX.length);
|
|
1899
2271
|
if (this.skillService) {
|
|
1900
2272
|
const skill = await this.skillService.getSkill(skillName);
|
|
1901
2273
|
if (skill) return this.executeSkill(skill);
|
|
@@ -2030,6 +2402,7 @@ async function createServer(options) {
|
|
|
2030
2402
|
} });
|
|
2031
2403
|
const clientManager = new McpClientManagerService();
|
|
2032
2404
|
let configSkills;
|
|
2405
|
+
let configId;
|
|
2033
2406
|
if (options?.configFilePath) {
|
|
2034
2407
|
let config;
|
|
2035
2408
|
try {
|
|
@@ -2041,6 +2414,7 @@ async function createServer(options) {
|
|
|
2041
2414
|
throw new Error(`Failed to load MCP configuration from '${options.configFilePath}': ${error instanceof Error ? error.message : String(error)}`);
|
|
2042
2415
|
}
|
|
2043
2416
|
configSkills = config.skills;
|
|
2417
|
+
configId = config.id;
|
|
2044
2418
|
const failedConnections = [];
|
|
2045
2419
|
const connectionPromises = Object.entries(config.mcpServers).map(async ([serverName, serverConfig]) => {
|
|
2046
2420
|
try {
|
|
@@ -2059,10 +2433,19 @@ async function createServer(options) {
|
|
|
2059
2433
|
if (failedConnections.length > 0 && failedConnections.length < Object.keys(config.mcpServers).length) console.error(`Warning: Some MCP server connections failed: ${failedConnections.map((f) => f.serverName).join(", ")}`);
|
|
2060
2434
|
if (failedConnections.length > 0 && failedConnections.length === Object.keys(config.mcpServers).length) throw new Error(`All MCP server connections failed: ${failedConnections.map((f) => `${f.serverName}: ${f.error.message}`).join(", ")}`);
|
|
2061
2435
|
}
|
|
2436
|
+
const serverId = options?.serverId || configId || generateServerId();
|
|
2437
|
+
console.error(`[one-mcp] Server ID: ${serverId}`);
|
|
2062
2438
|
const skillsConfig = options?.skills || configSkills;
|
|
2063
|
-
const
|
|
2064
|
-
const
|
|
2065
|
-
|
|
2439
|
+
const toolsRef = { describeTools: null };
|
|
2440
|
+
const skillService = skillsConfig && skillsConfig.paths.length > 0 ? new SkillService(process.cwd(), skillsConfig.paths, { onCacheInvalidated: () => {
|
|
2441
|
+
toolsRef.describeTools?.clearAutoDetectedSkillsCache();
|
|
2442
|
+
} }) : void 0;
|
|
2443
|
+
const describeTools = new DescribeToolsTool(clientManager, skillService, serverId);
|
|
2444
|
+
const useTool = new UseToolTool(clientManager, skillService, serverId);
|
|
2445
|
+
toolsRef.describeTools = describeTools;
|
|
2446
|
+
if (skillService) skillService.startWatching().catch((error) => {
|
|
2447
|
+
console.error(`[skill-watcher] File watcher failed (non-critical): ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
2448
|
+
});
|
|
2066
2449
|
server.setRequestHandler(__modelcontextprotocol_sdk_types_js.ListToolsRequestSchema, async () => ({ tools: [await describeTools.getDefinition(), useTool.getDefinition()] }));
|
|
2067
2450
|
server.setRequestHandler(__modelcontextprotocol_sdk_types_js.CallToolRequestSchema, async (request) => {
|
|
2068
2451
|
const { name, arguments: args } = request.params;
|