@agiflowai/one-mcp 0.2.6 → 0.2.8

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