@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.
@@ -1,15 +1,16 @@
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";
7
- import { createHash, randomUUID } from "node:crypto";
7
+ import { createHash, randomBytes, randomUUID } from "node:crypto";
8
8
  import { dirname, isAbsolute, join, resolve } from "node:path";
9
9
  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";
@@ -201,6 +202,7 @@ const ClaudeCodeStdioServerSchema = z.object({
201
202
  env: z.record(z.string(), z.string()).optional(),
202
203
  disabled: z.boolean().optional(),
203
204
  instruction: z.string().optional(),
205
+ timeout: z.number().positive().optional(),
204
206
  config: AdditionalConfigSchema
205
207
  });
206
208
  const ClaudeCodeHttpServerSchema = z.object({
@@ -209,6 +211,7 @@ const ClaudeCodeHttpServerSchema = z.object({
209
211
  type: z.enum(["http", "sse"]).optional(),
210
212
  disabled: z.boolean().optional(),
211
213
  instruction: z.string().optional(),
214
+ timeout: z.number().positive().optional(),
212
215
  config: AdditionalConfigSchema
213
216
  });
214
217
  const ClaudeCodeServerConfigSchema = z.union([ClaudeCodeStdioServerSchema, ClaudeCodeHttpServerSchema]);
@@ -239,6 +242,7 @@ const SkillsConfigSchema = z.object({ paths: z.array(z.string()) });
239
242
  * Full Claude Code MCP configuration schema
240
243
  */
241
244
  const ClaudeCodeMcpConfigSchema = z.object({
245
+ id: z.string().optional(),
242
246
  mcpServers: z.record(z.string(), ClaudeCodeServerConfigSchema),
243
247
  remoteConfigs: z.array(RemoteConfigSourceSchema).optional(),
244
248
  skills: SkillsConfigSchema.optional()
@@ -279,6 +283,7 @@ const McpServerConfigSchema = z.discriminatedUnion("transport", [
279
283
  toolBlacklist: z.array(z.string()).optional(),
280
284
  omitToolDescription: z.boolean().optional(),
281
285
  prompts: z.record(z.string(), InternalPromptConfigSchema).optional(),
286
+ timeout: z.number().positive().optional(),
282
287
  transport: z.literal("stdio"),
283
288
  config: McpStdioConfigSchema
284
289
  }),
@@ -288,6 +293,7 @@ const McpServerConfigSchema = z.discriminatedUnion("transport", [
288
293
  toolBlacklist: z.array(z.string()).optional(),
289
294
  omitToolDescription: z.boolean().optional(),
290
295
  prompts: z.record(z.string(), InternalPromptConfigSchema).optional(),
296
+ timeout: z.number().positive().optional(),
291
297
  transport: z.literal("http"),
292
298
  config: McpHttpConfigSchema
293
299
  }),
@@ -297,6 +303,7 @@ const McpServerConfigSchema = z.discriminatedUnion("transport", [
297
303
  toolBlacklist: z.array(z.string()).optional(),
298
304
  omitToolDescription: z.boolean().optional(),
299
305
  prompts: z.record(z.string(), InternalPromptConfigSchema).optional(),
306
+ timeout: z.number().positive().optional(),
300
307
  transport: z.literal("sse"),
301
308
  config: McpSseConfigSchema
302
309
  })
@@ -305,6 +312,7 @@ const McpServerConfigSchema = z.discriminatedUnion("transport", [
305
312
  * Full internal MCP configuration schema
306
313
  */
307
314
  const InternalMcpConfigSchema = z.object({
315
+ id: z.string().optional(),
308
316
  mcpServers: z.record(z.string(), McpServerConfigSchema),
309
317
  skills: SkillsConfigSchema.optional()
310
318
  });
@@ -330,6 +338,7 @@ function transformClaudeCodeConfig(claudeConfig) {
330
338
  toolBlacklist: stdioConfig.config?.toolBlacklist,
331
339
  omitToolDescription: stdioConfig.config?.omitToolDescription,
332
340
  prompts: stdioConfig.config?.prompts,
341
+ timeout: stdioConfig.timeout,
333
342
  transport: "stdio",
334
343
  config: {
335
344
  command: interpolatedCommand,
@@ -348,6 +357,7 @@ function transformClaudeCodeConfig(claudeConfig) {
348
357
  toolBlacklist: httpConfig.config?.toolBlacklist,
349
358
  omitToolDescription: httpConfig.config?.omitToolDescription,
350
359
  prompts: httpConfig.config?.prompts,
360
+ timeout: httpConfig.timeout,
351
361
  transport,
352
362
  config: {
353
363
  url: interpolatedUrl,
@@ -357,6 +367,7 @@ function transformClaudeCodeConfig(claudeConfig) {
357
367
  }
358
368
  }
359
369
  return {
370
+ id: claudeConfig.id,
360
371
  mcpServers: transformedServers,
361
372
  skills: claudeConfig.skills
362
373
  };
@@ -517,22 +528,21 @@ var RemoteConfigCacheService = class {
517
528
  try {
518
529
  if (!existsSync(this.cacheDir)) return;
519
530
  const now = Date.now();
520
- const files = await readdir(this.cacheDir);
521
- let expiredCount = 0;
522
- for (const file of files) {
523
- if (!file.endsWith(".json")) continue;
531
+ const jsonFiles = (await readdir(this.cacheDir)).filter((file) => file.endsWith(".json"));
532
+ const expiredCount = (await Promise.all(jsonFiles.map(async (file) => {
524
533
  const filePath = join(this.cacheDir, file);
525
534
  try {
526
535
  const content = await readFile(filePath, "utf-8");
527
536
  if (now > JSON.parse(content).expiresAt) {
528
537
  await unlink(filePath);
529
- expiredCount++;
538
+ return true;
530
539
  }
540
+ return false;
531
541
  } catch (error) {
532
542
  await unlink(filePath).catch(() => {});
533
- expiredCount++;
543
+ return true;
534
544
  }
535
- }
545
+ }))).filter(Boolean).length;
536
546
  if (expiredCount > 0) console.error(`Cleaned up ${expiredCount} expired remote config cache entries`);
537
547
  } catch (error) {
538
548
  console.error("Failed to clean expired remote config cache:", error);
@@ -548,14 +558,15 @@ var RemoteConfigCacheService = class {
548
558
  totalSize: 0
549
559
  };
550
560
  const jsonFiles = (await readdir(this.cacheDir)).filter((file) => file.endsWith(".json"));
551
- let totalSize = 0;
552
- for (const file of jsonFiles) {
561
+ const totalSize = (await Promise.all(jsonFiles.map(async (file) => {
553
562
  const filePath = join(this.cacheDir, file);
554
563
  try {
555
564
  const content = await readFile(filePath, "utf-8");
556
- totalSize += Buffer.byteLength(content, "utf-8");
557
- } catch {}
558
- }
565
+ return Buffer.byteLength(content, "utf-8");
566
+ } catch {
567
+ return 0;
568
+ }
569
+ }))).reduce((sum, size) => sum + size, 0);
559
570
  return {
560
571
  totalEntries: jsonFiles.length,
561
572
  totalSize
@@ -806,6 +817,8 @@ var ConfigFetcherService = class {
806
817
 
807
818
  //#endregion
808
819
  //#region src/services/McpClientManagerService.ts
820
+ /** Default connection timeout in milliseconds (30 seconds) */
821
+ const DEFAULT_CONNECTION_TIMEOUT_MS = 3e4;
809
822
  /**
810
823
  * MCP Client wrapper for managing individual server connections
811
824
  * This is an internal class used by McpClientManagerService
@@ -911,8 +924,10 @@ var McpClientManagerService = class {
911
924
  }
912
925
  /**
913
926
  * Connect to an MCP server based on its configuration with timeout
927
+ * Uses the timeout from server config, falling back to default (30s)
914
928
  */
915
- async connectToServer(serverName, config, timeoutMs = 1e4) {
929
+ async connectToServer(serverName, config) {
930
+ const timeoutMs = config.timeout ?? DEFAULT_CONNECTION_TIMEOUT_MS;
916
931
  if (this.clients.has(serverName)) throw new Error(`Client for ${serverName} is already connected`);
917
932
  const client = new Client({
918
933
  name: `@agiflowai/one-mcp-client`,
@@ -944,6 +959,7 @@ var McpClientManagerService = class {
944
959
  */
945
960
  async performConnection(mcpClient, config) {
946
961
  if (config.transport === "stdio") await this.connectStdioClient(mcpClient, config.config);
962
+ else if (config.transport === "http") await this.connectHttpClient(mcpClient, config.config);
947
963
  else if (config.transport === "sse") await this.connectSseClient(mcpClient, config.config);
948
964
  else throw new Error(`Unsupported transport type: ${config.transport}`);
949
965
  }
@@ -957,6 +973,10 @@ var McpClientManagerService = class {
957
973
  const childProcess = transport["_process"];
958
974
  if (childProcess) mcpClient.setChildProcess(childProcess);
959
975
  }
976
+ async connectHttpClient(mcpClient, config) {
977
+ const transport = new StreamableHTTPClientTransport(new URL(config.url), { requestInit: config.headers ? { headers: config.headers } : void 0 });
978
+ await mcpClient["client"].connect(transport);
979
+ }
960
980
  async connectSseClient(mcpClient, config) {
961
981
  const transport = new SSEClientTransport(new URL(config.url));
962
982
  await mcpClient["client"].connect(transport);
@@ -999,6 +1019,274 @@ var McpClientManagerService = class {
999
1019
  }
1000
1020
  };
1001
1021
 
1022
+ //#endregion
1023
+ //#region src/utils/findConfigFile.ts
1024
+ /**
1025
+ * Config File Finder Utility
1026
+ *
1027
+ * DESIGN PATTERNS:
1028
+ * - Utility function pattern for reusable logic
1029
+ * - Fail-fast pattern with early returns
1030
+ * - Environment variable configuration pattern
1031
+ *
1032
+ * CODING STANDARDS:
1033
+ * - Use sync filesystem operations for config discovery (performance)
1034
+ * - Check PROJECT_PATH environment variable first
1035
+ * - Fall back to current working directory
1036
+ * - Support both .yaml and .json extensions
1037
+ * - Return null if no config file is found
1038
+ *
1039
+ * AVOID:
1040
+ * - Throwing errors (return null instead for optional config)
1041
+ * - Hardcoded file names without extension variants
1042
+ * - Ignoring environment variables
1043
+ */
1044
+ /**
1045
+ * Find MCP configuration file by checking PROJECT_PATH first, then cwd
1046
+ * Looks for both mcp-config.yaml and mcp-config.json
1047
+ *
1048
+ * @returns Absolute path to config file, or null if not found
1049
+ */
1050
+ function findConfigFile() {
1051
+ const configFileNames = [
1052
+ "mcp-config.yaml",
1053
+ "mcp-config.yml",
1054
+ "mcp-config.json"
1055
+ ];
1056
+ const projectPath = process.env.PROJECT_PATH;
1057
+ if (projectPath) for (const fileName of configFileNames) {
1058
+ const configPath = resolve(projectPath, fileName);
1059
+ if (existsSync(configPath)) return configPath;
1060
+ }
1061
+ const cwd = process.cwd();
1062
+ for (const fileName of configFileNames) {
1063
+ const configPath = join(cwd, fileName);
1064
+ if (existsSync(configPath)) return configPath;
1065
+ }
1066
+ return null;
1067
+ }
1068
+
1069
+ //#endregion
1070
+ //#region src/utils/parseToolName.ts
1071
+ /**
1072
+ * Parse tool name to extract server and actual tool name
1073
+ * Supports both plain tool names and prefixed format: {serverName}__{toolName}
1074
+ *
1075
+ * @param toolName - The tool name to parse (e.g., "my_tool" or "server__my_tool")
1076
+ * @returns Parsed result with optional serverName and actualToolName
1077
+ *
1078
+ * @example
1079
+ * parseToolName("my_tool") // { actualToolName: "my_tool" }
1080
+ * parseToolName("server__my_tool") // { serverName: "server", actualToolName: "my_tool" }
1081
+ */
1082
+ function parseToolName(toolName) {
1083
+ const separatorIndex = toolName.indexOf("__");
1084
+ if (separatorIndex > 0) return {
1085
+ serverName: toolName.substring(0, separatorIndex),
1086
+ actualToolName: toolName.substring(separatorIndex + 2)
1087
+ };
1088
+ return { actualToolName: toolName };
1089
+ }
1090
+
1091
+ //#endregion
1092
+ //#region src/utils/parseFrontMatter.ts
1093
+ /**
1094
+ * Parses YAML front matter from a string content.
1095
+ * Front matter must be at the start of the content, delimited by `---`.
1096
+ *
1097
+ * Supports:
1098
+ * - Simple key: value pairs
1099
+ * - Literal block scalar (|) for multi-line preserving newlines
1100
+ * - Folded block scalar (>) for multi-line folding to single line
1101
+ *
1102
+ * @param content - The content string that may contain front matter
1103
+ * @returns Object with parsed front matter (or null) and remaining content
1104
+ *
1105
+ * @example
1106
+ * const result = parseFrontMatter(`---
1107
+ * name: my-skill
1108
+ * description: A skill description
1109
+ * ---
1110
+ * The actual content here`);
1111
+ * // result.frontMatter = { name: 'my-skill', description: 'A skill description' }
1112
+ * // result.content = 'The actual content here'
1113
+ *
1114
+ * @example
1115
+ * // Multi-line with literal block scalar
1116
+ * const result = parseFrontMatter(`---
1117
+ * name: my-skill
1118
+ * description: |
1119
+ * Line 1
1120
+ * Line 2
1121
+ * ---
1122
+ * Content`);
1123
+ * // result.frontMatter.description = 'Line 1\nLine 2'
1124
+ */
1125
+ function parseFrontMatter(content) {
1126
+ const trimmedContent = content.trimStart();
1127
+ if (!trimmedContent.startsWith("---")) return {
1128
+ frontMatter: null,
1129
+ content
1130
+ };
1131
+ const endDelimiterIndex = trimmedContent.indexOf("\n---", 3);
1132
+ if (endDelimiterIndex === -1) return {
1133
+ frontMatter: null,
1134
+ content
1135
+ };
1136
+ const yamlContent = trimmedContent.slice(4, endDelimiterIndex).trim();
1137
+ if (!yamlContent) return {
1138
+ frontMatter: null,
1139
+ content
1140
+ };
1141
+ const frontMatter = {};
1142
+ const lines = yamlContent.split("\n");
1143
+ let currentKey = null;
1144
+ let currentValue = [];
1145
+ let multiLineMode = null;
1146
+ let baseIndent = 0;
1147
+ const saveCurrentKey = () => {
1148
+ if (currentKey && currentValue.length > 0) if (multiLineMode === "literal") frontMatter[currentKey] = currentValue.join("\n").trimEnd();
1149
+ else if (multiLineMode === "folded") frontMatter[currentKey] = currentValue.join(" ").trim();
1150
+ else frontMatter[currentKey] = currentValue.join("").trim();
1151
+ currentKey = null;
1152
+ currentValue = [];
1153
+ multiLineMode = null;
1154
+ baseIndent = 0;
1155
+ };
1156
+ for (let i = 0; i < lines.length; i++) {
1157
+ const line = lines[i];
1158
+ const trimmedLine = line.trim();
1159
+ const colonIndex = line.indexOf(":");
1160
+ if (colonIndex !== -1 && !line.startsWith(" ") && !line.startsWith(" ")) {
1161
+ saveCurrentKey();
1162
+ const key = line.slice(0, colonIndex).trim();
1163
+ let value = line.slice(colonIndex + 1).trim();
1164
+ if (value === "|" || value === "|-") {
1165
+ currentKey = key;
1166
+ multiLineMode = "literal";
1167
+ if (i + 1 < lines.length) {
1168
+ const match = lines[i + 1].match(/^(\s+)/);
1169
+ baseIndent = match ? match[1].length : 2;
1170
+ }
1171
+ } else if (value === ">" || value === ">-") {
1172
+ currentKey = key;
1173
+ multiLineMode = "folded";
1174
+ if (i + 1 < lines.length) {
1175
+ const match = lines[i + 1].match(/^(\s+)/);
1176
+ baseIndent = match ? match[1].length : 2;
1177
+ }
1178
+ } else {
1179
+ if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
1180
+ if (key && value) frontMatter[key] = value;
1181
+ }
1182
+ } else if (multiLineMode && currentKey) {
1183
+ const lineIndent = line.match(/^(\s*)/)?.[1].length || 0;
1184
+ if (trimmedLine === "") currentValue.push("");
1185
+ else if (lineIndent >= baseIndent) {
1186
+ const unindentedLine = line.slice(baseIndent);
1187
+ currentValue.push(unindentedLine);
1188
+ } else saveCurrentKey();
1189
+ }
1190
+ }
1191
+ saveCurrentKey();
1192
+ return {
1193
+ frontMatter,
1194
+ content: trimmedContent.slice(endDelimiterIndex + 4).trimStart()
1195
+ };
1196
+ }
1197
+ /**
1198
+ * Checks if parsed front matter contains valid skill metadata.
1199
+ * A valid skill front matter must have both `name` and `description` fields.
1200
+ *
1201
+ * @param frontMatter - The parsed front matter object
1202
+ * @returns True if front matter contains valid skill metadata
1203
+ */
1204
+ function isValidSkillFrontMatter(frontMatter) {
1205
+ return frontMatter !== null && typeof frontMatter.name === "string" && frontMatter.name.length > 0 && typeof frontMatter.description === "string" && frontMatter.description.length > 0;
1206
+ }
1207
+ /**
1208
+ * Extracts skill front matter from content if present and valid.
1209
+ *
1210
+ * @param content - The content string that may contain skill front matter
1211
+ * @returns Object with skill metadata and content, or null if no valid skill front matter
1212
+ */
1213
+ function extractSkillFrontMatter(content) {
1214
+ const { frontMatter, content: remainingContent } = parseFrontMatter(content);
1215
+ if (frontMatter && isValidSkillFrontMatter(frontMatter)) return {
1216
+ skill: {
1217
+ name: frontMatter.name,
1218
+ description: frontMatter.description
1219
+ },
1220
+ content: remainingContent
1221
+ };
1222
+ return null;
1223
+ }
1224
+
1225
+ //#endregion
1226
+ //#region src/utils/generateServerId.ts
1227
+ /**
1228
+ * generateServerId Utilities
1229
+ *
1230
+ * DESIGN PATTERNS:
1231
+ * - Pure functions with no side effects
1232
+ * - Single responsibility per function
1233
+ * - Functional programming approach
1234
+ *
1235
+ * CODING STANDARDS:
1236
+ * - Export individual functions, not classes
1237
+ * - Use descriptive function names with verbs
1238
+ * - Add JSDoc comments for complex logic
1239
+ * - Keep functions small and focused
1240
+ *
1241
+ * AVOID:
1242
+ * - Side effects (mutating external state)
1243
+ * - Stateful logic (use services for state)
1244
+ * - Complex external dependencies
1245
+ */
1246
+ /**
1247
+ * Character set for generating human-readable IDs.
1248
+ * Excludes confusing characters: 0, O, 1, l, I
1249
+ */
1250
+ const CHARSET = "23456789abcdefghjkmnpqrstuvwxyz";
1251
+ /**
1252
+ * Default length for generated server IDs (6 characters)
1253
+ */
1254
+ const DEFAULT_ID_LENGTH = 6;
1255
+ /**
1256
+ * Generate a short, human-readable server ID.
1257
+ *
1258
+ * Uses Node.js crypto.randomBytes for cryptographically secure randomness
1259
+ * with rejection sampling to avoid modulo bias.
1260
+ *
1261
+ * The generated ID:
1262
+ * - Is 6 characters long by default
1263
+ * - Uses only lowercase alphanumeric characters
1264
+ * - Excludes confusing characters (0, O, 1, l, I)
1265
+ *
1266
+ * @param length - Length of the ID to generate (default: 6)
1267
+ * @returns A random, human-readable ID
1268
+ *
1269
+ * @example
1270
+ * generateServerId() // "abc234"
1271
+ * generateServerId(4) // "x7mn"
1272
+ */
1273
+ function generateServerId(length = DEFAULT_ID_LENGTH) {
1274
+ const charsetLength = 31;
1275
+ const maxUnbiased = Math.floor(256 / charsetLength) * charsetLength - 1;
1276
+ let result = "";
1277
+ let remaining = length;
1278
+ while (remaining > 0) {
1279
+ const bytes = randomBytes(remaining);
1280
+ for (let i = 0; i < bytes.length && remaining > 0; i++) {
1281
+ const byte = bytes[i];
1282
+ if (byte > maxUnbiased) continue;
1283
+ result += CHARSET[byte % charsetLength];
1284
+ remaining--;
1285
+ }
1286
+ }
1287
+ return result;
1288
+ }
1289
+
1002
1290
  //#endregion
1003
1291
  //#region src/services/SkillService.ts
1004
1292
  /**
@@ -1069,14 +1357,21 @@ var SkillService = class {
1069
1357
  skillPaths;
1070
1358
  cachedSkills = null;
1071
1359
  skillsByName = null;
1360
+ /** Active file watchers for skill directories */
1361
+ watchers = [];
1362
+ /** Callback invoked when cache is invalidated due to file changes */
1363
+ onCacheInvalidated;
1072
1364
  /**
1073
1365
  * Creates a new SkillService instance
1074
1366
  * @param cwd - Current working directory for resolving relative paths
1075
1367
  * @param skillPaths - Array of paths to skills directories
1368
+ * @param options - Optional configuration
1369
+ * @param options.onCacheInvalidated - Callback invoked when cache is invalidated due to file changes
1076
1370
  */
1077
- constructor(cwd, skillPaths) {
1371
+ constructor(cwd, skillPaths, options) {
1078
1372
  this.cwd = cwd;
1079
1373
  this.skillPaths = skillPaths;
1374
+ this.onCacheInvalidated = options?.onCacheInvalidated;
1080
1375
  }
1081
1376
  /**
1082
1377
  * Get all available skills from configured directories.
@@ -1092,13 +1387,13 @@ var SkillService = class {
1092
1387
  if (this.cachedSkills !== null) return this.cachedSkills;
1093
1388
  const skills = [];
1094
1389
  const loadedSkillNames = /* @__PURE__ */ new Set();
1095
- for (const skillPath of this.skillPaths) {
1390
+ const allDirSkills = await Promise.all(this.skillPaths.map(async (skillPath) => {
1096
1391
  const skillsDir = isAbsolute(skillPath) ? skillPath : join(this.cwd, skillPath);
1097
- const dirSkills = await this.loadSkillsFromDirectory(skillsDir, "project");
1098
- for (const skill of dirSkills) if (!loadedSkillNames.has(skill.name)) {
1099
- skills.push(skill);
1100
- loadedSkillNames.add(skill.name);
1101
- }
1392
+ return this.loadSkillsFromDirectory(skillsDir, "project");
1393
+ }));
1394
+ for (const dirSkills of allDirSkills) for (const skill of dirSkills) if (!loadedSkillNames.has(skill.name)) {
1395
+ skills.push(skill);
1396
+ loadedSkillNames.add(skill.name);
1102
1397
  }
1103
1398
  this.cachedSkills = skills;
1104
1399
  this.skillsByName = new Map(skills.map((skill) => [skill.name, skill]));
@@ -1122,6 +1417,60 @@ var SkillService = class {
1122
1417
  this.skillsByName = null;
1123
1418
  }
1124
1419
  /**
1420
+ * Starts watching skill directories for changes to SKILL.md files.
1421
+ * When changes are detected, the cache is automatically invalidated.
1422
+ *
1423
+ * Uses Node.js fs.watch with recursive option for efficient directory monitoring.
1424
+ * Only invalidates cache when SKILL.md files are modified.
1425
+ *
1426
+ * @example
1427
+ * const skillService = new SkillService(cwd, skillPaths, {
1428
+ * onCacheInvalidated: () => console.log('Skills cache invalidated')
1429
+ * });
1430
+ * await skillService.startWatching();
1431
+ */
1432
+ async startWatching() {
1433
+ this.stopWatching();
1434
+ const existenceChecks = await Promise.all(this.skillPaths.map(async (skillPath) => {
1435
+ const skillsDir = isAbsolute(skillPath) ? skillPath : join(this.cwd, skillPath);
1436
+ return {
1437
+ skillsDir,
1438
+ exists: await pathExists(skillsDir)
1439
+ };
1440
+ }));
1441
+ for (const { skillsDir, exists } of existenceChecks) {
1442
+ if (!exists) continue;
1443
+ const abortController = new AbortController();
1444
+ this.watchers.push(abortController);
1445
+ this.watchDirectory(skillsDir, abortController.signal).catch((error) => {
1446
+ if (error?.name !== "AbortError") console.error(`[skill-watcher] Error watching ${skillsDir}: ${error instanceof Error ? error.message : "Unknown error"}`);
1447
+ });
1448
+ }
1449
+ }
1450
+ /**
1451
+ * Stops all active file watchers.
1452
+ * Should be called when the service is being disposed.
1453
+ */
1454
+ stopWatching() {
1455
+ for (const controller of this.watchers) controller.abort();
1456
+ this.watchers = [];
1457
+ }
1458
+ /**
1459
+ * Watches a directory for changes to SKILL.md files.
1460
+ * @param dirPath - Directory path to watch
1461
+ * @param signal - AbortSignal to stop watching
1462
+ */
1463
+ async watchDirectory(dirPath, signal) {
1464
+ const watcher = watch(dirPath, {
1465
+ recursive: true,
1466
+ signal
1467
+ });
1468
+ for await (const event of watcher) if (event.filename && event.filename.endsWith("SKILL.md")) {
1469
+ this.clearCache();
1470
+ this.onCacheInvalidated?.();
1471
+ }
1472
+ }
1473
+ /**
1125
1474
  * Load skills from a directory.
1126
1475
  * Supports both flat structure (SKILL.md) and nested structure (name/SKILL.md).
1127
1476
  *
@@ -1148,38 +1497,54 @@ var SkillService = class {
1148
1497
  } catch (error) {
1149
1498
  throw new SkillLoadError(`Failed to read skills directory: ${error instanceof Error ? error.message : "Unknown error"}`, dirPath, error instanceof Error ? error : void 0);
1150
1499
  }
1151
- for (const entry of entries) {
1500
+ const entryStats = await Promise.all(entries.map(async (entry) => {
1152
1501
  const entryPath = join(dirPath, entry);
1153
- let entryStat;
1154
1502
  try {
1155
- entryStat = await stat(entryPath);
1503
+ return {
1504
+ entry,
1505
+ entryPath,
1506
+ stat: await stat(entryPath),
1507
+ error: null
1508
+ };
1156
1509
  } catch (error) {
1157
1510
  console.warn(`Skipping entry ${entryPath}: ${error instanceof Error ? error.message : "Unknown error"}`);
1158
- continue;
1511
+ return {
1512
+ entry,
1513
+ entryPath,
1514
+ stat: null,
1515
+ error
1516
+ };
1159
1517
  }
1518
+ }));
1519
+ const skillFilesToLoad = [];
1520
+ for (const { entry, entryPath, stat: entryStat } of entryStats) {
1521
+ if (!entryStat) continue;
1160
1522
  if (entryStat.isDirectory()) {
1161
1523
  const skillFilePath = join(entryPath, "SKILL.md");
1162
- try {
1163
- if (await pathExists(skillFilePath)) {
1164
- const skill = await this.loadSkillFile(skillFilePath, location);
1165
- if (skill) skills.push(skill);
1166
- }
1167
- } catch (error) {
1168
- console.warn(`Skipping skill at ${skillFilePath}: ${error instanceof Error ? error.message : "Unknown error"}`);
1169
- continue;
1170
- }
1171
- } else if (entry === "SKILL.md") try {
1172
- const skill = await this.loadSkillFile(entryPath, location);
1173
- if (skill) skills.push(skill);
1524
+ skillFilesToLoad.push({
1525
+ filePath: skillFilePath,
1526
+ isRootLevel: false
1527
+ });
1528
+ } else if (entry === "SKILL.md") skillFilesToLoad.push({
1529
+ filePath: entryPath,
1530
+ isRootLevel: true
1531
+ });
1532
+ }
1533
+ const loadResults = await Promise.all(skillFilesToLoad.map(async ({ filePath, isRootLevel }) => {
1534
+ try {
1535
+ if (!isRootLevel && !await pathExists(filePath)) return null;
1536
+ return await this.loadSkillFile(filePath, location);
1174
1537
  } catch (error) {
1175
- console.warn(`Skipping skill at ${entryPath}: ${error instanceof Error ? error.message : "Unknown error"}`);
1176
- continue;
1538
+ console.warn(`Skipping skill at ${filePath}: ${error instanceof Error ? error.message : "Unknown error"}`);
1539
+ return null;
1177
1540
  }
1178
- }
1541
+ }));
1542
+ for (const skill of loadResults) if (skill) skills.push(skill);
1179
1543
  return skills;
1180
1544
  }
1181
1545
  /**
1182
1546
  * Load a single skill file and parse its frontmatter.
1547
+ * Supports multi-line YAML values using literal (|) and folded (>) block scalars.
1183
1548
  *
1184
1549
  * @param filePath - Path to the SKILL.md file
1185
1550
  * @param location - Whether this is a 'project' or 'user' skill
@@ -1193,150 +1558,67 @@ var SkillService = class {
1193
1558
  * // Returns null if frontmatter is missing name or description
1194
1559
  */
1195
1560
  async loadSkillFile(filePath, location) {
1196
- let content;
1561
+ let fileContent;
1197
1562
  try {
1198
- content = await readFile(filePath, "utf-8");
1563
+ fileContent = await readFile(filePath, "utf-8");
1199
1564
  } catch (error) {
1200
1565
  throw new SkillLoadError(`Failed to read skill file: ${error instanceof Error ? error.message : "Unknown error"}`, filePath, error instanceof Error ? error : void 0);
1201
1566
  }
1202
- const { metadata, body } = this.parseFrontmatter(content);
1203
- if (!metadata.name || !metadata.description) return null;
1567
+ const { frontMatter, content } = parseFrontMatter(fileContent);
1568
+ if (!frontMatter || !frontMatter.name || !frontMatter.description) return null;
1204
1569
  return {
1205
- name: metadata.name,
1206
- description: metadata.description,
1570
+ name: frontMatter.name,
1571
+ description: frontMatter.description,
1207
1572
  location,
1208
- content: body,
1573
+ content,
1209
1574
  basePath: dirname(filePath)
1210
1575
  };
1211
1576
  }
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
1577
  };
1256
1578
 
1257
1579
  //#endregion
1258
- //#region src/utils/findConfigFile.ts
1580
+ //#region src/constants/index.ts
1259
1581
  /**
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
1582
+ * Shared constants for one-mcp package
1278
1583
  */
1279
1584
  /**
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
1585
+ * Prefix added to skill names when they clash with MCP tool names.
1586
+ * This ensures skills can be uniquely identified even when a tool has the same name.
1284
1587
  */
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
1588
+ const SKILL_PREFIX = "skill__";
1306
1589
  /**
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" }
1590
+ * Log prefix for skill detection messages.
1591
+ * Used to easily filter skill detection logs in stderr output.
1316
1592
  */
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";
1593
+ const LOG_PREFIX_SKILL_DETECTION = "[skill-detection]";
1594
+ /**
1595
+ * Prefix for prompt-based skill locations.
1596
+ * Format: "prompt:{serverName}:{promptName}"
1597
+ */
1598
+ const PROMPT_LOCATION_PREFIX = "prompt:";
1599
+ /**
1600
+ * Default server ID used when no ID is provided via CLI or config.
1601
+ * This fallback is used when auto-generation also fails.
1602
+ */
1603
+ const DEFAULT_SERVER_ID = "unknown";
1329
1604
 
1330
1605
  //#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";
1606
+ //#region src/templates/toolkit-description.liquid?raw
1607
+ 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";
1333
1608
 
1334
1609
  //#endregion
1335
1610
  //#region src/tools/DescribeToolsTool.ts
1336
1611
  /**
1337
- * Prefix used to identify skill invocations
1612
+ * Formats skill instructions with the loading command message prefix.
1613
+ * This message is used by Claude Code to indicate that a skill is being loaded.
1614
+ *
1615
+ * @param name - The skill name
1616
+ * @param instructions - The raw skill instructions/content
1617
+ * @returns Formatted instructions with command message prefix
1338
1618
  */
1339
- const SKILL_PREFIX$1 = "skill__";
1619
+ function formatSkillInstructions(name, instructions) {
1620
+ return `<command-message>The "${name}" skill is loading</command-message>\n${instructions}`;
1621
+ }
1340
1622
  /**
1341
1623
  * DescribeToolsTool provides progressive disclosure of MCP tools and skills.
1342
1624
  *
@@ -1359,22 +1641,93 @@ var DescribeToolsTool = class DescribeToolsTool {
1359
1641
  clientManager;
1360
1642
  skillService;
1361
1643
  liquid = new Liquid();
1644
+ /** Cache for auto-detected skills from prompt front-matter */
1645
+ autoDetectedSkillsCache = null;
1646
+ /** Unique server identifier for this one-mcp instance */
1647
+ serverId;
1362
1648
  /**
1363
1649
  * Creates a new DescribeToolsTool instance
1364
1650
  * @param clientManager - The MCP client manager for accessing remote servers
1365
1651
  * @param skillService - Optional skill service for loading skills
1652
+ * @param serverId - Unique server identifier for this one-mcp instance
1366
1653
  */
1367
- constructor(clientManager, skillService) {
1654
+ constructor(clientManager, skillService, serverId) {
1368
1655
  this.clientManager = clientManager;
1369
1656
  this.skillService = skillService;
1657
+ this.serverId = serverId || DEFAULT_SERVER_ID;
1658
+ }
1659
+ /**
1660
+ * Clears the cached auto-detected skills from prompt front-matter.
1661
+ * Use this when prompt configurations may have changed or when
1662
+ * the skill service cache is invalidated.
1663
+ */
1664
+ clearAutoDetectedSkillsCache() {
1665
+ this.autoDetectedSkillsCache = null;
1666
+ }
1667
+ /**
1668
+ * Detects and caches skills from prompt front-matter across all connected MCP servers.
1669
+ * Fetches all prompts and checks their content for YAML front-matter with name/description.
1670
+ * Results are cached to avoid repeated fetches.
1671
+ *
1672
+ * Error Handling Strategy:
1673
+ * - Errors are logged to stderr but do not fail the overall detection process
1674
+ * - This ensures partial results are returned even if some servers/prompts fail
1675
+ * - Common failure reasons: server temporarily unavailable, prompt requires arguments,
1676
+ * network timeout, or server doesn't support listPrompts
1677
+ * - Errors are prefixed with [skill-detection] for easy filtering in logs
1678
+ *
1679
+ * @returns Array of auto-detected skills from prompt front-matter
1680
+ */
1681
+ async detectSkillsFromPromptFrontMatter() {
1682
+ if (this.autoDetectedSkillsCache !== null) return this.autoDetectedSkillsCache;
1683
+ const clients = this.clientManager.getAllClients();
1684
+ let listPromptsFailures = 0;
1685
+ let fetchPromptFailures = 0;
1686
+ const autoDetectedSkills = (await Promise.all(clients.map(async (client) => {
1687
+ const detectedSkills = [];
1688
+ const configuredPromptNames = new Set(client.prompts ? Object.keys(client.prompts) : []);
1689
+ try {
1690
+ const prompts = await client.listPrompts();
1691
+ if (!prompts || prompts.length === 0) return detectedSkills;
1692
+ const promptResults = await Promise.all(prompts.map(async (promptInfo) => {
1693
+ if (configuredPromptNames.has(promptInfo.name)) return null;
1694
+ try {
1695
+ const skillExtraction = extractSkillFrontMatter(((await client.getPrompt(promptInfo.name)).messages || []).map((m) => {
1696
+ const content = m.content;
1697
+ if (typeof content === "string") return content;
1698
+ if (content && typeof content === "object" && "text" in content) return String(content.text);
1699
+ return "";
1700
+ }).join("\n"));
1701
+ if (skillExtraction) return {
1702
+ serverName: client.serverName,
1703
+ promptName: promptInfo.name,
1704
+ skill: skillExtraction.skill
1705
+ };
1706
+ return null;
1707
+ } catch (error) {
1708
+ fetchPromptFailures++;
1709
+ console.error(`${LOG_PREFIX_SKILL_DETECTION} Failed to fetch prompt '${promptInfo.name}' from ${client.serverName}: ${error instanceof Error ? error.message : "Unknown error"}`);
1710
+ return null;
1711
+ }
1712
+ }));
1713
+ for (const result of promptResults) if (result) detectedSkills.push(result);
1714
+ } catch (error) {
1715
+ listPromptsFailures++;
1716
+ console.error(`${LOG_PREFIX_SKILL_DETECTION} Failed to list prompts from ${client.serverName}: ${error instanceof Error ? error.message : "Unknown error"}`);
1717
+ }
1718
+ return detectedSkills;
1719
+ }))).flat();
1720
+ 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).`);
1721
+ this.autoDetectedSkillsCache = autoDetectedSkills;
1722
+ return autoDetectedSkills;
1370
1723
  }
1371
1724
  /**
1372
1725
  * Collects skills derived from prompt configurations across all connected MCP servers.
1373
- * Prompts with a skill configuration are converted to skill format for display.
1726
+ * Includes both explicitly configured prompts and auto-detected skills from front-matter.
1374
1727
  *
1375
1728
  * @returns Array of skill template data derived from prompts
1376
1729
  */
1377
- collectPromptSkills() {
1730
+ async collectPromptSkills() {
1378
1731
  const clients = this.clientManager.getAllClients();
1379
1732
  const promptSkills = [];
1380
1733
  for (const client of clients) {
@@ -1385,16 +1738,22 @@ var DescribeToolsTool = class DescribeToolsTool {
1385
1738
  description: promptConfig.skill.description
1386
1739
  });
1387
1740
  }
1741
+ const autoDetectedSkills = await this.detectSkillsFromPromptFrontMatter();
1742
+ for (const autoSkill of autoDetectedSkills) promptSkills.push({
1743
+ name: autoSkill.skill.name,
1744
+ displayName: autoSkill.skill.name,
1745
+ description: autoSkill.skill.description
1746
+ });
1388
1747
  return promptSkills;
1389
1748
  }
1390
1749
  /**
1391
1750
  * 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.
1751
+ * Searches both explicitly configured prompts and auto-detected skills from front-matter.
1393
1752
  *
1394
1753
  * @param skillName - The skill name to search for
1395
1754
  * @returns Object with serverName, promptName, and skill config, or undefined if not found
1396
1755
  */
1397
- findPromptSkill(skillName) {
1756
+ async findPromptSkill(skillName) {
1398
1757
  if (!skillName) return void 0;
1399
1758
  const clients = this.clientManager.getAllClients();
1400
1759
  for (const client of clients) {
@@ -1405,16 +1764,24 @@ var DescribeToolsTool = class DescribeToolsTool {
1405
1764
  skill: promptConfig.skill
1406
1765
  };
1407
1766
  }
1767
+ const autoDetectedSkills = await this.detectSkillsFromPromptFrontMatter();
1768
+ for (const autoSkill of autoDetectedSkills) if (autoSkill.skill.name === skillName) return {
1769
+ serverName: autoSkill.serverName,
1770
+ promptName: autoSkill.promptName,
1771
+ skill: autoSkill.skill,
1772
+ autoDetected: true
1773
+ };
1408
1774
  }
1409
1775
  /**
1410
1776
  * Retrieves skill content from a prompt-based skill configuration.
1411
1777
  * Fetches the prompt from the MCP server and extracts text content.
1778
+ * Handles both explicitly configured prompts and auto-detected skills from front-matter.
1412
1779
  *
1413
1780
  * @param skillName - The skill name being requested
1414
1781
  * @returns SkillDescription if found and successfully fetched, undefined otherwise
1415
1782
  */
1416
1783
  async getPromptSkillContent(skillName) {
1417
- const promptSkill = this.findPromptSkill(skillName);
1784
+ const promptSkill = await this.findPromptSkill(skillName);
1418
1785
  if (!promptSkill) return void 0;
1419
1786
  const client = this.clientManager.getClient(promptSkill.serverName);
1420
1787
  if (!client) {
@@ -1422,7 +1789,7 @@ var DescribeToolsTool = class DescribeToolsTool {
1422
1789
  return;
1423
1790
  }
1424
1791
  try {
1425
- const instructions = (await client.getPrompt(promptSkill.promptName)).messages?.map((m) => {
1792
+ const rawInstructions = (await client.getPrompt(promptSkill.promptName)).messages?.map((m) => {
1426
1793
  const content = m.content;
1427
1794
  if (typeof content === "string") return content;
1428
1795
  if (content && typeof content === "object" && "text" in content) return String(content.text);
@@ -1430,8 +1797,8 @@ var DescribeToolsTool = class DescribeToolsTool {
1430
1797
  }).join("\n") || "";
1431
1798
  return {
1432
1799
  name: promptSkill.skill.name,
1433
- location: promptSkill.skill.folder || `prompt:${promptSkill.serverName}/${promptSkill.promptName}`,
1434
- instructions
1800
+ location: promptSkill.skill.folder || `${PROMPT_LOCATION_PREFIX}${promptSkill.serverName}/${promptSkill.promptName}`,
1801
+ instructions: formatSkillInstructions(promptSkill.skill.name, rawInstructions)
1435
1802
  };
1436
1803
  } catch (error) {
1437
1804
  console.error(`Failed to get prompt-based skill '${skillName}': ${error instanceof Error ? error.message : "Unknown error"}`);
@@ -1439,49 +1806,18 @@ var DescribeToolsTool = class DescribeToolsTool {
1439
1806
  }
1440
1807
  }
1441
1808
  /**
1442
- * Builds the skills section of the tool description using a Liquid template.
1809
+ * Builds the combined toolkit description using a single Liquid template.
1443
1810
  *
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.
1811
+ * Collects all tools from connected MCP servers and all skills, then renders
1812
+ * them together using the toolkit-description.liquid template.
1478
1813
  *
1479
1814
  * Tool names are prefixed with serverName__ when the same tool exists
1480
- * on multiple servers to avoid ambiguity.
1815
+ * on multiple servers. Skill names are prefixed with skill__ when they
1816
+ * clash with MCP tools or other skills.
1481
1817
  *
1482
- * @returns Object with rendered servers section and set of all tool names for skill clash detection
1818
+ * @returns Object with rendered description and set of all tool names
1483
1819
  */
1484
- async buildServersSection() {
1820
+ async buildToolkitDescription() {
1485
1821
  const clients = this.clientManager.getAllClients();
1486
1822
  const toolToServers = /* @__PURE__ */ new Map();
1487
1823
  const serverToolsMap = /* @__PURE__ */ new Map();
@@ -1502,9 +1838,6 @@ var DescribeToolsTool = class DescribeToolsTool {
1502
1838
  }));
1503
1839
  /**
1504
1840
  * 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
1841
  */
1509
1842
  const formatToolName = (toolName, serverName) => {
1510
1843
  return (toolToServers.get(toolName) || []).length > 1 ? `${serverName}__${toolName}` : toolName;
@@ -1524,31 +1857,57 @@ var DescribeToolsTool = class DescribeToolsTool {
1524
1857
  toolNames: formattedTools.map((t) => t.displayName)
1525
1858
  };
1526
1859
  });
1860
+ const rawSkills = this.skillService ? await this.skillService.getSkills() : [];
1861
+ const promptSkills = await this.collectPromptSkills();
1862
+ const seenSkillNames = /* @__PURE__ */ new Set();
1863
+ const allSkillsData = [];
1864
+ for (const skill of rawSkills) if (!seenSkillNames.has(skill.name)) {
1865
+ seenSkillNames.add(skill.name);
1866
+ allSkillsData.push({
1867
+ name: skill.name,
1868
+ displayName: skill.name,
1869
+ description: skill.description
1870
+ });
1871
+ }
1872
+ for (const skill of promptSkills) if (!seenSkillNames.has(skill.name)) {
1873
+ seenSkillNames.add(skill.name);
1874
+ allSkillsData.push(skill);
1875
+ }
1876
+ const skills = allSkillsData.map((skill) => {
1877
+ const clashesWithMcpTool = allToolNames.has(skill.name);
1878
+ return {
1879
+ name: skill.name,
1880
+ displayName: clashesWithMcpTool ? `${SKILL_PREFIX}${skill.name}` : skill.name,
1881
+ description: skill.description
1882
+ };
1883
+ });
1527
1884
  return {
1528
- content: await this.liquid.parseAndRender(mcp_servers_description_default, { servers }),
1885
+ content: await this.liquid.parseAndRender(toolkit_description_default, {
1886
+ servers,
1887
+ skills,
1888
+ serverId: this.serverId
1889
+ }),
1529
1890
  toolNames: allToolNames
1530
1891
  };
1531
1892
  }
1532
1893
  /**
1533
- * Gets the tool definition including available servers, tools, and skills.
1894
+ * Gets the tool definition including available tools and skills in a unified format.
1534
1895
  *
1535
1896
  * 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
1897
+ * - All MCP tools from connected servers
1898
+ * - All available skills (file-based and prompt-based)
1899
+ * - Unified instructions for querying capability details
1539
1900
  *
1540
- * Tool names are prefixed with serverName__ when the same tool name
1541
- * exists on multiple servers to avoid ambiguity.
1901
+ * Tool names are prefixed with serverName__ when clashing.
1902
+ * Skill names are prefixed with skill__ when clashing.
1542
1903
  *
1543
1904
  * @returns Tool definition with description and input schema
1544
1905
  */
1545
1906
  async getDefinition() {
1546
- const serversResult = await this.buildServersSection();
1547
- const skillsSection = await this.buildSkillsSection(serversResult.toolNames);
1907
+ const { content } = await this.buildToolkitDescription();
1548
1908
  return {
1549
1909
  name: DescribeToolsTool.TOOL_NAME,
1550
- description: `${serversResult.content}
1551
- ${skillsSection}`,
1910
+ description: content,
1552
1911
  inputSchema: {
1553
1912
  type: "object",
1554
1913
  properties: { toolNames: {
@@ -1608,40 +1967,42 @@ ${skillsSection}`,
1608
1967
  serverToolsMap.set(client.serverName, []);
1609
1968
  }
1610
1969
  }));
1611
- const foundTools = [];
1612
- const foundSkills = [];
1613
- const notFoundItems = [];
1614
- for (const requestedName of toolNames) {
1615
- if (requestedName.startsWith(SKILL_PREFIX$1)) {
1616
- const skillName = requestedName.slice(7);
1970
+ const lookupResults = await Promise.all(toolNames.map(async (requestedName) => {
1971
+ const result$1 = {
1972
+ tools: [],
1973
+ skills: [],
1974
+ notFound: null
1975
+ };
1976
+ if (requestedName.startsWith(SKILL_PREFIX)) {
1977
+ const skillName = requestedName.slice(SKILL_PREFIX.length);
1617
1978
  if (this.skillService) {
1618
1979
  const skill = await this.skillService.getSkill(skillName);
1619
1980
  if (skill) {
1620
- foundSkills.push({
1981
+ result$1.skills.push({
1621
1982
  name: skill.name,
1622
1983
  location: skill.basePath,
1623
- instructions: skill.content
1984
+ instructions: formatSkillInstructions(skill.name, skill.content)
1624
1985
  });
1625
- continue;
1986
+ return result$1;
1626
1987
  }
1627
1988
  }
1628
1989
  const promptSkillContent = await this.getPromptSkillContent(skillName);
1629
1990
  if (promptSkillContent) {
1630
- foundSkills.push(promptSkillContent);
1631
- continue;
1991
+ result$1.skills.push(promptSkillContent);
1992
+ return result$1;
1632
1993
  }
1633
- notFoundItems.push(requestedName);
1634
- continue;
1994
+ result$1.notFound = requestedName;
1995
+ return result$1;
1635
1996
  }
1636
1997
  const { serverName, actualToolName } = parseToolName(requestedName);
1637
1998
  if (serverName) {
1638
1999
  const serverTools = serverToolsMap.get(serverName);
1639
2000
  if (!serverTools) {
1640
- notFoundItems.push(requestedName);
1641
- continue;
2001
+ result$1.notFound = requestedName;
2002
+ return result$1;
1642
2003
  }
1643
2004
  const tool = serverTools.find((t) => t.name === actualToolName);
1644
- if (tool) foundTools.push({
2005
+ if (tool) result$1.tools.push({
1645
2006
  server: serverName,
1646
2007
  tool: {
1647
2008
  name: tool.name,
@@ -1649,52 +2010,61 @@ ${skillsSection}`,
1649
2010
  inputSchema: tool.inputSchema
1650
2011
  }
1651
2012
  });
1652
- else notFoundItems.push(requestedName);
1653
- } else {
1654
- const servers = toolToServers.get(actualToolName);
1655
- if (!servers || servers.length === 0) {
1656
- if (this.skillService) {
1657
- const skill = await this.skillService.getSkill(actualToolName);
1658
- if (skill) {
1659
- foundSkills.push({
1660
- name: skill.name,
1661
- location: skill.basePath,
1662
- instructions: skill.content
1663
- });
1664
- continue;
1665
- }
1666
- }
1667
- const promptSkillContent = await this.getPromptSkillContent(actualToolName);
1668
- if (promptSkillContent) {
1669
- foundSkills.push(promptSkillContent);
1670
- continue;
2013
+ else result$1.notFound = requestedName;
2014
+ return result$1;
2015
+ }
2016
+ const servers = toolToServers.get(actualToolName);
2017
+ if (!servers || servers.length === 0) {
2018
+ if (this.skillService) {
2019
+ const skill = await this.skillService.getSkill(actualToolName);
2020
+ if (skill) {
2021
+ result$1.skills.push({
2022
+ name: skill.name,
2023
+ location: skill.basePath,
2024
+ instructions: formatSkillInstructions(skill.name, skill.content)
2025
+ });
2026
+ return result$1;
1671
2027
  }
1672
- notFoundItems.push(requestedName);
1673
- continue;
1674
2028
  }
1675
- if (servers.length === 1) {
1676
- const server = servers[0];
1677
- const tool = serverToolsMap.get(server).find((t) => t.name === actualToolName);
1678
- foundTools.push({
1679
- server,
1680
- tool: {
1681
- name: tool.name,
1682
- description: tool.description,
1683
- inputSchema: tool.inputSchema
1684
- }
1685
- });
1686
- } else for (const server of servers) {
1687
- const tool = serverToolsMap.get(server).find((t) => t.name === actualToolName);
1688
- foundTools.push({
1689
- server,
1690
- tool: {
1691
- name: tool.name,
1692
- description: tool.description,
1693
- inputSchema: tool.inputSchema
1694
- }
1695
- });
2029
+ const promptSkillContent = await this.getPromptSkillContent(actualToolName);
2030
+ if (promptSkillContent) {
2031
+ result$1.skills.push(promptSkillContent);
2032
+ return result$1;
1696
2033
  }
2034
+ result$1.notFound = requestedName;
2035
+ return result$1;
1697
2036
  }
2037
+ if (servers.length === 1) {
2038
+ const server = servers[0];
2039
+ const tool = serverToolsMap.get(server).find((t) => t.name === actualToolName);
2040
+ result$1.tools.push({
2041
+ server,
2042
+ tool: {
2043
+ name: tool.name,
2044
+ description: tool.description,
2045
+ inputSchema: tool.inputSchema
2046
+ }
2047
+ });
2048
+ } else for (const server of servers) {
2049
+ const tool = serverToolsMap.get(server).find((t) => t.name === actualToolName);
2050
+ result$1.tools.push({
2051
+ server,
2052
+ tool: {
2053
+ name: tool.name,
2054
+ description: tool.description,
2055
+ inputSchema: tool.inputSchema
2056
+ }
2057
+ });
2058
+ }
2059
+ return result$1;
2060
+ }));
2061
+ const foundTools = [];
2062
+ const foundSkills = [];
2063
+ const notFoundItems = [];
2064
+ for (const result$1 of lookupResults) {
2065
+ foundTools.push(...result$1.tools);
2066
+ foundSkills.push(...result$1.skills);
2067
+ if (result$1.notFound) notFoundItems.push(result$1.notFound);
1698
2068
  }
1699
2069
  if (foundTools.length === 0 && foundSkills.length === 0) return {
1700
2070
  content: [{
@@ -1737,10 +2107,6 @@ ${skillsSection}`,
1737
2107
  //#endregion
1738
2108
  //#region src/tools/UseToolTool.ts
1739
2109
  /**
1740
- * Prefix used to identify skill invocations (e.g., skill__pdf)
1741
- */
1742
- const SKILL_PREFIX = "skill__";
1743
- /**
1744
2110
  * UseToolTool executes MCP tools and skills with proper error handling.
1745
2111
  *
1746
2112
  * This tool supports three invocation patterns:
@@ -1757,14 +2123,18 @@ var UseToolTool = class UseToolTool {
1757
2123
  static TOOL_NAME = "use_tool";
1758
2124
  clientManager;
1759
2125
  skillService;
2126
+ /** Unique server identifier for this one-mcp instance */
2127
+ serverId;
1760
2128
  /**
1761
2129
  * Creates a new UseToolTool instance
1762
2130
  * @param clientManager - The MCP client manager for accessing remote servers
1763
2131
  * @param skillService - Optional skill service for loading and executing skills
2132
+ * @param serverId - Unique server identifier for this one-mcp instance
1764
2133
  */
1765
- constructor(clientManager, skillService) {
2134
+ constructor(clientManager, skillService, serverId) {
1766
2135
  this.clientManager = clientManager;
1767
2136
  this.skillService = skillService;
2137
+ this.serverId = serverId || DEFAULT_SERVER_ID;
1768
2138
  }
1769
2139
  /**
1770
2140
  * Returns the MCP tool definition with name, description, and input schema.
@@ -1780,6 +2150,8 @@ var UseToolTool = class UseToolTool {
1780
2150
  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:
1781
2151
  - Provide toolName and toolArgs based on the schema
1782
2152
  - If multiple servers provide the same tool, specify serverName
2153
+
2154
+ IMPORTANT: Only use tools discovered from describe_tools with id="${this.serverId}".
1783
2155
  `,
1784
2156
  inputSchema: {
1785
2157
  type: "object",
@@ -1866,7 +2238,7 @@ var UseToolTool = class UseToolTool {
1866
2238
  try {
1867
2239
  const { toolName: inputToolName, toolArgs = {} } = input;
1868
2240
  if (inputToolName.startsWith(SKILL_PREFIX)) {
1869
- const skillName = inputToolName.slice(7);
2241
+ const skillName = inputToolName.slice(SKILL_PREFIX.length);
1870
2242
  if (this.skillService) {
1871
2243
  const skill = await this.skillService.getSkill(skillName);
1872
2244
  if (skill) return this.executeSkill(skill);
@@ -2001,6 +2373,7 @@ async function createServer(options) {
2001
2373
  } });
2002
2374
  const clientManager = new McpClientManagerService();
2003
2375
  let configSkills;
2376
+ let configId;
2004
2377
  if (options?.configFilePath) {
2005
2378
  let config;
2006
2379
  try {
@@ -2012,6 +2385,7 @@ async function createServer(options) {
2012
2385
  throw new Error(`Failed to load MCP configuration from '${options.configFilePath}': ${error instanceof Error ? error.message : String(error)}`);
2013
2386
  }
2014
2387
  configSkills = config.skills;
2388
+ configId = config.id;
2015
2389
  const failedConnections = [];
2016
2390
  const connectionPromises = Object.entries(config.mcpServers).map(async ([serverName, serverConfig]) => {
2017
2391
  try {
@@ -2030,10 +2404,19 @@ async function createServer(options) {
2030
2404
  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(", ")}`);
2031
2405
  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
2406
  }
2407
+ const serverId = options?.serverId || configId || generateServerId();
2408
+ console.error(`[one-mcp] Server ID: ${serverId}`);
2033
2409
  const skillsConfig = options?.skills || configSkills;
2034
- const skillService = skillsConfig && skillsConfig.paths.length > 0 ? new SkillService(process.cwd(), skillsConfig.paths) : void 0;
2035
- const describeTools = new DescribeToolsTool(clientManager, skillService);
2036
- const useTool = new UseToolTool(clientManager, skillService);
2410
+ const toolsRef = { describeTools: null };
2411
+ const skillService = skillsConfig && skillsConfig.paths.length > 0 ? new SkillService(process.cwd(), skillsConfig.paths, { onCacheInvalidated: () => {
2412
+ toolsRef.describeTools?.clearAutoDetectedSkillsCache();
2413
+ } }) : void 0;
2414
+ const describeTools = new DescribeToolsTool(clientManager, skillService, serverId);
2415
+ const useTool = new UseToolTool(clientManager, skillService, serverId);
2416
+ toolsRef.describeTools = describeTools;
2417
+ if (skillService) skillService.startWatching().catch((error) => {
2418
+ console.error(`[skill-watcher] File watcher failed (non-critical): ${error instanceof Error ? error.message : "Unknown error"}`);
2419
+ });
2037
2420
  server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [await describeTools.getDefinition(), useTool.getDefinition()] }));
2038
2421
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
2039
2422
  const { name, arguments: args } = request.params;
@@ -2452,4 +2835,4 @@ var HttpTransportHandler = class {
2452
2835
  };
2453
2836
 
2454
2837
  //#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 };
2838
+ 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 };