@agiflowai/one-mcp 0.2.6 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +87 -5
- package/dist/cli.cjs +474 -11
- package/dist/cli.mjs +475 -12
- package/dist/{http-xSfxBa8A.cjs → http-BYBRKvD4.cjs} +454 -185
- package/dist/{http-D9BDXhHn.mjs → http-Bi2N9PUM.mjs} +456 -187
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +4 -0
- package/dist/index.d.mts +4 -0
- package/dist/index.mjs +1 -1
- package/package.json +1 -1
|
@@ -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]);
|
|
@@ -308,6 +311,7 @@ const McpServerConfigSchema = zod.z.discriminatedUnion("transport", [
|
|
|
308
311
|
toolBlacklist: zod.z.array(zod.z.string()).optional(),
|
|
309
312
|
omitToolDescription: zod.z.boolean().optional(),
|
|
310
313
|
prompts: zod.z.record(zod.z.string(), InternalPromptConfigSchema).optional(),
|
|
314
|
+
timeout: zod.z.number().positive().optional(),
|
|
311
315
|
transport: zod.z.literal("stdio"),
|
|
312
316
|
config: McpStdioConfigSchema
|
|
313
317
|
}),
|
|
@@ -317,6 +321,7 @@ const McpServerConfigSchema = zod.z.discriminatedUnion("transport", [
|
|
|
317
321
|
toolBlacklist: zod.z.array(zod.z.string()).optional(),
|
|
318
322
|
omitToolDescription: zod.z.boolean().optional(),
|
|
319
323
|
prompts: zod.z.record(zod.z.string(), InternalPromptConfigSchema).optional(),
|
|
324
|
+
timeout: zod.z.number().positive().optional(),
|
|
320
325
|
transport: zod.z.literal("http"),
|
|
321
326
|
config: McpHttpConfigSchema
|
|
322
327
|
}),
|
|
@@ -326,6 +331,7 @@ const McpServerConfigSchema = zod.z.discriminatedUnion("transport", [
|
|
|
326
331
|
toolBlacklist: zod.z.array(zod.z.string()).optional(),
|
|
327
332
|
omitToolDescription: zod.z.boolean().optional(),
|
|
328
333
|
prompts: zod.z.record(zod.z.string(), InternalPromptConfigSchema).optional(),
|
|
334
|
+
timeout: zod.z.number().positive().optional(),
|
|
329
335
|
transport: zod.z.literal("sse"),
|
|
330
336
|
config: McpSseConfigSchema
|
|
331
337
|
})
|
|
@@ -359,6 +365,7 @@ function transformClaudeCodeConfig(claudeConfig) {
|
|
|
359
365
|
toolBlacklist: stdioConfig.config?.toolBlacklist,
|
|
360
366
|
omitToolDescription: stdioConfig.config?.omitToolDescription,
|
|
361
367
|
prompts: stdioConfig.config?.prompts,
|
|
368
|
+
timeout: stdioConfig.timeout,
|
|
362
369
|
transport: "stdio",
|
|
363
370
|
config: {
|
|
364
371
|
command: interpolatedCommand,
|
|
@@ -377,6 +384,7 @@ function transformClaudeCodeConfig(claudeConfig) {
|
|
|
377
384
|
toolBlacklist: httpConfig.config?.toolBlacklist,
|
|
378
385
|
omitToolDescription: httpConfig.config?.omitToolDescription,
|
|
379
386
|
prompts: httpConfig.config?.prompts,
|
|
387
|
+
timeout: httpConfig.timeout,
|
|
380
388
|
transport,
|
|
381
389
|
config: {
|
|
382
390
|
url: interpolatedUrl,
|
|
@@ -835,6 +843,8 @@ var ConfigFetcherService = class {
|
|
|
835
843
|
|
|
836
844
|
//#endregion
|
|
837
845
|
//#region src/services/McpClientManagerService.ts
|
|
846
|
+
/** Default connection timeout in milliseconds (30 seconds) */
|
|
847
|
+
const DEFAULT_CONNECTION_TIMEOUT_MS = 3e4;
|
|
838
848
|
/**
|
|
839
849
|
* MCP Client wrapper for managing individual server connections
|
|
840
850
|
* This is an internal class used by McpClientManagerService
|
|
@@ -940,8 +950,10 @@ var McpClientManagerService = class {
|
|
|
940
950
|
}
|
|
941
951
|
/**
|
|
942
952
|
* Connect to an MCP server based on its configuration with timeout
|
|
953
|
+
* Uses the timeout from server config, falling back to default (30s)
|
|
943
954
|
*/
|
|
944
|
-
async connectToServer(serverName, config
|
|
955
|
+
async connectToServer(serverName, config) {
|
|
956
|
+
const timeoutMs = config.timeout ?? DEFAULT_CONNECTION_TIMEOUT_MS;
|
|
945
957
|
if (this.clients.has(serverName)) throw new Error(`Client for ${serverName} is already connected`);
|
|
946
958
|
const client = new __modelcontextprotocol_sdk_client_index_js.Client({
|
|
947
959
|
name: `@agiflowai/one-mcp-client`,
|
|
@@ -973,6 +985,7 @@ var McpClientManagerService = class {
|
|
|
973
985
|
*/
|
|
974
986
|
async performConnection(mcpClient, config) {
|
|
975
987
|
if (config.transport === "stdio") await this.connectStdioClient(mcpClient, config.config);
|
|
988
|
+
else if (config.transport === "http") await this.connectHttpClient(mcpClient, config.config);
|
|
976
989
|
else if (config.transport === "sse") await this.connectSseClient(mcpClient, config.config);
|
|
977
990
|
else throw new Error(`Unsupported transport type: ${config.transport}`);
|
|
978
991
|
}
|
|
@@ -986,6 +999,10 @@ var McpClientManagerService = class {
|
|
|
986
999
|
const childProcess = transport["_process"];
|
|
987
1000
|
if (childProcess) mcpClient.setChildProcess(childProcess);
|
|
988
1001
|
}
|
|
1002
|
+
async connectHttpClient(mcpClient, config) {
|
|
1003
|
+
const transport = new __modelcontextprotocol_sdk_client_streamableHttp_js.StreamableHTTPClientTransport(new URL(config.url), { requestInit: config.headers ? { headers: config.headers } : void 0 });
|
|
1004
|
+
await mcpClient["client"].connect(transport);
|
|
1005
|
+
}
|
|
989
1006
|
async connectSseClient(mcpClient, config) {
|
|
990
1007
|
const transport = new __modelcontextprotocol_sdk_client_sse_js.SSEClientTransport(new URL(config.url));
|
|
991
1008
|
await mcpClient["client"].connect(transport);
|
|
@@ -1028,6 +1045,209 @@ var McpClientManagerService = class {
|
|
|
1028
1045
|
}
|
|
1029
1046
|
};
|
|
1030
1047
|
|
|
1048
|
+
//#endregion
|
|
1049
|
+
//#region src/utils/findConfigFile.ts
|
|
1050
|
+
/**
|
|
1051
|
+
* Config File Finder Utility
|
|
1052
|
+
*
|
|
1053
|
+
* DESIGN PATTERNS:
|
|
1054
|
+
* - Utility function pattern for reusable logic
|
|
1055
|
+
* - Fail-fast pattern with early returns
|
|
1056
|
+
* - Environment variable configuration pattern
|
|
1057
|
+
*
|
|
1058
|
+
* CODING STANDARDS:
|
|
1059
|
+
* - Use sync filesystem operations for config discovery (performance)
|
|
1060
|
+
* - Check PROJECT_PATH environment variable first
|
|
1061
|
+
* - Fall back to current working directory
|
|
1062
|
+
* - Support both .yaml and .json extensions
|
|
1063
|
+
* - Return null if no config file is found
|
|
1064
|
+
*
|
|
1065
|
+
* AVOID:
|
|
1066
|
+
* - Throwing errors (return null instead for optional config)
|
|
1067
|
+
* - Hardcoded file names without extension variants
|
|
1068
|
+
* - Ignoring environment variables
|
|
1069
|
+
*/
|
|
1070
|
+
/**
|
|
1071
|
+
* Find MCP configuration file by checking PROJECT_PATH first, then cwd
|
|
1072
|
+
* Looks for both mcp-config.yaml and mcp-config.json
|
|
1073
|
+
*
|
|
1074
|
+
* @returns Absolute path to config file, or null if not found
|
|
1075
|
+
*/
|
|
1076
|
+
function findConfigFile() {
|
|
1077
|
+
const configFileNames = [
|
|
1078
|
+
"mcp-config.yaml",
|
|
1079
|
+
"mcp-config.yml",
|
|
1080
|
+
"mcp-config.json"
|
|
1081
|
+
];
|
|
1082
|
+
const projectPath = process.env.PROJECT_PATH;
|
|
1083
|
+
if (projectPath) for (const fileName of configFileNames) {
|
|
1084
|
+
const configPath = (0, node_path.resolve)(projectPath, fileName);
|
|
1085
|
+
if ((0, node_fs.existsSync)(configPath)) return configPath;
|
|
1086
|
+
}
|
|
1087
|
+
const cwd = process.cwd();
|
|
1088
|
+
for (const fileName of configFileNames) {
|
|
1089
|
+
const configPath = (0, node_path.join)(cwd, fileName);
|
|
1090
|
+
if ((0, node_fs.existsSync)(configPath)) return configPath;
|
|
1091
|
+
}
|
|
1092
|
+
return null;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
//#endregion
|
|
1096
|
+
//#region src/utils/parseToolName.ts
|
|
1097
|
+
/**
|
|
1098
|
+
* Parse tool name to extract server and actual tool name
|
|
1099
|
+
* Supports both plain tool names and prefixed format: {serverName}__{toolName}
|
|
1100
|
+
*
|
|
1101
|
+
* @param toolName - The tool name to parse (e.g., "my_tool" or "server__my_tool")
|
|
1102
|
+
* @returns Parsed result with optional serverName and actualToolName
|
|
1103
|
+
*
|
|
1104
|
+
* @example
|
|
1105
|
+
* parseToolName("my_tool") // { actualToolName: "my_tool" }
|
|
1106
|
+
* parseToolName("server__my_tool") // { serverName: "server", actualToolName: "my_tool" }
|
|
1107
|
+
*/
|
|
1108
|
+
function parseToolName(toolName) {
|
|
1109
|
+
const separatorIndex = toolName.indexOf("__");
|
|
1110
|
+
if (separatorIndex > 0) return {
|
|
1111
|
+
serverName: toolName.substring(0, separatorIndex),
|
|
1112
|
+
actualToolName: toolName.substring(separatorIndex + 2)
|
|
1113
|
+
};
|
|
1114
|
+
return { actualToolName: toolName };
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
//#endregion
|
|
1118
|
+
//#region src/utils/parseFrontMatter.ts
|
|
1119
|
+
/**
|
|
1120
|
+
* Parses YAML front matter from a string content.
|
|
1121
|
+
* Front matter must be at the start of the content, delimited by `---`.
|
|
1122
|
+
*
|
|
1123
|
+
* Supports:
|
|
1124
|
+
* - Simple key: value pairs
|
|
1125
|
+
* - Literal block scalar (|) for multi-line preserving newlines
|
|
1126
|
+
* - Folded block scalar (>) for multi-line folding to single line
|
|
1127
|
+
*
|
|
1128
|
+
* @param content - The content string that may contain front matter
|
|
1129
|
+
* @returns Object with parsed front matter (or null) and remaining content
|
|
1130
|
+
*
|
|
1131
|
+
* @example
|
|
1132
|
+
* const result = parseFrontMatter(`---
|
|
1133
|
+
* name: my-skill
|
|
1134
|
+
* description: A skill description
|
|
1135
|
+
* ---
|
|
1136
|
+
* The actual content here`);
|
|
1137
|
+
* // result.frontMatter = { name: 'my-skill', description: 'A skill description' }
|
|
1138
|
+
* // result.content = 'The actual content here'
|
|
1139
|
+
*
|
|
1140
|
+
* @example
|
|
1141
|
+
* // Multi-line with literal block scalar
|
|
1142
|
+
* const result = parseFrontMatter(`---
|
|
1143
|
+
* name: my-skill
|
|
1144
|
+
* description: |
|
|
1145
|
+
* Line 1
|
|
1146
|
+
* Line 2
|
|
1147
|
+
* ---
|
|
1148
|
+
* Content`);
|
|
1149
|
+
* // result.frontMatter.description = 'Line 1\nLine 2'
|
|
1150
|
+
*/
|
|
1151
|
+
function parseFrontMatter(content) {
|
|
1152
|
+
const trimmedContent = content.trimStart();
|
|
1153
|
+
if (!trimmedContent.startsWith("---")) return {
|
|
1154
|
+
frontMatter: null,
|
|
1155
|
+
content
|
|
1156
|
+
};
|
|
1157
|
+
const endDelimiterIndex = trimmedContent.indexOf("\n---", 3);
|
|
1158
|
+
if (endDelimiterIndex === -1) return {
|
|
1159
|
+
frontMatter: null,
|
|
1160
|
+
content
|
|
1161
|
+
};
|
|
1162
|
+
const yamlContent = trimmedContent.slice(4, endDelimiterIndex).trim();
|
|
1163
|
+
if (!yamlContent) return {
|
|
1164
|
+
frontMatter: null,
|
|
1165
|
+
content
|
|
1166
|
+
};
|
|
1167
|
+
const frontMatter = {};
|
|
1168
|
+
const lines = yamlContent.split("\n");
|
|
1169
|
+
let currentKey = null;
|
|
1170
|
+
let currentValue = [];
|
|
1171
|
+
let multiLineMode = null;
|
|
1172
|
+
let baseIndent = 0;
|
|
1173
|
+
const saveCurrentKey = () => {
|
|
1174
|
+
if (currentKey && currentValue.length > 0) if (multiLineMode === "literal") frontMatter[currentKey] = currentValue.join("\n").trimEnd();
|
|
1175
|
+
else if (multiLineMode === "folded") frontMatter[currentKey] = currentValue.join(" ").trim();
|
|
1176
|
+
else frontMatter[currentKey] = currentValue.join("").trim();
|
|
1177
|
+
currentKey = null;
|
|
1178
|
+
currentValue = [];
|
|
1179
|
+
multiLineMode = null;
|
|
1180
|
+
baseIndent = 0;
|
|
1181
|
+
};
|
|
1182
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1183
|
+
const line = lines[i];
|
|
1184
|
+
const trimmedLine = line.trim();
|
|
1185
|
+
const colonIndex = line.indexOf(":");
|
|
1186
|
+
if (colonIndex !== -1 && !line.startsWith(" ") && !line.startsWith(" ")) {
|
|
1187
|
+
saveCurrentKey();
|
|
1188
|
+
const key = line.slice(0, colonIndex).trim();
|
|
1189
|
+
let value = line.slice(colonIndex + 1).trim();
|
|
1190
|
+
if (value === "|" || value === "|-") {
|
|
1191
|
+
currentKey = key;
|
|
1192
|
+
multiLineMode = "literal";
|
|
1193
|
+
if (i + 1 < lines.length) {
|
|
1194
|
+
const match = lines[i + 1].match(/^(\s+)/);
|
|
1195
|
+
baseIndent = match ? match[1].length : 2;
|
|
1196
|
+
}
|
|
1197
|
+
} else if (value === ">" || value === ">-") {
|
|
1198
|
+
currentKey = key;
|
|
1199
|
+
multiLineMode = "folded";
|
|
1200
|
+
if (i + 1 < lines.length) {
|
|
1201
|
+
const match = lines[i + 1].match(/^(\s+)/);
|
|
1202
|
+
baseIndent = match ? match[1].length : 2;
|
|
1203
|
+
}
|
|
1204
|
+
} else {
|
|
1205
|
+
if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
|
|
1206
|
+
if (key && value) frontMatter[key] = value;
|
|
1207
|
+
}
|
|
1208
|
+
} else if (multiLineMode && currentKey) {
|
|
1209
|
+
const lineIndent = line.match(/^(\s*)/)?.[1].length || 0;
|
|
1210
|
+
if (trimmedLine === "") currentValue.push("");
|
|
1211
|
+
else if (lineIndent >= baseIndent) {
|
|
1212
|
+
const unindentedLine = line.slice(baseIndent);
|
|
1213
|
+
currentValue.push(unindentedLine);
|
|
1214
|
+
} else saveCurrentKey();
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
saveCurrentKey();
|
|
1218
|
+
return {
|
|
1219
|
+
frontMatter,
|
|
1220
|
+
content: trimmedContent.slice(endDelimiterIndex + 4).trimStart()
|
|
1221
|
+
};
|
|
1222
|
+
}
|
|
1223
|
+
/**
|
|
1224
|
+
* Checks if parsed front matter contains valid skill metadata.
|
|
1225
|
+
* A valid skill front matter must have both `name` and `description` fields.
|
|
1226
|
+
*
|
|
1227
|
+
* @param frontMatter - The parsed front matter object
|
|
1228
|
+
* @returns True if front matter contains valid skill metadata
|
|
1229
|
+
*/
|
|
1230
|
+
function isValidSkillFrontMatter(frontMatter) {
|
|
1231
|
+
return frontMatter !== null && typeof frontMatter.name === "string" && frontMatter.name.length > 0 && typeof frontMatter.description === "string" && frontMatter.description.length > 0;
|
|
1232
|
+
}
|
|
1233
|
+
/**
|
|
1234
|
+
* Extracts skill front matter from content if present and valid.
|
|
1235
|
+
*
|
|
1236
|
+
* @param content - The content string that may contain skill front matter
|
|
1237
|
+
* @returns Object with skill metadata and content, or null if no valid skill front matter
|
|
1238
|
+
*/
|
|
1239
|
+
function extractSkillFrontMatter(content) {
|
|
1240
|
+
const { frontMatter, content: remainingContent } = parseFrontMatter(content);
|
|
1241
|
+
if (frontMatter && isValidSkillFrontMatter(frontMatter)) return {
|
|
1242
|
+
skill: {
|
|
1243
|
+
name: frontMatter.name,
|
|
1244
|
+
description: frontMatter.description
|
|
1245
|
+
},
|
|
1246
|
+
content: remainingContent
|
|
1247
|
+
};
|
|
1248
|
+
return null;
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1031
1251
|
//#endregion
|
|
1032
1252
|
//#region src/services/SkillService.ts
|
|
1033
1253
|
/**
|
|
@@ -1098,14 +1318,21 @@ var SkillService = class {
|
|
|
1098
1318
|
skillPaths;
|
|
1099
1319
|
cachedSkills = null;
|
|
1100
1320
|
skillsByName = null;
|
|
1321
|
+
/** Active file watchers for skill directories */
|
|
1322
|
+
watchers = [];
|
|
1323
|
+
/** Callback invoked when cache is invalidated due to file changes */
|
|
1324
|
+
onCacheInvalidated;
|
|
1101
1325
|
/**
|
|
1102
1326
|
* Creates a new SkillService instance
|
|
1103
1327
|
* @param cwd - Current working directory for resolving relative paths
|
|
1104
1328
|
* @param skillPaths - Array of paths to skills directories
|
|
1329
|
+
* @param options - Optional configuration
|
|
1330
|
+
* @param options.onCacheInvalidated - Callback invoked when cache is invalidated due to file changes
|
|
1105
1331
|
*/
|
|
1106
|
-
constructor(cwd, skillPaths) {
|
|
1332
|
+
constructor(cwd, skillPaths, options) {
|
|
1107
1333
|
this.cwd = cwd;
|
|
1108
1334
|
this.skillPaths = skillPaths;
|
|
1335
|
+
this.onCacheInvalidated = options?.onCacheInvalidated;
|
|
1109
1336
|
}
|
|
1110
1337
|
/**
|
|
1111
1338
|
* Get all available skills from configured directories.
|
|
@@ -1151,6 +1378,54 @@ var SkillService = class {
|
|
|
1151
1378
|
this.skillsByName = null;
|
|
1152
1379
|
}
|
|
1153
1380
|
/**
|
|
1381
|
+
* Starts watching skill directories for changes to SKILL.md files.
|
|
1382
|
+
* When changes are detected, the cache is automatically invalidated.
|
|
1383
|
+
*
|
|
1384
|
+
* Uses Node.js fs.watch with recursive option for efficient directory monitoring.
|
|
1385
|
+
* Only invalidates cache when SKILL.md files are modified.
|
|
1386
|
+
*
|
|
1387
|
+
* @example
|
|
1388
|
+
* const skillService = new SkillService(cwd, skillPaths, {
|
|
1389
|
+
* onCacheInvalidated: () => console.log('Skills cache invalidated')
|
|
1390
|
+
* });
|
|
1391
|
+
* await skillService.startWatching();
|
|
1392
|
+
*/
|
|
1393
|
+
async startWatching() {
|
|
1394
|
+
this.stopWatching();
|
|
1395
|
+
for (const skillPath of this.skillPaths) {
|
|
1396
|
+
const skillsDir = (0, node_path.isAbsolute)(skillPath) ? skillPath : (0, node_path.join)(this.cwd, skillPath);
|
|
1397
|
+
if (!await pathExists(skillsDir)) continue;
|
|
1398
|
+
const abortController = new AbortController();
|
|
1399
|
+
this.watchers.push(abortController);
|
|
1400
|
+
this.watchDirectory(skillsDir, abortController.signal).catch((error) => {
|
|
1401
|
+
if (error?.name !== "AbortError") console.error(`[skill-watcher] Error watching ${skillsDir}: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
1402
|
+
});
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
/**
|
|
1406
|
+
* Stops all active file watchers.
|
|
1407
|
+
* Should be called when the service is being disposed.
|
|
1408
|
+
*/
|
|
1409
|
+
stopWatching() {
|
|
1410
|
+
for (const controller of this.watchers) controller.abort();
|
|
1411
|
+
this.watchers = [];
|
|
1412
|
+
}
|
|
1413
|
+
/**
|
|
1414
|
+
* Watches a directory for changes to SKILL.md files.
|
|
1415
|
+
* @param dirPath - Directory path to watch
|
|
1416
|
+
* @param signal - AbortSignal to stop watching
|
|
1417
|
+
*/
|
|
1418
|
+
async watchDirectory(dirPath, signal) {
|
|
1419
|
+
const watcher = (0, node_fs_promises.watch)(dirPath, {
|
|
1420
|
+
recursive: true,
|
|
1421
|
+
signal
|
|
1422
|
+
});
|
|
1423
|
+
for await (const event of watcher) if (event.filename && event.filename.endsWith("SKILL.md")) {
|
|
1424
|
+
this.clearCache();
|
|
1425
|
+
this.onCacheInvalidated?.();
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
/**
|
|
1154
1429
|
* Load skills from a directory.
|
|
1155
1430
|
* Supports both flat structure (SKILL.md) and nested structure (name/SKILL.md).
|
|
1156
1431
|
*
|
|
@@ -1209,6 +1484,7 @@ var SkillService = class {
|
|
|
1209
1484
|
}
|
|
1210
1485
|
/**
|
|
1211
1486
|
* Load a single skill file and parse its frontmatter.
|
|
1487
|
+
* Supports multi-line YAML values using literal (|) and folded (>) block scalars.
|
|
1212
1488
|
*
|
|
1213
1489
|
* @param filePath - Path to the SKILL.md file
|
|
1214
1490
|
* @param location - Whether this is a 'project' or 'user' skill
|
|
@@ -1222,150 +1498,62 @@ var SkillService = class {
|
|
|
1222
1498
|
* // Returns null if frontmatter is missing name or description
|
|
1223
1499
|
*/
|
|
1224
1500
|
async loadSkillFile(filePath, location) {
|
|
1225
|
-
let
|
|
1501
|
+
let fileContent;
|
|
1226
1502
|
try {
|
|
1227
|
-
|
|
1503
|
+
fileContent = await (0, node_fs_promises.readFile)(filePath, "utf-8");
|
|
1228
1504
|
} catch (error) {
|
|
1229
1505
|
throw new SkillLoadError(`Failed to read skill file: ${error instanceof Error ? error.message : "Unknown error"}`, filePath, error instanceof Error ? error : void 0);
|
|
1230
1506
|
}
|
|
1231
|
-
const {
|
|
1232
|
-
if (!
|
|
1507
|
+
const { frontMatter, content } = parseFrontMatter(fileContent);
|
|
1508
|
+
if (!frontMatter || !frontMatter.name || !frontMatter.description) return null;
|
|
1233
1509
|
return {
|
|
1234
|
-
name:
|
|
1235
|
-
description:
|
|
1510
|
+
name: frontMatter.name,
|
|
1511
|
+
description: frontMatter.description,
|
|
1236
1512
|
location,
|
|
1237
|
-
content
|
|
1513
|
+
content,
|
|
1238
1514
|
basePath: (0, node_path.dirname)(filePath)
|
|
1239
1515
|
};
|
|
1240
1516
|
}
|
|
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
1517
|
};
|
|
1285
1518
|
|
|
1286
1519
|
//#endregion
|
|
1287
|
-
//#region src/
|
|
1520
|
+
//#region src/constants/index.ts
|
|
1288
1521
|
/**
|
|
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
|
|
1522
|
+
* Shared constants for one-mcp package
|
|
1307
1523
|
*/
|
|
1308
1524
|
/**
|
|
1309
|
-
*
|
|
1310
|
-
*
|
|
1311
|
-
*
|
|
1312
|
-
* @returns Absolute path to config file, or null if not found
|
|
1525
|
+
* Prefix added to skill names when they clash with MCP tool names.
|
|
1526
|
+
* This ensures skills can be uniquely identified even when a tool has the same name.
|
|
1313
1527
|
*/
|
|
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
|
|
1528
|
+
const SKILL_PREFIX$1 = "skill__";
|
|
1335
1529
|
/**
|
|
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" }
|
|
1530
|
+
* Log prefix for skill detection messages.
|
|
1531
|
+
* Used to easily filter skill detection logs in stderr output.
|
|
1345
1532
|
*/
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
return { actualToolName: toolName };
|
|
1353
|
-
}
|
|
1354
|
-
|
|
1355
|
-
//#endregion
|
|
1356
|
-
//#region src/templates/skills-description.liquid?raw
|
|
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";
|
|
1533
|
+
const LOG_PREFIX_SKILL_DETECTION = "[skill-detection]";
|
|
1534
|
+
/**
|
|
1535
|
+
* Prefix for prompt-based skill locations.
|
|
1536
|
+
* Format: "prompt:{serverName}:{promptName}"
|
|
1537
|
+
*/
|
|
1538
|
+
const PROMPT_LOCATION_PREFIX = "prompt:";
|
|
1358
1539
|
|
|
1359
1540
|
//#endregion
|
|
1360
|
-
//#region src/templates/
|
|
1361
|
-
var
|
|
1541
|
+
//#region src/templates/toolkit-description.liquid?raw
|
|
1542
|
+
var toolkit_description_default = "<toolkit>\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
1543
|
|
|
1363
1544
|
//#endregion
|
|
1364
1545
|
//#region src/tools/DescribeToolsTool.ts
|
|
1365
1546
|
/**
|
|
1366
|
-
*
|
|
1547
|
+
* Formats skill instructions with the loading command message prefix.
|
|
1548
|
+
* This message is used by Claude Code to indicate that a skill is being loaded.
|
|
1549
|
+
*
|
|
1550
|
+
* @param name - The skill name
|
|
1551
|
+
* @param instructions - The raw skill instructions/content
|
|
1552
|
+
* @returns Formatted instructions with command message prefix
|
|
1367
1553
|
*/
|
|
1368
|
-
|
|
1554
|
+
function formatSkillInstructions(name, instructions) {
|
|
1555
|
+
return `<command-message>The "${name}" skill is loading</command-message>\n${instructions}`;
|
|
1556
|
+
}
|
|
1369
1557
|
/**
|
|
1370
1558
|
* DescribeToolsTool provides progressive disclosure of MCP tools and skills.
|
|
1371
1559
|
*
|
|
@@ -1388,6 +1576,8 @@ var DescribeToolsTool = class DescribeToolsTool {
|
|
|
1388
1576
|
clientManager;
|
|
1389
1577
|
skillService;
|
|
1390
1578
|
liquid = new liquidjs.Liquid();
|
|
1579
|
+
/** Cache for auto-detected skills from prompt front-matter */
|
|
1580
|
+
autoDetectedSkillsCache = null;
|
|
1391
1581
|
/**
|
|
1392
1582
|
* Creates a new DescribeToolsTool instance
|
|
1393
1583
|
* @param clientManager - The MCP client manager for accessing remote servers
|
|
@@ -1398,12 +1588,79 @@ var DescribeToolsTool = class DescribeToolsTool {
|
|
|
1398
1588
|
this.skillService = skillService;
|
|
1399
1589
|
}
|
|
1400
1590
|
/**
|
|
1591
|
+
* Clears the cached auto-detected skills from prompt front-matter.
|
|
1592
|
+
* Use this when prompt configurations may have changed or when
|
|
1593
|
+
* the skill service cache is invalidated.
|
|
1594
|
+
*/
|
|
1595
|
+
clearAutoDetectedSkillsCache() {
|
|
1596
|
+
this.autoDetectedSkillsCache = null;
|
|
1597
|
+
}
|
|
1598
|
+
/**
|
|
1599
|
+
* Detects and caches skills from prompt front-matter across all connected MCP servers.
|
|
1600
|
+
* Fetches all prompts and checks their content for YAML front-matter with name/description.
|
|
1601
|
+
* Results are cached to avoid repeated fetches.
|
|
1602
|
+
*
|
|
1603
|
+
* Error Handling Strategy:
|
|
1604
|
+
* - Errors are logged to stderr but do not fail the overall detection process
|
|
1605
|
+
* - This ensures partial results are returned even if some servers/prompts fail
|
|
1606
|
+
* - Common failure reasons: server temporarily unavailable, prompt requires arguments,
|
|
1607
|
+
* network timeout, or server doesn't support listPrompts
|
|
1608
|
+
* - Errors are prefixed with [skill-detection] for easy filtering in logs
|
|
1609
|
+
*
|
|
1610
|
+
* @returns Array of auto-detected skills from prompt front-matter
|
|
1611
|
+
*/
|
|
1612
|
+
async detectSkillsFromPromptFrontMatter() {
|
|
1613
|
+
if (this.autoDetectedSkillsCache !== null) return this.autoDetectedSkillsCache;
|
|
1614
|
+
const clients = this.clientManager.getAllClients();
|
|
1615
|
+
const autoDetectedSkills = [];
|
|
1616
|
+
let listPromptsFailures = 0;
|
|
1617
|
+
let fetchPromptFailures = 0;
|
|
1618
|
+
const fetchPromises = [];
|
|
1619
|
+
for (const client of clients) {
|
|
1620
|
+
const configuredPromptNames = new Set(client.prompts ? Object.keys(client.prompts) : []);
|
|
1621
|
+
const listPromptsPromise = (async () => {
|
|
1622
|
+
try {
|
|
1623
|
+
const prompts = await client.listPrompts();
|
|
1624
|
+
if (!prompts || prompts.length === 0) return;
|
|
1625
|
+
const promptFetchPromises = prompts.map(async (promptInfo) => {
|
|
1626
|
+
if (configuredPromptNames.has(promptInfo.name)) return;
|
|
1627
|
+
try {
|
|
1628
|
+
const skillExtraction = extractSkillFrontMatter(((await client.getPrompt(promptInfo.name)).messages || []).map((m) => {
|
|
1629
|
+
const content = m.content;
|
|
1630
|
+
if (typeof content === "string") return content;
|
|
1631
|
+
if (content && typeof content === "object" && "text" in content) return String(content.text);
|
|
1632
|
+
return "";
|
|
1633
|
+
}).join("\n"));
|
|
1634
|
+
if (skillExtraction) autoDetectedSkills.push({
|
|
1635
|
+
serverName: client.serverName,
|
|
1636
|
+
promptName: promptInfo.name,
|
|
1637
|
+
skill: skillExtraction.skill
|
|
1638
|
+
});
|
|
1639
|
+
} catch (error) {
|
|
1640
|
+
fetchPromptFailures++;
|
|
1641
|
+
console.error(`${LOG_PREFIX_SKILL_DETECTION} Failed to fetch prompt '${promptInfo.name}' from ${client.serverName}: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
1642
|
+
}
|
|
1643
|
+
});
|
|
1644
|
+
await Promise.all(promptFetchPromises);
|
|
1645
|
+
} catch (error) {
|
|
1646
|
+
listPromptsFailures++;
|
|
1647
|
+
console.error(`${LOG_PREFIX_SKILL_DETECTION} Failed to list prompts from ${client.serverName}: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
1648
|
+
}
|
|
1649
|
+
})();
|
|
1650
|
+
fetchPromises.push(listPromptsPromise);
|
|
1651
|
+
}
|
|
1652
|
+
await Promise.all(fetchPromises);
|
|
1653
|
+
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).`);
|
|
1654
|
+
this.autoDetectedSkillsCache = autoDetectedSkills;
|
|
1655
|
+
return autoDetectedSkills;
|
|
1656
|
+
}
|
|
1657
|
+
/**
|
|
1401
1658
|
* Collects skills derived from prompt configurations across all connected MCP servers.
|
|
1402
|
-
*
|
|
1659
|
+
* Includes both explicitly configured prompts and auto-detected skills from front-matter.
|
|
1403
1660
|
*
|
|
1404
1661
|
* @returns Array of skill template data derived from prompts
|
|
1405
1662
|
*/
|
|
1406
|
-
collectPromptSkills() {
|
|
1663
|
+
async collectPromptSkills() {
|
|
1407
1664
|
const clients = this.clientManager.getAllClients();
|
|
1408
1665
|
const promptSkills = [];
|
|
1409
1666
|
for (const client of clients) {
|
|
@@ -1414,16 +1671,22 @@ var DescribeToolsTool = class DescribeToolsTool {
|
|
|
1414
1671
|
description: promptConfig.skill.description
|
|
1415
1672
|
});
|
|
1416
1673
|
}
|
|
1674
|
+
const autoDetectedSkills = await this.detectSkillsFromPromptFrontMatter();
|
|
1675
|
+
for (const autoSkill of autoDetectedSkills) promptSkills.push({
|
|
1676
|
+
name: autoSkill.skill.name,
|
|
1677
|
+
displayName: autoSkill.skill.name,
|
|
1678
|
+
description: autoSkill.skill.description
|
|
1679
|
+
});
|
|
1417
1680
|
return promptSkills;
|
|
1418
1681
|
}
|
|
1419
1682
|
/**
|
|
1420
1683
|
* Finds a prompt-based skill by name from all connected MCP servers.
|
|
1421
|
-
*
|
|
1684
|
+
* Searches both explicitly configured prompts and auto-detected skills from front-matter.
|
|
1422
1685
|
*
|
|
1423
1686
|
* @param skillName - The skill name to search for
|
|
1424
1687
|
* @returns Object with serverName, promptName, and skill config, or undefined if not found
|
|
1425
1688
|
*/
|
|
1426
|
-
findPromptSkill(skillName) {
|
|
1689
|
+
async findPromptSkill(skillName) {
|
|
1427
1690
|
if (!skillName) return void 0;
|
|
1428
1691
|
const clients = this.clientManager.getAllClients();
|
|
1429
1692
|
for (const client of clients) {
|
|
@@ -1434,16 +1697,24 @@ var DescribeToolsTool = class DescribeToolsTool {
|
|
|
1434
1697
|
skill: promptConfig.skill
|
|
1435
1698
|
};
|
|
1436
1699
|
}
|
|
1700
|
+
const autoDetectedSkills = await this.detectSkillsFromPromptFrontMatter();
|
|
1701
|
+
for (const autoSkill of autoDetectedSkills) if (autoSkill.skill.name === skillName) return {
|
|
1702
|
+
serverName: autoSkill.serverName,
|
|
1703
|
+
promptName: autoSkill.promptName,
|
|
1704
|
+
skill: autoSkill.skill,
|
|
1705
|
+
autoDetected: true
|
|
1706
|
+
};
|
|
1437
1707
|
}
|
|
1438
1708
|
/**
|
|
1439
1709
|
* Retrieves skill content from a prompt-based skill configuration.
|
|
1440
1710
|
* Fetches the prompt from the MCP server and extracts text content.
|
|
1711
|
+
* Handles both explicitly configured prompts and auto-detected skills from front-matter.
|
|
1441
1712
|
*
|
|
1442
1713
|
* @param skillName - The skill name being requested
|
|
1443
1714
|
* @returns SkillDescription if found and successfully fetched, undefined otherwise
|
|
1444
1715
|
*/
|
|
1445
1716
|
async getPromptSkillContent(skillName) {
|
|
1446
|
-
const promptSkill = this.findPromptSkill(skillName);
|
|
1717
|
+
const promptSkill = await this.findPromptSkill(skillName);
|
|
1447
1718
|
if (!promptSkill) return void 0;
|
|
1448
1719
|
const client = this.clientManager.getClient(promptSkill.serverName);
|
|
1449
1720
|
if (!client) {
|
|
@@ -1451,7 +1722,7 @@ var DescribeToolsTool = class DescribeToolsTool {
|
|
|
1451
1722
|
return;
|
|
1452
1723
|
}
|
|
1453
1724
|
try {
|
|
1454
|
-
const
|
|
1725
|
+
const rawInstructions = (await client.getPrompt(promptSkill.promptName)).messages?.map((m) => {
|
|
1455
1726
|
const content = m.content;
|
|
1456
1727
|
if (typeof content === "string") return content;
|
|
1457
1728
|
if (content && typeof content === "object" && "text" in content) return String(content.text);
|
|
@@ -1459,8 +1730,8 @@ var DescribeToolsTool = class DescribeToolsTool {
|
|
|
1459
1730
|
}).join("\n") || "";
|
|
1460
1731
|
return {
|
|
1461
1732
|
name: promptSkill.skill.name,
|
|
1462
|
-
location: promptSkill.skill.folder ||
|
|
1463
|
-
instructions
|
|
1733
|
+
location: promptSkill.skill.folder || `${PROMPT_LOCATION_PREFIX}${promptSkill.serverName}/${promptSkill.promptName}`,
|
|
1734
|
+
instructions: formatSkillInstructions(promptSkill.skill.name, rawInstructions)
|
|
1464
1735
|
};
|
|
1465
1736
|
} catch (error) {
|
|
1466
1737
|
console.error(`Failed to get prompt-based skill '${skillName}': ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
@@ -1468,49 +1739,18 @@ var DescribeToolsTool = class DescribeToolsTool {
|
|
|
1468
1739
|
}
|
|
1469
1740
|
}
|
|
1470
1741
|
/**
|
|
1471
|
-
* Builds the
|
|
1742
|
+
* Builds the combined toolkit description using a single Liquid template.
|
|
1472
1743
|
*
|
|
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.
|
|
1744
|
+
* Collects all tools from connected MCP servers and all skills, then renders
|
|
1745
|
+
* them together using the toolkit-description.liquid template.
|
|
1507
1746
|
*
|
|
1508
1747
|
* Tool names are prefixed with serverName__ when the same tool exists
|
|
1509
|
-
* on multiple servers
|
|
1748
|
+
* on multiple servers. Skill names are prefixed with skill__ when they
|
|
1749
|
+
* clash with MCP tools or other skills.
|
|
1510
1750
|
*
|
|
1511
|
-
* @returns Object with rendered
|
|
1751
|
+
* @returns Object with rendered description and set of all tool names
|
|
1512
1752
|
*/
|
|
1513
|
-
async
|
|
1753
|
+
async buildToolkitDescription() {
|
|
1514
1754
|
const clients = this.clientManager.getAllClients();
|
|
1515
1755
|
const toolToServers = /* @__PURE__ */ new Map();
|
|
1516
1756
|
const serverToolsMap = /* @__PURE__ */ new Map();
|
|
@@ -1531,9 +1771,6 @@ var DescribeToolsTool = class DescribeToolsTool {
|
|
|
1531
1771
|
}));
|
|
1532
1772
|
/**
|
|
1533
1773
|
* 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
1774
|
*/
|
|
1538
1775
|
const formatToolName = (toolName, serverName) => {
|
|
1539
1776
|
return (toolToServers.get(toolName) || []).length > 1 ? `${serverName}__${toolName}` : toolName;
|
|
@@ -1553,31 +1790,56 @@ var DescribeToolsTool = class DescribeToolsTool {
|
|
|
1553
1790
|
toolNames: formattedTools.map((t) => t.displayName)
|
|
1554
1791
|
};
|
|
1555
1792
|
});
|
|
1793
|
+
const rawSkills = this.skillService ? await this.skillService.getSkills() : [];
|
|
1794
|
+
const promptSkills = await this.collectPromptSkills();
|
|
1795
|
+
const seenSkillNames = /* @__PURE__ */ new Set();
|
|
1796
|
+
const allSkillsData = [];
|
|
1797
|
+
for (const skill of rawSkills) if (!seenSkillNames.has(skill.name)) {
|
|
1798
|
+
seenSkillNames.add(skill.name);
|
|
1799
|
+
allSkillsData.push({
|
|
1800
|
+
name: skill.name,
|
|
1801
|
+
displayName: skill.name,
|
|
1802
|
+
description: skill.description
|
|
1803
|
+
});
|
|
1804
|
+
}
|
|
1805
|
+
for (const skill of promptSkills) if (!seenSkillNames.has(skill.name)) {
|
|
1806
|
+
seenSkillNames.add(skill.name);
|
|
1807
|
+
allSkillsData.push(skill);
|
|
1808
|
+
}
|
|
1809
|
+
const skills = allSkillsData.map((skill) => {
|
|
1810
|
+
const clashesWithMcpTool = allToolNames.has(skill.name);
|
|
1811
|
+
return {
|
|
1812
|
+
name: skill.name,
|
|
1813
|
+
displayName: clashesWithMcpTool ? `${SKILL_PREFIX$1}${skill.name}` : skill.name,
|
|
1814
|
+
description: skill.description
|
|
1815
|
+
};
|
|
1816
|
+
});
|
|
1556
1817
|
return {
|
|
1557
|
-
content: await this.liquid.parseAndRender(
|
|
1818
|
+
content: await this.liquid.parseAndRender(toolkit_description_default, {
|
|
1819
|
+
servers,
|
|
1820
|
+
skills
|
|
1821
|
+
}),
|
|
1558
1822
|
toolNames: allToolNames
|
|
1559
1823
|
};
|
|
1560
1824
|
}
|
|
1561
1825
|
/**
|
|
1562
|
-
* Gets the tool definition including available
|
|
1826
|
+
* Gets the tool definition including available tools and skills in a unified format.
|
|
1563
1827
|
*
|
|
1564
1828
|
* The definition includes:
|
|
1565
|
-
* -
|
|
1566
|
-
* -
|
|
1567
|
-
* -
|
|
1829
|
+
* - All MCP tools from connected servers
|
|
1830
|
+
* - All available skills (file-based and prompt-based)
|
|
1831
|
+
* - Unified instructions for querying capability details
|
|
1568
1832
|
*
|
|
1569
|
-
* Tool names are prefixed with serverName__ when
|
|
1570
|
-
*
|
|
1833
|
+
* Tool names are prefixed with serverName__ when clashing.
|
|
1834
|
+
* Skill names are prefixed with skill__ when clashing.
|
|
1571
1835
|
*
|
|
1572
1836
|
* @returns Tool definition with description and input schema
|
|
1573
1837
|
*/
|
|
1574
1838
|
async getDefinition() {
|
|
1575
|
-
const
|
|
1576
|
-
const skillsSection = await this.buildSkillsSection(serversResult.toolNames);
|
|
1839
|
+
const { content } = await this.buildToolkitDescription();
|
|
1577
1840
|
return {
|
|
1578
1841
|
name: DescribeToolsTool.TOOL_NAME,
|
|
1579
|
-
description:
|
|
1580
|
-
${skillsSection}`,
|
|
1842
|
+
description: content,
|
|
1581
1843
|
inputSchema: {
|
|
1582
1844
|
type: "object",
|
|
1583
1845
|
properties: { toolNames: {
|
|
@@ -1642,14 +1904,14 @@ ${skillsSection}`,
|
|
|
1642
1904
|
const notFoundItems = [];
|
|
1643
1905
|
for (const requestedName of toolNames) {
|
|
1644
1906
|
if (requestedName.startsWith(SKILL_PREFIX$1)) {
|
|
1645
|
-
const skillName = requestedName.slice(
|
|
1907
|
+
const skillName = requestedName.slice(SKILL_PREFIX$1.length);
|
|
1646
1908
|
if (this.skillService) {
|
|
1647
1909
|
const skill = await this.skillService.getSkill(skillName);
|
|
1648
1910
|
if (skill) {
|
|
1649
1911
|
foundSkills.push({
|
|
1650
1912
|
name: skill.name,
|
|
1651
1913
|
location: skill.basePath,
|
|
1652
|
-
instructions: skill.content
|
|
1914
|
+
instructions: formatSkillInstructions(skill.name, skill.content)
|
|
1653
1915
|
});
|
|
1654
1916
|
continue;
|
|
1655
1917
|
}
|
|
@@ -1688,7 +1950,7 @@ ${skillsSection}`,
|
|
|
1688
1950
|
foundSkills.push({
|
|
1689
1951
|
name: skill.name,
|
|
1690
1952
|
location: skill.basePath,
|
|
1691
|
-
instructions: skill.content
|
|
1953
|
+
instructions: formatSkillInstructions(skill.name, skill.content)
|
|
1692
1954
|
});
|
|
1693
1955
|
continue;
|
|
1694
1956
|
}
|
|
@@ -2060,9 +2322,16 @@ async function createServer(options) {
|
|
|
2060
2322
|
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
2323
|
}
|
|
2062
2324
|
const skillsConfig = options?.skills || configSkills;
|
|
2063
|
-
const
|
|
2325
|
+
const toolsRef = { describeTools: null };
|
|
2326
|
+
const skillService = skillsConfig && skillsConfig.paths.length > 0 ? new SkillService(process.cwd(), skillsConfig.paths, { onCacheInvalidated: () => {
|
|
2327
|
+
toolsRef.describeTools?.clearAutoDetectedSkillsCache();
|
|
2328
|
+
} }) : void 0;
|
|
2064
2329
|
const describeTools = new DescribeToolsTool(clientManager, skillService);
|
|
2065
2330
|
const useTool = new UseToolTool(clientManager, skillService);
|
|
2331
|
+
toolsRef.describeTools = describeTools;
|
|
2332
|
+
if (skillService) skillService.startWatching().catch((error) => {
|
|
2333
|
+
console.error(`[skill-watcher] File watcher failed (non-critical): ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
2334
|
+
});
|
|
2066
2335
|
server.setRequestHandler(__modelcontextprotocol_sdk_types_js.ListToolsRequestSchema, async () => ({ tools: [await describeTools.getDefinition(), useTool.getDefinition()] }));
|
|
2067
2336
|
server.setRequestHandler(__modelcontextprotocol_sdk_types_js.CallToolRequestSchema, async (request) => {
|
|
2068
2337
|
const { name, arguments: args } = request.params;
|