@agiflowai/one-mcp 0.2.6 → 0.2.7

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.
@@ -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");
@@ -973,6 +974,7 @@ var McpClientManagerService = class {
973
974
  */
974
975
  async performConnection(mcpClient, config) {
975
976
  if (config.transport === "stdio") await this.connectStdioClient(mcpClient, config.config);
977
+ else if (config.transport === "http") await this.connectHttpClient(mcpClient, config.config);
976
978
  else if (config.transport === "sse") await this.connectSseClient(mcpClient, config.config);
977
979
  else throw new Error(`Unsupported transport type: ${config.transport}`);
978
980
  }
@@ -986,6 +988,10 @@ var McpClientManagerService = class {
986
988
  const childProcess = transport["_process"];
987
989
  if (childProcess) mcpClient.setChildProcess(childProcess);
988
990
  }
991
+ async connectHttpClient(mcpClient, config) {
992
+ const transport = new __modelcontextprotocol_sdk_client_streamableHttp_js.StreamableHTTPClientTransport(new URL(config.url), { requestInit: config.headers ? { headers: config.headers } : void 0 });
993
+ await mcpClient["client"].connect(transport);
994
+ }
989
995
  async connectSseClient(mcpClient, config) {
990
996
  const transport = new __modelcontextprotocol_sdk_client_sse_js.SSEClientTransport(new URL(config.url));
991
997
  await mcpClient["client"].connect(transport);
@@ -1028,6 +1034,209 @@ var McpClientManagerService = class {
1028
1034
  }
1029
1035
  };
1030
1036
 
1037
+ //#endregion
1038
+ //#region src/utils/findConfigFile.ts
1039
+ /**
1040
+ * Config File Finder Utility
1041
+ *
1042
+ * DESIGN PATTERNS:
1043
+ * - Utility function pattern for reusable logic
1044
+ * - Fail-fast pattern with early returns
1045
+ * - Environment variable configuration pattern
1046
+ *
1047
+ * CODING STANDARDS:
1048
+ * - Use sync filesystem operations for config discovery (performance)
1049
+ * - Check PROJECT_PATH environment variable first
1050
+ * - Fall back to current working directory
1051
+ * - Support both .yaml and .json extensions
1052
+ * - Return null if no config file is found
1053
+ *
1054
+ * AVOID:
1055
+ * - Throwing errors (return null instead for optional config)
1056
+ * - Hardcoded file names without extension variants
1057
+ * - Ignoring environment variables
1058
+ */
1059
+ /**
1060
+ * Find MCP configuration file by checking PROJECT_PATH first, then cwd
1061
+ * Looks for both mcp-config.yaml and mcp-config.json
1062
+ *
1063
+ * @returns Absolute path to config file, or null if not found
1064
+ */
1065
+ function findConfigFile() {
1066
+ const configFileNames = [
1067
+ "mcp-config.yaml",
1068
+ "mcp-config.yml",
1069
+ "mcp-config.json"
1070
+ ];
1071
+ const projectPath = process.env.PROJECT_PATH;
1072
+ if (projectPath) for (const fileName of configFileNames) {
1073
+ const configPath = (0, node_path.resolve)(projectPath, fileName);
1074
+ if ((0, node_fs.existsSync)(configPath)) return configPath;
1075
+ }
1076
+ const cwd = process.cwd();
1077
+ for (const fileName of configFileNames) {
1078
+ const configPath = (0, node_path.join)(cwd, fileName);
1079
+ if ((0, node_fs.existsSync)(configPath)) return configPath;
1080
+ }
1081
+ return null;
1082
+ }
1083
+
1084
+ //#endregion
1085
+ //#region src/utils/parseToolName.ts
1086
+ /**
1087
+ * Parse tool name to extract server and actual tool name
1088
+ * Supports both plain tool names and prefixed format: {serverName}__{toolName}
1089
+ *
1090
+ * @param toolName - The tool name to parse (e.g., "my_tool" or "server__my_tool")
1091
+ * @returns Parsed result with optional serverName and actualToolName
1092
+ *
1093
+ * @example
1094
+ * parseToolName("my_tool") // { actualToolName: "my_tool" }
1095
+ * parseToolName("server__my_tool") // { serverName: "server", actualToolName: "my_tool" }
1096
+ */
1097
+ function parseToolName(toolName) {
1098
+ const separatorIndex = toolName.indexOf("__");
1099
+ if (separatorIndex > 0) return {
1100
+ serverName: toolName.substring(0, separatorIndex),
1101
+ actualToolName: toolName.substring(separatorIndex + 2)
1102
+ };
1103
+ return { actualToolName: toolName };
1104
+ }
1105
+
1106
+ //#endregion
1107
+ //#region src/utils/parseFrontMatter.ts
1108
+ /**
1109
+ * Parses YAML front matter from a string content.
1110
+ * Front matter must be at the start of the content, delimited by `---`.
1111
+ *
1112
+ * Supports:
1113
+ * - Simple key: value pairs
1114
+ * - Literal block scalar (|) for multi-line preserving newlines
1115
+ * - Folded block scalar (>) for multi-line folding to single line
1116
+ *
1117
+ * @param content - The content string that may contain front matter
1118
+ * @returns Object with parsed front matter (or null) and remaining content
1119
+ *
1120
+ * @example
1121
+ * const result = parseFrontMatter(`---
1122
+ * name: my-skill
1123
+ * description: A skill description
1124
+ * ---
1125
+ * The actual content here`);
1126
+ * // result.frontMatter = { name: 'my-skill', description: 'A skill description' }
1127
+ * // result.content = 'The actual content here'
1128
+ *
1129
+ * @example
1130
+ * // Multi-line with literal block scalar
1131
+ * const result = parseFrontMatter(`---
1132
+ * name: my-skill
1133
+ * description: |
1134
+ * Line 1
1135
+ * Line 2
1136
+ * ---
1137
+ * Content`);
1138
+ * // result.frontMatter.description = 'Line 1\nLine 2'
1139
+ */
1140
+ function parseFrontMatter(content) {
1141
+ const trimmedContent = content.trimStart();
1142
+ if (!trimmedContent.startsWith("---")) return {
1143
+ frontMatter: null,
1144
+ content
1145
+ };
1146
+ const endDelimiterIndex = trimmedContent.indexOf("\n---", 3);
1147
+ if (endDelimiterIndex === -1) return {
1148
+ frontMatter: null,
1149
+ content
1150
+ };
1151
+ const yamlContent = trimmedContent.slice(4, endDelimiterIndex).trim();
1152
+ if (!yamlContent) return {
1153
+ frontMatter: null,
1154
+ content
1155
+ };
1156
+ const frontMatter = {};
1157
+ const lines = yamlContent.split("\n");
1158
+ let currentKey = null;
1159
+ let currentValue = [];
1160
+ let multiLineMode = null;
1161
+ let baseIndent = 0;
1162
+ const saveCurrentKey = () => {
1163
+ if (currentKey && currentValue.length > 0) if (multiLineMode === "literal") frontMatter[currentKey] = currentValue.join("\n").trimEnd();
1164
+ else if (multiLineMode === "folded") frontMatter[currentKey] = currentValue.join(" ").trim();
1165
+ else frontMatter[currentKey] = currentValue.join("").trim();
1166
+ currentKey = null;
1167
+ currentValue = [];
1168
+ multiLineMode = null;
1169
+ baseIndent = 0;
1170
+ };
1171
+ for (let i = 0; i < lines.length; i++) {
1172
+ const line = lines[i];
1173
+ const trimmedLine = line.trim();
1174
+ const colonIndex = line.indexOf(":");
1175
+ if (colonIndex !== -1 && !line.startsWith(" ") && !line.startsWith(" ")) {
1176
+ saveCurrentKey();
1177
+ const key = line.slice(0, colonIndex).trim();
1178
+ let value = line.slice(colonIndex + 1).trim();
1179
+ if (value === "|" || value === "|-") {
1180
+ currentKey = key;
1181
+ multiLineMode = "literal";
1182
+ if (i + 1 < lines.length) {
1183
+ const match = lines[i + 1].match(/^(\s+)/);
1184
+ baseIndent = match ? match[1].length : 2;
1185
+ }
1186
+ } else if (value === ">" || value === ">-") {
1187
+ currentKey = key;
1188
+ multiLineMode = "folded";
1189
+ if (i + 1 < lines.length) {
1190
+ const match = lines[i + 1].match(/^(\s+)/);
1191
+ baseIndent = match ? match[1].length : 2;
1192
+ }
1193
+ } else {
1194
+ if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
1195
+ if (key && value) frontMatter[key] = value;
1196
+ }
1197
+ } else if (multiLineMode && currentKey) {
1198
+ const lineIndent = line.match(/^(\s*)/)?.[1].length || 0;
1199
+ if (trimmedLine === "") currentValue.push("");
1200
+ else if (lineIndent >= baseIndent) {
1201
+ const unindentedLine = line.slice(baseIndent);
1202
+ currentValue.push(unindentedLine);
1203
+ } else saveCurrentKey();
1204
+ }
1205
+ }
1206
+ saveCurrentKey();
1207
+ return {
1208
+ frontMatter,
1209
+ content: trimmedContent.slice(endDelimiterIndex + 4).trimStart()
1210
+ };
1211
+ }
1212
+ /**
1213
+ * Checks if parsed front matter contains valid skill metadata.
1214
+ * A valid skill front matter must have both `name` and `description` fields.
1215
+ *
1216
+ * @param frontMatter - The parsed front matter object
1217
+ * @returns True if front matter contains valid skill metadata
1218
+ */
1219
+ function isValidSkillFrontMatter(frontMatter) {
1220
+ return frontMatter !== null && typeof frontMatter.name === "string" && frontMatter.name.length > 0 && typeof frontMatter.description === "string" && frontMatter.description.length > 0;
1221
+ }
1222
+ /**
1223
+ * Extracts skill front matter from content if present and valid.
1224
+ *
1225
+ * @param content - The content string that may contain skill front matter
1226
+ * @returns Object with skill metadata and content, or null if no valid skill front matter
1227
+ */
1228
+ function extractSkillFrontMatter(content) {
1229
+ const { frontMatter, content: remainingContent } = parseFrontMatter(content);
1230
+ if (frontMatter && isValidSkillFrontMatter(frontMatter)) return {
1231
+ skill: {
1232
+ name: frontMatter.name,
1233
+ description: frontMatter.description
1234
+ },
1235
+ content: remainingContent
1236
+ };
1237
+ return null;
1238
+ }
1239
+
1031
1240
  //#endregion
1032
1241
  //#region src/services/SkillService.ts
1033
1242
  /**
@@ -1098,14 +1307,21 @@ var SkillService = class {
1098
1307
  skillPaths;
1099
1308
  cachedSkills = null;
1100
1309
  skillsByName = null;
1310
+ /** Active file watchers for skill directories */
1311
+ watchers = [];
1312
+ /** Callback invoked when cache is invalidated due to file changes */
1313
+ onCacheInvalidated;
1101
1314
  /**
1102
1315
  * Creates a new SkillService instance
1103
1316
  * @param cwd - Current working directory for resolving relative paths
1104
1317
  * @param skillPaths - Array of paths to skills directories
1318
+ * @param options - Optional configuration
1319
+ * @param options.onCacheInvalidated - Callback invoked when cache is invalidated due to file changes
1105
1320
  */
1106
- constructor(cwd, skillPaths) {
1321
+ constructor(cwd, skillPaths, options) {
1107
1322
  this.cwd = cwd;
1108
1323
  this.skillPaths = skillPaths;
1324
+ this.onCacheInvalidated = options?.onCacheInvalidated;
1109
1325
  }
1110
1326
  /**
1111
1327
  * Get all available skills from configured directories.
@@ -1151,6 +1367,54 @@ var SkillService = class {
1151
1367
  this.skillsByName = null;
1152
1368
  }
1153
1369
  /**
1370
+ * Starts watching skill directories for changes to SKILL.md files.
1371
+ * When changes are detected, the cache is automatically invalidated.
1372
+ *
1373
+ * Uses Node.js fs.watch with recursive option for efficient directory monitoring.
1374
+ * Only invalidates cache when SKILL.md files are modified.
1375
+ *
1376
+ * @example
1377
+ * const skillService = new SkillService(cwd, skillPaths, {
1378
+ * onCacheInvalidated: () => console.log('Skills cache invalidated')
1379
+ * });
1380
+ * await skillService.startWatching();
1381
+ */
1382
+ async startWatching() {
1383
+ this.stopWatching();
1384
+ for (const skillPath of this.skillPaths) {
1385
+ const skillsDir = (0, node_path.isAbsolute)(skillPath) ? skillPath : (0, node_path.join)(this.cwd, skillPath);
1386
+ if (!await pathExists(skillsDir)) continue;
1387
+ const abortController = new AbortController();
1388
+ this.watchers.push(abortController);
1389
+ this.watchDirectory(skillsDir, abortController.signal).catch((error) => {
1390
+ if (error?.name !== "AbortError") console.error(`[skill-watcher] Error watching ${skillsDir}: ${error instanceof Error ? error.message : "Unknown error"}`);
1391
+ });
1392
+ }
1393
+ }
1394
+ /**
1395
+ * Stops all active file watchers.
1396
+ * Should be called when the service is being disposed.
1397
+ */
1398
+ stopWatching() {
1399
+ for (const controller of this.watchers) controller.abort();
1400
+ this.watchers = [];
1401
+ }
1402
+ /**
1403
+ * Watches a directory for changes to SKILL.md files.
1404
+ * @param dirPath - Directory path to watch
1405
+ * @param signal - AbortSignal to stop watching
1406
+ */
1407
+ async watchDirectory(dirPath, signal) {
1408
+ const watcher = (0, node_fs_promises.watch)(dirPath, {
1409
+ recursive: true,
1410
+ signal
1411
+ });
1412
+ for await (const event of watcher) if (event.filename && event.filename.endsWith("SKILL.md")) {
1413
+ this.clearCache();
1414
+ this.onCacheInvalidated?.();
1415
+ }
1416
+ }
1417
+ /**
1154
1418
  * Load skills from a directory.
1155
1419
  * Supports both flat structure (SKILL.md) and nested structure (name/SKILL.md).
1156
1420
  *
@@ -1209,6 +1473,7 @@ var SkillService = class {
1209
1473
  }
1210
1474
  /**
1211
1475
  * Load a single skill file and parse its frontmatter.
1476
+ * Supports multi-line YAML values using literal (|) and folded (>) block scalars.
1212
1477
  *
1213
1478
  * @param filePath - Path to the SKILL.md file
1214
1479
  * @param location - Whether this is a 'project' or 'user' skill
@@ -1222,150 +1487,62 @@ var SkillService = class {
1222
1487
  * // Returns null if frontmatter is missing name or description
1223
1488
  */
1224
1489
  async loadSkillFile(filePath, location) {
1225
- let content;
1490
+ let fileContent;
1226
1491
  try {
1227
- content = await (0, node_fs_promises.readFile)(filePath, "utf-8");
1492
+ fileContent = await (0, node_fs_promises.readFile)(filePath, "utf-8");
1228
1493
  } catch (error) {
1229
1494
  throw new SkillLoadError(`Failed to read skill file: ${error instanceof Error ? error.message : "Unknown error"}`, filePath, error instanceof Error ? error : void 0);
1230
1495
  }
1231
- const { metadata, body } = this.parseFrontmatter(content);
1232
- if (!metadata.name || !metadata.description) return null;
1496
+ const { frontMatter, content } = parseFrontMatter(fileContent);
1497
+ if (!frontMatter || !frontMatter.name || !frontMatter.description) return null;
1233
1498
  return {
1234
- name: metadata.name,
1235
- description: metadata.description,
1499
+ name: frontMatter.name,
1500
+ description: frontMatter.description,
1236
1501
  location,
1237
- content: body,
1502
+ content,
1238
1503
  basePath: (0, node_path.dirname)(filePath)
1239
1504
  };
1240
1505
  }
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
1506
  };
1285
1507
 
1286
1508
  //#endregion
1287
- //#region src/utils/findConfigFile.ts
1509
+ //#region src/constants/index.ts
1288
1510
  /**
1289
- * Config File Finder Utility
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
1511
+ * Shared constants for one-mcp package
1307
1512
  */
1308
1513
  /**
1309
- * Find MCP configuration file by checking PROJECT_PATH first, then cwd
1310
- * Looks for both mcp-config.yaml and mcp-config.json
1311
- *
1312
- * @returns Absolute path to config file, or null if not found
1514
+ * Prefix added to skill names when they clash with MCP tool names.
1515
+ * This ensures skills can be uniquely identified even when a tool has the same name.
1313
1516
  */
1314
- function findConfigFile() {
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
1517
+ const SKILL_PREFIX$1 = "skill__";
1335
1518
  /**
1336
- * Parse tool name to extract server and actual tool name
1337
- * Supports both plain tool names and prefixed format: {serverName}__{toolName}
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" }
1519
+ * Log prefix for skill detection messages.
1520
+ * Used to easily filter skill detection logs in stderr output.
1345
1521
  */
1346
- function parseToolName(toolName) {
1347
- const separatorIndex = toolName.indexOf("__");
1348
- if (separatorIndex > 0) return {
1349
- serverName: toolName.substring(0, separatorIndex),
1350
- actualToolName: toolName.substring(separatorIndex + 2)
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";
1522
+ const LOG_PREFIX_SKILL_DETECTION = "[skill-detection]";
1523
+ /**
1524
+ * Prefix for prompt-based skill locations.
1525
+ * Format: "prompt:{serverName}:{promptName}"
1526
+ */
1527
+ const PROMPT_LOCATION_PREFIX = "prompt:";
1358
1528
 
1359
1529
  //#endregion
1360
- //#region src/templates/mcp-servers-description.liquid?raw
1361
- var mcp_servers_description_default = "<mcp_servers>\n<instructions>\nBefore you use any tools above, you MUST call this tool with a list of tool names to learn how to use them properly before use_tool; this includes:\n- Arguments schema needed to pass to the tool use\n- Description about each tool\n\nThis tool is optimized for batch queries - you can request multiple tools at once for better performance.\n</instructions>\n\n{% for server in servers -%}\n<server name=\"{{ server.name }}\">\n{% if server.instruction -%}\n<instruction>{{ server.instruction }}</instruction>\n{% endif -%}\n<tools>\n{% if server.omitToolDescription -%}\n{{ server.toolNames | join: \", \" }}\n{% else -%}\n{% for tool in server.tools -%}\n<item><name>{{ tool.displayName }}</name><description>{{ tool.description | default: \"No description\" }}</description></item>\n{% endfor -%}\n{% endif -%}\n</tools>\n</server>\n{% endfor -%}\n</mcp_servers>\n";
1530
+ //#region src/templates/toolkit-description.liquid?raw
1531
+ 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
1532
 
1363
1533
  //#endregion
1364
1534
  //#region src/tools/DescribeToolsTool.ts
1365
1535
  /**
1366
- * Prefix used to identify skill invocations
1536
+ * Formats skill instructions with the loading command message prefix.
1537
+ * This message is used by Claude Code to indicate that a skill is being loaded.
1538
+ *
1539
+ * @param name - The skill name
1540
+ * @param instructions - The raw skill instructions/content
1541
+ * @returns Formatted instructions with command message prefix
1367
1542
  */
1368
- const SKILL_PREFIX$1 = "skill__";
1543
+ function formatSkillInstructions(name, instructions) {
1544
+ return `<command-message>The "${name}" skill is loading</command-message>\n${instructions}`;
1545
+ }
1369
1546
  /**
1370
1547
  * DescribeToolsTool provides progressive disclosure of MCP tools and skills.
1371
1548
  *
@@ -1388,6 +1565,8 @@ var DescribeToolsTool = class DescribeToolsTool {
1388
1565
  clientManager;
1389
1566
  skillService;
1390
1567
  liquid = new liquidjs.Liquid();
1568
+ /** Cache for auto-detected skills from prompt front-matter */
1569
+ autoDetectedSkillsCache = null;
1391
1570
  /**
1392
1571
  * Creates a new DescribeToolsTool instance
1393
1572
  * @param clientManager - The MCP client manager for accessing remote servers
@@ -1398,12 +1577,79 @@ var DescribeToolsTool = class DescribeToolsTool {
1398
1577
  this.skillService = skillService;
1399
1578
  }
1400
1579
  /**
1580
+ * Clears the cached auto-detected skills from prompt front-matter.
1581
+ * Use this when prompt configurations may have changed or when
1582
+ * the skill service cache is invalidated.
1583
+ */
1584
+ clearAutoDetectedSkillsCache() {
1585
+ this.autoDetectedSkillsCache = null;
1586
+ }
1587
+ /**
1588
+ * Detects and caches skills from prompt front-matter across all connected MCP servers.
1589
+ * Fetches all prompts and checks their content for YAML front-matter with name/description.
1590
+ * Results are cached to avoid repeated fetches.
1591
+ *
1592
+ * Error Handling Strategy:
1593
+ * - Errors are logged to stderr but do not fail the overall detection process
1594
+ * - This ensures partial results are returned even if some servers/prompts fail
1595
+ * - Common failure reasons: server temporarily unavailable, prompt requires arguments,
1596
+ * network timeout, or server doesn't support listPrompts
1597
+ * - Errors are prefixed with [skill-detection] for easy filtering in logs
1598
+ *
1599
+ * @returns Array of auto-detected skills from prompt front-matter
1600
+ */
1601
+ async detectSkillsFromPromptFrontMatter() {
1602
+ if (this.autoDetectedSkillsCache !== null) return this.autoDetectedSkillsCache;
1603
+ const clients = this.clientManager.getAllClients();
1604
+ const autoDetectedSkills = [];
1605
+ let listPromptsFailures = 0;
1606
+ let fetchPromptFailures = 0;
1607
+ const fetchPromises = [];
1608
+ for (const client of clients) {
1609
+ const configuredPromptNames = new Set(client.prompts ? Object.keys(client.prompts) : []);
1610
+ const listPromptsPromise = (async () => {
1611
+ try {
1612
+ const prompts = await client.listPrompts();
1613
+ if (!prompts || prompts.length === 0) return;
1614
+ const promptFetchPromises = prompts.map(async (promptInfo) => {
1615
+ if (configuredPromptNames.has(promptInfo.name)) return;
1616
+ try {
1617
+ const skillExtraction = extractSkillFrontMatter(((await client.getPrompt(promptInfo.name)).messages || []).map((m) => {
1618
+ const content = m.content;
1619
+ if (typeof content === "string") return content;
1620
+ if (content && typeof content === "object" && "text" in content) return String(content.text);
1621
+ return "";
1622
+ }).join("\n"));
1623
+ if (skillExtraction) autoDetectedSkills.push({
1624
+ serverName: client.serverName,
1625
+ promptName: promptInfo.name,
1626
+ skill: skillExtraction.skill
1627
+ });
1628
+ } catch (error) {
1629
+ fetchPromptFailures++;
1630
+ console.error(`${LOG_PREFIX_SKILL_DETECTION} Failed to fetch prompt '${promptInfo.name}' from ${client.serverName}: ${error instanceof Error ? error.message : "Unknown error"}`);
1631
+ }
1632
+ });
1633
+ await Promise.all(promptFetchPromises);
1634
+ } catch (error) {
1635
+ listPromptsFailures++;
1636
+ console.error(`${LOG_PREFIX_SKILL_DETECTION} Failed to list prompts from ${client.serverName}: ${error instanceof Error ? error.message : "Unknown error"}`);
1637
+ }
1638
+ })();
1639
+ fetchPromises.push(listPromptsPromise);
1640
+ }
1641
+ await Promise.all(fetchPromises);
1642
+ 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).`);
1643
+ this.autoDetectedSkillsCache = autoDetectedSkills;
1644
+ return autoDetectedSkills;
1645
+ }
1646
+ /**
1401
1647
  * Collects skills derived from prompt configurations across all connected MCP servers.
1402
- * Prompts with a skill configuration are converted to skill format for display.
1648
+ * Includes both explicitly configured prompts and auto-detected skills from front-matter.
1403
1649
  *
1404
1650
  * @returns Array of skill template data derived from prompts
1405
1651
  */
1406
- collectPromptSkills() {
1652
+ async collectPromptSkills() {
1407
1653
  const clients = this.clientManager.getAllClients();
1408
1654
  const promptSkills = [];
1409
1655
  for (const client of clients) {
@@ -1414,16 +1660,22 @@ var DescribeToolsTool = class DescribeToolsTool {
1414
1660
  description: promptConfig.skill.description
1415
1661
  });
1416
1662
  }
1663
+ const autoDetectedSkills = await this.detectSkillsFromPromptFrontMatter();
1664
+ for (const autoSkill of autoDetectedSkills) promptSkills.push({
1665
+ name: autoSkill.skill.name,
1666
+ displayName: autoSkill.skill.name,
1667
+ description: autoSkill.skill.description
1668
+ });
1417
1669
  return promptSkills;
1418
1670
  }
1419
1671
  /**
1420
1672
  * Finds a prompt-based skill by name from all connected MCP servers.
1421
- * Returns the prompt name and skill config for fetching the prompt content.
1673
+ * Searches both explicitly configured prompts and auto-detected skills from front-matter.
1422
1674
  *
1423
1675
  * @param skillName - The skill name to search for
1424
1676
  * @returns Object with serverName, promptName, and skill config, or undefined if not found
1425
1677
  */
1426
- findPromptSkill(skillName) {
1678
+ async findPromptSkill(skillName) {
1427
1679
  if (!skillName) return void 0;
1428
1680
  const clients = this.clientManager.getAllClients();
1429
1681
  for (const client of clients) {
@@ -1434,16 +1686,24 @@ var DescribeToolsTool = class DescribeToolsTool {
1434
1686
  skill: promptConfig.skill
1435
1687
  };
1436
1688
  }
1689
+ const autoDetectedSkills = await this.detectSkillsFromPromptFrontMatter();
1690
+ for (const autoSkill of autoDetectedSkills) if (autoSkill.skill.name === skillName) return {
1691
+ serverName: autoSkill.serverName,
1692
+ promptName: autoSkill.promptName,
1693
+ skill: autoSkill.skill,
1694
+ autoDetected: true
1695
+ };
1437
1696
  }
1438
1697
  /**
1439
1698
  * Retrieves skill content from a prompt-based skill configuration.
1440
1699
  * Fetches the prompt from the MCP server and extracts text content.
1700
+ * Handles both explicitly configured prompts and auto-detected skills from front-matter.
1441
1701
  *
1442
1702
  * @param skillName - The skill name being requested
1443
1703
  * @returns SkillDescription if found and successfully fetched, undefined otherwise
1444
1704
  */
1445
1705
  async getPromptSkillContent(skillName) {
1446
- const promptSkill = this.findPromptSkill(skillName);
1706
+ const promptSkill = await this.findPromptSkill(skillName);
1447
1707
  if (!promptSkill) return void 0;
1448
1708
  const client = this.clientManager.getClient(promptSkill.serverName);
1449
1709
  if (!client) {
@@ -1451,7 +1711,7 @@ var DescribeToolsTool = class DescribeToolsTool {
1451
1711
  return;
1452
1712
  }
1453
1713
  try {
1454
- const instructions = (await client.getPrompt(promptSkill.promptName)).messages?.map((m) => {
1714
+ const rawInstructions = (await client.getPrompt(promptSkill.promptName)).messages?.map((m) => {
1455
1715
  const content = m.content;
1456
1716
  if (typeof content === "string") return content;
1457
1717
  if (content && typeof content === "object" && "text" in content) return String(content.text);
@@ -1459,8 +1719,8 @@ var DescribeToolsTool = class DescribeToolsTool {
1459
1719
  }).join("\n") || "";
1460
1720
  return {
1461
1721
  name: promptSkill.skill.name,
1462
- location: promptSkill.skill.folder || `prompt:${promptSkill.serverName}/${promptSkill.promptName}`,
1463
- instructions
1722
+ location: promptSkill.skill.folder || `${PROMPT_LOCATION_PREFIX}${promptSkill.serverName}/${promptSkill.promptName}`,
1723
+ instructions: formatSkillInstructions(promptSkill.skill.name, rawInstructions)
1464
1724
  };
1465
1725
  } catch (error) {
1466
1726
  console.error(`Failed to get prompt-based skill '${skillName}': ${error instanceof Error ? error.message : "Unknown error"}`);
@@ -1468,49 +1728,18 @@ var DescribeToolsTool = class DescribeToolsTool {
1468
1728
  }
1469
1729
  }
1470
1730
  /**
1471
- * Builds the skills section of the tool description using a Liquid template.
1731
+ * Builds the combined toolkit description using a single Liquid template.
1472
1732
  *
1473
- * Retrieves all available skills from the SkillService and prompt-based skills,
1474
- * then renders them using the skills-description.liquid template. Skills are only
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.
1733
+ * Collects all tools from connected MCP servers and all skills, then renders
1734
+ * them together using the toolkit-description.liquid template.
1507
1735
  *
1508
1736
  * Tool names are prefixed with serverName__ when the same tool exists
1509
- * on multiple servers to avoid ambiguity.
1737
+ * on multiple servers. Skill names are prefixed with skill__ when they
1738
+ * clash with MCP tools or other skills.
1510
1739
  *
1511
- * @returns Object with rendered servers section and set of all tool names for skill clash detection
1740
+ * @returns Object with rendered description and set of all tool names
1512
1741
  */
1513
- async buildServersSection() {
1742
+ async buildToolkitDescription() {
1514
1743
  const clients = this.clientManager.getAllClients();
1515
1744
  const toolToServers = /* @__PURE__ */ new Map();
1516
1745
  const serverToolsMap = /* @__PURE__ */ new Map();
@@ -1531,9 +1760,6 @@ var DescribeToolsTool = class DescribeToolsTool {
1531
1760
  }));
1532
1761
  /**
1533
1762
  * 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
1763
  */
1538
1764
  const formatToolName = (toolName, serverName) => {
1539
1765
  return (toolToServers.get(toolName) || []).length > 1 ? `${serverName}__${toolName}` : toolName;
@@ -1553,31 +1779,56 @@ var DescribeToolsTool = class DescribeToolsTool {
1553
1779
  toolNames: formattedTools.map((t) => t.displayName)
1554
1780
  };
1555
1781
  });
1782
+ const rawSkills = this.skillService ? await this.skillService.getSkills() : [];
1783
+ const promptSkills = await this.collectPromptSkills();
1784
+ const seenSkillNames = /* @__PURE__ */ new Set();
1785
+ const allSkillsData = [];
1786
+ for (const skill of rawSkills) if (!seenSkillNames.has(skill.name)) {
1787
+ seenSkillNames.add(skill.name);
1788
+ allSkillsData.push({
1789
+ name: skill.name,
1790
+ displayName: skill.name,
1791
+ description: skill.description
1792
+ });
1793
+ }
1794
+ for (const skill of promptSkills) if (!seenSkillNames.has(skill.name)) {
1795
+ seenSkillNames.add(skill.name);
1796
+ allSkillsData.push(skill);
1797
+ }
1798
+ const skills = allSkillsData.map((skill) => {
1799
+ const clashesWithMcpTool = allToolNames.has(skill.name);
1800
+ return {
1801
+ name: skill.name,
1802
+ displayName: clashesWithMcpTool ? `${SKILL_PREFIX$1}${skill.name}` : skill.name,
1803
+ description: skill.description
1804
+ };
1805
+ });
1556
1806
  return {
1557
- content: await this.liquid.parseAndRender(mcp_servers_description_default, { servers }),
1807
+ content: await this.liquid.parseAndRender(toolkit_description_default, {
1808
+ servers,
1809
+ skills
1810
+ }),
1558
1811
  toolNames: allToolNames
1559
1812
  };
1560
1813
  }
1561
1814
  /**
1562
- * Gets the tool definition including available servers, tools, and skills.
1815
+ * Gets the tool definition including available tools and skills in a unified format.
1563
1816
  *
1564
1817
  * The definition includes:
1565
- * - List of all connected MCP servers with their available tools
1566
- * - List of available skills with skill__ prefix
1567
- * - Usage instructions for querying tool/skill details
1818
+ * - All MCP tools from connected servers
1819
+ * - All available skills (file-based and prompt-based)
1820
+ * - Unified instructions for querying capability details
1568
1821
  *
1569
- * Tool names are prefixed with serverName__ when the same tool name
1570
- * exists on multiple servers to avoid ambiguity.
1822
+ * Tool names are prefixed with serverName__ when clashing.
1823
+ * Skill names are prefixed with skill__ when clashing.
1571
1824
  *
1572
1825
  * @returns Tool definition with description and input schema
1573
1826
  */
1574
1827
  async getDefinition() {
1575
- const serversResult = await this.buildServersSection();
1576
- const skillsSection = await this.buildSkillsSection(serversResult.toolNames);
1828
+ const { content } = await this.buildToolkitDescription();
1577
1829
  return {
1578
1830
  name: DescribeToolsTool.TOOL_NAME,
1579
- description: `${serversResult.content}
1580
- ${skillsSection}`,
1831
+ description: content,
1581
1832
  inputSchema: {
1582
1833
  type: "object",
1583
1834
  properties: { toolNames: {
@@ -1642,14 +1893,14 @@ ${skillsSection}`,
1642
1893
  const notFoundItems = [];
1643
1894
  for (const requestedName of toolNames) {
1644
1895
  if (requestedName.startsWith(SKILL_PREFIX$1)) {
1645
- const skillName = requestedName.slice(7);
1896
+ const skillName = requestedName.slice(SKILL_PREFIX$1.length);
1646
1897
  if (this.skillService) {
1647
1898
  const skill = await this.skillService.getSkill(skillName);
1648
1899
  if (skill) {
1649
1900
  foundSkills.push({
1650
1901
  name: skill.name,
1651
1902
  location: skill.basePath,
1652
- instructions: skill.content
1903
+ instructions: formatSkillInstructions(skill.name, skill.content)
1653
1904
  });
1654
1905
  continue;
1655
1906
  }
@@ -1688,7 +1939,7 @@ ${skillsSection}`,
1688
1939
  foundSkills.push({
1689
1940
  name: skill.name,
1690
1941
  location: skill.basePath,
1691
- instructions: skill.content
1942
+ instructions: formatSkillInstructions(skill.name, skill.content)
1692
1943
  });
1693
1944
  continue;
1694
1945
  }
@@ -2060,9 +2311,16 @@ async function createServer(options) {
2060
2311
  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
2312
  }
2062
2313
  const skillsConfig = options?.skills || configSkills;
2063
- const skillService = skillsConfig && skillsConfig.paths.length > 0 ? new SkillService(process.cwd(), skillsConfig.paths) : void 0;
2314
+ const toolsRef = { describeTools: null };
2315
+ const skillService = skillsConfig && skillsConfig.paths.length > 0 ? new SkillService(process.cwd(), skillsConfig.paths, { onCacheInvalidated: () => {
2316
+ toolsRef.describeTools?.clearAutoDetectedSkillsCache();
2317
+ } }) : void 0;
2064
2318
  const describeTools = new DescribeToolsTool(clientManager, skillService);
2065
2319
  const useTool = new UseToolTool(clientManager, skillService);
2320
+ toolsRef.describeTools = describeTools;
2321
+ if (skillService) skillService.startWatching().catch((error) => {
2322
+ console.error(`[skill-watcher] File watcher failed (non-critical): ${error instanceof Error ? error.message : "Unknown error"}`);
2323
+ });
2066
2324
  server.setRequestHandler(__modelcontextprotocol_sdk_types_js.ListToolsRequestSchema, async () => ({ tools: [await describeTools.getDefinition(), useTool.getDefinition()] }));
2067
2325
  server.setRequestHandler(__modelcontextprotocol_sdk_types_js.CallToolRequestSchema, async (request) => {
2068
2326
  const { name, arguments: args } = request.params;