@agiflowai/one-mcp 0.2.5 → 0.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +177 -0
- package/dist/cli.cjs +2 -2
- package/dist/cli.mjs +2 -2
- package/dist/{http-Q8LPwwwP.cjs → http-B4NAfsQl.cjs} +653 -183
- package/dist/{http-BKDyW8YB.mjs → http-DSkkpGJU.mjs} +656 -186
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +23 -1
- package/dist/index.d.mts +23 -1
- package/dist/index.mjs +1 -1
- package/package.json +4 -4
|
@@ -38,6 +38,7 @@ let node_os = require("node:os");
|
|
|
38
38
|
let __modelcontextprotocol_sdk_client_index_js = require("@modelcontextprotocol/sdk/client/index.js");
|
|
39
39
|
let __modelcontextprotocol_sdk_client_stdio_js = require("@modelcontextprotocol/sdk/client/stdio.js");
|
|
40
40
|
let __modelcontextprotocol_sdk_client_sse_js = require("@modelcontextprotocol/sdk/client/sse.js");
|
|
41
|
+
let __modelcontextprotocol_sdk_client_streamableHttp_js = require("@modelcontextprotocol/sdk/client/streamableHttp.js");
|
|
41
42
|
let liquidjs = require("liquidjs");
|
|
42
43
|
let __modelcontextprotocol_sdk_server_stdio_js = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
43
44
|
let __modelcontextprotocol_sdk_server_sse_js = require("@modelcontextprotocol/sdk/server/sse.js");
|
|
@@ -204,10 +205,25 @@ function validateRemoteConfigSource(source) {
|
|
|
204
205
|
* Claude Code / Claude Desktop standard MCP config format
|
|
205
206
|
* This is the format users write in their config files
|
|
206
207
|
*/
|
|
208
|
+
/**
|
|
209
|
+
* Prompt skill configuration schema
|
|
210
|
+
* Converts a prompt to an executable skill
|
|
211
|
+
*/
|
|
212
|
+
const PromptSkillConfigSchema = zod.z.object({
|
|
213
|
+
name: zod.z.string(),
|
|
214
|
+
description: zod.z.string(),
|
|
215
|
+
folder: zod.z.string().optional()
|
|
216
|
+
});
|
|
217
|
+
/**
|
|
218
|
+
* Prompt configuration schema
|
|
219
|
+
* Supports converting prompts to skills
|
|
220
|
+
*/
|
|
221
|
+
const PromptConfigSchema = zod.z.object({ skill: PromptSkillConfigSchema.optional() });
|
|
207
222
|
const AdditionalConfigSchema = zod.z.object({
|
|
208
223
|
instruction: zod.z.string().optional(),
|
|
209
224
|
toolBlacklist: zod.z.array(zod.z.string()).optional(),
|
|
210
|
-
omitToolDescription: zod.z.boolean().optional()
|
|
225
|
+
omitToolDescription: zod.z.boolean().optional(),
|
|
226
|
+
prompts: zod.z.record(zod.z.string(), PromptConfigSchema).optional()
|
|
211
227
|
}).optional();
|
|
212
228
|
const ClaudeCodeStdioServerSchema = zod.z.object({
|
|
213
229
|
command: zod.z.string(),
|
|
@@ -274,12 +290,25 @@ const McpSseConfigSchema = zod.z.object({
|
|
|
274
290
|
url: zod.z.string().url(),
|
|
275
291
|
headers: zod.z.record(zod.z.string(), zod.z.string()).optional()
|
|
276
292
|
});
|
|
293
|
+
/**
|
|
294
|
+
* Internal prompt skill configuration schema
|
|
295
|
+
*/
|
|
296
|
+
const InternalPromptSkillConfigSchema = zod.z.object({
|
|
297
|
+
name: zod.z.string(),
|
|
298
|
+
description: zod.z.string(),
|
|
299
|
+
folder: zod.z.string().optional()
|
|
300
|
+
});
|
|
301
|
+
/**
|
|
302
|
+
* Internal prompt configuration schema
|
|
303
|
+
*/
|
|
304
|
+
const InternalPromptConfigSchema = zod.z.object({ skill: InternalPromptSkillConfigSchema.optional() });
|
|
277
305
|
const McpServerConfigSchema = zod.z.discriminatedUnion("transport", [
|
|
278
306
|
zod.z.object({
|
|
279
307
|
name: zod.z.string(),
|
|
280
308
|
instruction: zod.z.string().optional(),
|
|
281
309
|
toolBlacklist: zod.z.array(zod.z.string()).optional(),
|
|
282
310
|
omitToolDescription: zod.z.boolean().optional(),
|
|
311
|
+
prompts: zod.z.record(zod.z.string(), InternalPromptConfigSchema).optional(),
|
|
283
312
|
transport: zod.z.literal("stdio"),
|
|
284
313
|
config: McpStdioConfigSchema
|
|
285
314
|
}),
|
|
@@ -288,6 +317,7 @@ const McpServerConfigSchema = zod.z.discriminatedUnion("transport", [
|
|
|
288
317
|
instruction: zod.z.string().optional(),
|
|
289
318
|
toolBlacklist: zod.z.array(zod.z.string()).optional(),
|
|
290
319
|
omitToolDescription: zod.z.boolean().optional(),
|
|
320
|
+
prompts: zod.z.record(zod.z.string(), InternalPromptConfigSchema).optional(),
|
|
291
321
|
transport: zod.z.literal("http"),
|
|
292
322
|
config: McpHttpConfigSchema
|
|
293
323
|
}),
|
|
@@ -296,6 +326,7 @@ const McpServerConfigSchema = zod.z.discriminatedUnion("transport", [
|
|
|
296
326
|
instruction: zod.z.string().optional(),
|
|
297
327
|
toolBlacklist: zod.z.array(zod.z.string()).optional(),
|
|
298
328
|
omitToolDescription: zod.z.boolean().optional(),
|
|
329
|
+
prompts: zod.z.record(zod.z.string(), InternalPromptConfigSchema).optional(),
|
|
299
330
|
transport: zod.z.literal("sse"),
|
|
300
331
|
config: McpSseConfigSchema
|
|
301
332
|
})
|
|
@@ -328,6 +359,7 @@ function transformClaudeCodeConfig(claudeConfig) {
|
|
|
328
359
|
instruction: stdioConfig.instruction || stdioConfig.config?.instruction,
|
|
329
360
|
toolBlacklist: stdioConfig.config?.toolBlacklist,
|
|
330
361
|
omitToolDescription: stdioConfig.config?.omitToolDescription,
|
|
362
|
+
prompts: stdioConfig.config?.prompts,
|
|
331
363
|
transport: "stdio",
|
|
332
364
|
config: {
|
|
333
365
|
command: interpolatedCommand,
|
|
@@ -345,6 +377,7 @@ function transformClaudeCodeConfig(claudeConfig) {
|
|
|
345
377
|
instruction: httpConfig.instruction || httpConfig.config?.instruction,
|
|
346
378
|
toolBlacklist: httpConfig.config?.toolBlacklist,
|
|
347
379
|
omitToolDescription: httpConfig.config?.omitToolDescription,
|
|
380
|
+
prompts: httpConfig.config?.prompts,
|
|
348
381
|
transport,
|
|
349
382
|
config: {
|
|
350
383
|
url: interpolatedUrl,
|
|
@@ -805,12 +838,14 @@ var ConfigFetcherService = class {
|
|
|
805
838
|
//#region src/services/McpClientManagerService.ts
|
|
806
839
|
/**
|
|
807
840
|
* MCP Client wrapper for managing individual server connections
|
|
841
|
+
* This is an internal class used by McpClientManagerService
|
|
808
842
|
*/
|
|
809
843
|
var McpClient = class {
|
|
810
844
|
serverName;
|
|
811
845
|
serverInstruction;
|
|
812
846
|
toolBlacklist;
|
|
813
847
|
omitToolDescription;
|
|
848
|
+
prompts;
|
|
814
849
|
transport;
|
|
815
850
|
client;
|
|
816
851
|
childProcess;
|
|
@@ -820,6 +855,7 @@ var McpClient = class {
|
|
|
820
855
|
this.serverInstruction = config.instruction;
|
|
821
856
|
this.toolBlacklist = config.toolBlacklist;
|
|
822
857
|
this.omitToolDescription = config.omitToolDescription;
|
|
858
|
+
this.prompts = config.prompts;
|
|
823
859
|
this.transport = transport;
|
|
824
860
|
this.client = client;
|
|
825
861
|
}
|
|
@@ -915,7 +951,8 @@ var McpClientManagerService = class {
|
|
|
915
951
|
const mcpClient = new McpClient(serverName, config.transport, client, {
|
|
916
952
|
instruction: config.instruction,
|
|
917
953
|
toolBlacklist: config.toolBlacklist,
|
|
918
|
-
omitToolDescription: config.omitToolDescription
|
|
954
|
+
omitToolDescription: config.omitToolDescription,
|
|
955
|
+
prompts: config.prompts
|
|
919
956
|
});
|
|
920
957
|
try {
|
|
921
958
|
await Promise.race([this.performConnection(mcpClient, config), new Promise((_, reject) => setTimeout(() => reject(/* @__PURE__ */ new Error(`Connection timeout after ${timeoutMs}ms`)), timeoutMs))]);
|
|
@@ -937,6 +974,7 @@ var McpClientManagerService = class {
|
|
|
937
974
|
*/
|
|
938
975
|
async performConnection(mcpClient, config) {
|
|
939
976
|
if (config.transport === "stdio") await this.connectStdioClient(mcpClient, config.config);
|
|
977
|
+
else if (config.transport === "http") await this.connectHttpClient(mcpClient, config.config);
|
|
940
978
|
else if (config.transport === "sse") await this.connectSseClient(mcpClient, config.config);
|
|
941
979
|
else throw new Error(`Unsupported transport type: ${config.transport}`);
|
|
942
980
|
}
|
|
@@ -950,6 +988,10 @@ var McpClientManagerService = class {
|
|
|
950
988
|
const childProcess = transport["_process"];
|
|
951
989
|
if (childProcess) mcpClient.setChildProcess(childProcess);
|
|
952
990
|
}
|
|
991
|
+
async connectHttpClient(mcpClient, config) {
|
|
992
|
+
const transport = new __modelcontextprotocol_sdk_client_streamableHttp_js.StreamableHTTPClientTransport(new URL(config.url), { requestInit: config.headers ? { headers: config.headers } : void 0 });
|
|
993
|
+
await mcpClient["client"].connect(transport);
|
|
994
|
+
}
|
|
953
995
|
async connectSseClient(mcpClient, config) {
|
|
954
996
|
const transport = new __modelcontextprotocol_sdk_client_sse_js.SSEClientTransport(new URL(config.url));
|
|
955
997
|
await mcpClient["client"].connect(transport);
|
|
@@ -992,6 +1034,209 @@ var McpClientManagerService = class {
|
|
|
992
1034
|
}
|
|
993
1035
|
};
|
|
994
1036
|
|
|
1037
|
+
//#endregion
|
|
1038
|
+
//#region src/utils/findConfigFile.ts
|
|
1039
|
+
/**
|
|
1040
|
+
* Config File Finder Utility
|
|
1041
|
+
*
|
|
1042
|
+
* DESIGN PATTERNS:
|
|
1043
|
+
* - Utility function pattern for reusable logic
|
|
1044
|
+
* - Fail-fast pattern with early returns
|
|
1045
|
+
* - Environment variable configuration pattern
|
|
1046
|
+
*
|
|
1047
|
+
* CODING STANDARDS:
|
|
1048
|
+
* - Use sync filesystem operations for config discovery (performance)
|
|
1049
|
+
* - Check PROJECT_PATH environment variable first
|
|
1050
|
+
* - Fall back to current working directory
|
|
1051
|
+
* - Support both .yaml and .json extensions
|
|
1052
|
+
* - Return null if no config file is found
|
|
1053
|
+
*
|
|
1054
|
+
* AVOID:
|
|
1055
|
+
* - Throwing errors (return null instead for optional config)
|
|
1056
|
+
* - Hardcoded file names without extension variants
|
|
1057
|
+
* - Ignoring environment variables
|
|
1058
|
+
*/
|
|
1059
|
+
/**
|
|
1060
|
+
* Find MCP configuration file by checking PROJECT_PATH first, then cwd
|
|
1061
|
+
* Looks for both mcp-config.yaml and mcp-config.json
|
|
1062
|
+
*
|
|
1063
|
+
* @returns Absolute path to config file, or null if not found
|
|
1064
|
+
*/
|
|
1065
|
+
function findConfigFile() {
|
|
1066
|
+
const configFileNames = [
|
|
1067
|
+
"mcp-config.yaml",
|
|
1068
|
+
"mcp-config.yml",
|
|
1069
|
+
"mcp-config.json"
|
|
1070
|
+
];
|
|
1071
|
+
const projectPath = process.env.PROJECT_PATH;
|
|
1072
|
+
if (projectPath) for (const fileName of configFileNames) {
|
|
1073
|
+
const configPath = (0, node_path.resolve)(projectPath, fileName);
|
|
1074
|
+
if ((0, node_fs.existsSync)(configPath)) return configPath;
|
|
1075
|
+
}
|
|
1076
|
+
const cwd = process.cwd();
|
|
1077
|
+
for (const fileName of configFileNames) {
|
|
1078
|
+
const configPath = (0, node_path.join)(cwd, fileName);
|
|
1079
|
+
if ((0, node_fs.existsSync)(configPath)) return configPath;
|
|
1080
|
+
}
|
|
1081
|
+
return null;
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
//#endregion
|
|
1085
|
+
//#region src/utils/parseToolName.ts
|
|
1086
|
+
/**
|
|
1087
|
+
* Parse tool name to extract server and actual tool name
|
|
1088
|
+
* Supports both plain tool names and prefixed format: {serverName}__{toolName}
|
|
1089
|
+
*
|
|
1090
|
+
* @param toolName - The tool name to parse (e.g., "my_tool" or "server__my_tool")
|
|
1091
|
+
* @returns Parsed result with optional serverName and actualToolName
|
|
1092
|
+
*
|
|
1093
|
+
* @example
|
|
1094
|
+
* parseToolName("my_tool") // { actualToolName: "my_tool" }
|
|
1095
|
+
* parseToolName("server__my_tool") // { serverName: "server", actualToolName: "my_tool" }
|
|
1096
|
+
*/
|
|
1097
|
+
function parseToolName(toolName) {
|
|
1098
|
+
const separatorIndex = toolName.indexOf("__");
|
|
1099
|
+
if (separatorIndex > 0) return {
|
|
1100
|
+
serverName: toolName.substring(0, separatorIndex),
|
|
1101
|
+
actualToolName: toolName.substring(separatorIndex + 2)
|
|
1102
|
+
};
|
|
1103
|
+
return { actualToolName: toolName };
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
//#endregion
|
|
1107
|
+
//#region src/utils/parseFrontMatter.ts
|
|
1108
|
+
/**
|
|
1109
|
+
* Parses YAML front matter from a string content.
|
|
1110
|
+
* Front matter must be at the start of the content, delimited by `---`.
|
|
1111
|
+
*
|
|
1112
|
+
* Supports:
|
|
1113
|
+
* - Simple key: value pairs
|
|
1114
|
+
* - Literal block scalar (|) for multi-line preserving newlines
|
|
1115
|
+
* - Folded block scalar (>) for multi-line folding to single line
|
|
1116
|
+
*
|
|
1117
|
+
* @param content - The content string that may contain front matter
|
|
1118
|
+
* @returns Object with parsed front matter (or null) and remaining content
|
|
1119
|
+
*
|
|
1120
|
+
* @example
|
|
1121
|
+
* const result = parseFrontMatter(`---
|
|
1122
|
+
* name: my-skill
|
|
1123
|
+
* description: A skill description
|
|
1124
|
+
* ---
|
|
1125
|
+
* The actual content here`);
|
|
1126
|
+
* // result.frontMatter = { name: 'my-skill', description: 'A skill description' }
|
|
1127
|
+
* // result.content = 'The actual content here'
|
|
1128
|
+
*
|
|
1129
|
+
* @example
|
|
1130
|
+
* // Multi-line with literal block scalar
|
|
1131
|
+
* const result = parseFrontMatter(`---
|
|
1132
|
+
* name: my-skill
|
|
1133
|
+
* description: |
|
|
1134
|
+
* Line 1
|
|
1135
|
+
* Line 2
|
|
1136
|
+
* ---
|
|
1137
|
+
* Content`);
|
|
1138
|
+
* // result.frontMatter.description = 'Line 1\nLine 2'
|
|
1139
|
+
*/
|
|
1140
|
+
function parseFrontMatter(content) {
|
|
1141
|
+
const trimmedContent = content.trimStart();
|
|
1142
|
+
if (!trimmedContent.startsWith("---")) return {
|
|
1143
|
+
frontMatter: null,
|
|
1144
|
+
content
|
|
1145
|
+
};
|
|
1146
|
+
const endDelimiterIndex = trimmedContent.indexOf("\n---", 3);
|
|
1147
|
+
if (endDelimiterIndex === -1) return {
|
|
1148
|
+
frontMatter: null,
|
|
1149
|
+
content
|
|
1150
|
+
};
|
|
1151
|
+
const yamlContent = trimmedContent.slice(4, endDelimiterIndex).trim();
|
|
1152
|
+
if (!yamlContent) return {
|
|
1153
|
+
frontMatter: null,
|
|
1154
|
+
content
|
|
1155
|
+
};
|
|
1156
|
+
const frontMatter = {};
|
|
1157
|
+
const lines = yamlContent.split("\n");
|
|
1158
|
+
let currentKey = null;
|
|
1159
|
+
let currentValue = [];
|
|
1160
|
+
let multiLineMode = null;
|
|
1161
|
+
let baseIndent = 0;
|
|
1162
|
+
const saveCurrentKey = () => {
|
|
1163
|
+
if (currentKey && currentValue.length > 0) if (multiLineMode === "literal") frontMatter[currentKey] = currentValue.join("\n").trimEnd();
|
|
1164
|
+
else if (multiLineMode === "folded") frontMatter[currentKey] = currentValue.join(" ").trim();
|
|
1165
|
+
else frontMatter[currentKey] = currentValue.join("").trim();
|
|
1166
|
+
currentKey = null;
|
|
1167
|
+
currentValue = [];
|
|
1168
|
+
multiLineMode = null;
|
|
1169
|
+
baseIndent = 0;
|
|
1170
|
+
};
|
|
1171
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1172
|
+
const line = lines[i];
|
|
1173
|
+
const trimmedLine = line.trim();
|
|
1174
|
+
const colonIndex = line.indexOf(":");
|
|
1175
|
+
if (colonIndex !== -1 && !line.startsWith(" ") && !line.startsWith(" ")) {
|
|
1176
|
+
saveCurrentKey();
|
|
1177
|
+
const key = line.slice(0, colonIndex).trim();
|
|
1178
|
+
let value = line.slice(colonIndex + 1).trim();
|
|
1179
|
+
if (value === "|" || value === "|-") {
|
|
1180
|
+
currentKey = key;
|
|
1181
|
+
multiLineMode = "literal";
|
|
1182
|
+
if (i + 1 < lines.length) {
|
|
1183
|
+
const match = lines[i + 1].match(/^(\s+)/);
|
|
1184
|
+
baseIndent = match ? match[1].length : 2;
|
|
1185
|
+
}
|
|
1186
|
+
} else if (value === ">" || value === ">-") {
|
|
1187
|
+
currentKey = key;
|
|
1188
|
+
multiLineMode = "folded";
|
|
1189
|
+
if (i + 1 < lines.length) {
|
|
1190
|
+
const match = lines[i + 1].match(/^(\s+)/);
|
|
1191
|
+
baseIndent = match ? match[1].length : 2;
|
|
1192
|
+
}
|
|
1193
|
+
} else {
|
|
1194
|
+
if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
|
|
1195
|
+
if (key && value) frontMatter[key] = value;
|
|
1196
|
+
}
|
|
1197
|
+
} else if (multiLineMode && currentKey) {
|
|
1198
|
+
const lineIndent = line.match(/^(\s*)/)?.[1].length || 0;
|
|
1199
|
+
if (trimmedLine === "") currentValue.push("");
|
|
1200
|
+
else if (lineIndent >= baseIndent) {
|
|
1201
|
+
const unindentedLine = line.slice(baseIndent);
|
|
1202
|
+
currentValue.push(unindentedLine);
|
|
1203
|
+
} else saveCurrentKey();
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
saveCurrentKey();
|
|
1207
|
+
return {
|
|
1208
|
+
frontMatter,
|
|
1209
|
+
content: trimmedContent.slice(endDelimiterIndex + 4).trimStart()
|
|
1210
|
+
};
|
|
1211
|
+
}
|
|
1212
|
+
/**
|
|
1213
|
+
* Checks if parsed front matter contains valid skill metadata.
|
|
1214
|
+
* A valid skill front matter must have both `name` and `description` fields.
|
|
1215
|
+
*
|
|
1216
|
+
* @param frontMatter - The parsed front matter object
|
|
1217
|
+
* @returns True if front matter contains valid skill metadata
|
|
1218
|
+
*/
|
|
1219
|
+
function isValidSkillFrontMatter(frontMatter) {
|
|
1220
|
+
return frontMatter !== null && typeof frontMatter.name === "string" && frontMatter.name.length > 0 && typeof frontMatter.description === "string" && frontMatter.description.length > 0;
|
|
1221
|
+
}
|
|
1222
|
+
/**
|
|
1223
|
+
* Extracts skill front matter from content if present and valid.
|
|
1224
|
+
*
|
|
1225
|
+
* @param content - The content string that may contain skill front matter
|
|
1226
|
+
* @returns Object with skill metadata and content, or null if no valid skill front matter
|
|
1227
|
+
*/
|
|
1228
|
+
function extractSkillFrontMatter(content) {
|
|
1229
|
+
const { frontMatter, content: remainingContent } = parseFrontMatter(content);
|
|
1230
|
+
if (frontMatter && isValidSkillFrontMatter(frontMatter)) return {
|
|
1231
|
+
skill: {
|
|
1232
|
+
name: frontMatter.name,
|
|
1233
|
+
description: frontMatter.description
|
|
1234
|
+
},
|
|
1235
|
+
content: remainingContent
|
|
1236
|
+
};
|
|
1237
|
+
return null;
|
|
1238
|
+
}
|
|
1239
|
+
|
|
995
1240
|
//#endregion
|
|
996
1241
|
//#region src/services/SkillService.ts
|
|
997
1242
|
/**
|
|
@@ -1062,14 +1307,21 @@ var SkillService = class {
|
|
|
1062
1307
|
skillPaths;
|
|
1063
1308
|
cachedSkills = null;
|
|
1064
1309
|
skillsByName = null;
|
|
1310
|
+
/** Active file watchers for skill directories */
|
|
1311
|
+
watchers = [];
|
|
1312
|
+
/** Callback invoked when cache is invalidated due to file changes */
|
|
1313
|
+
onCacheInvalidated;
|
|
1065
1314
|
/**
|
|
1066
1315
|
* Creates a new SkillService instance
|
|
1067
1316
|
* @param cwd - Current working directory for resolving relative paths
|
|
1068
1317
|
* @param skillPaths - Array of paths to skills directories
|
|
1318
|
+
* @param options - Optional configuration
|
|
1319
|
+
* @param options.onCacheInvalidated - Callback invoked when cache is invalidated due to file changes
|
|
1069
1320
|
*/
|
|
1070
|
-
constructor(cwd, skillPaths) {
|
|
1321
|
+
constructor(cwd, skillPaths, options) {
|
|
1071
1322
|
this.cwd = cwd;
|
|
1072
1323
|
this.skillPaths = skillPaths;
|
|
1324
|
+
this.onCacheInvalidated = options?.onCacheInvalidated;
|
|
1073
1325
|
}
|
|
1074
1326
|
/**
|
|
1075
1327
|
* Get all available skills from configured directories.
|
|
@@ -1115,6 +1367,54 @@ var SkillService = class {
|
|
|
1115
1367
|
this.skillsByName = null;
|
|
1116
1368
|
}
|
|
1117
1369
|
/**
|
|
1370
|
+
* Starts watching skill directories for changes to SKILL.md files.
|
|
1371
|
+
* When changes are detected, the cache is automatically invalidated.
|
|
1372
|
+
*
|
|
1373
|
+
* Uses Node.js fs.watch with recursive option for efficient directory monitoring.
|
|
1374
|
+
* Only invalidates cache when SKILL.md files are modified.
|
|
1375
|
+
*
|
|
1376
|
+
* @example
|
|
1377
|
+
* const skillService = new SkillService(cwd, skillPaths, {
|
|
1378
|
+
* onCacheInvalidated: () => console.log('Skills cache invalidated')
|
|
1379
|
+
* });
|
|
1380
|
+
* await skillService.startWatching();
|
|
1381
|
+
*/
|
|
1382
|
+
async startWatching() {
|
|
1383
|
+
this.stopWatching();
|
|
1384
|
+
for (const skillPath of this.skillPaths) {
|
|
1385
|
+
const skillsDir = (0, node_path.isAbsolute)(skillPath) ? skillPath : (0, node_path.join)(this.cwd, skillPath);
|
|
1386
|
+
if (!await pathExists(skillsDir)) continue;
|
|
1387
|
+
const abortController = new AbortController();
|
|
1388
|
+
this.watchers.push(abortController);
|
|
1389
|
+
this.watchDirectory(skillsDir, abortController.signal).catch((error) => {
|
|
1390
|
+
if (error?.name !== "AbortError") console.error(`[skill-watcher] Error watching ${skillsDir}: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
1391
|
+
});
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
/**
|
|
1395
|
+
* Stops all active file watchers.
|
|
1396
|
+
* Should be called when the service is being disposed.
|
|
1397
|
+
*/
|
|
1398
|
+
stopWatching() {
|
|
1399
|
+
for (const controller of this.watchers) controller.abort();
|
|
1400
|
+
this.watchers = [];
|
|
1401
|
+
}
|
|
1402
|
+
/**
|
|
1403
|
+
* Watches a directory for changes to SKILL.md files.
|
|
1404
|
+
* @param dirPath - Directory path to watch
|
|
1405
|
+
* @param signal - AbortSignal to stop watching
|
|
1406
|
+
*/
|
|
1407
|
+
async watchDirectory(dirPath, signal) {
|
|
1408
|
+
const watcher = (0, node_fs_promises.watch)(dirPath, {
|
|
1409
|
+
recursive: true,
|
|
1410
|
+
signal
|
|
1411
|
+
});
|
|
1412
|
+
for await (const event of watcher) if (event.filename && event.filename.endsWith("SKILL.md")) {
|
|
1413
|
+
this.clearCache();
|
|
1414
|
+
this.onCacheInvalidated?.();
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
/**
|
|
1118
1418
|
* Load skills from a directory.
|
|
1119
1419
|
* Supports both flat structure (SKILL.md) and nested structure (name/SKILL.md).
|
|
1120
1420
|
*
|
|
@@ -1173,6 +1473,7 @@ var SkillService = class {
|
|
|
1173
1473
|
}
|
|
1174
1474
|
/**
|
|
1175
1475
|
* Load a single skill file and parse its frontmatter.
|
|
1476
|
+
* Supports multi-line YAML values using literal (|) and folded (>) block scalars.
|
|
1176
1477
|
*
|
|
1177
1478
|
* @param filePath - Path to the SKILL.md file
|
|
1178
1479
|
* @param location - Whether this is a 'project' or 'user' skill
|
|
@@ -1186,150 +1487,62 @@ var SkillService = class {
|
|
|
1186
1487
|
* // Returns null if frontmatter is missing name or description
|
|
1187
1488
|
*/
|
|
1188
1489
|
async loadSkillFile(filePath, location) {
|
|
1189
|
-
let
|
|
1490
|
+
let fileContent;
|
|
1190
1491
|
try {
|
|
1191
|
-
|
|
1492
|
+
fileContent = await (0, node_fs_promises.readFile)(filePath, "utf-8");
|
|
1192
1493
|
} catch (error) {
|
|
1193
1494
|
throw new SkillLoadError(`Failed to read skill file: ${error instanceof Error ? error.message : "Unknown error"}`, filePath, error instanceof Error ? error : void 0);
|
|
1194
1495
|
}
|
|
1195
|
-
const {
|
|
1196
|
-
if (!
|
|
1496
|
+
const { frontMatter, content } = parseFrontMatter(fileContent);
|
|
1497
|
+
if (!frontMatter || !frontMatter.name || !frontMatter.description) return null;
|
|
1197
1498
|
return {
|
|
1198
|
-
name:
|
|
1199
|
-
description:
|
|
1499
|
+
name: frontMatter.name,
|
|
1500
|
+
description: frontMatter.description,
|
|
1200
1501
|
location,
|
|
1201
|
-
content
|
|
1502
|
+
content,
|
|
1202
1503
|
basePath: (0, node_path.dirname)(filePath)
|
|
1203
1504
|
};
|
|
1204
1505
|
}
|
|
1205
|
-
/**
|
|
1206
|
-
* Parse YAML frontmatter from markdown content.
|
|
1207
|
-
* Frontmatter is delimited by --- at start and end.
|
|
1208
|
-
*
|
|
1209
|
-
* @param content - Full markdown content with frontmatter
|
|
1210
|
-
* @returns Parsed metadata and body content
|
|
1211
|
-
*
|
|
1212
|
-
* @example
|
|
1213
|
-
* // Input content:
|
|
1214
|
-
* // ---
|
|
1215
|
-
* // name: my-skill
|
|
1216
|
-
* // description: A sample skill
|
|
1217
|
-
* // ---
|
|
1218
|
-
* // # Skill Content
|
|
1219
|
-
* // This is the skill body.
|
|
1220
|
-
*
|
|
1221
|
-
* const result = parseFrontmatter(content);
|
|
1222
|
-
* // result.metadata = { name: 'my-skill', description: 'A sample skill' }
|
|
1223
|
-
* // result.body = '# Skill Content\nThis is the skill body.'
|
|
1224
|
-
*/
|
|
1225
|
-
parseFrontmatter(content) {
|
|
1226
|
-
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
|
|
1227
|
-
if (!match) return {
|
|
1228
|
-
metadata: {},
|
|
1229
|
-
body: content
|
|
1230
|
-
};
|
|
1231
|
-
const [, frontmatter, body] = match;
|
|
1232
|
-
const metadata = {};
|
|
1233
|
-
const lines = frontmatter.split("\n");
|
|
1234
|
-
for (const line of lines) {
|
|
1235
|
-
const colonIndex = line.indexOf(":");
|
|
1236
|
-
if (colonIndex > 0) {
|
|
1237
|
-
const key = line.slice(0, colonIndex).trim();
|
|
1238
|
-
let value = line.slice(colonIndex + 1).trim();
|
|
1239
|
-
if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
|
|
1240
|
-
if (key === "name" || key === "description" || key === "license") metadata[key] = value;
|
|
1241
|
-
}
|
|
1242
|
-
}
|
|
1243
|
-
return {
|
|
1244
|
-
metadata,
|
|
1245
|
-
body: body.trim()
|
|
1246
|
-
};
|
|
1247
|
-
}
|
|
1248
1506
|
};
|
|
1249
1507
|
|
|
1250
1508
|
//#endregion
|
|
1251
|
-
//#region src/
|
|
1509
|
+
//#region src/constants/index.ts
|
|
1252
1510
|
/**
|
|
1253
|
-
*
|
|
1254
|
-
*
|
|
1255
|
-
* DESIGN PATTERNS:
|
|
1256
|
-
* - Utility function pattern for reusable logic
|
|
1257
|
-
* - Fail-fast pattern with early returns
|
|
1258
|
-
* - Environment variable configuration pattern
|
|
1259
|
-
*
|
|
1260
|
-
* CODING STANDARDS:
|
|
1261
|
-
* - Use sync filesystem operations for config discovery (performance)
|
|
1262
|
-
* - Check PROJECT_PATH environment variable first
|
|
1263
|
-
* - Fall back to current working directory
|
|
1264
|
-
* - Support both .yaml and .json extensions
|
|
1265
|
-
* - Return null if no config file is found
|
|
1266
|
-
*
|
|
1267
|
-
* AVOID:
|
|
1268
|
-
* - Throwing errors (return null instead for optional config)
|
|
1269
|
-
* - Hardcoded file names without extension variants
|
|
1270
|
-
* - Ignoring environment variables
|
|
1511
|
+
* Shared constants for one-mcp package
|
|
1271
1512
|
*/
|
|
1272
1513
|
/**
|
|
1273
|
-
*
|
|
1274
|
-
*
|
|
1275
|
-
*
|
|
1276
|
-
* @returns Absolute path to config file, or null if not found
|
|
1514
|
+
* Prefix added to skill names when they clash with MCP tool names.
|
|
1515
|
+
* This ensures skills can be uniquely identified even when a tool has the same name.
|
|
1277
1516
|
*/
|
|
1278
|
-
|
|
1279
|
-
const configFileNames = [
|
|
1280
|
-
"mcp-config.yaml",
|
|
1281
|
-
"mcp-config.yml",
|
|
1282
|
-
"mcp-config.json"
|
|
1283
|
-
];
|
|
1284
|
-
const projectPath = process.env.PROJECT_PATH;
|
|
1285
|
-
if (projectPath) for (const fileName of configFileNames) {
|
|
1286
|
-
const configPath = (0, node_path.resolve)(projectPath, fileName);
|
|
1287
|
-
if ((0, node_fs.existsSync)(configPath)) return configPath;
|
|
1288
|
-
}
|
|
1289
|
-
const cwd = process.cwd();
|
|
1290
|
-
for (const fileName of configFileNames) {
|
|
1291
|
-
const configPath = (0, node_path.join)(cwd, fileName);
|
|
1292
|
-
if ((0, node_fs.existsSync)(configPath)) return configPath;
|
|
1293
|
-
}
|
|
1294
|
-
return null;
|
|
1295
|
-
}
|
|
1296
|
-
|
|
1297
|
-
//#endregion
|
|
1298
|
-
//#region src/utils/parseToolName.ts
|
|
1517
|
+
const SKILL_PREFIX$1 = "skill__";
|
|
1299
1518
|
/**
|
|
1300
|
-
*
|
|
1301
|
-
*
|
|
1302
|
-
*
|
|
1303
|
-
* @param toolName - The tool name to parse (e.g., "my_tool" or "server__my_tool")
|
|
1304
|
-
* @returns Parsed result with optional serverName and actualToolName
|
|
1305
|
-
*
|
|
1306
|
-
* @example
|
|
1307
|
-
* parseToolName("my_tool") // { actualToolName: "my_tool" }
|
|
1308
|
-
* parseToolName("server__my_tool") // { serverName: "server", actualToolName: "my_tool" }
|
|
1519
|
+
* Log prefix for skill detection messages.
|
|
1520
|
+
* Used to easily filter skill detection logs in stderr output.
|
|
1309
1521
|
*/
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
return { actualToolName: toolName };
|
|
1317
|
-
}
|
|
1318
|
-
|
|
1319
|
-
//#endregion
|
|
1320
|
-
//#region src/templates/skills-description.liquid?raw
|
|
1321
|
-
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";
|
|
1522
|
+
const LOG_PREFIX_SKILL_DETECTION = "[skill-detection]";
|
|
1523
|
+
/**
|
|
1524
|
+
* Prefix for prompt-based skill locations.
|
|
1525
|
+
* Format: "prompt:{serverName}:{promptName}"
|
|
1526
|
+
*/
|
|
1527
|
+
const PROMPT_LOCATION_PREFIX = "prompt:";
|
|
1322
1528
|
|
|
1323
1529
|
//#endregion
|
|
1324
|
-
//#region src/templates/
|
|
1325
|
-
var
|
|
1530
|
+
//#region src/templates/toolkit-description.liquid?raw
|
|
1531
|
+
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";
|
|
1326
1532
|
|
|
1327
1533
|
//#endregion
|
|
1328
1534
|
//#region src/tools/DescribeToolsTool.ts
|
|
1329
1535
|
/**
|
|
1330
|
-
*
|
|
1536
|
+
* Formats skill instructions with the loading command message prefix.
|
|
1537
|
+
* This message is used by Claude Code to indicate that a skill is being loaded.
|
|
1538
|
+
*
|
|
1539
|
+
* @param name - The skill name
|
|
1540
|
+
* @param instructions - The raw skill instructions/content
|
|
1541
|
+
* @returns Formatted instructions with command message prefix
|
|
1331
1542
|
*/
|
|
1332
|
-
|
|
1543
|
+
function formatSkillInstructions(name, instructions) {
|
|
1544
|
+
return `<command-message>The "${name}" skill is loading</command-message>\n${instructions}`;
|
|
1545
|
+
}
|
|
1333
1546
|
/**
|
|
1334
1547
|
* DescribeToolsTool provides progressive disclosure of MCP tools and skills.
|
|
1335
1548
|
*
|
|
@@ -1352,6 +1565,8 @@ var DescribeToolsTool = class DescribeToolsTool {
|
|
|
1352
1565
|
clientManager;
|
|
1353
1566
|
skillService;
|
|
1354
1567
|
liquid = new liquidjs.Liquid();
|
|
1568
|
+
/** Cache for auto-detected skills from prompt front-matter */
|
|
1569
|
+
autoDetectedSkillsCache = null;
|
|
1355
1570
|
/**
|
|
1356
1571
|
* Creates a new DescribeToolsTool instance
|
|
1357
1572
|
* @param clientManager - The MCP client manager for accessing remote servers
|
|
@@ -1362,43 +1577,169 @@ var DescribeToolsTool = class DescribeToolsTool {
|
|
|
1362
1577
|
this.skillService = skillService;
|
|
1363
1578
|
}
|
|
1364
1579
|
/**
|
|
1365
|
-
*
|
|
1580
|
+
* Clears the cached auto-detected skills from prompt front-matter.
|
|
1581
|
+
* Use this when prompt configurations may have changed or when
|
|
1582
|
+
* the skill service cache is invalidated.
|
|
1583
|
+
*/
|
|
1584
|
+
clearAutoDetectedSkillsCache() {
|
|
1585
|
+
this.autoDetectedSkillsCache = null;
|
|
1586
|
+
}
|
|
1587
|
+
/**
|
|
1588
|
+
* Detects and caches skills from prompt front-matter across all connected MCP servers.
|
|
1589
|
+
* Fetches all prompts and checks their content for YAML front-matter with name/description.
|
|
1590
|
+
* Results are cached to avoid repeated fetches.
|
|
1366
1591
|
*
|
|
1367
|
-
*
|
|
1368
|
-
*
|
|
1369
|
-
*
|
|
1592
|
+
* Error Handling Strategy:
|
|
1593
|
+
* - Errors are logged to stderr but do not fail the overall detection process
|
|
1594
|
+
* - This ensures partial results are returned even if some servers/prompts fail
|
|
1595
|
+
* - Common failure reasons: server temporarily unavailable, prompt requires arguments,
|
|
1596
|
+
* network timeout, or server doesn't support listPrompts
|
|
1597
|
+
* - Errors are prefixed with [skill-detection] for easy filtering in logs
|
|
1370
1598
|
*
|
|
1371
|
-
* @
|
|
1372
|
-
* @returns Rendered skills section string with available skills list
|
|
1599
|
+
* @returns Array of auto-detected skills from prompt front-matter
|
|
1373
1600
|
*/
|
|
1374
|
-
async
|
|
1375
|
-
|
|
1376
|
-
const
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1601
|
+
async detectSkillsFromPromptFrontMatter() {
|
|
1602
|
+
if (this.autoDetectedSkillsCache !== null) return this.autoDetectedSkillsCache;
|
|
1603
|
+
const clients = this.clientManager.getAllClients();
|
|
1604
|
+
const autoDetectedSkills = [];
|
|
1605
|
+
let listPromptsFailures = 0;
|
|
1606
|
+
let fetchPromptFailures = 0;
|
|
1607
|
+
const fetchPromises = [];
|
|
1608
|
+
for (const client of clients) {
|
|
1609
|
+
const configuredPromptNames = new Set(client.prompts ? Object.keys(client.prompts) : []);
|
|
1610
|
+
const listPromptsPromise = (async () => {
|
|
1611
|
+
try {
|
|
1612
|
+
const prompts = await client.listPrompts();
|
|
1613
|
+
if (!prompts || prompts.length === 0) return;
|
|
1614
|
+
const promptFetchPromises = prompts.map(async (promptInfo) => {
|
|
1615
|
+
if (configuredPromptNames.has(promptInfo.name)) return;
|
|
1616
|
+
try {
|
|
1617
|
+
const skillExtraction = extractSkillFrontMatter(((await client.getPrompt(promptInfo.name)).messages || []).map((m) => {
|
|
1618
|
+
const content = m.content;
|
|
1619
|
+
if (typeof content === "string") return content;
|
|
1620
|
+
if (content && typeof content === "object" && "text" in content) return String(content.text);
|
|
1621
|
+
return "";
|
|
1622
|
+
}).join("\n"));
|
|
1623
|
+
if (skillExtraction) autoDetectedSkills.push({
|
|
1624
|
+
serverName: client.serverName,
|
|
1625
|
+
promptName: promptInfo.name,
|
|
1626
|
+
skill: skillExtraction.skill
|
|
1627
|
+
});
|
|
1628
|
+
} catch (error) {
|
|
1629
|
+
fetchPromptFailures++;
|
|
1630
|
+
console.error(`${LOG_PREFIX_SKILL_DETECTION} Failed to fetch prompt '${promptInfo.name}' from ${client.serverName}: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
1631
|
+
}
|
|
1632
|
+
});
|
|
1633
|
+
await Promise.all(promptFetchPromises);
|
|
1634
|
+
} catch (error) {
|
|
1635
|
+
listPromptsFailures++;
|
|
1636
|
+
console.error(`${LOG_PREFIX_SKILL_DETECTION} Failed to list prompts from ${client.serverName}: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
1637
|
+
}
|
|
1638
|
+
})();
|
|
1639
|
+
fetchPromises.push(listPromptsPromise);
|
|
1640
|
+
}
|
|
1641
|
+
await Promise.all(fetchPromises);
|
|
1642
|
+
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).`);
|
|
1643
|
+
this.autoDetectedSkillsCache = autoDetectedSkills;
|
|
1644
|
+
return autoDetectedSkills;
|
|
1645
|
+
}
|
|
1646
|
+
/**
|
|
1647
|
+
* Collects skills derived from prompt configurations across all connected MCP servers.
|
|
1648
|
+
* Includes both explicitly configured prompts and auto-detected skills from front-matter.
|
|
1649
|
+
*
|
|
1650
|
+
* @returns Array of skill template data derived from prompts
|
|
1651
|
+
*/
|
|
1652
|
+
async collectPromptSkills() {
|
|
1653
|
+
const clients = this.clientManager.getAllClients();
|
|
1654
|
+
const promptSkills = [];
|
|
1655
|
+
for (const client of clients) {
|
|
1656
|
+
if (!client.prompts) continue;
|
|
1657
|
+
for (const promptConfig of Object.values(client.prompts)) if (promptConfig.skill) promptSkills.push({
|
|
1658
|
+
name: promptConfig.skill.name,
|
|
1659
|
+
displayName: promptConfig.skill.name,
|
|
1660
|
+
description: promptConfig.skill.description
|
|
1661
|
+
});
|
|
1662
|
+
}
|
|
1663
|
+
const autoDetectedSkills = await this.detectSkillsFromPromptFrontMatter();
|
|
1664
|
+
for (const autoSkill of autoDetectedSkills) promptSkills.push({
|
|
1665
|
+
name: autoSkill.skill.name,
|
|
1666
|
+
displayName: autoSkill.skill.name,
|
|
1667
|
+
description: autoSkill.skill.description
|
|
1668
|
+
});
|
|
1669
|
+
return promptSkills;
|
|
1670
|
+
}
|
|
1671
|
+
/**
|
|
1672
|
+
* Finds a prompt-based skill by name from all connected MCP servers.
|
|
1673
|
+
* Searches both explicitly configured prompts and auto-detected skills from front-matter.
|
|
1674
|
+
*
|
|
1675
|
+
* @param skillName - The skill name to search for
|
|
1676
|
+
* @returns Object with serverName, promptName, and skill config, or undefined if not found
|
|
1677
|
+
*/
|
|
1678
|
+
async findPromptSkill(skillName) {
|
|
1679
|
+
if (!skillName) return void 0;
|
|
1680
|
+
const clients = this.clientManager.getAllClients();
|
|
1681
|
+
for (const client of clients) {
|
|
1682
|
+
if (!client.prompts) continue;
|
|
1683
|
+
for (const [promptName, promptConfig] of Object.entries(client.prompts)) if (promptConfig.skill && promptConfig.skill.name === skillName) return {
|
|
1684
|
+
serverName: client.serverName,
|
|
1685
|
+
promptName,
|
|
1686
|
+
skill: promptConfig.skill
|
|
1687
|
+
};
|
|
1688
|
+
}
|
|
1689
|
+
const autoDetectedSkills = await this.detectSkillsFromPromptFrontMatter();
|
|
1690
|
+
for (const autoSkill of autoDetectedSkills) if (autoSkill.skill.name === skillName) return {
|
|
1691
|
+
serverName: autoSkill.serverName,
|
|
1692
|
+
promptName: autoSkill.promptName,
|
|
1693
|
+
skill: autoSkill.skill,
|
|
1694
|
+
autoDetected: true
|
|
1695
|
+
};
|
|
1696
|
+
}
|
|
1697
|
+
/**
|
|
1698
|
+
* Retrieves skill content from a prompt-based skill configuration.
|
|
1699
|
+
* Fetches the prompt from the MCP server and extracts text content.
|
|
1700
|
+
* Handles both explicitly configured prompts and auto-detected skills from front-matter.
|
|
1701
|
+
*
|
|
1702
|
+
* @param skillName - The skill name being requested
|
|
1703
|
+
* @returns SkillDescription if found and successfully fetched, undefined otherwise
|
|
1704
|
+
*/
|
|
1705
|
+
async getPromptSkillContent(skillName) {
|
|
1706
|
+
const promptSkill = await this.findPromptSkill(skillName);
|
|
1707
|
+
if (!promptSkill) return void 0;
|
|
1708
|
+
const client = this.clientManager.getClient(promptSkill.serverName);
|
|
1709
|
+
if (!client) {
|
|
1710
|
+
console.error(`Client not found for server '${promptSkill.serverName}' when fetching prompt skill '${skillName}'`);
|
|
1711
|
+
return;
|
|
1712
|
+
}
|
|
1713
|
+
try {
|
|
1714
|
+
const rawInstructions = (await client.getPrompt(promptSkill.promptName)).messages?.map((m) => {
|
|
1715
|
+
const content = m.content;
|
|
1716
|
+
if (typeof content === "string") return content;
|
|
1717
|
+
if (content && typeof content === "object" && "text" in content) return String(content.text);
|
|
1718
|
+
return "";
|
|
1719
|
+
}).join("\n") || "";
|
|
1382
1720
|
return {
|
|
1383
|
-
name: skill.name,
|
|
1384
|
-
|
|
1385
|
-
|
|
1721
|
+
name: promptSkill.skill.name,
|
|
1722
|
+
location: promptSkill.skill.folder || `${PROMPT_LOCATION_PREFIX}${promptSkill.serverName}/${promptSkill.promptName}`,
|
|
1723
|
+
instructions: formatSkillInstructions(promptSkill.skill.name, rawInstructions)
|
|
1386
1724
|
};
|
|
1387
|
-
})
|
|
1388
|
-
|
|
1725
|
+
} catch (error) {
|
|
1726
|
+
console.error(`Failed to get prompt-based skill '${skillName}': ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
1727
|
+
return;
|
|
1728
|
+
}
|
|
1389
1729
|
}
|
|
1390
1730
|
/**
|
|
1391
|
-
* Builds the
|
|
1731
|
+
* Builds the combined toolkit description using a single Liquid template.
|
|
1392
1732
|
*
|
|
1393
|
-
* Collects all tools from connected MCP servers
|
|
1394
|
-
*
|
|
1733
|
+
* Collects all tools from connected MCP servers and all skills, then renders
|
|
1734
|
+
* them together using the toolkit-description.liquid template.
|
|
1395
1735
|
*
|
|
1396
1736
|
* Tool names are prefixed with serverName__ when the same tool exists
|
|
1397
|
-
* on multiple servers
|
|
1737
|
+
* on multiple servers. Skill names are prefixed with skill__ when they
|
|
1738
|
+
* clash with MCP tools or other skills.
|
|
1398
1739
|
*
|
|
1399
|
-
* @returns Object with rendered
|
|
1740
|
+
* @returns Object with rendered description and set of all tool names
|
|
1400
1741
|
*/
|
|
1401
|
-
async
|
|
1742
|
+
async buildToolkitDescription() {
|
|
1402
1743
|
const clients = this.clientManager.getAllClients();
|
|
1403
1744
|
const toolToServers = /* @__PURE__ */ new Map();
|
|
1404
1745
|
const serverToolsMap = /* @__PURE__ */ new Map();
|
|
@@ -1419,9 +1760,6 @@ var DescribeToolsTool = class DescribeToolsTool {
|
|
|
1419
1760
|
}));
|
|
1420
1761
|
/**
|
|
1421
1762
|
* Formats tool name with server prefix if the tool exists on multiple servers
|
|
1422
|
-
* @param toolName - The original tool name
|
|
1423
|
-
* @param serverName - The server providing this tool
|
|
1424
|
-
* @returns Tool name prefixed with serverName__ if clashing, otherwise plain name
|
|
1425
1763
|
*/
|
|
1426
1764
|
const formatToolName = (toolName, serverName) => {
|
|
1427
1765
|
return (toolToServers.get(toolName) || []).length > 1 ? `${serverName}__${toolName}` : toolName;
|
|
@@ -1441,31 +1779,56 @@ var DescribeToolsTool = class DescribeToolsTool {
|
|
|
1441
1779
|
toolNames: formattedTools.map((t) => t.displayName)
|
|
1442
1780
|
};
|
|
1443
1781
|
});
|
|
1782
|
+
const rawSkills = this.skillService ? await this.skillService.getSkills() : [];
|
|
1783
|
+
const promptSkills = await this.collectPromptSkills();
|
|
1784
|
+
const seenSkillNames = /* @__PURE__ */ new Set();
|
|
1785
|
+
const allSkillsData = [];
|
|
1786
|
+
for (const skill of rawSkills) if (!seenSkillNames.has(skill.name)) {
|
|
1787
|
+
seenSkillNames.add(skill.name);
|
|
1788
|
+
allSkillsData.push({
|
|
1789
|
+
name: skill.name,
|
|
1790
|
+
displayName: skill.name,
|
|
1791
|
+
description: skill.description
|
|
1792
|
+
});
|
|
1793
|
+
}
|
|
1794
|
+
for (const skill of promptSkills) if (!seenSkillNames.has(skill.name)) {
|
|
1795
|
+
seenSkillNames.add(skill.name);
|
|
1796
|
+
allSkillsData.push(skill);
|
|
1797
|
+
}
|
|
1798
|
+
const skills = allSkillsData.map((skill) => {
|
|
1799
|
+
const clashesWithMcpTool = allToolNames.has(skill.name);
|
|
1800
|
+
return {
|
|
1801
|
+
name: skill.name,
|
|
1802
|
+
displayName: clashesWithMcpTool ? `${SKILL_PREFIX$1}${skill.name}` : skill.name,
|
|
1803
|
+
description: skill.description
|
|
1804
|
+
};
|
|
1805
|
+
});
|
|
1444
1806
|
return {
|
|
1445
|
-
content: await this.liquid.parseAndRender(
|
|
1807
|
+
content: await this.liquid.parseAndRender(toolkit_description_default, {
|
|
1808
|
+
servers,
|
|
1809
|
+
skills
|
|
1810
|
+
}),
|
|
1446
1811
|
toolNames: allToolNames
|
|
1447
1812
|
};
|
|
1448
1813
|
}
|
|
1449
1814
|
/**
|
|
1450
|
-
* Gets the tool definition including available
|
|
1815
|
+
* Gets the tool definition including available tools and skills in a unified format.
|
|
1451
1816
|
*
|
|
1452
1817
|
* The definition includes:
|
|
1453
|
-
* -
|
|
1454
|
-
* -
|
|
1455
|
-
* -
|
|
1818
|
+
* - All MCP tools from connected servers
|
|
1819
|
+
* - All available skills (file-based and prompt-based)
|
|
1820
|
+
* - Unified instructions for querying capability details
|
|
1456
1821
|
*
|
|
1457
|
-
* Tool names are prefixed with serverName__ when
|
|
1458
|
-
*
|
|
1822
|
+
* Tool names are prefixed with serverName__ when clashing.
|
|
1823
|
+
* Skill names are prefixed with skill__ when clashing.
|
|
1459
1824
|
*
|
|
1460
1825
|
* @returns Tool definition with description and input schema
|
|
1461
1826
|
*/
|
|
1462
1827
|
async getDefinition() {
|
|
1463
|
-
const
|
|
1464
|
-
const skillsSection = await this.buildSkillsSection(serversResult.toolNames);
|
|
1828
|
+
const { content } = await this.buildToolkitDescription();
|
|
1465
1829
|
return {
|
|
1466
1830
|
name: DescribeToolsTool.TOOL_NAME,
|
|
1467
|
-
description:
|
|
1468
|
-
${skillsSection}`,
|
|
1831
|
+
description: content,
|
|
1469
1832
|
inputSchema: {
|
|
1470
1833
|
type: "object",
|
|
1471
1834
|
properties: { toolNames: {
|
|
@@ -1530,16 +1893,24 @@ ${skillsSection}`,
|
|
|
1530
1893
|
const notFoundItems = [];
|
|
1531
1894
|
for (const requestedName of toolNames) {
|
|
1532
1895
|
if (requestedName.startsWith(SKILL_PREFIX$1)) {
|
|
1533
|
-
const skillName = requestedName.slice(
|
|
1896
|
+
const skillName = requestedName.slice(SKILL_PREFIX$1.length);
|
|
1534
1897
|
if (this.skillService) {
|
|
1535
1898
|
const skill = await this.skillService.getSkill(skillName);
|
|
1536
|
-
if (skill)
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1899
|
+
if (skill) {
|
|
1900
|
+
foundSkills.push({
|
|
1901
|
+
name: skill.name,
|
|
1902
|
+
location: skill.basePath,
|
|
1903
|
+
instructions: formatSkillInstructions(skill.name, skill.content)
|
|
1904
|
+
});
|
|
1905
|
+
continue;
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
const promptSkillContent = await this.getPromptSkillContent(skillName);
|
|
1909
|
+
if (promptSkillContent) {
|
|
1910
|
+
foundSkills.push(promptSkillContent);
|
|
1911
|
+
continue;
|
|
1912
|
+
}
|
|
1913
|
+
notFoundItems.push(requestedName);
|
|
1543
1914
|
continue;
|
|
1544
1915
|
}
|
|
1545
1916
|
const { serverName, actualToolName } = parseToolName(requestedName);
|
|
@@ -1568,11 +1939,16 @@ ${skillsSection}`,
|
|
|
1568
1939
|
foundSkills.push({
|
|
1569
1940
|
name: skill.name,
|
|
1570
1941
|
location: skill.basePath,
|
|
1571
|
-
instructions: skill.content
|
|
1942
|
+
instructions: formatSkillInstructions(skill.name, skill.content)
|
|
1572
1943
|
});
|
|
1573
1944
|
continue;
|
|
1574
1945
|
}
|
|
1575
1946
|
}
|
|
1947
|
+
const promptSkillContent = await this.getPromptSkillContent(actualToolName);
|
|
1948
|
+
if (promptSkillContent) {
|
|
1949
|
+
foundSkills.push(promptSkillContent);
|
|
1950
|
+
continue;
|
|
1951
|
+
}
|
|
1576
1952
|
notFoundItems.push(requestedName);
|
|
1577
1953
|
continue;
|
|
1578
1954
|
}
|
|
@@ -1681,7 +2057,7 @@ var UseToolTool = class UseToolTool {
|
|
|
1681
2057
|
getDefinition() {
|
|
1682
2058
|
return {
|
|
1683
2059
|
name: UseToolTool.TOOL_NAME,
|
|
1684
|
-
description: `Execute an MCP tool with provided arguments. You MUST call describe_tools first to discover the tool's correct arguments. Then to use tool:
|
|
2060
|
+
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:
|
|
1685
2061
|
- Provide toolName and toolArgs based on the schema
|
|
1686
2062
|
- If multiple servers provide the same tool, specify serverName
|
|
1687
2063
|
`,
|
|
@@ -1720,6 +2096,37 @@ var UseToolTool = class UseToolTool {
|
|
|
1720
2096
|
}] };
|
|
1721
2097
|
}
|
|
1722
2098
|
/**
|
|
2099
|
+
* Finds a prompt-based skill by name from all connected MCP servers.
|
|
2100
|
+
*
|
|
2101
|
+
* @param skillName - The skill name to search for
|
|
2102
|
+
* @returns PromptSkillMatch if found, undefined otherwise
|
|
2103
|
+
*/
|
|
2104
|
+
findPromptSkill(skillName) {
|
|
2105
|
+
if (!skillName) return void 0;
|
|
2106
|
+
const clients = this.clientManager.getAllClients();
|
|
2107
|
+
for (const client of clients) {
|
|
2108
|
+
if (!client.prompts) continue;
|
|
2109
|
+
for (const [promptName, promptConfig] of Object.entries(client.prompts)) if (promptConfig.skill && promptConfig.skill.name === skillName) return {
|
|
2110
|
+
serverName: client.serverName,
|
|
2111
|
+
promptName,
|
|
2112
|
+
skill: promptConfig.skill
|
|
2113
|
+
};
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
/**
|
|
2117
|
+
* Returns guidance message for prompt-based skill invocation.
|
|
2118
|
+
*
|
|
2119
|
+
* @param promptSkill - The prompt skill match that was found
|
|
2120
|
+
* @returns CallToolResult with guidance message
|
|
2121
|
+
*/
|
|
2122
|
+
executePromptSkill(promptSkill) {
|
|
2123
|
+
const location = promptSkill.skill.folder || `prompt:${promptSkill.serverName}/${promptSkill.promptName}`;
|
|
2124
|
+
return { content: [{
|
|
2125
|
+
type: "text",
|
|
2126
|
+
text: `Skill "${promptSkill.skill.name}" found. Skills provide instructions and should not be executed via use_tool.\n\nUse describe_tools to view the skill details at: ${location}\n\nThen follow the skill's instructions directly.`
|
|
2127
|
+
}] };
|
|
2128
|
+
}
|
|
2129
|
+
/**
|
|
1723
2130
|
* Executes a tool or skill based on the provided input.
|
|
1724
2131
|
*
|
|
1725
2132
|
* Handles three invocation patterns:
|
|
@@ -1740,22 +2147,19 @@ var UseToolTool = class UseToolTool {
|
|
|
1740
2147
|
const { toolName: inputToolName, toolArgs = {} } = input;
|
|
1741
2148
|
if (inputToolName.startsWith(SKILL_PREFIX)) {
|
|
1742
2149
|
const skillName = inputToolName.slice(7);
|
|
1743
|
-
if (
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
const skill = await this.skillService.getSkill(skillName);
|
|
1751
|
-
if (!skill) return {
|
|
2150
|
+
if (this.skillService) {
|
|
2151
|
+
const skill = await this.skillService.getSkill(skillName);
|
|
2152
|
+
if (skill) return this.executeSkill(skill);
|
|
2153
|
+
}
|
|
2154
|
+
const promptSkill = this.findPromptSkill(skillName);
|
|
2155
|
+
if (promptSkill) return this.executePromptSkill(promptSkill);
|
|
2156
|
+
return {
|
|
1752
2157
|
content: [{
|
|
1753
2158
|
type: "text",
|
|
1754
2159
|
text: `Skill "${skillName}" not found. Use describe_tools to see available skills.`
|
|
1755
2160
|
}],
|
|
1756
2161
|
isError: true
|
|
1757
2162
|
};
|
|
1758
|
-
return this.executeSkill(skill);
|
|
1759
2163
|
}
|
|
1760
2164
|
const clients = this.clientManager.getAllClients();
|
|
1761
2165
|
const { serverName, actualToolName } = parseToolName(inputToolName);
|
|
@@ -1803,6 +2207,8 @@ var UseToolTool = class UseToolTool {
|
|
|
1803
2207
|
const skill = await this.skillService.getSkill(actualToolName);
|
|
1804
2208
|
if (skill) return this.executeSkill(skill);
|
|
1805
2209
|
}
|
|
2210
|
+
const promptSkill = this.findPromptSkill(actualToolName);
|
|
2211
|
+
if (promptSkill) return this.executePromptSkill(promptSkill);
|
|
1806
2212
|
return {
|
|
1807
2213
|
content: [{
|
|
1808
2214
|
type: "text",
|
|
@@ -1869,7 +2275,10 @@ async function createServer(options) {
|
|
|
1869
2275
|
const server = new __modelcontextprotocol_sdk_server_index_js.Server({
|
|
1870
2276
|
name: "@agiflowai/one-mcp",
|
|
1871
2277
|
version: "0.1.0"
|
|
1872
|
-
}, { capabilities: {
|
|
2278
|
+
}, { capabilities: {
|
|
2279
|
+
tools: {},
|
|
2280
|
+
prompts: {}
|
|
2281
|
+
} });
|
|
1873
2282
|
const clientManager = new McpClientManagerService();
|
|
1874
2283
|
let configSkills;
|
|
1875
2284
|
if (options?.configFilePath) {
|
|
@@ -1902,9 +2311,16 @@ async function createServer(options) {
|
|
|
1902
2311
|
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(", ")}`);
|
|
1903
2312
|
}
|
|
1904
2313
|
const skillsConfig = options?.skills || configSkills;
|
|
1905
|
-
const
|
|
2314
|
+
const toolsRef = { describeTools: null };
|
|
2315
|
+
const skillService = skillsConfig && skillsConfig.paths.length > 0 ? new SkillService(process.cwd(), skillsConfig.paths, { onCacheInvalidated: () => {
|
|
2316
|
+
toolsRef.describeTools?.clearAutoDetectedSkillsCache();
|
|
2317
|
+
} }) : void 0;
|
|
1906
2318
|
const describeTools = new DescribeToolsTool(clientManager, skillService);
|
|
1907
2319
|
const useTool = new UseToolTool(clientManager, skillService);
|
|
2320
|
+
toolsRef.describeTools = describeTools;
|
|
2321
|
+
if (skillService) skillService.startWatching().catch((error) => {
|
|
2322
|
+
console.error(`[skill-watcher] File watcher failed (non-critical): ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
2323
|
+
});
|
|
1908
2324
|
server.setRequestHandler(__modelcontextprotocol_sdk_types_js.ListToolsRequestSchema, async () => ({ tools: [await describeTools.getDefinition(), useTool.getDefinition()] }));
|
|
1909
2325
|
server.setRequestHandler(__modelcontextprotocol_sdk_types_js.CallToolRequestSchema, async (request) => {
|
|
1910
2326
|
const { name, arguments: args } = request.params;
|
|
@@ -1920,6 +2336,60 @@ async function createServer(options) {
|
|
|
1920
2336
|
}
|
|
1921
2337
|
throw new Error(`Unknown tool: ${name}`);
|
|
1922
2338
|
});
|
|
2339
|
+
server.setRequestHandler(__modelcontextprotocol_sdk_types_js.ListPromptsRequestSchema, async () => {
|
|
2340
|
+
const clients = clientManager.getAllClients();
|
|
2341
|
+
const promptToServers = /* @__PURE__ */ new Map();
|
|
2342
|
+
const serverPromptsMap = /* @__PURE__ */ new Map();
|
|
2343
|
+
await Promise.all(clients.map(async (client) => {
|
|
2344
|
+
try {
|
|
2345
|
+
const prompts = await client.listPrompts();
|
|
2346
|
+
serverPromptsMap.set(client.serverName, prompts);
|
|
2347
|
+
for (const prompt of prompts) {
|
|
2348
|
+
if (!promptToServers.has(prompt.name)) promptToServers.set(prompt.name, []);
|
|
2349
|
+
promptToServers.get(prompt.name).push(client.serverName);
|
|
2350
|
+
}
|
|
2351
|
+
} catch (error) {
|
|
2352
|
+
console.error(`Failed to list prompts from ${client.serverName}:`, error);
|
|
2353
|
+
serverPromptsMap.set(client.serverName, []);
|
|
2354
|
+
}
|
|
2355
|
+
}));
|
|
2356
|
+
const aggregatedPrompts = [];
|
|
2357
|
+
for (const client of clients) {
|
|
2358
|
+
const prompts = serverPromptsMap.get(client.serverName) || [];
|
|
2359
|
+
for (const prompt of prompts) {
|
|
2360
|
+
const hasClash = (promptToServers.get(prompt.name) || []).length > 1;
|
|
2361
|
+
aggregatedPrompts.push({
|
|
2362
|
+
name: hasClash ? `${client.serverName}__${prompt.name}` : prompt.name,
|
|
2363
|
+
description: prompt.description,
|
|
2364
|
+
arguments: prompt.arguments
|
|
2365
|
+
});
|
|
2366
|
+
}
|
|
2367
|
+
}
|
|
2368
|
+
return { prompts: aggregatedPrompts };
|
|
2369
|
+
});
|
|
2370
|
+
server.setRequestHandler(__modelcontextprotocol_sdk_types_js.GetPromptRequestSchema, async (request) => {
|
|
2371
|
+
const { name, arguments: args } = request.params;
|
|
2372
|
+
const clients = clientManager.getAllClients();
|
|
2373
|
+
const { serverName, actualToolName: actualPromptName } = parseToolName(name);
|
|
2374
|
+
if (serverName) {
|
|
2375
|
+
const client$1 = clientManager.getClient(serverName);
|
|
2376
|
+
if (!client$1) throw new Error(`Server not found: ${serverName}`);
|
|
2377
|
+
return await client$1.getPrompt(actualPromptName, args);
|
|
2378
|
+
}
|
|
2379
|
+
const serversWithPrompt = [];
|
|
2380
|
+
await Promise.all(clients.map(async (client$1) => {
|
|
2381
|
+
try {
|
|
2382
|
+
if ((await client$1.listPrompts()).some((p) => p.name === name)) serversWithPrompt.push(client$1.serverName);
|
|
2383
|
+
} catch (error) {
|
|
2384
|
+
console.error(`Failed to list prompts from ${client$1.serverName}:`, error);
|
|
2385
|
+
}
|
|
2386
|
+
}));
|
|
2387
|
+
if (serversWithPrompt.length === 0) throw new Error(`Prompt not found: ${name}`);
|
|
2388
|
+
if (serversWithPrompt.length > 1) throw new Error(`Prompt "${name}" exists on multiple servers: ${serversWithPrompt.join(", ")}. Use the prefixed format (e.g., "${serversWithPrompt[0]}__${name}") to specify which server to use.`);
|
|
2389
|
+
const client = clientManager.getClient(serversWithPrompt[0]);
|
|
2390
|
+
if (!client) throw new Error(`Server not found: ${serversWithPrompt[0]}`);
|
|
2391
|
+
return await client.getPrompt(name, args);
|
|
2392
|
+
});
|
|
1923
2393
|
return server;
|
|
1924
2394
|
}
|
|
1925
2395
|
|