@agiflowai/one-mcp 0.2.3 → 0.2.5

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, ListToolsRequestSchema, isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
3
- import { mkdir, readFile, readdir, unlink, writeFile } from "node:fs/promises";
3
+ import { access, mkdir, readFile, readdir, stat, unlink, 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
7
  import { createHash, randomUUID } from "node:crypto";
8
- import { join, resolve } from "node:path";
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 { Liquid } from "liquidjs";
13
14
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
14
15
  import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
15
16
  import express from "express";
@@ -216,11 +217,16 @@ const RemoteConfigSourceSchema = z.object({
216
217
  ]).optional()
217
218
  });
218
219
  /**
220
+ * Skills configuration schema
221
+ */
222
+ const SkillsConfigSchema = z.object({ paths: z.array(z.string()) });
223
+ /**
219
224
  * Full Claude Code MCP configuration schema
220
225
  */
221
226
  const ClaudeCodeMcpConfigSchema = z.object({
222
227
  mcpServers: z.record(z.string(), ClaudeCodeServerConfigSchema),
223
- remoteConfigs: z.array(RemoteConfigSourceSchema).optional()
228
+ remoteConfigs: z.array(RemoteConfigSourceSchema).optional(),
229
+ skills: SkillsConfigSchema.optional()
224
230
  });
225
231
  /**
226
232
  * Internal MCP config format
@@ -268,7 +274,10 @@ const McpServerConfigSchema = z.discriminatedUnion("transport", [
268
274
  /**
269
275
  * Full internal MCP configuration schema
270
276
  */
271
- const InternalMcpConfigSchema = z.object({ mcpServers: z.record(z.string(), McpServerConfigSchema) });
277
+ const InternalMcpConfigSchema = z.object({
278
+ mcpServers: z.record(z.string(), McpServerConfigSchema),
279
+ skills: SkillsConfigSchema.optional()
280
+ });
272
281
  /**
273
282
  * Transform Claude Code config to internal format
274
283
  * Converts standard Claude Code MCP configuration to normalized internal format
@@ -315,7 +324,10 @@ function transformClaudeCodeConfig(claudeConfig) {
315
324
  };
316
325
  }
317
326
  }
318
- return { mcpServers: transformedServers };
327
+ return {
328
+ mcpServers: transformedServers,
329
+ skills: claudeConfig.skills
330
+ };
319
331
  }
320
332
  /**
321
333
  * Parse and validate MCP config from raw JSON
@@ -951,6 +963,261 @@ var McpClientManagerService = class {
951
963
  }
952
964
  };
953
965
 
966
+ //#endregion
967
+ //#region src/services/SkillService.ts
968
+ /**
969
+ * SkillService
970
+ *
971
+ * DESIGN PATTERNS:
972
+ * - Service pattern for business logic encapsulation
973
+ * - Single responsibility principle
974
+ * - Lazy loading pattern for skill discovery
975
+ *
976
+ * CODING STANDARDS:
977
+ * - Use async/await for asynchronous operations
978
+ * - Throw descriptive errors for error cases
979
+ * - Keep methods focused and well-named
980
+ * - Document complex logic with comments
981
+ *
982
+ * AVOID:
983
+ * - Mixing concerns (keep focused on single domain)
984
+ * - Direct tool implementation (services should be tool-agnostic)
985
+ */
986
+ /**
987
+ * Error thrown when skill loading fails
988
+ */
989
+ var SkillLoadError = class extends Error {
990
+ constructor(message, filePath, cause) {
991
+ super(message);
992
+ this.filePath = filePath;
993
+ this.cause = cause;
994
+ this.name = "SkillLoadError";
995
+ }
996
+ };
997
+ /**
998
+ * Check if a path exists asynchronously
999
+ * @param path - Path to check
1000
+ * @returns true if path exists, false otherwise
1001
+ * @throws Error for unexpected filesystem errors (permission denied, etc.)
1002
+ */
1003
+ async function pathExists(path) {
1004
+ try {
1005
+ await access(path);
1006
+ return true;
1007
+ } catch (error) {
1008
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") return false;
1009
+ throw new Error(`Failed to check path existence for "${path}": ${error instanceof Error ? error.message : "Unknown error"}`);
1010
+ }
1011
+ }
1012
+ /**
1013
+ * Service for loading and managing skills from configured skill directories.
1014
+ *
1015
+ * Skills are markdown files with YAML frontmatter that can be invoked via
1016
+ * the skill__ prefix in describe_tools and use_tool.
1017
+ *
1018
+ * Skills are only enabled when explicitly configured via the `skills.paths` array
1019
+ * in the MCP config.
1020
+ *
1021
+ * @example
1022
+ * // Config with skills enabled:
1023
+ * // skills:
1024
+ * // paths:
1025
+ * // - ".claude/skills"
1026
+ * // - "/absolute/path/to/skills"
1027
+ *
1028
+ * const skillService = new SkillService('/project/root', ['.claude/skills']);
1029
+ * const skills = await skillService.getSkills();
1030
+ */
1031
+ var SkillService = class {
1032
+ cwd;
1033
+ skillPaths;
1034
+ cachedSkills = null;
1035
+ skillsByName = null;
1036
+ /**
1037
+ * Creates a new SkillService instance
1038
+ * @param cwd - Current working directory for resolving relative paths
1039
+ * @param skillPaths - Array of paths to skills directories
1040
+ */
1041
+ constructor(cwd, skillPaths) {
1042
+ this.cwd = cwd;
1043
+ this.skillPaths = skillPaths;
1044
+ }
1045
+ /**
1046
+ * Get all available skills from configured directories.
1047
+ * Results are cached after first load.
1048
+ *
1049
+ * Skills from earlier entries in the config take precedence over
1050
+ * skills with the same name from later entries.
1051
+ *
1052
+ * @returns Array of loaded skills
1053
+ * @throws SkillLoadError if a critical error occurs during loading
1054
+ */
1055
+ async getSkills() {
1056
+ if (this.cachedSkills !== null) return this.cachedSkills;
1057
+ const skills = [];
1058
+ const loadedSkillNames = /* @__PURE__ */ new Set();
1059
+ for (const skillPath of this.skillPaths) {
1060
+ const skillsDir = isAbsolute(skillPath) ? skillPath : join(this.cwd, skillPath);
1061
+ const dirSkills = await this.loadSkillsFromDirectory(skillsDir, "project");
1062
+ for (const skill of dirSkills) if (!loadedSkillNames.has(skill.name)) {
1063
+ skills.push(skill);
1064
+ loadedSkillNames.add(skill.name);
1065
+ }
1066
+ }
1067
+ this.cachedSkills = skills;
1068
+ this.skillsByName = new Map(skills.map((skill) => [skill.name, skill]));
1069
+ return skills;
1070
+ }
1071
+ /**
1072
+ * Get a specific skill by name with O(1) lookup from cache.
1073
+ * @param name - The skill name (without skill__ prefix)
1074
+ * @returns The skill if found, undefined otherwise
1075
+ */
1076
+ async getSkill(name) {
1077
+ if (this.skillsByName === null) await this.getSkills();
1078
+ return this.skillsByName?.get(name);
1079
+ }
1080
+ /**
1081
+ * Clears the cached skills to force a fresh reload on the next getSkills() or getSkill() call.
1082
+ * Use this when skill files have been modified on disk.
1083
+ */
1084
+ clearCache() {
1085
+ this.cachedSkills = null;
1086
+ this.skillsByName = null;
1087
+ }
1088
+ /**
1089
+ * Load skills from a directory.
1090
+ * Supports both flat structure (SKILL.md) and nested structure (name/SKILL.md).
1091
+ *
1092
+ * @param dirPath - Path to the skills directory
1093
+ * @param location - Whether this is a 'project' or 'user' skill directory
1094
+ * @returns Array of successfully loaded skills (skips invalid skills)
1095
+ * @throws SkillLoadError if there's a critical I/O error
1096
+ *
1097
+ * @example
1098
+ * // Load skills from project directory
1099
+ * const skills = await this.loadSkillsFromDirectory('/path/to/.claude/skills', 'project');
1100
+ * // Returns: [{ name: 'pdf', description: '...', location: 'project', ... }]
1101
+ */
1102
+ async loadSkillsFromDirectory(dirPath, location) {
1103
+ const skills = [];
1104
+ try {
1105
+ if (!await pathExists(dirPath)) return skills;
1106
+ } catch (error) {
1107
+ throw new SkillLoadError(`Cannot access skills directory: ${error instanceof Error ? error.message : "Unknown error"}`, dirPath, error instanceof Error ? error : void 0);
1108
+ }
1109
+ let entries;
1110
+ try {
1111
+ entries = await readdir(dirPath);
1112
+ } catch (error) {
1113
+ throw new SkillLoadError(`Failed to read skills directory: ${error instanceof Error ? error.message : "Unknown error"}`, dirPath, error instanceof Error ? error : void 0);
1114
+ }
1115
+ for (const entry of entries) {
1116
+ const entryPath = join(dirPath, entry);
1117
+ let entryStat;
1118
+ try {
1119
+ entryStat = await stat(entryPath);
1120
+ } catch (error) {
1121
+ console.warn(`Skipping entry ${entryPath}: ${error instanceof Error ? error.message : "Unknown error"}`);
1122
+ continue;
1123
+ }
1124
+ if (entryStat.isDirectory()) {
1125
+ const skillFilePath = join(entryPath, "SKILL.md");
1126
+ try {
1127
+ if (await pathExists(skillFilePath)) {
1128
+ const skill = await this.loadSkillFile(skillFilePath, location);
1129
+ if (skill) skills.push(skill);
1130
+ }
1131
+ } catch (error) {
1132
+ console.warn(`Skipping skill at ${skillFilePath}: ${error instanceof Error ? error.message : "Unknown error"}`);
1133
+ continue;
1134
+ }
1135
+ } else if (entry === "SKILL.md") try {
1136
+ const skill = await this.loadSkillFile(entryPath, location);
1137
+ if (skill) skills.push(skill);
1138
+ } catch (error) {
1139
+ console.warn(`Skipping skill at ${entryPath}: ${error instanceof Error ? error.message : "Unknown error"}`);
1140
+ continue;
1141
+ }
1142
+ }
1143
+ return skills;
1144
+ }
1145
+ /**
1146
+ * Load a single skill file and parse its frontmatter.
1147
+ *
1148
+ * @param filePath - Path to the SKILL.md file
1149
+ * @param location - Whether this is a 'project' or 'user' skill
1150
+ * @returns The loaded skill, or null if the file is invalid (missing required frontmatter)
1151
+ * @throws SkillLoadError if there's an I/O error reading the file
1152
+ *
1153
+ * @example
1154
+ * // Load a skill from a file
1155
+ * const skill = await this.loadSkillFile('/path/to/pdf/SKILL.md', 'project');
1156
+ * // Returns: { name: 'pdf', description: 'PDF skill', location: 'project', content: '...', basePath: '/path/to/pdf' }
1157
+ * // Returns null if frontmatter is missing name or description
1158
+ */
1159
+ async loadSkillFile(filePath, location) {
1160
+ let content;
1161
+ try {
1162
+ content = await readFile(filePath, "utf-8");
1163
+ } catch (error) {
1164
+ throw new SkillLoadError(`Failed to read skill file: ${error instanceof Error ? error.message : "Unknown error"}`, filePath, error instanceof Error ? error : void 0);
1165
+ }
1166
+ const { metadata, body } = this.parseFrontmatter(content);
1167
+ if (!metadata.name || !metadata.description) return null;
1168
+ return {
1169
+ name: metadata.name,
1170
+ description: metadata.description,
1171
+ location,
1172
+ content: body,
1173
+ basePath: dirname(filePath)
1174
+ };
1175
+ }
1176
+ /**
1177
+ * Parse YAML frontmatter from markdown content.
1178
+ * Frontmatter is delimited by --- at start and end.
1179
+ *
1180
+ * @param content - Full markdown content with frontmatter
1181
+ * @returns Parsed metadata and body content
1182
+ *
1183
+ * @example
1184
+ * // Input content:
1185
+ * // ---
1186
+ * // name: my-skill
1187
+ * // description: A sample skill
1188
+ * // ---
1189
+ * // # Skill Content
1190
+ * // This is the skill body.
1191
+ *
1192
+ * const result = parseFrontmatter(content);
1193
+ * // result.metadata = { name: 'my-skill', description: 'A sample skill' }
1194
+ * // result.body = '# Skill Content\nThis is the skill body.'
1195
+ */
1196
+ parseFrontmatter(content) {
1197
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
1198
+ if (!match) return {
1199
+ metadata: {},
1200
+ body: content
1201
+ };
1202
+ const [, frontmatter, body] = match;
1203
+ const metadata = {};
1204
+ const lines = frontmatter.split("\n");
1205
+ for (const line of lines) {
1206
+ const colonIndex = line.indexOf(":");
1207
+ if (colonIndex > 0) {
1208
+ const key = line.slice(0, colonIndex).trim();
1209
+ let value = line.slice(colonIndex + 1).trim();
1210
+ if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
1211
+ if (key === "name" || key === "description" || key === "license") metadata[key] = value;
1212
+ }
1213
+ }
1214
+ return {
1215
+ metadata,
1216
+ body: body.trim()
1217
+ };
1218
+ }
1219
+ };
1220
+
954
1221
  //#endregion
955
1222
  //#region src/utils/findConfigFile.ts
956
1223
  /**
@@ -1020,15 +1287,89 @@ function parseToolName(toolName) {
1020
1287
  return { actualToolName: toolName };
1021
1288
  }
1022
1289
 
1290
+ //#endregion
1291
+ //#region src/templates/skills-description.liquid?raw
1292
+ 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<skill-item><name>{{ skill.displayName }}</name><description>{{ skill.description }}</description></skill-item>\n{% endfor -%}\n</available_skills>\n</skills>\n{% endif %}\n\n<usage_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</usage_instructions>\n";
1293
+
1294
+ //#endregion
1295
+ //#region src/templates/mcp-servers-description.liquid?raw
1296
+ var mcp_servers_description_default = "<mcp_servers>\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<tool-item><name>{{ tool.displayName }}</name><description>{{ tool.description | default: \"No description\" }}</description></tool-item>\n{% endfor -%}\n{% endif -%}\n</tools>\n</server>\n{% endfor -%}\n</mcp_servers>\n";
1297
+
1023
1298
  //#endregion
1024
1299
  //#region src/tools/DescribeToolsTool.ts
1300
+ /**
1301
+ * Prefix used to identify skill invocations
1302
+ */
1303
+ const SKILL_PREFIX$1 = "skill__";
1304
+ /**
1305
+ * DescribeToolsTool provides progressive disclosure of MCP tools and skills.
1306
+ *
1307
+ * This tool lists available tools from all connected MCP servers and skills
1308
+ * from the configured skills directories. Users can query for specific tools
1309
+ * or skills to get detailed input schemas and descriptions.
1310
+ *
1311
+ * Tool naming conventions:
1312
+ * - Unique tools: use plain name (e.g., "browser_click")
1313
+ * - Clashing tools: use serverName__toolName format (e.g., "playwright__click")
1314
+ * - Skills: use skill__skillName format (e.g., "skill__pdf")
1315
+ *
1316
+ * @example
1317
+ * const tool = new DescribeToolsTool(clientManager, skillService);
1318
+ * const definition = await tool.getDefinition();
1319
+ * const result = await tool.execute({ toolNames: ['browser_click', 'skill__pdf'] });
1320
+ */
1025
1321
  var DescribeToolsTool = class DescribeToolsTool {
1026
1322
  static TOOL_NAME = "describe_tools";
1027
1323
  clientManager;
1028
- constructor(clientManager) {
1324
+ skillService;
1325
+ liquid = new Liquid();
1326
+ /**
1327
+ * Creates a new DescribeToolsTool instance
1328
+ * @param clientManager - The MCP client manager for accessing remote servers
1329
+ * @param skillService - Optional skill service for loading skills
1330
+ */
1331
+ constructor(clientManager, skillService) {
1029
1332
  this.clientManager = clientManager;
1333
+ this.skillService = skillService;
1030
1334
  }
1031
- async getDefinition() {
1335
+ /**
1336
+ * Builds the skills section of the tool description using a Liquid template.
1337
+ *
1338
+ * Retrieves all available skills from the SkillService and renders them
1339
+ * using the skills-description.liquid template. Skills are only prefixed
1340
+ * with skill__ when their name clashes with an MCP tool or another skill.
1341
+ *
1342
+ * @param mcpToolNames - Set of MCP tool names to check for clashes
1343
+ * @returns Rendered skills section string with available skills list
1344
+ */
1345
+ async buildSkillsSection(mcpToolNames) {
1346
+ const rawSkills = this.skillService ? await this.skillService.getSkills() : [];
1347
+ const skillNameCounts = /* @__PURE__ */ new Map();
1348
+ for (const skill of rawSkills) skillNameCounts.set(skill.name, (skillNameCounts.get(skill.name) || 0) + 1);
1349
+ const skills = rawSkills.map((skill) => {
1350
+ const clashesWithMcpTool = mcpToolNames.has(skill.name);
1351
+ const clashesWithOtherSkill = (skillNameCounts.get(skill.name) || 0) > 1;
1352
+ const needsPrefix = clashesWithMcpTool || clashesWithOtherSkill;
1353
+ return {
1354
+ name: skill.name,
1355
+ displayName: needsPrefix ? `${SKILL_PREFIX$1}${skill.name}` : skill.name,
1356
+ description: skill.description
1357
+ };
1358
+ });
1359
+ return this.liquid.parseAndRender(skills_description_default, { skills });
1360
+ }
1361
+ /**
1362
+ * Builds the MCP servers section of the tool description using a Liquid template.
1363
+ *
1364
+ * Collects all tools from connected MCP servers, detects name clashes,
1365
+ * and renders them using the mcp-servers-description.liquid template.
1366
+ *
1367
+ * Tool names are prefixed with serverName__ when the same tool exists
1368
+ * on multiple servers to avoid ambiguity.
1369
+ *
1370
+ * @returns Object with rendered servers section and set of all tool names for skill clash detection
1371
+ */
1372
+ async buildServersSection() {
1032
1373
  const clients = this.clientManager.getAllClients();
1033
1374
  const toolToServers = /* @__PURE__ */ new Map();
1034
1375
  const serverToolsMap = /* @__PURE__ */ new Map();
@@ -1047,30 +1388,63 @@ var DescribeToolsTool = class DescribeToolsTool {
1047
1388
  serverToolsMap.set(client.serverName, []);
1048
1389
  }
1049
1390
  }));
1050
- const serverDescriptions = clients.map((client) => {
1051
- const tools = serverToolsMap.get(client.serverName) || [];
1052
- const formatToolName = (toolName) => {
1053
- return (toolToServers.get(toolName) || []).length > 1 ? `${client.serverName}__${toolName}` : toolName;
1391
+ /**
1392
+ * Formats tool name with server prefix if the tool exists on multiple servers
1393
+ * @param toolName - The original tool name
1394
+ * @param serverName - The server providing this tool
1395
+ * @returns Tool name prefixed with serverName__ if clashing, otherwise plain name
1396
+ */
1397
+ const formatToolName = (toolName, serverName) => {
1398
+ return (toolToServers.get(toolName) || []).length > 1 ? `${serverName}__${toolName}` : toolName;
1399
+ };
1400
+ const allToolNames = /* @__PURE__ */ new Set();
1401
+ const servers = clients.map((client) => {
1402
+ const formattedTools = (serverToolsMap.get(client.serverName) || []).map((t) => ({
1403
+ displayName: formatToolName(t.name, client.serverName),
1404
+ description: t.description
1405
+ }));
1406
+ for (const tool of formattedTools) allToolNames.add(tool.displayName);
1407
+ return {
1408
+ name: client.serverName,
1409
+ instruction: client.serverInstruction,
1410
+ omitToolDescription: client.omitToolDescription || false,
1411
+ tools: formattedTools,
1412
+ toolNames: formattedTools.map((t) => t.displayName)
1054
1413
  };
1055
- const toolList = client.omitToolDescription ? tools.map((t) => formatToolName(t.name)).join(", ") : tools.map((t) => `${formatToolName(t.name)}: """${t.description || "No description"}"""`).join("\n");
1056
- const instructionLine = client.serverInstruction ? `\n"""${client.serverInstruction}"""` : "";
1057
- return `\n\n### Server: ${client.serverName}${instructionLine}\n\n- Available tools:\n${toolList || "No tools available"}`;
1058
1414
  });
1415
+ return {
1416
+ content: await this.liquid.parseAndRender(mcp_servers_description_default, { servers }),
1417
+ toolNames: allToolNames
1418
+ };
1419
+ }
1420
+ /**
1421
+ * Gets the tool definition including available servers, tools, and skills.
1422
+ *
1423
+ * The definition includes:
1424
+ * - List of all connected MCP servers with their available tools
1425
+ * - List of available skills with skill__ prefix
1426
+ * - Usage instructions for querying tool/skill details
1427
+ *
1428
+ * Tool names are prefixed with serverName__ when the same tool name
1429
+ * exists on multiple servers to avoid ambiguity.
1430
+ *
1431
+ * @returns Tool definition with description and input schema
1432
+ */
1433
+ async getDefinition() {
1434
+ const serversResult = await this.buildServersSection();
1435
+ const skillsSection = await this.buildSkillsSection(serversResult.toolNames);
1059
1436
  return {
1060
1437
  name: DescribeToolsTool.TOOL_NAME,
1061
- description: `## Available MCP Servers:${serverDescriptions.join("")}
1062
-
1063
- ## Usage:
1064
- Before 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:
1065
- - Arguments schema needed to pass to the tool use
1066
- - Description about each tool
1067
-
1068
- This tool is optimized for batch queries - you can request multiple tools at once for better performance.`,
1438
+ description: `${serversResult.content}
1439
+ ${skillsSection}`,
1069
1440
  inputSchema: {
1070
1441
  type: "object",
1071
1442
  properties: { toolNames: {
1072
1443
  type: "array",
1073
- items: { type: "string" },
1444
+ items: {
1445
+ type: "string",
1446
+ minLength: 1
1447
+ },
1074
1448
  description: "List of tool names to get detailed information about",
1075
1449
  minItems: 1
1076
1450
  } },
@@ -1079,6 +1453,17 @@ This tool is optimized for batch queries - you can request multiple tools at onc
1079
1453
  }
1080
1454
  };
1081
1455
  }
1456
+ /**
1457
+ * Executes tool description lookup for the requested tool and skill names.
1458
+ *
1459
+ * Handles three types of lookups:
1460
+ * 1. skill__name - Returns skill information from SkillService
1461
+ * 2. serverName__toolName - Returns tool from specific server
1462
+ * 3. plainToolName - Returns tool(s) from all servers (multiple if clashing)
1463
+ *
1464
+ * @param input - Object containing toolNames array
1465
+ * @returns CallToolResult with tool/skill descriptions or error
1466
+ */
1082
1467
  async execute(input) {
1083
1468
  try {
1084
1469
  const { toolNames } = input;
@@ -1096,9 +1481,13 @@ This tool is optimized for batch queries - you can request multiple tools at onc
1096
1481
  try {
1097
1482
  const tools = await client.listTools();
1098
1483
  const blacklist = new Set(client.toolBlacklist || []);
1099
- const filteredTools = tools.filter((t) => !blacklist.has(t.name));
1100
- serverToolsMap.set(client.serverName, filteredTools);
1101
- for (const tool of filteredTools) {
1484
+ const typedTools = tools.filter((t) => !blacklist.has(t.name)).map((t) => ({
1485
+ name: t.name,
1486
+ description: t.description,
1487
+ inputSchema: t.inputSchema
1488
+ }));
1489
+ serverToolsMap.set(client.serverName, typedTools);
1490
+ for (const tool of typedTools) {
1102
1491
  if (!toolToServers.has(tool.name)) toolToServers.set(tool.name, []);
1103
1492
  toolToServers.get(tool.name).push(client.serverName);
1104
1493
  }
@@ -1108,13 +1497,27 @@ This tool is optimized for batch queries - you can request multiple tools at onc
1108
1497
  }
1109
1498
  }));
1110
1499
  const foundTools = [];
1111
- const notFoundTools = [];
1112
- for (const requestedToolName of toolNames) {
1113
- const { serverName, actualToolName } = parseToolName(requestedToolName);
1500
+ const foundSkills = [];
1501
+ const notFoundItems = [];
1502
+ for (const requestedName of toolNames) {
1503
+ if (requestedName.startsWith(SKILL_PREFIX$1)) {
1504
+ const skillName = requestedName.slice(7);
1505
+ if (this.skillService) {
1506
+ const skill = await this.skillService.getSkill(skillName);
1507
+ if (skill) foundSkills.push({
1508
+ name: skill.name,
1509
+ location: skill.basePath,
1510
+ instructions: skill.content
1511
+ });
1512
+ else notFoundItems.push(requestedName);
1513
+ } else notFoundItems.push(requestedName);
1514
+ continue;
1515
+ }
1516
+ const { serverName, actualToolName } = parseToolName(requestedName);
1114
1517
  if (serverName) {
1115
1518
  const serverTools = serverToolsMap.get(serverName);
1116
1519
  if (!serverTools) {
1117
- notFoundTools.push(requestedToolName);
1520
+ notFoundItems.push(requestedName);
1118
1521
  continue;
1119
1522
  }
1120
1523
  const tool = serverTools.find((t) => t.name === actualToolName);
@@ -1126,11 +1529,22 @@ This tool is optimized for batch queries - you can request multiple tools at onc
1126
1529
  inputSchema: tool.inputSchema
1127
1530
  }
1128
1531
  });
1129
- else notFoundTools.push(requestedToolName);
1532
+ else notFoundItems.push(requestedName);
1130
1533
  } else {
1131
1534
  const servers = toolToServers.get(actualToolName);
1132
1535
  if (!servers || servers.length === 0) {
1133
- notFoundTools.push(requestedToolName);
1536
+ if (this.skillService) {
1537
+ const skill = await this.skillService.getSkill(actualToolName);
1538
+ if (skill) {
1539
+ foundSkills.push({
1540
+ name: skill.name,
1541
+ location: skill.basePath,
1542
+ instructions: skill.content
1543
+ });
1544
+ continue;
1545
+ }
1546
+ }
1547
+ notFoundItems.push(requestedName);
1134
1548
  continue;
1135
1549
  }
1136
1550
  if (servers.length === 1) {
@@ -1157,17 +1571,27 @@ This tool is optimized for batch queries - you can request multiple tools at onc
1157
1571
  }
1158
1572
  }
1159
1573
  }
1160
- if (foundTools.length === 0) return {
1574
+ if (foundTools.length === 0 && foundSkills.length === 0) return {
1161
1575
  content: [{
1162
1576
  type: "text",
1163
- text: `None of the requested tools found on any connected server.\nRequested: ${toolNames.join(", ")}\nUse describe_tools to see available tools.`
1577
+ text: `None of the requested tools/skills found.\nRequested: ${toolNames.join(", ")}\nUse describe_tools to see available tools and skills.`
1164
1578
  }],
1165
1579
  isError: true
1166
1580
  };
1167
- const result = { tools: foundTools };
1168
- if (notFoundTools.length > 0) {
1169
- result.notFound = notFoundTools;
1170
- result.warnings = [`Tools not found: ${notFoundTools.join(", ")}`];
1581
+ const result = {};
1582
+ const nextSteps = [];
1583
+ if (foundTools.length > 0) {
1584
+ result.tools = foundTools;
1585
+ nextSteps.push("For MCP tools: Use the use_tool function with toolName and toolArgs based on the inputSchema above.");
1586
+ }
1587
+ if (foundSkills.length > 0) {
1588
+ result.skills = foundSkills;
1589
+ nextSteps.push(`For skill, just follow skill's description to continue.`);
1590
+ }
1591
+ if (nextSteps.length > 0) result.nextSteps = nextSteps;
1592
+ if (notFoundItems.length > 0) {
1593
+ result.notFound = notFoundItems;
1594
+ result.warnings = [`Items not found: ${notFoundItems.join(", ")}`];
1171
1595
  }
1172
1596
  return { content: [{
1173
1597
  type: "text",
@@ -1187,12 +1611,44 @@ This tool is optimized for batch queries - you can request multiple tools at onc
1187
1611
 
1188
1612
  //#endregion
1189
1613
  //#region src/tools/UseToolTool.ts
1614
+ /**
1615
+ * Prefix used to identify skill invocations (e.g., skill__pdf)
1616
+ */
1617
+ const SKILL_PREFIX = "skill__";
1618
+ /**
1619
+ * UseToolTool executes MCP tools and skills with proper error handling.
1620
+ *
1621
+ * This tool supports three invocation patterns:
1622
+ * 1. skill__skillName - Invokes a skill from the configured skills directory
1623
+ * 2. serverName__toolName - Invokes a tool on a specific MCP server
1624
+ * 3. plainToolName - Searches all servers for a unique tool match
1625
+ *
1626
+ * @example
1627
+ * const tool = new UseToolTool(clientManager, skillService);
1628
+ * await tool.execute({ toolName: 'skill__pdf' }); // Invoke a skill
1629
+ * await tool.execute({ toolName: 'playwright__browser_click', toolArgs: { ref: 'btn' } });
1630
+ */
1190
1631
  var UseToolTool = class UseToolTool {
1191
1632
  static TOOL_NAME = "use_tool";
1192
1633
  clientManager;
1193
- constructor(clientManager) {
1634
+ skillService;
1635
+ /**
1636
+ * Creates a new UseToolTool instance
1637
+ * @param clientManager - The MCP client manager for accessing remote servers
1638
+ * @param skillService - Optional skill service for loading and executing skills
1639
+ */
1640
+ constructor(clientManager, skillService) {
1194
1641
  this.clientManager = clientManager;
1642
+ this.skillService = skillService;
1195
1643
  }
1644
+ /**
1645
+ * Returns the MCP tool definition with name, description, and input schema.
1646
+ *
1647
+ * The definition describes how to use this tool to execute MCP tools or skills,
1648
+ * including the skill__ prefix format for skill invocations.
1649
+ *
1650
+ * @returns The tool definition conforming to MCP spec
1651
+ */
1196
1652
  getDefinition() {
1197
1653
  return {
1198
1654
  name: UseToolTool.TOOL_NAME,
@@ -1205,7 +1661,8 @@ var UseToolTool = class UseToolTool {
1205
1661
  properties: {
1206
1662
  toolName: {
1207
1663
  type: "string",
1208
- description: "Name of the tool to execute"
1664
+ description: "Name of the tool to execute",
1665
+ minLength: 1
1209
1666
  },
1210
1667
  toolArgs: {
1211
1668
  type: "object",
@@ -1217,9 +1674,60 @@ var UseToolTool = class UseToolTool {
1217
1674
  }
1218
1675
  };
1219
1676
  }
1677
+ /**
1678
+ * Returns guidance message for skill invocation.
1679
+ *
1680
+ * Skills are not executed via use_tool - they provide instructions that should
1681
+ * be followed directly. This method returns a message directing users to use
1682
+ * describe_tools to get the skill details and follow its instructions.
1683
+ *
1684
+ * @param skill - The skill that was requested
1685
+ * @returns CallToolResult with guidance message
1686
+ */
1687
+ executeSkill(skill) {
1688
+ return { content: [{
1689
+ type: "text",
1690
+ text: `Skill "${skill.name}" found. Skills provide instructions and should not be executed via use_tool.\n\nUse describe_tools to view the skill details at: ${skill.basePath}\n\nThen follow the skill's instructions directly.`
1691
+ }] };
1692
+ }
1693
+ /**
1694
+ * Executes a tool or skill based on the provided input.
1695
+ *
1696
+ * Handles three invocation patterns:
1697
+ * 1. skill__skillName - Routes to skill execution via SkillService
1698
+ * 2. serverName__toolName - Routes to specific MCP server
1699
+ * 3. plainToolName - Searches all servers for unique match
1700
+ *
1701
+ * Edge cases:
1702
+ * - Returns error if skill not found when using skill__ prefix
1703
+ * - Returns error if tool is blacklisted on target server
1704
+ * - Returns disambiguation message if tool exists on multiple servers
1705
+ *
1706
+ * @param input - The tool/skill name and optional arguments
1707
+ * @returns CallToolResult with execution output or error
1708
+ */
1220
1709
  async execute(input) {
1221
1710
  try {
1222
1711
  const { toolName: inputToolName, toolArgs = {} } = input;
1712
+ if (inputToolName.startsWith(SKILL_PREFIX)) {
1713
+ const skillName = inputToolName.slice(7);
1714
+ if (!this.skillService) return {
1715
+ content: [{
1716
+ type: "text",
1717
+ text: `Skills are not configured. Cannot execute skill "${skillName}".`
1718
+ }],
1719
+ isError: true
1720
+ };
1721
+ const skill = await this.skillService.getSkill(skillName);
1722
+ if (!skill) return {
1723
+ content: [{
1724
+ type: "text",
1725
+ text: `Skill "${skillName}" not found. Use describe_tools to see available skills.`
1726
+ }],
1727
+ isError: true
1728
+ };
1729
+ return this.executeSkill(skill);
1730
+ }
1223
1731
  const clients = this.clientManager.getAllClients();
1224
1732
  const { serverName, actualToolName } = parseToolName(inputToolName);
1225
1733
  if (serverName) {
@@ -1261,13 +1769,19 @@ var UseToolTool = class UseToolTool {
1261
1769
  return null;
1262
1770
  }));
1263
1771
  matchingServers.push(...results.filter((r) => r !== null));
1264
- if (matchingServers.length === 0) return {
1265
- content: [{
1266
- type: "text",
1267
- text: `Tool "${actualToolName}" not found on any connected server. Use describe_tools to see available tools.`
1268
- }],
1269
- isError: true
1270
- };
1772
+ if (matchingServers.length === 0) {
1773
+ if (this.skillService) {
1774
+ const skill = await this.skillService.getSkill(actualToolName);
1775
+ if (skill) return this.executeSkill(skill);
1776
+ }
1777
+ return {
1778
+ content: [{
1779
+ type: "text",
1780
+ text: `Tool or skill "${actualToolName}" not found. Use describe_tools to see available tools and skills.`
1781
+ }],
1782
+ isError: true
1783
+ };
1784
+ }
1271
1785
  if (matchingServers.length > 1) return {
1272
1786
  content: [{
1273
1787
  type: "text",
@@ -1328,30 +1842,53 @@ async function createServer(options) {
1328
1842
  version: "0.1.0"
1329
1843
  }, { capabilities: { tools: {} } });
1330
1844
  const clientManager = new McpClientManagerService();
1331
- if (options?.configFilePath) try {
1332
- const config = await new ConfigFetcherService({
1333
- configFilePath: options.configFilePath,
1334
- useCache: !options.noCache
1335
- }).fetchConfiguration(options.noCache || false);
1845
+ let configSkills;
1846
+ if (options?.configFilePath) {
1847
+ let config;
1848
+ try {
1849
+ config = await new ConfigFetcherService({
1850
+ configFilePath: options.configFilePath,
1851
+ useCache: !options.noCache
1852
+ }).fetchConfiguration(options.noCache || false);
1853
+ } catch (error) {
1854
+ throw new Error(`Failed to load MCP configuration from '${options.configFilePath}': ${error instanceof Error ? error.message : String(error)}`);
1855
+ }
1856
+ configSkills = config.skills;
1857
+ const failedConnections = [];
1336
1858
  const connectionPromises = Object.entries(config.mcpServers).map(async ([serverName, serverConfig]) => {
1337
1859
  try {
1338
1860
  await clientManager.connectToServer(serverName, serverConfig);
1339
1861
  console.error(`Connected to MCP server: ${serverName}`);
1340
1862
  } catch (error) {
1863
+ const err = error instanceof Error ? error : new Error(String(error));
1864
+ failedConnections.push({
1865
+ serverName,
1866
+ error: err
1867
+ });
1341
1868
  console.error(`Failed to connect to ${serverName}:`, error);
1342
1869
  }
1343
1870
  });
1344
1871
  await Promise.all(connectionPromises);
1345
- } catch (error) {
1346
- console.error("Failed to load MCP configuration:", error);
1872
+ 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(", ")}`);
1873
+ 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(", ")}`);
1347
1874
  }
1348
- const describeTools = new DescribeToolsTool(clientManager);
1349
- const useTool = new UseToolTool(clientManager);
1875
+ const skillsConfig = options?.skills || configSkills;
1876
+ const skillService = skillsConfig && skillsConfig.paths.length > 0 ? new SkillService(process.cwd(), skillsConfig.paths) : void 0;
1877
+ const describeTools = new DescribeToolsTool(clientManager, skillService);
1878
+ const useTool = new UseToolTool(clientManager, skillService);
1350
1879
  server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [await describeTools.getDefinition(), useTool.getDefinition()] }));
1351
1880
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
1352
1881
  const { name, arguments: args } = request.params;
1353
- if (name === DescribeToolsTool.TOOL_NAME) return await describeTools.execute(args);
1354
- if (name === UseToolTool.TOOL_NAME) return await useTool.execute(args);
1882
+ if (name === DescribeToolsTool.TOOL_NAME) try {
1883
+ return await describeTools.execute(args);
1884
+ } catch (error) {
1885
+ throw new Error(`Failed to execute ${name}: ${error instanceof Error ? error.message : String(error)}`);
1886
+ }
1887
+ if (name === UseToolTool.TOOL_NAME) try {
1888
+ return await useTool.execute(args);
1889
+ } catch (error) {
1890
+ throw new Error(`Failed to execute ${name}: ${error instanceof Error ? error.message : String(error)}`);
1891
+ }
1355
1892
  throw new Error(`Unknown tool: ${name}`);
1356
1893
  });
1357
1894
  return server;
@@ -1703,4 +2240,4 @@ var HttpTransportHandler = class {
1703
2240
  };
1704
2241
 
1705
2242
  //#endregion
1706
- export { findConfigFile as a, createServer as i, SseTransportHandler as n, McpClientManagerService as o, StdioTransportHandler as r, ConfigFetcherService as s, HttpTransportHandler as t };
2243
+ 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 };