@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.
@@ -1,6 +1,6 @@
1
1
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
2
  import { CallToolRequestSchema, GetPromptRequestSchema, ListPromptsRequestSchema, ListToolsRequestSchema, isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
3
- import { access, mkdir, readFile, readdir, stat, unlink, writeFile } from "node:fs/promises";
3
+ import { access, mkdir, readFile, readdir, stat, unlink, watch, writeFile } from "node:fs/promises";
4
4
  import { existsSync } from "node:fs";
5
5
  import yaml from "js-yaml";
6
6
  import { z } from "zod";
@@ -10,6 +10,7 @@ import { tmpdir } from "node:os";
10
10
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
11
11
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
12
12
  import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
13
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
13
14
  import { Liquid } from "liquidjs";
14
15
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
15
16
  import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
@@ -944,6 +945,7 @@ var McpClientManagerService = class {
944
945
  */
945
946
  async performConnection(mcpClient, config) {
946
947
  if (config.transport === "stdio") await this.connectStdioClient(mcpClient, config.config);
948
+ else if (config.transport === "http") await this.connectHttpClient(mcpClient, config.config);
947
949
  else if (config.transport === "sse") await this.connectSseClient(mcpClient, config.config);
948
950
  else throw new Error(`Unsupported transport type: ${config.transport}`);
949
951
  }
@@ -957,6 +959,10 @@ var McpClientManagerService = class {
957
959
  const childProcess = transport["_process"];
958
960
  if (childProcess) mcpClient.setChildProcess(childProcess);
959
961
  }
962
+ async connectHttpClient(mcpClient, config) {
963
+ const transport = new StreamableHTTPClientTransport(new URL(config.url), { requestInit: config.headers ? { headers: config.headers } : void 0 });
964
+ await mcpClient["client"].connect(transport);
965
+ }
960
966
  async connectSseClient(mcpClient, config) {
961
967
  const transport = new SSEClientTransport(new URL(config.url));
962
968
  await mcpClient["client"].connect(transport);
@@ -999,6 +1005,209 @@ var McpClientManagerService = class {
999
1005
  }
1000
1006
  };
1001
1007
 
1008
+ //#endregion
1009
+ //#region src/utils/findConfigFile.ts
1010
+ /**
1011
+ * Config File Finder Utility
1012
+ *
1013
+ * DESIGN PATTERNS:
1014
+ * - Utility function pattern for reusable logic
1015
+ * - Fail-fast pattern with early returns
1016
+ * - Environment variable configuration pattern
1017
+ *
1018
+ * CODING STANDARDS:
1019
+ * - Use sync filesystem operations for config discovery (performance)
1020
+ * - Check PROJECT_PATH environment variable first
1021
+ * - Fall back to current working directory
1022
+ * - Support both .yaml and .json extensions
1023
+ * - Return null if no config file is found
1024
+ *
1025
+ * AVOID:
1026
+ * - Throwing errors (return null instead for optional config)
1027
+ * - Hardcoded file names without extension variants
1028
+ * - Ignoring environment variables
1029
+ */
1030
+ /**
1031
+ * Find MCP configuration file by checking PROJECT_PATH first, then cwd
1032
+ * Looks for both mcp-config.yaml and mcp-config.json
1033
+ *
1034
+ * @returns Absolute path to config file, or null if not found
1035
+ */
1036
+ function findConfigFile() {
1037
+ const configFileNames = [
1038
+ "mcp-config.yaml",
1039
+ "mcp-config.yml",
1040
+ "mcp-config.json"
1041
+ ];
1042
+ const projectPath = process.env.PROJECT_PATH;
1043
+ if (projectPath) for (const fileName of configFileNames) {
1044
+ const configPath = resolve(projectPath, fileName);
1045
+ if (existsSync(configPath)) return configPath;
1046
+ }
1047
+ const cwd = process.cwd();
1048
+ for (const fileName of configFileNames) {
1049
+ const configPath = join(cwd, fileName);
1050
+ if (existsSync(configPath)) return configPath;
1051
+ }
1052
+ return null;
1053
+ }
1054
+
1055
+ //#endregion
1056
+ //#region src/utils/parseToolName.ts
1057
+ /**
1058
+ * Parse tool name to extract server and actual tool name
1059
+ * Supports both plain tool names and prefixed format: {serverName}__{toolName}
1060
+ *
1061
+ * @param toolName - The tool name to parse (e.g., "my_tool" or "server__my_tool")
1062
+ * @returns Parsed result with optional serverName and actualToolName
1063
+ *
1064
+ * @example
1065
+ * parseToolName("my_tool") // { actualToolName: "my_tool" }
1066
+ * parseToolName("server__my_tool") // { serverName: "server", actualToolName: "my_tool" }
1067
+ */
1068
+ function parseToolName(toolName) {
1069
+ const separatorIndex = toolName.indexOf("__");
1070
+ if (separatorIndex > 0) return {
1071
+ serverName: toolName.substring(0, separatorIndex),
1072
+ actualToolName: toolName.substring(separatorIndex + 2)
1073
+ };
1074
+ return { actualToolName: toolName };
1075
+ }
1076
+
1077
+ //#endregion
1078
+ //#region src/utils/parseFrontMatter.ts
1079
+ /**
1080
+ * Parses YAML front matter from a string content.
1081
+ * Front matter must be at the start of the content, delimited by `---`.
1082
+ *
1083
+ * Supports:
1084
+ * - Simple key: value pairs
1085
+ * - Literal block scalar (|) for multi-line preserving newlines
1086
+ * - Folded block scalar (>) for multi-line folding to single line
1087
+ *
1088
+ * @param content - The content string that may contain front matter
1089
+ * @returns Object with parsed front matter (or null) and remaining content
1090
+ *
1091
+ * @example
1092
+ * const result = parseFrontMatter(`---
1093
+ * name: my-skill
1094
+ * description: A skill description
1095
+ * ---
1096
+ * The actual content here`);
1097
+ * // result.frontMatter = { name: 'my-skill', description: 'A skill description' }
1098
+ * // result.content = 'The actual content here'
1099
+ *
1100
+ * @example
1101
+ * // Multi-line with literal block scalar
1102
+ * const result = parseFrontMatter(`---
1103
+ * name: my-skill
1104
+ * description: |
1105
+ * Line 1
1106
+ * Line 2
1107
+ * ---
1108
+ * Content`);
1109
+ * // result.frontMatter.description = 'Line 1\nLine 2'
1110
+ */
1111
+ function parseFrontMatter(content) {
1112
+ const trimmedContent = content.trimStart();
1113
+ if (!trimmedContent.startsWith("---")) return {
1114
+ frontMatter: null,
1115
+ content
1116
+ };
1117
+ const endDelimiterIndex = trimmedContent.indexOf("\n---", 3);
1118
+ if (endDelimiterIndex === -1) return {
1119
+ frontMatter: null,
1120
+ content
1121
+ };
1122
+ const yamlContent = trimmedContent.slice(4, endDelimiterIndex).trim();
1123
+ if (!yamlContent) return {
1124
+ frontMatter: null,
1125
+ content
1126
+ };
1127
+ const frontMatter = {};
1128
+ const lines = yamlContent.split("\n");
1129
+ let currentKey = null;
1130
+ let currentValue = [];
1131
+ let multiLineMode = null;
1132
+ let baseIndent = 0;
1133
+ const saveCurrentKey = () => {
1134
+ if (currentKey && currentValue.length > 0) if (multiLineMode === "literal") frontMatter[currentKey] = currentValue.join("\n").trimEnd();
1135
+ else if (multiLineMode === "folded") frontMatter[currentKey] = currentValue.join(" ").trim();
1136
+ else frontMatter[currentKey] = currentValue.join("").trim();
1137
+ currentKey = null;
1138
+ currentValue = [];
1139
+ multiLineMode = null;
1140
+ baseIndent = 0;
1141
+ };
1142
+ for (let i = 0; i < lines.length; i++) {
1143
+ const line = lines[i];
1144
+ const trimmedLine = line.trim();
1145
+ const colonIndex = line.indexOf(":");
1146
+ if (colonIndex !== -1 && !line.startsWith(" ") && !line.startsWith(" ")) {
1147
+ saveCurrentKey();
1148
+ const key = line.slice(0, colonIndex).trim();
1149
+ let value = line.slice(colonIndex + 1).trim();
1150
+ if (value === "|" || value === "|-") {
1151
+ currentKey = key;
1152
+ multiLineMode = "literal";
1153
+ if (i + 1 < lines.length) {
1154
+ const match = lines[i + 1].match(/^(\s+)/);
1155
+ baseIndent = match ? match[1].length : 2;
1156
+ }
1157
+ } else if (value === ">" || value === ">-") {
1158
+ currentKey = key;
1159
+ multiLineMode = "folded";
1160
+ if (i + 1 < lines.length) {
1161
+ const match = lines[i + 1].match(/^(\s+)/);
1162
+ baseIndent = match ? match[1].length : 2;
1163
+ }
1164
+ } else {
1165
+ if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
1166
+ if (key && value) frontMatter[key] = value;
1167
+ }
1168
+ } else if (multiLineMode && currentKey) {
1169
+ const lineIndent = line.match(/^(\s*)/)?.[1].length || 0;
1170
+ if (trimmedLine === "") currentValue.push("");
1171
+ else if (lineIndent >= baseIndent) {
1172
+ const unindentedLine = line.slice(baseIndent);
1173
+ currentValue.push(unindentedLine);
1174
+ } else saveCurrentKey();
1175
+ }
1176
+ }
1177
+ saveCurrentKey();
1178
+ return {
1179
+ frontMatter,
1180
+ content: trimmedContent.slice(endDelimiterIndex + 4).trimStart()
1181
+ };
1182
+ }
1183
+ /**
1184
+ * Checks if parsed front matter contains valid skill metadata.
1185
+ * A valid skill front matter must have both `name` and `description` fields.
1186
+ *
1187
+ * @param frontMatter - The parsed front matter object
1188
+ * @returns True if front matter contains valid skill metadata
1189
+ */
1190
+ function isValidSkillFrontMatter(frontMatter) {
1191
+ return frontMatter !== null && typeof frontMatter.name === "string" && frontMatter.name.length > 0 && typeof frontMatter.description === "string" && frontMatter.description.length > 0;
1192
+ }
1193
+ /**
1194
+ * Extracts skill front matter from content if present and valid.
1195
+ *
1196
+ * @param content - The content string that may contain skill front matter
1197
+ * @returns Object with skill metadata and content, or null if no valid skill front matter
1198
+ */
1199
+ function extractSkillFrontMatter(content) {
1200
+ const { frontMatter, content: remainingContent } = parseFrontMatter(content);
1201
+ if (frontMatter && isValidSkillFrontMatter(frontMatter)) return {
1202
+ skill: {
1203
+ name: frontMatter.name,
1204
+ description: frontMatter.description
1205
+ },
1206
+ content: remainingContent
1207
+ };
1208
+ return null;
1209
+ }
1210
+
1002
1211
  //#endregion
1003
1212
  //#region src/services/SkillService.ts
1004
1213
  /**
@@ -1069,14 +1278,21 @@ var SkillService = class {
1069
1278
  skillPaths;
1070
1279
  cachedSkills = null;
1071
1280
  skillsByName = null;
1281
+ /** Active file watchers for skill directories */
1282
+ watchers = [];
1283
+ /** Callback invoked when cache is invalidated due to file changes */
1284
+ onCacheInvalidated;
1072
1285
  /**
1073
1286
  * Creates a new SkillService instance
1074
1287
  * @param cwd - Current working directory for resolving relative paths
1075
1288
  * @param skillPaths - Array of paths to skills directories
1289
+ * @param options - Optional configuration
1290
+ * @param options.onCacheInvalidated - Callback invoked when cache is invalidated due to file changes
1076
1291
  */
1077
- constructor(cwd, skillPaths) {
1292
+ constructor(cwd, skillPaths, options) {
1078
1293
  this.cwd = cwd;
1079
1294
  this.skillPaths = skillPaths;
1295
+ this.onCacheInvalidated = options?.onCacheInvalidated;
1080
1296
  }
1081
1297
  /**
1082
1298
  * Get all available skills from configured directories.
@@ -1122,6 +1338,54 @@ var SkillService = class {
1122
1338
  this.skillsByName = null;
1123
1339
  }
1124
1340
  /**
1341
+ * Starts watching skill directories for changes to SKILL.md files.
1342
+ * When changes are detected, the cache is automatically invalidated.
1343
+ *
1344
+ * Uses Node.js fs.watch with recursive option for efficient directory monitoring.
1345
+ * Only invalidates cache when SKILL.md files are modified.
1346
+ *
1347
+ * @example
1348
+ * const skillService = new SkillService(cwd, skillPaths, {
1349
+ * onCacheInvalidated: () => console.log('Skills cache invalidated')
1350
+ * });
1351
+ * await skillService.startWatching();
1352
+ */
1353
+ async startWatching() {
1354
+ this.stopWatching();
1355
+ for (const skillPath of this.skillPaths) {
1356
+ const skillsDir = isAbsolute(skillPath) ? skillPath : join(this.cwd, skillPath);
1357
+ if (!await pathExists(skillsDir)) continue;
1358
+ const abortController = new AbortController();
1359
+ this.watchers.push(abortController);
1360
+ this.watchDirectory(skillsDir, abortController.signal).catch((error) => {
1361
+ if (error?.name !== "AbortError") console.error(`[skill-watcher] Error watching ${skillsDir}: ${error instanceof Error ? error.message : "Unknown error"}`);
1362
+ });
1363
+ }
1364
+ }
1365
+ /**
1366
+ * Stops all active file watchers.
1367
+ * Should be called when the service is being disposed.
1368
+ */
1369
+ stopWatching() {
1370
+ for (const controller of this.watchers) controller.abort();
1371
+ this.watchers = [];
1372
+ }
1373
+ /**
1374
+ * Watches a directory for changes to SKILL.md files.
1375
+ * @param dirPath - Directory path to watch
1376
+ * @param signal - AbortSignal to stop watching
1377
+ */
1378
+ async watchDirectory(dirPath, signal) {
1379
+ const watcher = watch(dirPath, {
1380
+ recursive: true,
1381
+ signal
1382
+ });
1383
+ for await (const event of watcher) if (event.filename && event.filename.endsWith("SKILL.md")) {
1384
+ this.clearCache();
1385
+ this.onCacheInvalidated?.();
1386
+ }
1387
+ }
1388
+ /**
1125
1389
  * Load skills from a directory.
1126
1390
  * Supports both flat structure (SKILL.md) and nested structure (name/SKILL.md).
1127
1391
  *
@@ -1180,6 +1444,7 @@ var SkillService = class {
1180
1444
  }
1181
1445
  /**
1182
1446
  * Load a single skill file and parse its frontmatter.
1447
+ * Supports multi-line YAML values using literal (|) and folded (>) block scalars.
1183
1448
  *
1184
1449
  * @param filePath - Path to the SKILL.md file
1185
1450
  * @param location - Whether this is a 'project' or 'user' skill
@@ -1193,150 +1458,62 @@ var SkillService = class {
1193
1458
  * // Returns null if frontmatter is missing name or description
1194
1459
  */
1195
1460
  async loadSkillFile(filePath, location) {
1196
- let content;
1461
+ let fileContent;
1197
1462
  try {
1198
- content = await readFile(filePath, "utf-8");
1463
+ fileContent = await readFile(filePath, "utf-8");
1199
1464
  } catch (error) {
1200
1465
  throw new SkillLoadError(`Failed to read skill file: ${error instanceof Error ? error.message : "Unknown error"}`, filePath, error instanceof Error ? error : void 0);
1201
1466
  }
1202
- const { metadata, body } = this.parseFrontmatter(content);
1203
- if (!metadata.name || !metadata.description) return null;
1467
+ const { frontMatter, content } = parseFrontMatter(fileContent);
1468
+ if (!frontMatter || !frontMatter.name || !frontMatter.description) return null;
1204
1469
  return {
1205
- name: metadata.name,
1206
- description: metadata.description,
1470
+ name: frontMatter.name,
1471
+ description: frontMatter.description,
1207
1472
  location,
1208
- content: body,
1473
+ content,
1209
1474
  basePath: dirname(filePath)
1210
1475
  };
1211
1476
  }
1212
- /**
1213
- * Parse YAML frontmatter from markdown content.
1214
- * Frontmatter is delimited by --- at start and end.
1215
- *
1216
- * @param content - Full markdown content with frontmatter
1217
- * @returns Parsed metadata and body content
1218
- *
1219
- * @example
1220
- * // Input content:
1221
- * // ---
1222
- * // name: my-skill
1223
- * // description: A sample skill
1224
- * // ---
1225
- * // # Skill Content
1226
- * // This is the skill body.
1227
- *
1228
- * const result = parseFrontmatter(content);
1229
- * // result.metadata = { name: 'my-skill', description: 'A sample skill' }
1230
- * // result.body = '# Skill Content\nThis is the skill body.'
1231
- */
1232
- parseFrontmatter(content) {
1233
- const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
1234
- if (!match) return {
1235
- metadata: {},
1236
- body: content
1237
- };
1238
- const [, frontmatter, body] = match;
1239
- const metadata = {};
1240
- const lines = frontmatter.split("\n");
1241
- for (const line of lines) {
1242
- const colonIndex = line.indexOf(":");
1243
- if (colonIndex > 0) {
1244
- const key = line.slice(0, colonIndex).trim();
1245
- let value = line.slice(colonIndex + 1).trim();
1246
- if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
1247
- if (key === "name" || key === "description" || key === "license") metadata[key] = value;
1248
- }
1249
- }
1250
- return {
1251
- metadata,
1252
- body: body.trim()
1253
- };
1254
- }
1255
1477
  };
1256
1478
 
1257
1479
  //#endregion
1258
- //#region src/utils/findConfigFile.ts
1480
+ //#region src/constants/index.ts
1259
1481
  /**
1260
- * Config File Finder Utility
1261
- *
1262
- * DESIGN PATTERNS:
1263
- * - Utility function pattern for reusable logic
1264
- * - Fail-fast pattern with early returns
1265
- * - Environment variable configuration pattern
1266
- *
1267
- * CODING STANDARDS:
1268
- * - Use sync filesystem operations for config discovery (performance)
1269
- * - Check PROJECT_PATH environment variable first
1270
- * - Fall back to current working directory
1271
- * - Support both .yaml and .json extensions
1272
- * - Return null if no config file is found
1273
- *
1274
- * AVOID:
1275
- * - Throwing errors (return null instead for optional config)
1276
- * - Hardcoded file names without extension variants
1277
- * - Ignoring environment variables
1482
+ * Shared constants for one-mcp package
1278
1483
  */
1279
1484
  /**
1280
- * Find MCP configuration file by checking PROJECT_PATH first, then cwd
1281
- * Looks for both mcp-config.yaml and mcp-config.json
1282
- *
1283
- * @returns Absolute path to config file, or null if not found
1485
+ * Prefix added to skill names when they clash with MCP tool names.
1486
+ * This ensures skills can be uniquely identified even when a tool has the same name.
1284
1487
  */
1285
- function findConfigFile() {
1286
- const configFileNames = [
1287
- "mcp-config.yaml",
1288
- "mcp-config.yml",
1289
- "mcp-config.json"
1290
- ];
1291
- const projectPath = process.env.PROJECT_PATH;
1292
- if (projectPath) for (const fileName of configFileNames) {
1293
- const configPath = resolve(projectPath, fileName);
1294
- if (existsSync(configPath)) return configPath;
1295
- }
1296
- const cwd = process.cwd();
1297
- for (const fileName of configFileNames) {
1298
- const configPath = join(cwd, fileName);
1299
- if (existsSync(configPath)) return configPath;
1300
- }
1301
- return null;
1302
- }
1303
-
1304
- //#endregion
1305
- //#region src/utils/parseToolName.ts
1488
+ const SKILL_PREFIX$1 = "skill__";
1306
1489
  /**
1307
- * Parse tool name to extract server and actual tool name
1308
- * Supports both plain tool names and prefixed format: {serverName}__{toolName}
1309
- *
1310
- * @param toolName - The tool name to parse (e.g., "my_tool" or "server__my_tool")
1311
- * @returns Parsed result with optional serverName and actualToolName
1312
- *
1313
- * @example
1314
- * parseToolName("my_tool") // { actualToolName: "my_tool" }
1315
- * parseToolName("server__my_tool") // { serverName: "server", actualToolName: "my_tool" }
1490
+ * Log prefix for skill detection messages.
1491
+ * Used to easily filter skill detection logs in stderr output.
1316
1492
  */
1317
- function parseToolName(toolName) {
1318
- const separatorIndex = toolName.indexOf("__");
1319
- if (separatorIndex > 0) return {
1320
- serverName: toolName.substring(0, separatorIndex),
1321
- actualToolName: toolName.substring(separatorIndex + 2)
1322
- };
1323
- return { actualToolName: toolName };
1324
- }
1325
-
1326
- //#endregion
1327
- //#region src/templates/skills-description.liquid?raw
1328
- 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";
1493
+ const LOG_PREFIX_SKILL_DETECTION = "[skill-detection]";
1494
+ /**
1495
+ * Prefix for prompt-based skill locations.
1496
+ * Format: "prompt:{serverName}:{promptName}"
1497
+ */
1498
+ const PROMPT_LOCATION_PREFIX = "prompt:";
1329
1499
 
1330
1500
  //#endregion
1331
- //#region src/templates/mcp-servers-description.liquid?raw
1332
- 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";
1501
+ //#region src/templates/toolkit-description.liquid?raw
1502
+ 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";
1333
1503
 
1334
1504
  //#endregion
1335
1505
  //#region src/tools/DescribeToolsTool.ts
1336
1506
  /**
1337
- * Prefix used to identify skill invocations
1507
+ * Formats skill instructions with the loading command message prefix.
1508
+ * This message is used by Claude Code to indicate that a skill is being loaded.
1509
+ *
1510
+ * @param name - The skill name
1511
+ * @param instructions - The raw skill instructions/content
1512
+ * @returns Formatted instructions with command message prefix
1338
1513
  */
1339
- const SKILL_PREFIX$1 = "skill__";
1514
+ function formatSkillInstructions(name, instructions) {
1515
+ return `<command-message>The "${name}" skill is loading</command-message>\n${instructions}`;
1516
+ }
1340
1517
  /**
1341
1518
  * DescribeToolsTool provides progressive disclosure of MCP tools and skills.
1342
1519
  *
@@ -1359,6 +1536,8 @@ var DescribeToolsTool = class DescribeToolsTool {
1359
1536
  clientManager;
1360
1537
  skillService;
1361
1538
  liquid = new Liquid();
1539
+ /** Cache for auto-detected skills from prompt front-matter */
1540
+ autoDetectedSkillsCache = null;
1362
1541
  /**
1363
1542
  * Creates a new DescribeToolsTool instance
1364
1543
  * @param clientManager - The MCP client manager for accessing remote servers
@@ -1369,12 +1548,79 @@ var DescribeToolsTool = class DescribeToolsTool {
1369
1548
  this.skillService = skillService;
1370
1549
  }
1371
1550
  /**
1551
+ * Clears the cached auto-detected skills from prompt front-matter.
1552
+ * Use this when prompt configurations may have changed or when
1553
+ * the skill service cache is invalidated.
1554
+ */
1555
+ clearAutoDetectedSkillsCache() {
1556
+ this.autoDetectedSkillsCache = null;
1557
+ }
1558
+ /**
1559
+ * Detects and caches skills from prompt front-matter across all connected MCP servers.
1560
+ * Fetches all prompts and checks their content for YAML front-matter with name/description.
1561
+ * Results are cached to avoid repeated fetches.
1562
+ *
1563
+ * Error Handling Strategy:
1564
+ * - Errors are logged to stderr but do not fail the overall detection process
1565
+ * - This ensures partial results are returned even if some servers/prompts fail
1566
+ * - Common failure reasons: server temporarily unavailable, prompt requires arguments,
1567
+ * network timeout, or server doesn't support listPrompts
1568
+ * - Errors are prefixed with [skill-detection] for easy filtering in logs
1569
+ *
1570
+ * @returns Array of auto-detected skills from prompt front-matter
1571
+ */
1572
+ async detectSkillsFromPromptFrontMatter() {
1573
+ if (this.autoDetectedSkillsCache !== null) return this.autoDetectedSkillsCache;
1574
+ const clients = this.clientManager.getAllClients();
1575
+ const autoDetectedSkills = [];
1576
+ let listPromptsFailures = 0;
1577
+ let fetchPromptFailures = 0;
1578
+ const fetchPromises = [];
1579
+ for (const client of clients) {
1580
+ const configuredPromptNames = new Set(client.prompts ? Object.keys(client.prompts) : []);
1581
+ const listPromptsPromise = (async () => {
1582
+ try {
1583
+ const prompts = await client.listPrompts();
1584
+ if (!prompts || prompts.length === 0) return;
1585
+ const promptFetchPromises = prompts.map(async (promptInfo) => {
1586
+ if (configuredPromptNames.has(promptInfo.name)) return;
1587
+ try {
1588
+ const skillExtraction = extractSkillFrontMatter(((await client.getPrompt(promptInfo.name)).messages || []).map((m) => {
1589
+ const content = m.content;
1590
+ if (typeof content === "string") return content;
1591
+ if (content && typeof content === "object" && "text" in content) return String(content.text);
1592
+ return "";
1593
+ }).join("\n"));
1594
+ if (skillExtraction) autoDetectedSkills.push({
1595
+ serverName: client.serverName,
1596
+ promptName: promptInfo.name,
1597
+ skill: skillExtraction.skill
1598
+ });
1599
+ } catch (error) {
1600
+ fetchPromptFailures++;
1601
+ console.error(`${LOG_PREFIX_SKILL_DETECTION} Failed to fetch prompt '${promptInfo.name}' from ${client.serverName}: ${error instanceof Error ? error.message : "Unknown error"}`);
1602
+ }
1603
+ });
1604
+ await Promise.all(promptFetchPromises);
1605
+ } catch (error) {
1606
+ listPromptsFailures++;
1607
+ console.error(`${LOG_PREFIX_SKILL_DETECTION} Failed to list prompts from ${client.serverName}: ${error instanceof Error ? error.message : "Unknown error"}`);
1608
+ }
1609
+ })();
1610
+ fetchPromises.push(listPromptsPromise);
1611
+ }
1612
+ await Promise.all(fetchPromises);
1613
+ 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).`);
1614
+ this.autoDetectedSkillsCache = autoDetectedSkills;
1615
+ return autoDetectedSkills;
1616
+ }
1617
+ /**
1372
1618
  * Collects skills derived from prompt configurations across all connected MCP servers.
1373
- * Prompts with a skill configuration are converted to skill format for display.
1619
+ * Includes both explicitly configured prompts and auto-detected skills from front-matter.
1374
1620
  *
1375
1621
  * @returns Array of skill template data derived from prompts
1376
1622
  */
1377
- collectPromptSkills() {
1623
+ async collectPromptSkills() {
1378
1624
  const clients = this.clientManager.getAllClients();
1379
1625
  const promptSkills = [];
1380
1626
  for (const client of clients) {
@@ -1385,16 +1631,22 @@ var DescribeToolsTool = class DescribeToolsTool {
1385
1631
  description: promptConfig.skill.description
1386
1632
  });
1387
1633
  }
1634
+ const autoDetectedSkills = await this.detectSkillsFromPromptFrontMatter();
1635
+ for (const autoSkill of autoDetectedSkills) promptSkills.push({
1636
+ name: autoSkill.skill.name,
1637
+ displayName: autoSkill.skill.name,
1638
+ description: autoSkill.skill.description
1639
+ });
1388
1640
  return promptSkills;
1389
1641
  }
1390
1642
  /**
1391
1643
  * Finds a prompt-based skill by name from all connected MCP servers.
1392
- * Returns the prompt name and skill config for fetching the prompt content.
1644
+ * Searches both explicitly configured prompts and auto-detected skills from front-matter.
1393
1645
  *
1394
1646
  * @param skillName - The skill name to search for
1395
1647
  * @returns Object with serverName, promptName, and skill config, or undefined if not found
1396
1648
  */
1397
- findPromptSkill(skillName) {
1649
+ async findPromptSkill(skillName) {
1398
1650
  if (!skillName) return void 0;
1399
1651
  const clients = this.clientManager.getAllClients();
1400
1652
  for (const client of clients) {
@@ -1405,16 +1657,24 @@ var DescribeToolsTool = class DescribeToolsTool {
1405
1657
  skill: promptConfig.skill
1406
1658
  };
1407
1659
  }
1660
+ const autoDetectedSkills = await this.detectSkillsFromPromptFrontMatter();
1661
+ for (const autoSkill of autoDetectedSkills) if (autoSkill.skill.name === skillName) return {
1662
+ serverName: autoSkill.serverName,
1663
+ promptName: autoSkill.promptName,
1664
+ skill: autoSkill.skill,
1665
+ autoDetected: true
1666
+ };
1408
1667
  }
1409
1668
  /**
1410
1669
  * Retrieves skill content from a prompt-based skill configuration.
1411
1670
  * Fetches the prompt from the MCP server and extracts text content.
1671
+ * Handles both explicitly configured prompts and auto-detected skills from front-matter.
1412
1672
  *
1413
1673
  * @param skillName - The skill name being requested
1414
1674
  * @returns SkillDescription if found and successfully fetched, undefined otherwise
1415
1675
  */
1416
1676
  async getPromptSkillContent(skillName) {
1417
- const promptSkill = this.findPromptSkill(skillName);
1677
+ const promptSkill = await this.findPromptSkill(skillName);
1418
1678
  if (!promptSkill) return void 0;
1419
1679
  const client = this.clientManager.getClient(promptSkill.serverName);
1420
1680
  if (!client) {
@@ -1422,7 +1682,7 @@ var DescribeToolsTool = class DescribeToolsTool {
1422
1682
  return;
1423
1683
  }
1424
1684
  try {
1425
- const instructions = (await client.getPrompt(promptSkill.promptName)).messages?.map((m) => {
1685
+ const rawInstructions = (await client.getPrompt(promptSkill.promptName)).messages?.map((m) => {
1426
1686
  const content = m.content;
1427
1687
  if (typeof content === "string") return content;
1428
1688
  if (content && typeof content === "object" && "text" in content) return String(content.text);
@@ -1430,8 +1690,8 @@ var DescribeToolsTool = class DescribeToolsTool {
1430
1690
  }).join("\n") || "";
1431
1691
  return {
1432
1692
  name: promptSkill.skill.name,
1433
- location: promptSkill.skill.folder || `prompt:${promptSkill.serverName}/${promptSkill.promptName}`,
1434
- instructions
1693
+ location: promptSkill.skill.folder || `${PROMPT_LOCATION_PREFIX}${promptSkill.serverName}/${promptSkill.promptName}`,
1694
+ instructions: formatSkillInstructions(promptSkill.skill.name, rawInstructions)
1435
1695
  };
1436
1696
  } catch (error) {
1437
1697
  console.error(`Failed to get prompt-based skill '${skillName}': ${error instanceof Error ? error.message : "Unknown error"}`);
@@ -1439,49 +1699,18 @@ var DescribeToolsTool = class DescribeToolsTool {
1439
1699
  }
1440
1700
  }
1441
1701
  /**
1442
- * Builds the skills section of the tool description using a Liquid template.
1702
+ * Builds the combined toolkit description using a single Liquid template.
1443
1703
  *
1444
- * Retrieves all available skills from the SkillService and prompt-based skills,
1445
- * then renders them using the skills-description.liquid template. Skills are only
1446
- * prefixed with skill__ when their name clashes with an MCP tool or another skill.
1447
- *
1448
- * @param mcpToolNames - Set of MCP tool names to check for clashes
1449
- * @returns Rendered skills section string with available skills list
1450
- */
1451
- async buildSkillsSection(mcpToolNames) {
1452
- const rawSkills = this.skillService ? await this.skillService.getSkills() : [];
1453
- const promptSkills = this.collectPromptSkills();
1454
- const allSkillsData = [...rawSkills.map((skill) => ({
1455
- name: skill.name,
1456
- displayName: skill.name,
1457
- description: skill.description
1458
- })), ...promptSkills];
1459
- const skillNameCounts = /* @__PURE__ */ new Map();
1460
- for (const skill of allSkillsData) skillNameCounts.set(skill.name, (skillNameCounts.get(skill.name) || 0) + 1);
1461
- const skills = allSkillsData.map((skill) => {
1462
- const clashesWithMcpTool = mcpToolNames.has(skill.name);
1463
- const clashesWithOtherSkill = (skillNameCounts.get(skill.name) || 0) > 1;
1464
- const needsPrefix = clashesWithMcpTool || clashesWithOtherSkill;
1465
- return {
1466
- name: skill.name,
1467
- displayName: needsPrefix ? `${SKILL_PREFIX$1}${skill.name}` : skill.name,
1468
- description: skill.description
1469
- };
1470
- });
1471
- return this.liquid.parseAndRender(skills_description_default, { skills });
1472
- }
1473
- /**
1474
- * Builds the MCP servers section of the tool description using a Liquid template.
1475
- *
1476
- * Collects all tools from connected MCP servers, detects name clashes,
1477
- * and renders them using the mcp-servers-description.liquid template.
1704
+ * Collects all tools from connected MCP servers and all skills, then renders
1705
+ * them together using the toolkit-description.liquid template.
1478
1706
  *
1479
1707
  * Tool names are prefixed with serverName__ when the same tool exists
1480
- * on multiple servers to avoid ambiguity.
1708
+ * on multiple servers. Skill names are prefixed with skill__ when they
1709
+ * clash with MCP tools or other skills.
1481
1710
  *
1482
- * @returns Object with rendered servers section and set of all tool names for skill clash detection
1711
+ * @returns Object with rendered description and set of all tool names
1483
1712
  */
1484
- async buildServersSection() {
1713
+ async buildToolkitDescription() {
1485
1714
  const clients = this.clientManager.getAllClients();
1486
1715
  const toolToServers = /* @__PURE__ */ new Map();
1487
1716
  const serverToolsMap = /* @__PURE__ */ new Map();
@@ -1502,9 +1731,6 @@ var DescribeToolsTool = class DescribeToolsTool {
1502
1731
  }));
1503
1732
  /**
1504
1733
  * Formats tool name with server prefix if the tool exists on multiple servers
1505
- * @param toolName - The original tool name
1506
- * @param serverName - The server providing this tool
1507
- * @returns Tool name prefixed with serverName__ if clashing, otherwise plain name
1508
1734
  */
1509
1735
  const formatToolName = (toolName, serverName) => {
1510
1736
  return (toolToServers.get(toolName) || []).length > 1 ? `${serverName}__${toolName}` : toolName;
@@ -1524,31 +1750,56 @@ var DescribeToolsTool = class DescribeToolsTool {
1524
1750
  toolNames: formattedTools.map((t) => t.displayName)
1525
1751
  };
1526
1752
  });
1753
+ const rawSkills = this.skillService ? await this.skillService.getSkills() : [];
1754
+ const promptSkills = await this.collectPromptSkills();
1755
+ const seenSkillNames = /* @__PURE__ */ new Set();
1756
+ const allSkillsData = [];
1757
+ for (const skill of rawSkills) if (!seenSkillNames.has(skill.name)) {
1758
+ seenSkillNames.add(skill.name);
1759
+ allSkillsData.push({
1760
+ name: skill.name,
1761
+ displayName: skill.name,
1762
+ description: skill.description
1763
+ });
1764
+ }
1765
+ for (const skill of promptSkills) if (!seenSkillNames.has(skill.name)) {
1766
+ seenSkillNames.add(skill.name);
1767
+ allSkillsData.push(skill);
1768
+ }
1769
+ const skills = allSkillsData.map((skill) => {
1770
+ const clashesWithMcpTool = allToolNames.has(skill.name);
1771
+ return {
1772
+ name: skill.name,
1773
+ displayName: clashesWithMcpTool ? `${SKILL_PREFIX$1}${skill.name}` : skill.name,
1774
+ description: skill.description
1775
+ };
1776
+ });
1527
1777
  return {
1528
- content: await this.liquid.parseAndRender(mcp_servers_description_default, { servers }),
1778
+ content: await this.liquid.parseAndRender(toolkit_description_default, {
1779
+ servers,
1780
+ skills
1781
+ }),
1529
1782
  toolNames: allToolNames
1530
1783
  };
1531
1784
  }
1532
1785
  /**
1533
- * Gets the tool definition including available servers, tools, and skills.
1786
+ * Gets the tool definition including available tools and skills in a unified format.
1534
1787
  *
1535
1788
  * The definition includes:
1536
- * - List of all connected MCP servers with their available tools
1537
- * - List of available skills with skill__ prefix
1538
- * - Usage instructions for querying tool/skill details
1789
+ * - All MCP tools from connected servers
1790
+ * - All available skills (file-based and prompt-based)
1791
+ * - Unified instructions for querying capability details
1539
1792
  *
1540
- * Tool names are prefixed with serverName__ when the same tool name
1541
- * exists on multiple servers to avoid ambiguity.
1793
+ * Tool names are prefixed with serverName__ when clashing.
1794
+ * Skill names are prefixed with skill__ when clashing.
1542
1795
  *
1543
1796
  * @returns Tool definition with description and input schema
1544
1797
  */
1545
1798
  async getDefinition() {
1546
- const serversResult = await this.buildServersSection();
1547
- const skillsSection = await this.buildSkillsSection(serversResult.toolNames);
1799
+ const { content } = await this.buildToolkitDescription();
1548
1800
  return {
1549
1801
  name: DescribeToolsTool.TOOL_NAME,
1550
- description: `${serversResult.content}
1551
- ${skillsSection}`,
1802
+ description: content,
1552
1803
  inputSchema: {
1553
1804
  type: "object",
1554
1805
  properties: { toolNames: {
@@ -1613,14 +1864,14 @@ ${skillsSection}`,
1613
1864
  const notFoundItems = [];
1614
1865
  for (const requestedName of toolNames) {
1615
1866
  if (requestedName.startsWith(SKILL_PREFIX$1)) {
1616
- const skillName = requestedName.slice(7);
1867
+ const skillName = requestedName.slice(SKILL_PREFIX$1.length);
1617
1868
  if (this.skillService) {
1618
1869
  const skill = await this.skillService.getSkill(skillName);
1619
1870
  if (skill) {
1620
1871
  foundSkills.push({
1621
1872
  name: skill.name,
1622
1873
  location: skill.basePath,
1623
- instructions: skill.content
1874
+ instructions: formatSkillInstructions(skill.name, skill.content)
1624
1875
  });
1625
1876
  continue;
1626
1877
  }
@@ -1659,7 +1910,7 @@ ${skillsSection}`,
1659
1910
  foundSkills.push({
1660
1911
  name: skill.name,
1661
1912
  location: skill.basePath,
1662
- instructions: skill.content
1913
+ instructions: formatSkillInstructions(skill.name, skill.content)
1663
1914
  });
1664
1915
  continue;
1665
1916
  }
@@ -2031,9 +2282,16 @@ async function createServer(options) {
2031
2282
  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(", ")}`);
2032
2283
  }
2033
2284
  const skillsConfig = options?.skills || configSkills;
2034
- const skillService = skillsConfig && skillsConfig.paths.length > 0 ? new SkillService(process.cwd(), skillsConfig.paths) : void 0;
2285
+ const toolsRef = { describeTools: null };
2286
+ const skillService = skillsConfig && skillsConfig.paths.length > 0 ? new SkillService(process.cwd(), skillsConfig.paths, { onCacheInvalidated: () => {
2287
+ toolsRef.describeTools?.clearAutoDetectedSkillsCache();
2288
+ } }) : void 0;
2035
2289
  const describeTools = new DescribeToolsTool(clientManager, skillService);
2036
2290
  const useTool = new UseToolTool(clientManager, skillService);
2291
+ toolsRef.describeTools = describeTools;
2292
+ if (skillService) skillService.startWatching().catch((error) => {
2293
+ console.error(`[skill-watcher] File watcher failed (non-critical): ${error instanceof Error ? error.message : "Unknown error"}`);
2294
+ });
2037
2295
  server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [await describeTools.getDefinition(), useTool.getDefinition()] }));
2038
2296
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
2039
2297
  const { name, arguments: args } = request.params;
@@ -2452,4 +2710,4 @@ var HttpTransportHandler = class {
2452
2710
  };
2453
2711
 
2454
2712
  //#endregion
2455
- export { findConfigFile as a, ConfigFetcherService as c, createServer as i, SseTransportHandler as n, SkillService as o, StdioTransportHandler as r, McpClientManagerService as s, HttpTransportHandler as t };
2713
+ export { SkillService as a, ConfigFetcherService as c, createServer as i, SseTransportHandler as n, findConfigFile as o, StdioTransportHandler as r, McpClientManagerService as s, HttpTransportHandler as t };