@agiflowai/one-mcp 0.2.6 → 0.3.0
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.
- package/README.md +87 -5
- package/dist/cli.cjs +474 -11
- package/dist/cli.mjs +475 -12
- package/dist/{http-xSfxBa8A.cjs → http-BYBRKvD4.cjs} +454 -185
- package/dist/{http-D9BDXhHn.mjs → http-Bi2N9PUM.mjs} +456 -187
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +4 -0
- package/dist/index.d.mts +4 -0
- package/dist/index.mjs +1 -1
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
2
|
import { CallToolRequestSchema, GetPromptRequestSchema, ListPromptsRequestSchema, ListToolsRequestSchema, isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
3
|
-
import { access, mkdir, readFile, readdir, stat, unlink, writeFile } from "node:fs/promises";
|
|
3
|
+
import { access, mkdir, readFile, readdir, stat, unlink, watch, writeFile } from "node:fs/promises";
|
|
4
4
|
import { existsSync } from "node:fs";
|
|
5
5
|
import yaml from "js-yaml";
|
|
6
6
|
import { z } from "zod";
|
|
@@ -10,6 +10,7 @@ import { tmpdir } from "node:os";
|
|
|
10
10
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
11
11
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
12
12
|
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
13
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
13
14
|
import { Liquid } from "liquidjs";
|
|
14
15
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
15
16
|
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
@@ -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]);
|
|
@@ -279,6 +282,7 @@ const McpServerConfigSchema = z.discriminatedUnion("transport", [
|
|
|
279
282
|
toolBlacklist: z.array(z.string()).optional(),
|
|
280
283
|
omitToolDescription: z.boolean().optional(),
|
|
281
284
|
prompts: z.record(z.string(), InternalPromptConfigSchema).optional(),
|
|
285
|
+
timeout: z.number().positive().optional(),
|
|
282
286
|
transport: z.literal("stdio"),
|
|
283
287
|
config: McpStdioConfigSchema
|
|
284
288
|
}),
|
|
@@ -288,6 +292,7 @@ const McpServerConfigSchema = z.discriminatedUnion("transport", [
|
|
|
288
292
|
toolBlacklist: z.array(z.string()).optional(),
|
|
289
293
|
omitToolDescription: z.boolean().optional(),
|
|
290
294
|
prompts: z.record(z.string(), InternalPromptConfigSchema).optional(),
|
|
295
|
+
timeout: z.number().positive().optional(),
|
|
291
296
|
transport: z.literal("http"),
|
|
292
297
|
config: McpHttpConfigSchema
|
|
293
298
|
}),
|
|
@@ -297,6 +302,7 @@ const McpServerConfigSchema = z.discriminatedUnion("transport", [
|
|
|
297
302
|
toolBlacklist: z.array(z.string()).optional(),
|
|
298
303
|
omitToolDescription: z.boolean().optional(),
|
|
299
304
|
prompts: z.record(z.string(), InternalPromptConfigSchema).optional(),
|
|
305
|
+
timeout: z.number().positive().optional(),
|
|
300
306
|
transport: z.literal("sse"),
|
|
301
307
|
config: McpSseConfigSchema
|
|
302
308
|
})
|
|
@@ -330,6 +336,7 @@ function transformClaudeCodeConfig(claudeConfig) {
|
|
|
330
336
|
toolBlacklist: stdioConfig.config?.toolBlacklist,
|
|
331
337
|
omitToolDescription: stdioConfig.config?.omitToolDescription,
|
|
332
338
|
prompts: stdioConfig.config?.prompts,
|
|
339
|
+
timeout: stdioConfig.timeout,
|
|
333
340
|
transport: "stdio",
|
|
334
341
|
config: {
|
|
335
342
|
command: interpolatedCommand,
|
|
@@ -348,6 +355,7 @@ function transformClaudeCodeConfig(claudeConfig) {
|
|
|
348
355
|
toolBlacklist: httpConfig.config?.toolBlacklist,
|
|
349
356
|
omitToolDescription: httpConfig.config?.omitToolDescription,
|
|
350
357
|
prompts: httpConfig.config?.prompts,
|
|
358
|
+
timeout: httpConfig.timeout,
|
|
351
359
|
transport,
|
|
352
360
|
config: {
|
|
353
361
|
url: interpolatedUrl,
|
|
@@ -806,6 +814,8 @@ var ConfigFetcherService = class {
|
|
|
806
814
|
|
|
807
815
|
//#endregion
|
|
808
816
|
//#region src/services/McpClientManagerService.ts
|
|
817
|
+
/** Default connection timeout in milliseconds (30 seconds) */
|
|
818
|
+
const DEFAULT_CONNECTION_TIMEOUT_MS = 3e4;
|
|
809
819
|
/**
|
|
810
820
|
* MCP Client wrapper for managing individual server connections
|
|
811
821
|
* This is an internal class used by McpClientManagerService
|
|
@@ -911,8 +921,10 @@ var McpClientManagerService = class {
|
|
|
911
921
|
}
|
|
912
922
|
/**
|
|
913
923
|
* Connect to an MCP server based on its configuration with timeout
|
|
924
|
+
* Uses the timeout from server config, falling back to default (30s)
|
|
914
925
|
*/
|
|
915
|
-
async connectToServer(serverName, config
|
|
926
|
+
async connectToServer(serverName, config) {
|
|
927
|
+
const timeoutMs = config.timeout ?? DEFAULT_CONNECTION_TIMEOUT_MS;
|
|
916
928
|
if (this.clients.has(serverName)) throw new Error(`Client for ${serverName} is already connected`);
|
|
917
929
|
const client = new Client({
|
|
918
930
|
name: `@agiflowai/one-mcp-client`,
|
|
@@ -944,6 +956,7 @@ var McpClientManagerService = class {
|
|
|
944
956
|
*/
|
|
945
957
|
async performConnection(mcpClient, config) {
|
|
946
958
|
if (config.transport === "stdio") await this.connectStdioClient(mcpClient, config.config);
|
|
959
|
+
else if (config.transport === "http") await this.connectHttpClient(mcpClient, config.config);
|
|
947
960
|
else if (config.transport === "sse") await this.connectSseClient(mcpClient, config.config);
|
|
948
961
|
else throw new Error(`Unsupported transport type: ${config.transport}`);
|
|
949
962
|
}
|
|
@@ -957,6 +970,10 @@ var McpClientManagerService = class {
|
|
|
957
970
|
const childProcess = transport["_process"];
|
|
958
971
|
if (childProcess) mcpClient.setChildProcess(childProcess);
|
|
959
972
|
}
|
|
973
|
+
async connectHttpClient(mcpClient, config) {
|
|
974
|
+
const transport = new StreamableHTTPClientTransport(new URL(config.url), { requestInit: config.headers ? { headers: config.headers } : void 0 });
|
|
975
|
+
await mcpClient["client"].connect(transport);
|
|
976
|
+
}
|
|
960
977
|
async connectSseClient(mcpClient, config) {
|
|
961
978
|
const transport = new SSEClientTransport(new URL(config.url));
|
|
962
979
|
await mcpClient["client"].connect(transport);
|
|
@@ -999,6 +1016,209 @@ var McpClientManagerService = class {
|
|
|
999
1016
|
}
|
|
1000
1017
|
};
|
|
1001
1018
|
|
|
1019
|
+
//#endregion
|
|
1020
|
+
//#region src/utils/findConfigFile.ts
|
|
1021
|
+
/**
|
|
1022
|
+
* Config File Finder Utility
|
|
1023
|
+
*
|
|
1024
|
+
* DESIGN PATTERNS:
|
|
1025
|
+
* - Utility function pattern for reusable logic
|
|
1026
|
+
* - Fail-fast pattern with early returns
|
|
1027
|
+
* - Environment variable configuration pattern
|
|
1028
|
+
*
|
|
1029
|
+
* CODING STANDARDS:
|
|
1030
|
+
* - Use sync filesystem operations for config discovery (performance)
|
|
1031
|
+
* - Check PROJECT_PATH environment variable first
|
|
1032
|
+
* - Fall back to current working directory
|
|
1033
|
+
* - Support both .yaml and .json extensions
|
|
1034
|
+
* - Return null if no config file is found
|
|
1035
|
+
*
|
|
1036
|
+
* AVOID:
|
|
1037
|
+
* - Throwing errors (return null instead for optional config)
|
|
1038
|
+
* - Hardcoded file names without extension variants
|
|
1039
|
+
* - Ignoring environment variables
|
|
1040
|
+
*/
|
|
1041
|
+
/**
|
|
1042
|
+
* Find MCP configuration file by checking PROJECT_PATH first, then cwd
|
|
1043
|
+
* Looks for both mcp-config.yaml and mcp-config.json
|
|
1044
|
+
*
|
|
1045
|
+
* @returns Absolute path to config file, or null if not found
|
|
1046
|
+
*/
|
|
1047
|
+
function findConfigFile() {
|
|
1048
|
+
const configFileNames = [
|
|
1049
|
+
"mcp-config.yaml",
|
|
1050
|
+
"mcp-config.yml",
|
|
1051
|
+
"mcp-config.json"
|
|
1052
|
+
];
|
|
1053
|
+
const projectPath = process.env.PROJECT_PATH;
|
|
1054
|
+
if (projectPath) for (const fileName of configFileNames) {
|
|
1055
|
+
const configPath = resolve(projectPath, fileName);
|
|
1056
|
+
if (existsSync(configPath)) return configPath;
|
|
1057
|
+
}
|
|
1058
|
+
const cwd = process.cwd();
|
|
1059
|
+
for (const fileName of configFileNames) {
|
|
1060
|
+
const configPath = join(cwd, fileName);
|
|
1061
|
+
if (existsSync(configPath)) return configPath;
|
|
1062
|
+
}
|
|
1063
|
+
return null;
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
//#endregion
|
|
1067
|
+
//#region src/utils/parseToolName.ts
|
|
1068
|
+
/**
|
|
1069
|
+
* Parse tool name to extract server and actual tool name
|
|
1070
|
+
* Supports both plain tool names and prefixed format: {serverName}__{toolName}
|
|
1071
|
+
*
|
|
1072
|
+
* @param toolName - The tool name to parse (e.g., "my_tool" or "server__my_tool")
|
|
1073
|
+
* @returns Parsed result with optional serverName and actualToolName
|
|
1074
|
+
*
|
|
1075
|
+
* @example
|
|
1076
|
+
* parseToolName("my_tool") // { actualToolName: "my_tool" }
|
|
1077
|
+
* parseToolName("server__my_tool") // { serverName: "server", actualToolName: "my_tool" }
|
|
1078
|
+
*/
|
|
1079
|
+
function parseToolName(toolName) {
|
|
1080
|
+
const separatorIndex = toolName.indexOf("__");
|
|
1081
|
+
if (separatorIndex > 0) return {
|
|
1082
|
+
serverName: toolName.substring(0, separatorIndex),
|
|
1083
|
+
actualToolName: toolName.substring(separatorIndex + 2)
|
|
1084
|
+
};
|
|
1085
|
+
return { actualToolName: toolName };
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
//#endregion
|
|
1089
|
+
//#region src/utils/parseFrontMatter.ts
|
|
1090
|
+
/**
|
|
1091
|
+
* Parses YAML front matter from a string content.
|
|
1092
|
+
* Front matter must be at the start of the content, delimited by `---`.
|
|
1093
|
+
*
|
|
1094
|
+
* Supports:
|
|
1095
|
+
* - Simple key: value pairs
|
|
1096
|
+
* - Literal block scalar (|) for multi-line preserving newlines
|
|
1097
|
+
* - Folded block scalar (>) for multi-line folding to single line
|
|
1098
|
+
*
|
|
1099
|
+
* @param content - The content string that may contain front matter
|
|
1100
|
+
* @returns Object with parsed front matter (or null) and remaining content
|
|
1101
|
+
*
|
|
1102
|
+
* @example
|
|
1103
|
+
* const result = parseFrontMatter(`---
|
|
1104
|
+
* name: my-skill
|
|
1105
|
+
* description: A skill description
|
|
1106
|
+
* ---
|
|
1107
|
+
* The actual content here`);
|
|
1108
|
+
* // result.frontMatter = { name: 'my-skill', description: 'A skill description' }
|
|
1109
|
+
* // result.content = 'The actual content here'
|
|
1110
|
+
*
|
|
1111
|
+
* @example
|
|
1112
|
+
* // Multi-line with literal block scalar
|
|
1113
|
+
* const result = parseFrontMatter(`---
|
|
1114
|
+
* name: my-skill
|
|
1115
|
+
* description: |
|
|
1116
|
+
* Line 1
|
|
1117
|
+
* Line 2
|
|
1118
|
+
* ---
|
|
1119
|
+
* Content`);
|
|
1120
|
+
* // result.frontMatter.description = 'Line 1\nLine 2'
|
|
1121
|
+
*/
|
|
1122
|
+
function parseFrontMatter(content) {
|
|
1123
|
+
const trimmedContent = content.trimStart();
|
|
1124
|
+
if (!trimmedContent.startsWith("---")) return {
|
|
1125
|
+
frontMatter: null,
|
|
1126
|
+
content
|
|
1127
|
+
};
|
|
1128
|
+
const endDelimiterIndex = trimmedContent.indexOf("\n---", 3);
|
|
1129
|
+
if (endDelimiterIndex === -1) return {
|
|
1130
|
+
frontMatter: null,
|
|
1131
|
+
content
|
|
1132
|
+
};
|
|
1133
|
+
const yamlContent = trimmedContent.slice(4, endDelimiterIndex).trim();
|
|
1134
|
+
if (!yamlContent) return {
|
|
1135
|
+
frontMatter: null,
|
|
1136
|
+
content
|
|
1137
|
+
};
|
|
1138
|
+
const frontMatter = {};
|
|
1139
|
+
const lines = yamlContent.split("\n");
|
|
1140
|
+
let currentKey = null;
|
|
1141
|
+
let currentValue = [];
|
|
1142
|
+
let multiLineMode = null;
|
|
1143
|
+
let baseIndent = 0;
|
|
1144
|
+
const saveCurrentKey = () => {
|
|
1145
|
+
if (currentKey && currentValue.length > 0) if (multiLineMode === "literal") frontMatter[currentKey] = currentValue.join("\n").trimEnd();
|
|
1146
|
+
else if (multiLineMode === "folded") frontMatter[currentKey] = currentValue.join(" ").trim();
|
|
1147
|
+
else frontMatter[currentKey] = currentValue.join("").trim();
|
|
1148
|
+
currentKey = null;
|
|
1149
|
+
currentValue = [];
|
|
1150
|
+
multiLineMode = null;
|
|
1151
|
+
baseIndent = 0;
|
|
1152
|
+
};
|
|
1153
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1154
|
+
const line = lines[i];
|
|
1155
|
+
const trimmedLine = line.trim();
|
|
1156
|
+
const colonIndex = line.indexOf(":");
|
|
1157
|
+
if (colonIndex !== -1 && !line.startsWith(" ") && !line.startsWith(" ")) {
|
|
1158
|
+
saveCurrentKey();
|
|
1159
|
+
const key = line.slice(0, colonIndex).trim();
|
|
1160
|
+
let value = line.slice(colonIndex + 1).trim();
|
|
1161
|
+
if (value === "|" || value === "|-") {
|
|
1162
|
+
currentKey = key;
|
|
1163
|
+
multiLineMode = "literal";
|
|
1164
|
+
if (i + 1 < lines.length) {
|
|
1165
|
+
const match = lines[i + 1].match(/^(\s+)/);
|
|
1166
|
+
baseIndent = match ? match[1].length : 2;
|
|
1167
|
+
}
|
|
1168
|
+
} else if (value === ">" || value === ">-") {
|
|
1169
|
+
currentKey = key;
|
|
1170
|
+
multiLineMode = "folded";
|
|
1171
|
+
if (i + 1 < lines.length) {
|
|
1172
|
+
const match = lines[i + 1].match(/^(\s+)/);
|
|
1173
|
+
baseIndent = match ? match[1].length : 2;
|
|
1174
|
+
}
|
|
1175
|
+
} else {
|
|
1176
|
+
if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
|
|
1177
|
+
if (key && value) frontMatter[key] = value;
|
|
1178
|
+
}
|
|
1179
|
+
} else if (multiLineMode && currentKey) {
|
|
1180
|
+
const lineIndent = line.match(/^(\s*)/)?.[1].length || 0;
|
|
1181
|
+
if (trimmedLine === "") currentValue.push("");
|
|
1182
|
+
else if (lineIndent >= baseIndent) {
|
|
1183
|
+
const unindentedLine = line.slice(baseIndent);
|
|
1184
|
+
currentValue.push(unindentedLine);
|
|
1185
|
+
} else saveCurrentKey();
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
saveCurrentKey();
|
|
1189
|
+
return {
|
|
1190
|
+
frontMatter,
|
|
1191
|
+
content: trimmedContent.slice(endDelimiterIndex + 4).trimStart()
|
|
1192
|
+
};
|
|
1193
|
+
}
|
|
1194
|
+
/**
|
|
1195
|
+
* Checks if parsed front matter contains valid skill metadata.
|
|
1196
|
+
* A valid skill front matter must have both `name` and `description` fields.
|
|
1197
|
+
*
|
|
1198
|
+
* @param frontMatter - The parsed front matter object
|
|
1199
|
+
* @returns True if front matter contains valid skill metadata
|
|
1200
|
+
*/
|
|
1201
|
+
function isValidSkillFrontMatter(frontMatter) {
|
|
1202
|
+
return frontMatter !== null && typeof frontMatter.name === "string" && frontMatter.name.length > 0 && typeof frontMatter.description === "string" && frontMatter.description.length > 0;
|
|
1203
|
+
}
|
|
1204
|
+
/**
|
|
1205
|
+
* Extracts skill front matter from content if present and valid.
|
|
1206
|
+
*
|
|
1207
|
+
* @param content - The content string that may contain skill front matter
|
|
1208
|
+
* @returns Object with skill metadata and content, or null if no valid skill front matter
|
|
1209
|
+
*/
|
|
1210
|
+
function extractSkillFrontMatter(content) {
|
|
1211
|
+
const { frontMatter, content: remainingContent } = parseFrontMatter(content);
|
|
1212
|
+
if (frontMatter && isValidSkillFrontMatter(frontMatter)) return {
|
|
1213
|
+
skill: {
|
|
1214
|
+
name: frontMatter.name,
|
|
1215
|
+
description: frontMatter.description
|
|
1216
|
+
},
|
|
1217
|
+
content: remainingContent
|
|
1218
|
+
};
|
|
1219
|
+
return null;
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1002
1222
|
//#endregion
|
|
1003
1223
|
//#region src/services/SkillService.ts
|
|
1004
1224
|
/**
|
|
@@ -1069,14 +1289,21 @@ var SkillService = class {
|
|
|
1069
1289
|
skillPaths;
|
|
1070
1290
|
cachedSkills = null;
|
|
1071
1291
|
skillsByName = null;
|
|
1292
|
+
/** Active file watchers for skill directories */
|
|
1293
|
+
watchers = [];
|
|
1294
|
+
/** Callback invoked when cache is invalidated due to file changes */
|
|
1295
|
+
onCacheInvalidated;
|
|
1072
1296
|
/**
|
|
1073
1297
|
* Creates a new SkillService instance
|
|
1074
1298
|
* @param cwd - Current working directory for resolving relative paths
|
|
1075
1299
|
* @param skillPaths - Array of paths to skills directories
|
|
1300
|
+
* @param options - Optional configuration
|
|
1301
|
+
* @param options.onCacheInvalidated - Callback invoked when cache is invalidated due to file changes
|
|
1076
1302
|
*/
|
|
1077
|
-
constructor(cwd, skillPaths) {
|
|
1303
|
+
constructor(cwd, skillPaths, options) {
|
|
1078
1304
|
this.cwd = cwd;
|
|
1079
1305
|
this.skillPaths = skillPaths;
|
|
1306
|
+
this.onCacheInvalidated = options?.onCacheInvalidated;
|
|
1080
1307
|
}
|
|
1081
1308
|
/**
|
|
1082
1309
|
* Get all available skills from configured directories.
|
|
@@ -1122,6 +1349,54 @@ var SkillService = class {
|
|
|
1122
1349
|
this.skillsByName = null;
|
|
1123
1350
|
}
|
|
1124
1351
|
/**
|
|
1352
|
+
* Starts watching skill directories for changes to SKILL.md files.
|
|
1353
|
+
* When changes are detected, the cache is automatically invalidated.
|
|
1354
|
+
*
|
|
1355
|
+
* Uses Node.js fs.watch with recursive option for efficient directory monitoring.
|
|
1356
|
+
* Only invalidates cache when SKILL.md files are modified.
|
|
1357
|
+
*
|
|
1358
|
+
* @example
|
|
1359
|
+
* const skillService = new SkillService(cwd, skillPaths, {
|
|
1360
|
+
* onCacheInvalidated: () => console.log('Skills cache invalidated')
|
|
1361
|
+
* });
|
|
1362
|
+
* await skillService.startWatching();
|
|
1363
|
+
*/
|
|
1364
|
+
async startWatching() {
|
|
1365
|
+
this.stopWatching();
|
|
1366
|
+
for (const skillPath of this.skillPaths) {
|
|
1367
|
+
const skillsDir = isAbsolute(skillPath) ? skillPath : join(this.cwd, skillPath);
|
|
1368
|
+
if (!await pathExists(skillsDir)) continue;
|
|
1369
|
+
const abortController = new AbortController();
|
|
1370
|
+
this.watchers.push(abortController);
|
|
1371
|
+
this.watchDirectory(skillsDir, abortController.signal).catch((error) => {
|
|
1372
|
+
if (error?.name !== "AbortError") console.error(`[skill-watcher] Error watching ${skillsDir}: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
1373
|
+
});
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
/**
|
|
1377
|
+
* Stops all active file watchers.
|
|
1378
|
+
* Should be called when the service is being disposed.
|
|
1379
|
+
*/
|
|
1380
|
+
stopWatching() {
|
|
1381
|
+
for (const controller of this.watchers) controller.abort();
|
|
1382
|
+
this.watchers = [];
|
|
1383
|
+
}
|
|
1384
|
+
/**
|
|
1385
|
+
* Watches a directory for changes to SKILL.md files.
|
|
1386
|
+
* @param dirPath - Directory path to watch
|
|
1387
|
+
* @param signal - AbortSignal to stop watching
|
|
1388
|
+
*/
|
|
1389
|
+
async watchDirectory(dirPath, signal) {
|
|
1390
|
+
const watcher = watch(dirPath, {
|
|
1391
|
+
recursive: true,
|
|
1392
|
+
signal
|
|
1393
|
+
});
|
|
1394
|
+
for await (const event of watcher) if (event.filename && event.filename.endsWith("SKILL.md")) {
|
|
1395
|
+
this.clearCache();
|
|
1396
|
+
this.onCacheInvalidated?.();
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
/**
|
|
1125
1400
|
* Load skills from a directory.
|
|
1126
1401
|
* Supports both flat structure (SKILL.md) and nested structure (name/SKILL.md).
|
|
1127
1402
|
*
|
|
@@ -1180,6 +1455,7 @@ var SkillService = class {
|
|
|
1180
1455
|
}
|
|
1181
1456
|
/**
|
|
1182
1457
|
* Load a single skill file and parse its frontmatter.
|
|
1458
|
+
* Supports multi-line YAML values using literal (|) and folded (>) block scalars.
|
|
1183
1459
|
*
|
|
1184
1460
|
* @param filePath - Path to the SKILL.md file
|
|
1185
1461
|
* @param location - Whether this is a 'project' or 'user' skill
|
|
@@ -1193,150 +1469,62 @@ var SkillService = class {
|
|
|
1193
1469
|
* // Returns null if frontmatter is missing name or description
|
|
1194
1470
|
*/
|
|
1195
1471
|
async loadSkillFile(filePath, location) {
|
|
1196
|
-
let
|
|
1472
|
+
let fileContent;
|
|
1197
1473
|
try {
|
|
1198
|
-
|
|
1474
|
+
fileContent = await readFile(filePath, "utf-8");
|
|
1199
1475
|
} catch (error) {
|
|
1200
1476
|
throw new SkillLoadError(`Failed to read skill file: ${error instanceof Error ? error.message : "Unknown error"}`, filePath, error instanceof Error ? error : void 0);
|
|
1201
1477
|
}
|
|
1202
|
-
const {
|
|
1203
|
-
if (!
|
|
1478
|
+
const { frontMatter, content } = parseFrontMatter(fileContent);
|
|
1479
|
+
if (!frontMatter || !frontMatter.name || !frontMatter.description) return null;
|
|
1204
1480
|
return {
|
|
1205
|
-
name:
|
|
1206
|
-
description:
|
|
1481
|
+
name: frontMatter.name,
|
|
1482
|
+
description: frontMatter.description,
|
|
1207
1483
|
location,
|
|
1208
|
-
content
|
|
1484
|
+
content,
|
|
1209
1485
|
basePath: dirname(filePath)
|
|
1210
1486
|
};
|
|
1211
1487
|
}
|
|
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
1488
|
};
|
|
1256
1489
|
|
|
1257
1490
|
//#endregion
|
|
1258
|
-
//#region src/
|
|
1491
|
+
//#region src/constants/index.ts
|
|
1259
1492
|
/**
|
|
1260
|
-
*
|
|
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
|
|
1493
|
+
* Shared constants for one-mcp package
|
|
1278
1494
|
*/
|
|
1279
1495
|
/**
|
|
1280
|
-
*
|
|
1281
|
-
*
|
|
1282
|
-
*
|
|
1283
|
-
* @returns Absolute path to config file, or null if not found
|
|
1496
|
+
* Prefix added to skill names when they clash with MCP tool names.
|
|
1497
|
+
* This ensures skills can be uniquely identified even when a tool has the same name.
|
|
1284
1498
|
*/
|
|
1285
|
-
|
|
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
|
|
1499
|
+
const SKILL_PREFIX$1 = "skill__";
|
|
1306
1500
|
/**
|
|
1307
|
-
*
|
|
1308
|
-
*
|
|
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" }
|
|
1501
|
+
* Log prefix for skill detection messages.
|
|
1502
|
+
* Used to easily filter skill detection logs in stderr output.
|
|
1316
1503
|
*/
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
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";
|
|
1504
|
+
const LOG_PREFIX_SKILL_DETECTION = "[skill-detection]";
|
|
1505
|
+
/**
|
|
1506
|
+
* Prefix for prompt-based skill locations.
|
|
1507
|
+
* Format: "prompt:{serverName}:{promptName}"
|
|
1508
|
+
*/
|
|
1509
|
+
const PROMPT_LOCATION_PREFIX = "prompt:";
|
|
1329
1510
|
|
|
1330
1511
|
//#endregion
|
|
1331
|
-
//#region src/templates/
|
|
1332
|
-
var
|
|
1512
|
+
//#region src/templates/toolkit-description.liquid?raw
|
|
1513
|
+
var toolkit_description_default = "<toolkit>\n<instruction>\nBefore you use any capabilities below, you MUST call this tool with a list of names to learn how to use them properly; this includes:\n- For tools: Arguments schema needed to pass to use_tool\n- For skills: Detailed instructions that will expand when invoked (Prefer to be explored first when relevant)\n\nThis tool is optimized for batch queries - you can request multiple capabilities at once for better performance.\n\nHow to invoke:\n- For MCP tools: Use use_tool with toolName and toolArgs based on the schema\n- For skills: Use this tool with the skill name to get expanded instructions\n</instruction>\n\n<available_capabilities>\n{% for server in servers -%}\n<group name=\"{{ server.name }}\">\n{% if server.instruction -%}\n<group_instruction>{{ server.instruction }}</group_instruction>\n{% endif -%}\n{% if server.omitToolDescription -%}\n{% for toolName in server.toolNames -%}\n<item name=\"{{ toolName }}\"></item>\n{% endfor -%}\n{% else -%}\n{% for tool in server.tools -%}\n<item name=\"{{ tool.displayName }}\"><description>{{ tool.description | default: \"No description\" }}</description></item>\n{% endfor -%}\n{% endif -%}\n</group>\n{% endfor -%}\n{% if skills.size > 0 -%}\n<group name=\"skills\">\n{% for skill in skills -%}\n<item name=\"{{ skill.displayName }}\"><description>{{ skill.description }}</description></item>\n{% endfor -%}\n</group>\n{% endif -%}\n</available_capabilities>\n</toolkit>\n";
|
|
1333
1514
|
|
|
1334
1515
|
//#endregion
|
|
1335
1516
|
//#region src/tools/DescribeToolsTool.ts
|
|
1336
1517
|
/**
|
|
1337
|
-
*
|
|
1518
|
+
* Formats skill instructions with the loading command message prefix.
|
|
1519
|
+
* This message is used by Claude Code to indicate that a skill is being loaded.
|
|
1520
|
+
*
|
|
1521
|
+
* @param name - The skill name
|
|
1522
|
+
* @param instructions - The raw skill instructions/content
|
|
1523
|
+
* @returns Formatted instructions with command message prefix
|
|
1338
1524
|
*/
|
|
1339
|
-
|
|
1525
|
+
function formatSkillInstructions(name, instructions) {
|
|
1526
|
+
return `<command-message>The "${name}" skill is loading</command-message>\n${instructions}`;
|
|
1527
|
+
}
|
|
1340
1528
|
/**
|
|
1341
1529
|
* DescribeToolsTool provides progressive disclosure of MCP tools and skills.
|
|
1342
1530
|
*
|
|
@@ -1359,6 +1547,8 @@ var DescribeToolsTool = class DescribeToolsTool {
|
|
|
1359
1547
|
clientManager;
|
|
1360
1548
|
skillService;
|
|
1361
1549
|
liquid = new Liquid();
|
|
1550
|
+
/** Cache for auto-detected skills from prompt front-matter */
|
|
1551
|
+
autoDetectedSkillsCache = null;
|
|
1362
1552
|
/**
|
|
1363
1553
|
* Creates a new DescribeToolsTool instance
|
|
1364
1554
|
* @param clientManager - The MCP client manager for accessing remote servers
|
|
@@ -1369,12 +1559,79 @@ var DescribeToolsTool = class DescribeToolsTool {
|
|
|
1369
1559
|
this.skillService = skillService;
|
|
1370
1560
|
}
|
|
1371
1561
|
/**
|
|
1562
|
+
* Clears the cached auto-detected skills from prompt front-matter.
|
|
1563
|
+
* Use this when prompt configurations may have changed or when
|
|
1564
|
+
* the skill service cache is invalidated.
|
|
1565
|
+
*/
|
|
1566
|
+
clearAutoDetectedSkillsCache() {
|
|
1567
|
+
this.autoDetectedSkillsCache = null;
|
|
1568
|
+
}
|
|
1569
|
+
/**
|
|
1570
|
+
* Detects and caches skills from prompt front-matter across all connected MCP servers.
|
|
1571
|
+
* Fetches all prompts and checks their content for YAML front-matter with name/description.
|
|
1572
|
+
* Results are cached to avoid repeated fetches.
|
|
1573
|
+
*
|
|
1574
|
+
* Error Handling Strategy:
|
|
1575
|
+
* - Errors are logged to stderr but do not fail the overall detection process
|
|
1576
|
+
* - This ensures partial results are returned even if some servers/prompts fail
|
|
1577
|
+
* - Common failure reasons: server temporarily unavailable, prompt requires arguments,
|
|
1578
|
+
* network timeout, or server doesn't support listPrompts
|
|
1579
|
+
* - Errors are prefixed with [skill-detection] for easy filtering in logs
|
|
1580
|
+
*
|
|
1581
|
+
* @returns Array of auto-detected skills from prompt front-matter
|
|
1582
|
+
*/
|
|
1583
|
+
async detectSkillsFromPromptFrontMatter() {
|
|
1584
|
+
if (this.autoDetectedSkillsCache !== null) return this.autoDetectedSkillsCache;
|
|
1585
|
+
const clients = this.clientManager.getAllClients();
|
|
1586
|
+
const autoDetectedSkills = [];
|
|
1587
|
+
let listPromptsFailures = 0;
|
|
1588
|
+
let fetchPromptFailures = 0;
|
|
1589
|
+
const fetchPromises = [];
|
|
1590
|
+
for (const client of clients) {
|
|
1591
|
+
const configuredPromptNames = new Set(client.prompts ? Object.keys(client.prompts) : []);
|
|
1592
|
+
const listPromptsPromise = (async () => {
|
|
1593
|
+
try {
|
|
1594
|
+
const prompts = await client.listPrompts();
|
|
1595
|
+
if (!prompts || prompts.length === 0) return;
|
|
1596
|
+
const promptFetchPromises = prompts.map(async (promptInfo) => {
|
|
1597
|
+
if (configuredPromptNames.has(promptInfo.name)) return;
|
|
1598
|
+
try {
|
|
1599
|
+
const skillExtraction = extractSkillFrontMatter(((await client.getPrompt(promptInfo.name)).messages || []).map((m) => {
|
|
1600
|
+
const content = m.content;
|
|
1601
|
+
if (typeof content === "string") return content;
|
|
1602
|
+
if (content && typeof content === "object" && "text" in content) return String(content.text);
|
|
1603
|
+
return "";
|
|
1604
|
+
}).join("\n"));
|
|
1605
|
+
if (skillExtraction) autoDetectedSkills.push({
|
|
1606
|
+
serverName: client.serverName,
|
|
1607
|
+
promptName: promptInfo.name,
|
|
1608
|
+
skill: skillExtraction.skill
|
|
1609
|
+
});
|
|
1610
|
+
} catch (error) {
|
|
1611
|
+
fetchPromptFailures++;
|
|
1612
|
+
console.error(`${LOG_PREFIX_SKILL_DETECTION} Failed to fetch prompt '${promptInfo.name}' from ${client.serverName}: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
1613
|
+
}
|
|
1614
|
+
});
|
|
1615
|
+
await Promise.all(promptFetchPromises);
|
|
1616
|
+
} catch (error) {
|
|
1617
|
+
listPromptsFailures++;
|
|
1618
|
+
console.error(`${LOG_PREFIX_SKILL_DETECTION} Failed to list prompts from ${client.serverName}: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
1619
|
+
}
|
|
1620
|
+
})();
|
|
1621
|
+
fetchPromises.push(listPromptsPromise);
|
|
1622
|
+
}
|
|
1623
|
+
await Promise.all(fetchPromises);
|
|
1624
|
+
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).`);
|
|
1625
|
+
this.autoDetectedSkillsCache = autoDetectedSkills;
|
|
1626
|
+
return autoDetectedSkills;
|
|
1627
|
+
}
|
|
1628
|
+
/**
|
|
1372
1629
|
* Collects skills derived from prompt configurations across all connected MCP servers.
|
|
1373
|
-
*
|
|
1630
|
+
* Includes both explicitly configured prompts and auto-detected skills from front-matter.
|
|
1374
1631
|
*
|
|
1375
1632
|
* @returns Array of skill template data derived from prompts
|
|
1376
1633
|
*/
|
|
1377
|
-
collectPromptSkills() {
|
|
1634
|
+
async collectPromptSkills() {
|
|
1378
1635
|
const clients = this.clientManager.getAllClients();
|
|
1379
1636
|
const promptSkills = [];
|
|
1380
1637
|
for (const client of clients) {
|
|
@@ -1385,16 +1642,22 @@ var DescribeToolsTool = class DescribeToolsTool {
|
|
|
1385
1642
|
description: promptConfig.skill.description
|
|
1386
1643
|
});
|
|
1387
1644
|
}
|
|
1645
|
+
const autoDetectedSkills = await this.detectSkillsFromPromptFrontMatter();
|
|
1646
|
+
for (const autoSkill of autoDetectedSkills) promptSkills.push({
|
|
1647
|
+
name: autoSkill.skill.name,
|
|
1648
|
+
displayName: autoSkill.skill.name,
|
|
1649
|
+
description: autoSkill.skill.description
|
|
1650
|
+
});
|
|
1388
1651
|
return promptSkills;
|
|
1389
1652
|
}
|
|
1390
1653
|
/**
|
|
1391
1654
|
* Finds a prompt-based skill by name from all connected MCP servers.
|
|
1392
|
-
*
|
|
1655
|
+
* Searches both explicitly configured prompts and auto-detected skills from front-matter.
|
|
1393
1656
|
*
|
|
1394
1657
|
* @param skillName - The skill name to search for
|
|
1395
1658
|
* @returns Object with serverName, promptName, and skill config, or undefined if not found
|
|
1396
1659
|
*/
|
|
1397
|
-
findPromptSkill(skillName) {
|
|
1660
|
+
async findPromptSkill(skillName) {
|
|
1398
1661
|
if (!skillName) return void 0;
|
|
1399
1662
|
const clients = this.clientManager.getAllClients();
|
|
1400
1663
|
for (const client of clients) {
|
|
@@ -1405,16 +1668,24 @@ var DescribeToolsTool = class DescribeToolsTool {
|
|
|
1405
1668
|
skill: promptConfig.skill
|
|
1406
1669
|
};
|
|
1407
1670
|
}
|
|
1671
|
+
const autoDetectedSkills = await this.detectSkillsFromPromptFrontMatter();
|
|
1672
|
+
for (const autoSkill of autoDetectedSkills) if (autoSkill.skill.name === skillName) return {
|
|
1673
|
+
serverName: autoSkill.serverName,
|
|
1674
|
+
promptName: autoSkill.promptName,
|
|
1675
|
+
skill: autoSkill.skill,
|
|
1676
|
+
autoDetected: true
|
|
1677
|
+
};
|
|
1408
1678
|
}
|
|
1409
1679
|
/**
|
|
1410
1680
|
* Retrieves skill content from a prompt-based skill configuration.
|
|
1411
1681
|
* Fetches the prompt from the MCP server and extracts text content.
|
|
1682
|
+
* Handles both explicitly configured prompts and auto-detected skills from front-matter.
|
|
1412
1683
|
*
|
|
1413
1684
|
* @param skillName - The skill name being requested
|
|
1414
1685
|
* @returns SkillDescription if found and successfully fetched, undefined otherwise
|
|
1415
1686
|
*/
|
|
1416
1687
|
async getPromptSkillContent(skillName) {
|
|
1417
|
-
const promptSkill = this.findPromptSkill(skillName);
|
|
1688
|
+
const promptSkill = await this.findPromptSkill(skillName);
|
|
1418
1689
|
if (!promptSkill) return void 0;
|
|
1419
1690
|
const client = this.clientManager.getClient(promptSkill.serverName);
|
|
1420
1691
|
if (!client) {
|
|
@@ -1422,7 +1693,7 @@ var DescribeToolsTool = class DescribeToolsTool {
|
|
|
1422
1693
|
return;
|
|
1423
1694
|
}
|
|
1424
1695
|
try {
|
|
1425
|
-
const
|
|
1696
|
+
const rawInstructions = (await client.getPrompt(promptSkill.promptName)).messages?.map((m) => {
|
|
1426
1697
|
const content = m.content;
|
|
1427
1698
|
if (typeof content === "string") return content;
|
|
1428
1699
|
if (content && typeof content === "object" && "text" in content) return String(content.text);
|
|
@@ -1430,8 +1701,8 @@ var DescribeToolsTool = class DescribeToolsTool {
|
|
|
1430
1701
|
}).join("\n") || "";
|
|
1431
1702
|
return {
|
|
1432
1703
|
name: promptSkill.skill.name,
|
|
1433
|
-
location: promptSkill.skill.folder ||
|
|
1434
|
-
instructions
|
|
1704
|
+
location: promptSkill.skill.folder || `${PROMPT_LOCATION_PREFIX}${promptSkill.serverName}/${promptSkill.promptName}`,
|
|
1705
|
+
instructions: formatSkillInstructions(promptSkill.skill.name, rawInstructions)
|
|
1435
1706
|
};
|
|
1436
1707
|
} catch (error) {
|
|
1437
1708
|
console.error(`Failed to get prompt-based skill '${skillName}': ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
@@ -1439,49 +1710,18 @@ var DescribeToolsTool = class DescribeToolsTool {
|
|
|
1439
1710
|
}
|
|
1440
1711
|
}
|
|
1441
1712
|
/**
|
|
1442
|
-
* Builds the
|
|
1713
|
+
* Builds the combined toolkit description using a single Liquid template.
|
|
1443
1714
|
*
|
|
1444
|
-
*
|
|
1445
|
-
*
|
|
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.
|
|
1715
|
+
* Collects all tools from connected MCP servers and all skills, then renders
|
|
1716
|
+
* them together using the toolkit-description.liquid template.
|
|
1478
1717
|
*
|
|
1479
1718
|
* Tool names are prefixed with serverName__ when the same tool exists
|
|
1480
|
-
* on multiple servers
|
|
1719
|
+
* on multiple servers. Skill names are prefixed with skill__ when they
|
|
1720
|
+
* clash with MCP tools or other skills.
|
|
1481
1721
|
*
|
|
1482
|
-
* @returns Object with rendered
|
|
1722
|
+
* @returns Object with rendered description and set of all tool names
|
|
1483
1723
|
*/
|
|
1484
|
-
async
|
|
1724
|
+
async buildToolkitDescription() {
|
|
1485
1725
|
const clients = this.clientManager.getAllClients();
|
|
1486
1726
|
const toolToServers = /* @__PURE__ */ new Map();
|
|
1487
1727
|
const serverToolsMap = /* @__PURE__ */ new Map();
|
|
@@ -1502,9 +1742,6 @@ var DescribeToolsTool = class DescribeToolsTool {
|
|
|
1502
1742
|
}));
|
|
1503
1743
|
/**
|
|
1504
1744
|
* 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
1745
|
*/
|
|
1509
1746
|
const formatToolName = (toolName, serverName) => {
|
|
1510
1747
|
return (toolToServers.get(toolName) || []).length > 1 ? `${serverName}__${toolName}` : toolName;
|
|
@@ -1524,31 +1761,56 @@ var DescribeToolsTool = class DescribeToolsTool {
|
|
|
1524
1761
|
toolNames: formattedTools.map((t) => t.displayName)
|
|
1525
1762
|
};
|
|
1526
1763
|
});
|
|
1764
|
+
const rawSkills = this.skillService ? await this.skillService.getSkills() : [];
|
|
1765
|
+
const promptSkills = await this.collectPromptSkills();
|
|
1766
|
+
const seenSkillNames = /* @__PURE__ */ new Set();
|
|
1767
|
+
const allSkillsData = [];
|
|
1768
|
+
for (const skill of rawSkills) if (!seenSkillNames.has(skill.name)) {
|
|
1769
|
+
seenSkillNames.add(skill.name);
|
|
1770
|
+
allSkillsData.push({
|
|
1771
|
+
name: skill.name,
|
|
1772
|
+
displayName: skill.name,
|
|
1773
|
+
description: skill.description
|
|
1774
|
+
});
|
|
1775
|
+
}
|
|
1776
|
+
for (const skill of promptSkills) if (!seenSkillNames.has(skill.name)) {
|
|
1777
|
+
seenSkillNames.add(skill.name);
|
|
1778
|
+
allSkillsData.push(skill);
|
|
1779
|
+
}
|
|
1780
|
+
const skills = allSkillsData.map((skill) => {
|
|
1781
|
+
const clashesWithMcpTool = allToolNames.has(skill.name);
|
|
1782
|
+
return {
|
|
1783
|
+
name: skill.name,
|
|
1784
|
+
displayName: clashesWithMcpTool ? `${SKILL_PREFIX$1}${skill.name}` : skill.name,
|
|
1785
|
+
description: skill.description
|
|
1786
|
+
};
|
|
1787
|
+
});
|
|
1527
1788
|
return {
|
|
1528
|
-
content: await this.liquid.parseAndRender(
|
|
1789
|
+
content: await this.liquid.parseAndRender(toolkit_description_default, {
|
|
1790
|
+
servers,
|
|
1791
|
+
skills
|
|
1792
|
+
}),
|
|
1529
1793
|
toolNames: allToolNames
|
|
1530
1794
|
};
|
|
1531
1795
|
}
|
|
1532
1796
|
/**
|
|
1533
|
-
* Gets the tool definition including available
|
|
1797
|
+
* Gets the tool definition including available tools and skills in a unified format.
|
|
1534
1798
|
*
|
|
1535
1799
|
* The definition includes:
|
|
1536
|
-
* -
|
|
1537
|
-
* -
|
|
1538
|
-
* -
|
|
1800
|
+
* - All MCP tools from connected servers
|
|
1801
|
+
* - All available skills (file-based and prompt-based)
|
|
1802
|
+
* - Unified instructions for querying capability details
|
|
1539
1803
|
*
|
|
1540
|
-
* Tool names are prefixed with serverName__ when
|
|
1541
|
-
*
|
|
1804
|
+
* Tool names are prefixed with serverName__ when clashing.
|
|
1805
|
+
* Skill names are prefixed with skill__ when clashing.
|
|
1542
1806
|
*
|
|
1543
1807
|
* @returns Tool definition with description and input schema
|
|
1544
1808
|
*/
|
|
1545
1809
|
async getDefinition() {
|
|
1546
|
-
const
|
|
1547
|
-
const skillsSection = await this.buildSkillsSection(serversResult.toolNames);
|
|
1810
|
+
const { content } = await this.buildToolkitDescription();
|
|
1548
1811
|
return {
|
|
1549
1812
|
name: DescribeToolsTool.TOOL_NAME,
|
|
1550
|
-
description:
|
|
1551
|
-
${skillsSection}`,
|
|
1813
|
+
description: content,
|
|
1552
1814
|
inputSchema: {
|
|
1553
1815
|
type: "object",
|
|
1554
1816
|
properties: { toolNames: {
|
|
@@ -1613,14 +1875,14 @@ ${skillsSection}`,
|
|
|
1613
1875
|
const notFoundItems = [];
|
|
1614
1876
|
for (const requestedName of toolNames) {
|
|
1615
1877
|
if (requestedName.startsWith(SKILL_PREFIX$1)) {
|
|
1616
|
-
const skillName = requestedName.slice(
|
|
1878
|
+
const skillName = requestedName.slice(SKILL_PREFIX$1.length);
|
|
1617
1879
|
if (this.skillService) {
|
|
1618
1880
|
const skill = await this.skillService.getSkill(skillName);
|
|
1619
1881
|
if (skill) {
|
|
1620
1882
|
foundSkills.push({
|
|
1621
1883
|
name: skill.name,
|
|
1622
1884
|
location: skill.basePath,
|
|
1623
|
-
instructions: skill.content
|
|
1885
|
+
instructions: formatSkillInstructions(skill.name, skill.content)
|
|
1624
1886
|
});
|
|
1625
1887
|
continue;
|
|
1626
1888
|
}
|
|
@@ -1659,7 +1921,7 @@ ${skillsSection}`,
|
|
|
1659
1921
|
foundSkills.push({
|
|
1660
1922
|
name: skill.name,
|
|
1661
1923
|
location: skill.basePath,
|
|
1662
|
-
instructions: skill.content
|
|
1924
|
+
instructions: formatSkillInstructions(skill.name, skill.content)
|
|
1663
1925
|
});
|
|
1664
1926
|
continue;
|
|
1665
1927
|
}
|
|
@@ -2031,9 +2293,16 @@ async function createServer(options) {
|
|
|
2031
2293
|
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
2294
|
}
|
|
2033
2295
|
const skillsConfig = options?.skills || configSkills;
|
|
2034
|
-
const
|
|
2296
|
+
const toolsRef = { describeTools: null };
|
|
2297
|
+
const skillService = skillsConfig && skillsConfig.paths.length > 0 ? new SkillService(process.cwd(), skillsConfig.paths, { onCacheInvalidated: () => {
|
|
2298
|
+
toolsRef.describeTools?.clearAutoDetectedSkillsCache();
|
|
2299
|
+
} }) : void 0;
|
|
2035
2300
|
const describeTools = new DescribeToolsTool(clientManager, skillService);
|
|
2036
2301
|
const useTool = new UseToolTool(clientManager, skillService);
|
|
2302
|
+
toolsRef.describeTools = describeTools;
|
|
2303
|
+
if (skillService) skillService.startWatching().catch((error) => {
|
|
2304
|
+
console.error(`[skill-watcher] File watcher failed (non-critical): ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
2305
|
+
});
|
|
2037
2306
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [await describeTools.getDefinition(), useTool.getDefinition()] }));
|
|
2038
2307
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
2039
2308
|
const { name, arguments: args } = request.params;
|
|
@@ -2452,4 +2721,4 @@ var HttpTransportHandler = class {
|
|
|
2452
2721
|
};
|
|
2453
2722
|
|
|
2454
2723
|
//#endregion
|
|
2455
|
-
export {
|
|
2724
|
+
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 };
|