@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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
|
-
import { CallToolRequestSchema, ListToolsRequestSchema, isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
3
|
-
import { access, mkdir, readFile, readdir, stat, unlink, writeFile } from "node:fs/promises";
|
|
2
|
+
import { CallToolRequestSchema, GetPromptRequestSchema, ListPromptsRequestSchema, ListToolsRequestSchema, isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
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";
|
|
@@ -175,10 +176,25 @@ function validateRemoteConfigSource(source) {
|
|
|
175
176
|
* Claude Code / Claude Desktop standard MCP config format
|
|
176
177
|
* This is the format users write in their config files
|
|
177
178
|
*/
|
|
179
|
+
/**
|
|
180
|
+
* Prompt skill configuration schema
|
|
181
|
+
* Converts a prompt to an executable skill
|
|
182
|
+
*/
|
|
183
|
+
const PromptSkillConfigSchema = z.object({
|
|
184
|
+
name: z.string(),
|
|
185
|
+
description: z.string(),
|
|
186
|
+
folder: z.string().optional()
|
|
187
|
+
});
|
|
188
|
+
/**
|
|
189
|
+
* Prompt configuration schema
|
|
190
|
+
* Supports converting prompts to skills
|
|
191
|
+
*/
|
|
192
|
+
const PromptConfigSchema = z.object({ skill: PromptSkillConfigSchema.optional() });
|
|
178
193
|
const AdditionalConfigSchema = z.object({
|
|
179
194
|
instruction: z.string().optional(),
|
|
180
195
|
toolBlacklist: z.array(z.string()).optional(),
|
|
181
|
-
omitToolDescription: z.boolean().optional()
|
|
196
|
+
omitToolDescription: z.boolean().optional(),
|
|
197
|
+
prompts: z.record(z.string(), PromptConfigSchema).optional()
|
|
182
198
|
}).optional();
|
|
183
199
|
const ClaudeCodeStdioServerSchema = z.object({
|
|
184
200
|
command: z.string(),
|
|
@@ -245,12 +261,25 @@ const McpSseConfigSchema = z.object({
|
|
|
245
261
|
url: z.string().url(),
|
|
246
262
|
headers: z.record(z.string(), z.string()).optional()
|
|
247
263
|
});
|
|
264
|
+
/**
|
|
265
|
+
* Internal prompt skill configuration schema
|
|
266
|
+
*/
|
|
267
|
+
const InternalPromptSkillConfigSchema = z.object({
|
|
268
|
+
name: z.string(),
|
|
269
|
+
description: z.string(),
|
|
270
|
+
folder: z.string().optional()
|
|
271
|
+
});
|
|
272
|
+
/**
|
|
273
|
+
* Internal prompt configuration schema
|
|
274
|
+
*/
|
|
275
|
+
const InternalPromptConfigSchema = z.object({ skill: InternalPromptSkillConfigSchema.optional() });
|
|
248
276
|
const McpServerConfigSchema = z.discriminatedUnion("transport", [
|
|
249
277
|
z.object({
|
|
250
278
|
name: z.string(),
|
|
251
279
|
instruction: z.string().optional(),
|
|
252
280
|
toolBlacklist: z.array(z.string()).optional(),
|
|
253
281
|
omitToolDescription: z.boolean().optional(),
|
|
282
|
+
prompts: z.record(z.string(), InternalPromptConfigSchema).optional(),
|
|
254
283
|
transport: z.literal("stdio"),
|
|
255
284
|
config: McpStdioConfigSchema
|
|
256
285
|
}),
|
|
@@ -259,6 +288,7 @@ const McpServerConfigSchema = z.discriminatedUnion("transport", [
|
|
|
259
288
|
instruction: z.string().optional(),
|
|
260
289
|
toolBlacklist: z.array(z.string()).optional(),
|
|
261
290
|
omitToolDescription: z.boolean().optional(),
|
|
291
|
+
prompts: z.record(z.string(), InternalPromptConfigSchema).optional(),
|
|
262
292
|
transport: z.literal("http"),
|
|
263
293
|
config: McpHttpConfigSchema
|
|
264
294
|
}),
|
|
@@ -267,6 +297,7 @@ const McpServerConfigSchema = z.discriminatedUnion("transport", [
|
|
|
267
297
|
instruction: z.string().optional(),
|
|
268
298
|
toolBlacklist: z.array(z.string()).optional(),
|
|
269
299
|
omitToolDescription: z.boolean().optional(),
|
|
300
|
+
prompts: z.record(z.string(), InternalPromptConfigSchema).optional(),
|
|
270
301
|
transport: z.literal("sse"),
|
|
271
302
|
config: McpSseConfigSchema
|
|
272
303
|
})
|
|
@@ -299,6 +330,7 @@ function transformClaudeCodeConfig(claudeConfig) {
|
|
|
299
330
|
instruction: stdioConfig.instruction || stdioConfig.config?.instruction,
|
|
300
331
|
toolBlacklist: stdioConfig.config?.toolBlacklist,
|
|
301
332
|
omitToolDescription: stdioConfig.config?.omitToolDescription,
|
|
333
|
+
prompts: stdioConfig.config?.prompts,
|
|
302
334
|
transport: "stdio",
|
|
303
335
|
config: {
|
|
304
336
|
command: interpolatedCommand,
|
|
@@ -316,6 +348,7 @@ function transformClaudeCodeConfig(claudeConfig) {
|
|
|
316
348
|
instruction: httpConfig.instruction || httpConfig.config?.instruction,
|
|
317
349
|
toolBlacklist: httpConfig.config?.toolBlacklist,
|
|
318
350
|
omitToolDescription: httpConfig.config?.omitToolDescription,
|
|
351
|
+
prompts: httpConfig.config?.prompts,
|
|
319
352
|
transport,
|
|
320
353
|
config: {
|
|
321
354
|
url: interpolatedUrl,
|
|
@@ -776,12 +809,14 @@ var ConfigFetcherService = class {
|
|
|
776
809
|
//#region src/services/McpClientManagerService.ts
|
|
777
810
|
/**
|
|
778
811
|
* MCP Client wrapper for managing individual server connections
|
|
812
|
+
* This is an internal class used by McpClientManagerService
|
|
779
813
|
*/
|
|
780
814
|
var McpClient = class {
|
|
781
815
|
serverName;
|
|
782
816
|
serverInstruction;
|
|
783
817
|
toolBlacklist;
|
|
784
818
|
omitToolDescription;
|
|
819
|
+
prompts;
|
|
785
820
|
transport;
|
|
786
821
|
client;
|
|
787
822
|
childProcess;
|
|
@@ -791,6 +826,7 @@ var McpClient = class {
|
|
|
791
826
|
this.serverInstruction = config.instruction;
|
|
792
827
|
this.toolBlacklist = config.toolBlacklist;
|
|
793
828
|
this.omitToolDescription = config.omitToolDescription;
|
|
829
|
+
this.prompts = config.prompts;
|
|
794
830
|
this.transport = transport;
|
|
795
831
|
this.client = client;
|
|
796
832
|
}
|
|
@@ -886,7 +922,8 @@ var McpClientManagerService = class {
|
|
|
886
922
|
const mcpClient = new McpClient(serverName, config.transport, client, {
|
|
887
923
|
instruction: config.instruction,
|
|
888
924
|
toolBlacklist: config.toolBlacklist,
|
|
889
|
-
omitToolDescription: config.omitToolDescription
|
|
925
|
+
omitToolDescription: config.omitToolDescription,
|
|
926
|
+
prompts: config.prompts
|
|
890
927
|
});
|
|
891
928
|
try {
|
|
892
929
|
await Promise.race([this.performConnection(mcpClient, config), new Promise((_, reject) => setTimeout(() => reject(/* @__PURE__ */ new Error(`Connection timeout after ${timeoutMs}ms`)), timeoutMs))]);
|
|
@@ -908,6 +945,7 @@ var McpClientManagerService = class {
|
|
|
908
945
|
*/
|
|
909
946
|
async performConnection(mcpClient, config) {
|
|
910
947
|
if (config.transport === "stdio") await this.connectStdioClient(mcpClient, config.config);
|
|
948
|
+
else if (config.transport === "http") await this.connectHttpClient(mcpClient, config.config);
|
|
911
949
|
else if (config.transport === "sse") await this.connectSseClient(mcpClient, config.config);
|
|
912
950
|
else throw new Error(`Unsupported transport type: ${config.transport}`);
|
|
913
951
|
}
|
|
@@ -921,6 +959,10 @@ var McpClientManagerService = class {
|
|
|
921
959
|
const childProcess = transport["_process"];
|
|
922
960
|
if (childProcess) mcpClient.setChildProcess(childProcess);
|
|
923
961
|
}
|
|
962
|
+
async connectHttpClient(mcpClient, config) {
|
|
963
|
+
const transport = new StreamableHTTPClientTransport(new URL(config.url), { requestInit: config.headers ? { headers: config.headers } : void 0 });
|
|
964
|
+
await mcpClient["client"].connect(transport);
|
|
965
|
+
}
|
|
924
966
|
async connectSseClient(mcpClient, config) {
|
|
925
967
|
const transport = new SSEClientTransport(new URL(config.url));
|
|
926
968
|
await mcpClient["client"].connect(transport);
|
|
@@ -963,6 +1005,209 @@ var McpClientManagerService = class {
|
|
|
963
1005
|
}
|
|
964
1006
|
};
|
|
965
1007
|
|
|
1008
|
+
//#endregion
|
|
1009
|
+
//#region src/utils/findConfigFile.ts
|
|
1010
|
+
/**
|
|
1011
|
+
* Config File Finder Utility
|
|
1012
|
+
*
|
|
1013
|
+
* DESIGN PATTERNS:
|
|
1014
|
+
* - Utility function pattern for reusable logic
|
|
1015
|
+
* - Fail-fast pattern with early returns
|
|
1016
|
+
* - Environment variable configuration pattern
|
|
1017
|
+
*
|
|
1018
|
+
* CODING STANDARDS:
|
|
1019
|
+
* - Use sync filesystem operations for config discovery (performance)
|
|
1020
|
+
* - Check PROJECT_PATH environment variable first
|
|
1021
|
+
* - Fall back to current working directory
|
|
1022
|
+
* - Support both .yaml and .json extensions
|
|
1023
|
+
* - Return null if no config file is found
|
|
1024
|
+
*
|
|
1025
|
+
* AVOID:
|
|
1026
|
+
* - Throwing errors (return null instead for optional config)
|
|
1027
|
+
* - Hardcoded file names without extension variants
|
|
1028
|
+
* - Ignoring environment variables
|
|
1029
|
+
*/
|
|
1030
|
+
/**
|
|
1031
|
+
* Find MCP configuration file by checking PROJECT_PATH first, then cwd
|
|
1032
|
+
* Looks for both mcp-config.yaml and mcp-config.json
|
|
1033
|
+
*
|
|
1034
|
+
* @returns Absolute path to config file, or null if not found
|
|
1035
|
+
*/
|
|
1036
|
+
function findConfigFile() {
|
|
1037
|
+
const configFileNames = [
|
|
1038
|
+
"mcp-config.yaml",
|
|
1039
|
+
"mcp-config.yml",
|
|
1040
|
+
"mcp-config.json"
|
|
1041
|
+
];
|
|
1042
|
+
const projectPath = process.env.PROJECT_PATH;
|
|
1043
|
+
if (projectPath) for (const fileName of configFileNames) {
|
|
1044
|
+
const configPath = resolve(projectPath, fileName);
|
|
1045
|
+
if (existsSync(configPath)) return configPath;
|
|
1046
|
+
}
|
|
1047
|
+
const cwd = process.cwd();
|
|
1048
|
+
for (const fileName of configFileNames) {
|
|
1049
|
+
const configPath = join(cwd, fileName);
|
|
1050
|
+
if (existsSync(configPath)) return configPath;
|
|
1051
|
+
}
|
|
1052
|
+
return null;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
//#endregion
|
|
1056
|
+
//#region src/utils/parseToolName.ts
|
|
1057
|
+
/**
|
|
1058
|
+
* Parse tool name to extract server and actual tool name
|
|
1059
|
+
* Supports both plain tool names and prefixed format: {serverName}__{toolName}
|
|
1060
|
+
*
|
|
1061
|
+
* @param toolName - The tool name to parse (e.g., "my_tool" or "server__my_tool")
|
|
1062
|
+
* @returns Parsed result with optional serverName and actualToolName
|
|
1063
|
+
*
|
|
1064
|
+
* @example
|
|
1065
|
+
* parseToolName("my_tool") // { actualToolName: "my_tool" }
|
|
1066
|
+
* parseToolName("server__my_tool") // { serverName: "server", actualToolName: "my_tool" }
|
|
1067
|
+
*/
|
|
1068
|
+
function parseToolName(toolName) {
|
|
1069
|
+
const separatorIndex = toolName.indexOf("__");
|
|
1070
|
+
if (separatorIndex > 0) return {
|
|
1071
|
+
serverName: toolName.substring(0, separatorIndex),
|
|
1072
|
+
actualToolName: toolName.substring(separatorIndex + 2)
|
|
1073
|
+
};
|
|
1074
|
+
return { actualToolName: toolName };
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
//#endregion
|
|
1078
|
+
//#region src/utils/parseFrontMatter.ts
|
|
1079
|
+
/**
|
|
1080
|
+
* Parses YAML front matter from a string content.
|
|
1081
|
+
* Front matter must be at the start of the content, delimited by `---`.
|
|
1082
|
+
*
|
|
1083
|
+
* Supports:
|
|
1084
|
+
* - Simple key: value pairs
|
|
1085
|
+
* - Literal block scalar (|) for multi-line preserving newlines
|
|
1086
|
+
* - Folded block scalar (>) for multi-line folding to single line
|
|
1087
|
+
*
|
|
1088
|
+
* @param content - The content string that may contain front matter
|
|
1089
|
+
* @returns Object with parsed front matter (or null) and remaining content
|
|
1090
|
+
*
|
|
1091
|
+
* @example
|
|
1092
|
+
* const result = parseFrontMatter(`---
|
|
1093
|
+
* name: my-skill
|
|
1094
|
+
* description: A skill description
|
|
1095
|
+
* ---
|
|
1096
|
+
* The actual content here`);
|
|
1097
|
+
* // result.frontMatter = { name: 'my-skill', description: 'A skill description' }
|
|
1098
|
+
* // result.content = 'The actual content here'
|
|
1099
|
+
*
|
|
1100
|
+
* @example
|
|
1101
|
+
* // Multi-line with literal block scalar
|
|
1102
|
+
* const result = parseFrontMatter(`---
|
|
1103
|
+
* name: my-skill
|
|
1104
|
+
* description: |
|
|
1105
|
+
* Line 1
|
|
1106
|
+
* Line 2
|
|
1107
|
+
* ---
|
|
1108
|
+
* Content`);
|
|
1109
|
+
* // result.frontMatter.description = 'Line 1\nLine 2'
|
|
1110
|
+
*/
|
|
1111
|
+
function parseFrontMatter(content) {
|
|
1112
|
+
const trimmedContent = content.trimStart();
|
|
1113
|
+
if (!trimmedContent.startsWith("---")) return {
|
|
1114
|
+
frontMatter: null,
|
|
1115
|
+
content
|
|
1116
|
+
};
|
|
1117
|
+
const endDelimiterIndex = trimmedContent.indexOf("\n---", 3);
|
|
1118
|
+
if (endDelimiterIndex === -1) return {
|
|
1119
|
+
frontMatter: null,
|
|
1120
|
+
content
|
|
1121
|
+
};
|
|
1122
|
+
const yamlContent = trimmedContent.slice(4, endDelimiterIndex).trim();
|
|
1123
|
+
if (!yamlContent) return {
|
|
1124
|
+
frontMatter: null,
|
|
1125
|
+
content
|
|
1126
|
+
};
|
|
1127
|
+
const frontMatter = {};
|
|
1128
|
+
const lines = yamlContent.split("\n");
|
|
1129
|
+
let currentKey = null;
|
|
1130
|
+
let currentValue = [];
|
|
1131
|
+
let multiLineMode = null;
|
|
1132
|
+
let baseIndent = 0;
|
|
1133
|
+
const saveCurrentKey = () => {
|
|
1134
|
+
if (currentKey && currentValue.length > 0) if (multiLineMode === "literal") frontMatter[currentKey] = currentValue.join("\n").trimEnd();
|
|
1135
|
+
else if (multiLineMode === "folded") frontMatter[currentKey] = currentValue.join(" ").trim();
|
|
1136
|
+
else frontMatter[currentKey] = currentValue.join("").trim();
|
|
1137
|
+
currentKey = null;
|
|
1138
|
+
currentValue = [];
|
|
1139
|
+
multiLineMode = null;
|
|
1140
|
+
baseIndent = 0;
|
|
1141
|
+
};
|
|
1142
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1143
|
+
const line = lines[i];
|
|
1144
|
+
const trimmedLine = line.trim();
|
|
1145
|
+
const colonIndex = line.indexOf(":");
|
|
1146
|
+
if (colonIndex !== -1 && !line.startsWith(" ") && !line.startsWith(" ")) {
|
|
1147
|
+
saveCurrentKey();
|
|
1148
|
+
const key = line.slice(0, colonIndex).trim();
|
|
1149
|
+
let value = line.slice(colonIndex + 1).trim();
|
|
1150
|
+
if (value === "|" || value === "|-") {
|
|
1151
|
+
currentKey = key;
|
|
1152
|
+
multiLineMode = "literal";
|
|
1153
|
+
if (i + 1 < lines.length) {
|
|
1154
|
+
const match = lines[i + 1].match(/^(\s+)/);
|
|
1155
|
+
baseIndent = match ? match[1].length : 2;
|
|
1156
|
+
}
|
|
1157
|
+
} else if (value === ">" || value === ">-") {
|
|
1158
|
+
currentKey = key;
|
|
1159
|
+
multiLineMode = "folded";
|
|
1160
|
+
if (i + 1 < lines.length) {
|
|
1161
|
+
const match = lines[i + 1].match(/^(\s+)/);
|
|
1162
|
+
baseIndent = match ? match[1].length : 2;
|
|
1163
|
+
}
|
|
1164
|
+
} else {
|
|
1165
|
+
if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
|
|
1166
|
+
if (key && value) frontMatter[key] = value;
|
|
1167
|
+
}
|
|
1168
|
+
} else if (multiLineMode && currentKey) {
|
|
1169
|
+
const lineIndent = line.match(/^(\s*)/)?.[1].length || 0;
|
|
1170
|
+
if (trimmedLine === "") currentValue.push("");
|
|
1171
|
+
else if (lineIndent >= baseIndent) {
|
|
1172
|
+
const unindentedLine = line.slice(baseIndent);
|
|
1173
|
+
currentValue.push(unindentedLine);
|
|
1174
|
+
} else saveCurrentKey();
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
saveCurrentKey();
|
|
1178
|
+
return {
|
|
1179
|
+
frontMatter,
|
|
1180
|
+
content: trimmedContent.slice(endDelimiterIndex + 4).trimStart()
|
|
1181
|
+
};
|
|
1182
|
+
}
|
|
1183
|
+
/**
|
|
1184
|
+
* Checks if parsed front matter contains valid skill metadata.
|
|
1185
|
+
* A valid skill front matter must have both `name` and `description` fields.
|
|
1186
|
+
*
|
|
1187
|
+
* @param frontMatter - The parsed front matter object
|
|
1188
|
+
* @returns True if front matter contains valid skill metadata
|
|
1189
|
+
*/
|
|
1190
|
+
function isValidSkillFrontMatter(frontMatter) {
|
|
1191
|
+
return frontMatter !== null && typeof frontMatter.name === "string" && frontMatter.name.length > 0 && typeof frontMatter.description === "string" && frontMatter.description.length > 0;
|
|
1192
|
+
}
|
|
1193
|
+
/**
|
|
1194
|
+
* Extracts skill front matter from content if present and valid.
|
|
1195
|
+
*
|
|
1196
|
+
* @param content - The content string that may contain skill front matter
|
|
1197
|
+
* @returns Object with skill metadata and content, or null if no valid skill front matter
|
|
1198
|
+
*/
|
|
1199
|
+
function extractSkillFrontMatter(content) {
|
|
1200
|
+
const { frontMatter, content: remainingContent } = parseFrontMatter(content);
|
|
1201
|
+
if (frontMatter && isValidSkillFrontMatter(frontMatter)) return {
|
|
1202
|
+
skill: {
|
|
1203
|
+
name: frontMatter.name,
|
|
1204
|
+
description: frontMatter.description
|
|
1205
|
+
},
|
|
1206
|
+
content: remainingContent
|
|
1207
|
+
};
|
|
1208
|
+
return null;
|
|
1209
|
+
}
|
|
1210
|
+
|
|
966
1211
|
//#endregion
|
|
967
1212
|
//#region src/services/SkillService.ts
|
|
968
1213
|
/**
|
|
@@ -1033,14 +1278,21 @@ var SkillService = class {
|
|
|
1033
1278
|
skillPaths;
|
|
1034
1279
|
cachedSkills = null;
|
|
1035
1280
|
skillsByName = null;
|
|
1281
|
+
/** Active file watchers for skill directories */
|
|
1282
|
+
watchers = [];
|
|
1283
|
+
/** Callback invoked when cache is invalidated due to file changes */
|
|
1284
|
+
onCacheInvalidated;
|
|
1036
1285
|
/**
|
|
1037
1286
|
* Creates a new SkillService instance
|
|
1038
1287
|
* @param cwd - Current working directory for resolving relative paths
|
|
1039
1288
|
* @param skillPaths - Array of paths to skills directories
|
|
1289
|
+
* @param options - Optional configuration
|
|
1290
|
+
* @param options.onCacheInvalidated - Callback invoked when cache is invalidated due to file changes
|
|
1040
1291
|
*/
|
|
1041
|
-
constructor(cwd, skillPaths) {
|
|
1292
|
+
constructor(cwd, skillPaths, options) {
|
|
1042
1293
|
this.cwd = cwd;
|
|
1043
1294
|
this.skillPaths = skillPaths;
|
|
1295
|
+
this.onCacheInvalidated = options?.onCacheInvalidated;
|
|
1044
1296
|
}
|
|
1045
1297
|
/**
|
|
1046
1298
|
* Get all available skills from configured directories.
|
|
@@ -1086,6 +1338,54 @@ var SkillService = class {
|
|
|
1086
1338
|
this.skillsByName = null;
|
|
1087
1339
|
}
|
|
1088
1340
|
/**
|
|
1341
|
+
* Starts watching skill directories for changes to SKILL.md files.
|
|
1342
|
+
* When changes are detected, the cache is automatically invalidated.
|
|
1343
|
+
*
|
|
1344
|
+
* Uses Node.js fs.watch with recursive option for efficient directory monitoring.
|
|
1345
|
+
* Only invalidates cache when SKILL.md files are modified.
|
|
1346
|
+
*
|
|
1347
|
+
* @example
|
|
1348
|
+
* const skillService = new SkillService(cwd, skillPaths, {
|
|
1349
|
+
* onCacheInvalidated: () => console.log('Skills cache invalidated')
|
|
1350
|
+
* });
|
|
1351
|
+
* await skillService.startWatching();
|
|
1352
|
+
*/
|
|
1353
|
+
async startWatching() {
|
|
1354
|
+
this.stopWatching();
|
|
1355
|
+
for (const skillPath of this.skillPaths) {
|
|
1356
|
+
const skillsDir = isAbsolute(skillPath) ? skillPath : join(this.cwd, skillPath);
|
|
1357
|
+
if (!await pathExists(skillsDir)) continue;
|
|
1358
|
+
const abortController = new AbortController();
|
|
1359
|
+
this.watchers.push(abortController);
|
|
1360
|
+
this.watchDirectory(skillsDir, abortController.signal).catch((error) => {
|
|
1361
|
+
if (error?.name !== "AbortError") console.error(`[skill-watcher] Error watching ${skillsDir}: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
1362
|
+
});
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
/**
|
|
1366
|
+
* Stops all active file watchers.
|
|
1367
|
+
* Should be called when the service is being disposed.
|
|
1368
|
+
*/
|
|
1369
|
+
stopWatching() {
|
|
1370
|
+
for (const controller of this.watchers) controller.abort();
|
|
1371
|
+
this.watchers = [];
|
|
1372
|
+
}
|
|
1373
|
+
/**
|
|
1374
|
+
* Watches a directory for changes to SKILL.md files.
|
|
1375
|
+
* @param dirPath - Directory path to watch
|
|
1376
|
+
* @param signal - AbortSignal to stop watching
|
|
1377
|
+
*/
|
|
1378
|
+
async watchDirectory(dirPath, signal) {
|
|
1379
|
+
const watcher = watch(dirPath, {
|
|
1380
|
+
recursive: true,
|
|
1381
|
+
signal
|
|
1382
|
+
});
|
|
1383
|
+
for await (const event of watcher) if (event.filename && event.filename.endsWith("SKILL.md")) {
|
|
1384
|
+
this.clearCache();
|
|
1385
|
+
this.onCacheInvalidated?.();
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
/**
|
|
1089
1389
|
* Load skills from a directory.
|
|
1090
1390
|
* Supports both flat structure (SKILL.md) and nested structure (name/SKILL.md).
|
|
1091
1391
|
*
|
|
@@ -1144,6 +1444,7 @@ var SkillService = class {
|
|
|
1144
1444
|
}
|
|
1145
1445
|
/**
|
|
1146
1446
|
* Load a single skill file and parse its frontmatter.
|
|
1447
|
+
* Supports multi-line YAML values using literal (|) and folded (>) block scalars.
|
|
1147
1448
|
*
|
|
1148
1449
|
* @param filePath - Path to the SKILL.md file
|
|
1149
1450
|
* @param location - Whether this is a 'project' or 'user' skill
|
|
@@ -1157,150 +1458,62 @@ var SkillService = class {
|
|
|
1157
1458
|
* // Returns null if frontmatter is missing name or description
|
|
1158
1459
|
*/
|
|
1159
1460
|
async loadSkillFile(filePath, location) {
|
|
1160
|
-
let
|
|
1461
|
+
let fileContent;
|
|
1161
1462
|
try {
|
|
1162
|
-
|
|
1463
|
+
fileContent = await readFile(filePath, "utf-8");
|
|
1163
1464
|
} catch (error) {
|
|
1164
1465
|
throw new SkillLoadError(`Failed to read skill file: ${error instanceof Error ? error.message : "Unknown error"}`, filePath, error instanceof Error ? error : void 0);
|
|
1165
1466
|
}
|
|
1166
|
-
const {
|
|
1167
|
-
if (!
|
|
1467
|
+
const { frontMatter, content } = parseFrontMatter(fileContent);
|
|
1468
|
+
if (!frontMatter || !frontMatter.name || !frontMatter.description) return null;
|
|
1168
1469
|
return {
|
|
1169
|
-
name:
|
|
1170
|
-
description:
|
|
1470
|
+
name: frontMatter.name,
|
|
1471
|
+
description: frontMatter.description,
|
|
1171
1472
|
location,
|
|
1172
|
-
content
|
|
1473
|
+
content,
|
|
1173
1474
|
basePath: dirname(filePath)
|
|
1174
1475
|
};
|
|
1175
1476
|
}
|
|
1176
|
-
/**
|
|
1177
|
-
* Parse YAML frontmatter from markdown content.
|
|
1178
|
-
* Frontmatter is delimited by --- at start and end.
|
|
1179
|
-
*
|
|
1180
|
-
* @param content - Full markdown content with frontmatter
|
|
1181
|
-
* @returns Parsed metadata and body content
|
|
1182
|
-
*
|
|
1183
|
-
* @example
|
|
1184
|
-
* // Input content:
|
|
1185
|
-
* // ---
|
|
1186
|
-
* // name: my-skill
|
|
1187
|
-
* // description: A sample skill
|
|
1188
|
-
* // ---
|
|
1189
|
-
* // # Skill Content
|
|
1190
|
-
* // This is the skill body.
|
|
1191
|
-
*
|
|
1192
|
-
* const result = parseFrontmatter(content);
|
|
1193
|
-
* // result.metadata = { name: 'my-skill', description: 'A sample skill' }
|
|
1194
|
-
* // result.body = '# Skill Content\nThis is the skill body.'
|
|
1195
|
-
*/
|
|
1196
|
-
parseFrontmatter(content) {
|
|
1197
|
-
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
|
|
1198
|
-
if (!match) return {
|
|
1199
|
-
metadata: {},
|
|
1200
|
-
body: content
|
|
1201
|
-
};
|
|
1202
|
-
const [, frontmatter, body] = match;
|
|
1203
|
-
const metadata = {};
|
|
1204
|
-
const lines = frontmatter.split("\n");
|
|
1205
|
-
for (const line of lines) {
|
|
1206
|
-
const colonIndex = line.indexOf(":");
|
|
1207
|
-
if (colonIndex > 0) {
|
|
1208
|
-
const key = line.slice(0, colonIndex).trim();
|
|
1209
|
-
let value = line.slice(colonIndex + 1).trim();
|
|
1210
|
-
if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
|
|
1211
|
-
if (key === "name" || key === "description" || key === "license") metadata[key] = value;
|
|
1212
|
-
}
|
|
1213
|
-
}
|
|
1214
|
-
return {
|
|
1215
|
-
metadata,
|
|
1216
|
-
body: body.trim()
|
|
1217
|
-
};
|
|
1218
|
-
}
|
|
1219
1477
|
};
|
|
1220
1478
|
|
|
1221
1479
|
//#endregion
|
|
1222
|
-
//#region src/
|
|
1480
|
+
//#region src/constants/index.ts
|
|
1223
1481
|
/**
|
|
1224
|
-
*
|
|
1225
|
-
*
|
|
1226
|
-
* DESIGN PATTERNS:
|
|
1227
|
-
* - Utility function pattern for reusable logic
|
|
1228
|
-
* - Fail-fast pattern with early returns
|
|
1229
|
-
* - Environment variable configuration pattern
|
|
1230
|
-
*
|
|
1231
|
-
* CODING STANDARDS:
|
|
1232
|
-
* - Use sync filesystem operations for config discovery (performance)
|
|
1233
|
-
* - Check PROJECT_PATH environment variable first
|
|
1234
|
-
* - Fall back to current working directory
|
|
1235
|
-
* - Support both .yaml and .json extensions
|
|
1236
|
-
* - Return null if no config file is found
|
|
1237
|
-
*
|
|
1238
|
-
* AVOID:
|
|
1239
|
-
* - Throwing errors (return null instead for optional config)
|
|
1240
|
-
* - Hardcoded file names without extension variants
|
|
1241
|
-
* - Ignoring environment variables
|
|
1482
|
+
* Shared constants for one-mcp package
|
|
1242
1483
|
*/
|
|
1243
1484
|
/**
|
|
1244
|
-
*
|
|
1245
|
-
*
|
|
1246
|
-
*
|
|
1247
|
-
* @returns Absolute path to config file, or null if not found
|
|
1485
|
+
* Prefix added to skill names when they clash with MCP tool names.
|
|
1486
|
+
* This ensures skills can be uniquely identified even when a tool has the same name.
|
|
1248
1487
|
*/
|
|
1249
|
-
|
|
1250
|
-
const configFileNames = [
|
|
1251
|
-
"mcp-config.yaml",
|
|
1252
|
-
"mcp-config.yml",
|
|
1253
|
-
"mcp-config.json"
|
|
1254
|
-
];
|
|
1255
|
-
const projectPath = process.env.PROJECT_PATH;
|
|
1256
|
-
if (projectPath) for (const fileName of configFileNames) {
|
|
1257
|
-
const configPath = resolve(projectPath, fileName);
|
|
1258
|
-
if (existsSync(configPath)) return configPath;
|
|
1259
|
-
}
|
|
1260
|
-
const cwd = process.cwd();
|
|
1261
|
-
for (const fileName of configFileNames) {
|
|
1262
|
-
const configPath = join(cwd, fileName);
|
|
1263
|
-
if (existsSync(configPath)) return configPath;
|
|
1264
|
-
}
|
|
1265
|
-
return null;
|
|
1266
|
-
}
|
|
1267
|
-
|
|
1268
|
-
//#endregion
|
|
1269
|
-
//#region src/utils/parseToolName.ts
|
|
1488
|
+
const SKILL_PREFIX$1 = "skill__";
|
|
1270
1489
|
/**
|
|
1271
|
-
*
|
|
1272
|
-
*
|
|
1273
|
-
*
|
|
1274
|
-
* @param toolName - The tool name to parse (e.g., "my_tool" or "server__my_tool")
|
|
1275
|
-
* @returns Parsed result with optional serverName and actualToolName
|
|
1276
|
-
*
|
|
1277
|
-
* @example
|
|
1278
|
-
* parseToolName("my_tool") // { actualToolName: "my_tool" }
|
|
1279
|
-
* parseToolName("server__my_tool") // { serverName: "server", actualToolName: "my_tool" }
|
|
1490
|
+
* Log prefix for skill detection messages.
|
|
1491
|
+
* Used to easily filter skill detection logs in stderr output.
|
|
1280
1492
|
*/
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
return { actualToolName: toolName };
|
|
1288
|
-
}
|
|
1289
|
-
|
|
1290
|
-
//#endregion
|
|
1291
|
-
//#region src/templates/skills-description.liquid?raw
|
|
1292
|
-
var skills_description_default = "{% if skills.size > 0 %}\n<skills>\n<instructions>\nWhen users ask you to perform tasks, check if any of the available skills below can help complete the task more effectively. Skills provide specialized capabilities and domain knowledge.\n\nHow to use skills:\n- Invoke skills using this tool with the skill name only (no arguments)\n- When you invoke a skill, you will see <command-message>The \"{name}\" skill is loading</command-message>\n- The skill's prompt will expand and provide detailed instructions on how to complete the task\n- Examples:\n - `skill: \"pdf\"` - invoke the pdf skill\n - `skill: \"xlsx\"` - invoke the xlsx skill\n - `skill: \"ms-office-suite:pdf\"` - invoke using fully qualified name\n\nImportant:\n- Only use skills listed in <available_skills> below\n- Do not invoke a skill that is already running\n- Do not use this tool for built-in CLI commands (like /help, /clear, etc.)\n</instructions>\n\n<available_skills>\n{% for skill in skills -%}\n<skill-item><name>{{ skill.displayName }}</name><description>{{ skill.description }}</description></skill-item>\n{% endfor -%}\n</available_skills>\n</skills>\n{% endif %}\n\n<usage_instructions>\nBefore you use any tools above, you MUST call this tool with a list of tool names to learn how to use them properly before use_tool; this includes:\n- Arguments schema needed to pass to the tool use\n- Description about each tool\n\nThis tool is optimized for batch queries - you can request multiple tools at once for better performance.\n</usage_instructions>\n";
|
|
1493
|
+
const LOG_PREFIX_SKILL_DETECTION = "[skill-detection]";
|
|
1494
|
+
/**
|
|
1495
|
+
* Prefix for prompt-based skill locations.
|
|
1496
|
+
* Format: "prompt:{serverName}:{promptName}"
|
|
1497
|
+
*/
|
|
1498
|
+
const PROMPT_LOCATION_PREFIX = "prompt:";
|
|
1293
1499
|
|
|
1294
1500
|
//#endregion
|
|
1295
|
-
//#region src/templates/
|
|
1296
|
-
var
|
|
1501
|
+
//#region src/templates/toolkit-description.liquid?raw
|
|
1502
|
+
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";
|
|
1297
1503
|
|
|
1298
1504
|
//#endregion
|
|
1299
1505
|
//#region src/tools/DescribeToolsTool.ts
|
|
1300
1506
|
/**
|
|
1301
|
-
*
|
|
1507
|
+
* Formats skill instructions with the loading command message prefix.
|
|
1508
|
+
* This message is used by Claude Code to indicate that a skill is being loaded.
|
|
1509
|
+
*
|
|
1510
|
+
* @param name - The skill name
|
|
1511
|
+
* @param instructions - The raw skill instructions/content
|
|
1512
|
+
* @returns Formatted instructions with command message prefix
|
|
1302
1513
|
*/
|
|
1303
|
-
|
|
1514
|
+
function formatSkillInstructions(name, instructions) {
|
|
1515
|
+
return `<command-message>The "${name}" skill is loading</command-message>\n${instructions}`;
|
|
1516
|
+
}
|
|
1304
1517
|
/**
|
|
1305
1518
|
* DescribeToolsTool provides progressive disclosure of MCP tools and skills.
|
|
1306
1519
|
*
|
|
@@ -1323,6 +1536,8 @@ var DescribeToolsTool = class DescribeToolsTool {
|
|
|
1323
1536
|
clientManager;
|
|
1324
1537
|
skillService;
|
|
1325
1538
|
liquid = new Liquid();
|
|
1539
|
+
/** Cache for auto-detected skills from prompt front-matter */
|
|
1540
|
+
autoDetectedSkillsCache = null;
|
|
1326
1541
|
/**
|
|
1327
1542
|
* Creates a new DescribeToolsTool instance
|
|
1328
1543
|
* @param clientManager - The MCP client manager for accessing remote servers
|
|
@@ -1333,43 +1548,169 @@ var DescribeToolsTool = class DescribeToolsTool {
|
|
|
1333
1548
|
this.skillService = skillService;
|
|
1334
1549
|
}
|
|
1335
1550
|
/**
|
|
1336
|
-
*
|
|
1551
|
+
* Clears the cached auto-detected skills from prompt front-matter.
|
|
1552
|
+
* Use this when prompt configurations may have changed or when
|
|
1553
|
+
* the skill service cache is invalidated.
|
|
1554
|
+
*/
|
|
1555
|
+
clearAutoDetectedSkillsCache() {
|
|
1556
|
+
this.autoDetectedSkillsCache = null;
|
|
1557
|
+
}
|
|
1558
|
+
/**
|
|
1559
|
+
* Detects and caches skills from prompt front-matter across all connected MCP servers.
|
|
1560
|
+
* Fetches all prompts and checks their content for YAML front-matter with name/description.
|
|
1561
|
+
* Results are cached to avoid repeated fetches.
|
|
1337
1562
|
*
|
|
1338
|
-
*
|
|
1339
|
-
*
|
|
1340
|
-
*
|
|
1563
|
+
* Error Handling Strategy:
|
|
1564
|
+
* - Errors are logged to stderr but do not fail the overall detection process
|
|
1565
|
+
* - This ensures partial results are returned even if some servers/prompts fail
|
|
1566
|
+
* - Common failure reasons: server temporarily unavailable, prompt requires arguments,
|
|
1567
|
+
* network timeout, or server doesn't support listPrompts
|
|
1568
|
+
* - Errors are prefixed with [skill-detection] for easy filtering in logs
|
|
1341
1569
|
*
|
|
1342
|
-
* @
|
|
1343
|
-
* @returns Rendered skills section string with available skills list
|
|
1570
|
+
* @returns Array of auto-detected skills from prompt front-matter
|
|
1344
1571
|
*/
|
|
1345
|
-
async
|
|
1346
|
-
|
|
1347
|
-
const
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1572
|
+
async detectSkillsFromPromptFrontMatter() {
|
|
1573
|
+
if (this.autoDetectedSkillsCache !== null) return this.autoDetectedSkillsCache;
|
|
1574
|
+
const clients = this.clientManager.getAllClients();
|
|
1575
|
+
const autoDetectedSkills = [];
|
|
1576
|
+
let listPromptsFailures = 0;
|
|
1577
|
+
let fetchPromptFailures = 0;
|
|
1578
|
+
const fetchPromises = [];
|
|
1579
|
+
for (const client of clients) {
|
|
1580
|
+
const configuredPromptNames = new Set(client.prompts ? Object.keys(client.prompts) : []);
|
|
1581
|
+
const listPromptsPromise = (async () => {
|
|
1582
|
+
try {
|
|
1583
|
+
const prompts = await client.listPrompts();
|
|
1584
|
+
if (!prompts || prompts.length === 0) return;
|
|
1585
|
+
const promptFetchPromises = prompts.map(async (promptInfo) => {
|
|
1586
|
+
if (configuredPromptNames.has(promptInfo.name)) return;
|
|
1587
|
+
try {
|
|
1588
|
+
const skillExtraction = extractSkillFrontMatter(((await client.getPrompt(promptInfo.name)).messages || []).map((m) => {
|
|
1589
|
+
const content = m.content;
|
|
1590
|
+
if (typeof content === "string") return content;
|
|
1591
|
+
if (content && typeof content === "object" && "text" in content) return String(content.text);
|
|
1592
|
+
return "";
|
|
1593
|
+
}).join("\n"));
|
|
1594
|
+
if (skillExtraction) autoDetectedSkills.push({
|
|
1595
|
+
serverName: client.serverName,
|
|
1596
|
+
promptName: promptInfo.name,
|
|
1597
|
+
skill: skillExtraction.skill
|
|
1598
|
+
});
|
|
1599
|
+
} catch (error) {
|
|
1600
|
+
fetchPromptFailures++;
|
|
1601
|
+
console.error(`${LOG_PREFIX_SKILL_DETECTION} Failed to fetch prompt '${promptInfo.name}' from ${client.serverName}: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
1602
|
+
}
|
|
1603
|
+
});
|
|
1604
|
+
await Promise.all(promptFetchPromises);
|
|
1605
|
+
} catch (error) {
|
|
1606
|
+
listPromptsFailures++;
|
|
1607
|
+
console.error(`${LOG_PREFIX_SKILL_DETECTION} Failed to list prompts from ${client.serverName}: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
1608
|
+
}
|
|
1609
|
+
})();
|
|
1610
|
+
fetchPromises.push(listPromptsPromise);
|
|
1611
|
+
}
|
|
1612
|
+
await Promise.all(fetchPromises);
|
|
1613
|
+
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).`);
|
|
1614
|
+
this.autoDetectedSkillsCache = autoDetectedSkills;
|
|
1615
|
+
return autoDetectedSkills;
|
|
1616
|
+
}
|
|
1617
|
+
/**
|
|
1618
|
+
* Collects skills derived from prompt configurations across all connected MCP servers.
|
|
1619
|
+
* Includes both explicitly configured prompts and auto-detected skills from front-matter.
|
|
1620
|
+
*
|
|
1621
|
+
* @returns Array of skill template data derived from prompts
|
|
1622
|
+
*/
|
|
1623
|
+
async collectPromptSkills() {
|
|
1624
|
+
const clients = this.clientManager.getAllClients();
|
|
1625
|
+
const promptSkills = [];
|
|
1626
|
+
for (const client of clients) {
|
|
1627
|
+
if (!client.prompts) continue;
|
|
1628
|
+
for (const promptConfig of Object.values(client.prompts)) if (promptConfig.skill) promptSkills.push({
|
|
1629
|
+
name: promptConfig.skill.name,
|
|
1630
|
+
displayName: promptConfig.skill.name,
|
|
1631
|
+
description: promptConfig.skill.description
|
|
1632
|
+
});
|
|
1633
|
+
}
|
|
1634
|
+
const autoDetectedSkills = await this.detectSkillsFromPromptFrontMatter();
|
|
1635
|
+
for (const autoSkill of autoDetectedSkills) promptSkills.push({
|
|
1636
|
+
name: autoSkill.skill.name,
|
|
1637
|
+
displayName: autoSkill.skill.name,
|
|
1638
|
+
description: autoSkill.skill.description
|
|
1639
|
+
});
|
|
1640
|
+
return promptSkills;
|
|
1641
|
+
}
|
|
1642
|
+
/**
|
|
1643
|
+
* Finds a prompt-based skill by name from all connected MCP servers.
|
|
1644
|
+
* Searches both explicitly configured prompts and auto-detected skills from front-matter.
|
|
1645
|
+
*
|
|
1646
|
+
* @param skillName - The skill name to search for
|
|
1647
|
+
* @returns Object with serverName, promptName, and skill config, or undefined if not found
|
|
1648
|
+
*/
|
|
1649
|
+
async findPromptSkill(skillName) {
|
|
1650
|
+
if (!skillName) return void 0;
|
|
1651
|
+
const clients = this.clientManager.getAllClients();
|
|
1652
|
+
for (const client of clients) {
|
|
1653
|
+
if (!client.prompts) continue;
|
|
1654
|
+
for (const [promptName, promptConfig] of Object.entries(client.prompts)) if (promptConfig.skill && promptConfig.skill.name === skillName) return {
|
|
1655
|
+
serverName: client.serverName,
|
|
1656
|
+
promptName,
|
|
1657
|
+
skill: promptConfig.skill
|
|
1658
|
+
};
|
|
1659
|
+
}
|
|
1660
|
+
const autoDetectedSkills = await this.detectSkillsFromPromptFrontMatter();
|
|
1661
|
+
for (const autoSkill of autoDetectedSkills) if (autoSkill.skill.name === skillName) return {
|
|
1662
|
+
serverName: autoSkill.serverName,
|
|
1663
|
+
promptName: autoSkill.promptName,
|
|
1664
|
+
skill: autoSkill.skill,
|
|
1665
|
+
autoDetected: true
|
|
1666
|
+
};
|
|
1667
|
+
}
|
|
1668
|
+
/**
|
|
1669
|
+
* Retrieves skill content from a prompt-based skill configuration.
|
|
1670
|
+
* Fetches the prompt from the MCP server and extracts text content.
|
|
1671
|
+
* Handles both explicitly configured prompts and auto-detected skills from front-matter.
|
|
1672
|
+
*
|
|
1673
|
+
* @param skillName - The skill name being requested
|
|
1674
|
+
* @returns SkillDescription if found and successfully fetched, undefined otherwise
|
|
1675
|
+
*/
|
|
1676
|
+
async getPromptSkillContent(skillName) {
|
|
1677
|
+
const promptSkill = await this.findPromptSkill(skillName);
|
|
1678
|
+
if (!promptSkill) return void 0;
|
|
1679
|
+
const client = this.clientManager.getClient(promptSkill.serverName);
|
|
1680
|
+
if (!client) {
|
|
1681
|
+
console.error(`Client not found for server '${promptSkill.serverName}' when fetching prompt skill '${skillName}'`);
|
|
1682
|
+
return;
|
|
1683
|
+
}
|
|
1684
|
+
try {
|
|
1685
|
+
const rawInstructions = (await client.getPrompt(promptSkill.promptName)).messages?.map((m) => {
|
|
1686
|
+
const content = m.content;
|
|
1687
|
+
if (typeof content === "string") return content;
|
|
1688
|
+
if (content && typeof content === "object" && "text" in content) return String(content.text);
|
|
1689
|
+
return "";
|
|
1690
|
+
}).join("\n") || "";
|
|
1353
1691
|
return {
|
|
1354
|
-
name: skill.name,
|
|
1355
|
-
|
|
1356
|
-
|
|
1692
|
+
name: promptSkill.skill.name,
|
|
1693
|
+
location: promptSkill.skill.folder || `${PROMPT_LOCATION_PREFIX}${promptSkill.serverName}/${promptSkill.promptName}`,
|
|
1694
|
+
instructions: formatSkillInstructions(promptSkill.skill.name, rawInstructions)
|
|
1357
1695
|
};
|
|
1358
|
-
})
|
|
1359
|
-
|
|
1696
|
+
} catch (error) {
|
|
1697
|
+
console.error(`Failed to get prompt-based skill '${skillName}': ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
1698
|
+
return;
|
|
1699
|
+
}
|
|
1360
1700
|
}
|
|
1361
1701
|
/**
|
|
1362
|
-
* Builds the
|
|
1702
|
+
* Builds the combined toolkit description using a single Liquid template.
|
|
1363
1703
|
*
|
|
1364
|
-
* Collects all tools from connected MCP servers
|
|
1365
|
-
*
|
|
1704
|
+
* Collects all tools from connected MCP servers and all skills, then renders
|
|
1705
|
+
* them together using the toolkit-description.liquid template.
|
|
1366
1706
|
*
|
|
1367
1707
|
* Tool names are prefixed with serverName__ when the same tool exists
|
|
1368
|
-
* on multiple servers
|
|
1708
|
+
* on multiple servers. Skill names are prefixed with skill__ when they
|
|
1709
|
+
* clash with MCP tools or other skills.
|
|
1369
1710
|
*
|
|
1370
|
-
* @returns Object with rendered
|
|
1711
|
+
* @returns Object with rendered description and set of all tool names
|
|
1371
1712
|
*/
|
|
1372
|
-
async
|
|
1713
|
+
async buildToolkitDescription() {
|
|
1373
1714
|
const clients = this.clientManager.getAllClients();
|
|
1374
1715
|
const toolToServers = /* @__PURE__ */ new Map();
|
|
1375
1716
|
const serverToolsMap = /* @__PURE__ */ new Map();
|
|
@@ -1390,9 +1731,6 @@ var DescribeToolsTool = class DescribeToolsTool {
|
|
|
1390
1731
|
}));
|
|
1391
1732
|
/**
|
|
1392
1733
|
* Formats tool name with server prefix if the tool exists on multiple servers
|
|
1393
|
-
* @param toolName - The original tool name
|
|
1394
|
-
* @param serverName - The server providing this tool
|
|
1395
|
-
* @returns Tool name prefixed with serverName__ if clashing, otherwise plain name
|
|
1396
1734
|
*/
|
|
1397
1735
|
const formatToolName = (toolName, serverName) => {
|
|
1398
1736
|
return (toolToServers.get(toolName) || []).length > 1 ? `${serverName}__${toolName}` : toolName;
|
|
@@ -1412,31 +1750,56 @@ var DescribeToolsTool = class DescribeToolsTool {
|
|
|
1412
1750
|
toolNames: formattedTools.map((t) => t.displayName)
|
|
1413
1751
|
};
|
|
1414
1752
|
});
|
|
1753
|
+
const rawSkills = this.skillService ? await this.skillService.getSkills() : [];
|
|
1754
|
+
const promptSkills = await this.collectPromptSkills();
|
|
1755
|
+
const seenSkillNames = /* @__PURE__ */ new Set();
|
|
1756
|
+
const allSkillsData = [];
|
|
1757
|
+
for (const skill of rawSkills) if (!seenSkillNames.has(skill.name)) {
|
|
1758
|
+
seenSkillNames.add(skill.name);
|
|
1759
|
+
allSkillsData.push({
|
|
1760
|
+
name: skill.name,
|
|
1761
|
+
displayName: skill.name,
|
|
1762
|
+
description: skill.description
|
|
1763
|
+
});
|
|
1764
|
+
}
|
|
1765
|
+
for (const skill of promptSkills) if (!seenSkillNames.has(skill.name)) {
|
|
1766
|
+
seenSkillNames.add(skill.name);
|
|
1767
|
+
allSkillsData.push(skill);
|
|
1768
|
+
}
|
|
1769
|
+
const skills = allSkillsData.map((skill) => {
|
|
1770
|
+
const clashesWithMcpTool = allToolNames.has(skill.name);
|
|
1771
|
+
return {
|
|
1772
|
+
name: skill.name,
|
|
1773
|
+
displayName: clashesWithMcpTool ? `${SKILL_PREFIX$1}${skill.name}` : skill.name,
|
|
1774
|
+
description: skill.description
|
|
1775
|
+
};
|
|
1776
|
+
});
|
|
1415
1777
|
return {
|
|
1416
|
-
content: await this.liquid.parseAndRender(
|
|
1778
|
+
content: await this.liquid.parseAndRender(toolkit_description_default, {
|
|
1779
|
+
servers,
|
|
1780
|
+
skills
|
|
1781
|
+
}),
|
|
1417
1782
|
toolNames: allToolNames
|
|
1418
1783
|
};
|
|
1419
1784
|
}
|
|
1420
1785
|
/**
|
|
1421
|
-
* Gets the tool definition including available
|
|
1786
|
+
* Gets the tool definition including available tools and skills in a unified format.
|
|
1422
1787
|
*
|
|
1423
1788
|
* The definition includes:
|
|
1424
|
-
* -
|
|
1425
|
-
* -
|
|
1426
|
-
* -
|
|
1789
|
+
* - All MCP tools from connected servers
|
|
1790
|
+
* - All available skills (file-based and prompt-based)
|
|
1791
|
+
* - Unified instructions for querying capability details
|
|
1427
1792
|
*
|
|
1428
|
-
* Tool names are prefixed with serverName__ when
|
|
1429
|
-
*
|
|
1793
|
+
* Tool names are prefixed with serverName__ when clashing.
|
|
1794
|
+
* Skill names are prefixed with skill__ when clashing.
|
|
1430
1795
|
*
|
|
1431
1796
|
* @returns Tool definition with description and input schema
|
|
1432
1797
|
*/
|
|
1433
1798
|
async getDefinition() {
|
|
1434
|
-
const
|
|
1435
|
-
const skillsSection = await this.buildSkillsSection(serversResult.toolNames);
|
|
1799
|
+
const { content } = await this.buildToolkitDescription();
|
|
1436
1800
|
return {
|
|
1437
1801
|
name: DescribeToolsTool.TOOL_NAME,
|
|
1438
|
-
description:
|
|
1439
|
-
${skillsSection}`,
|
|
1802
|
+
description: content,
|
|
1440
1803
|
inputSchema: {
|
|
1441
1804
|
type: "object",
|
|
1442
1805
|
properties: { toolNames: {
|
|
@@ -1501,16 +1864,24 @@ ${skillsSection}`,
|
|
|
1501
1864
|
const notFoundItems = [];
|
|
1502
1865
|
for (const requestedName of toolNames) {
|
|
1503
1866
|
if (requestedName.startsWith(SKILL_PREFIX$1)) {
|
|
1504
|
-
const skillName = requestedName.slice(
|
|
1867
|
+
const skillName = requestedName.slice(SKILL_PREFIX$1.length);
|
|
1505
1868
|
if (this.skillService) {
|
|
1506
1869
|
const skill = await this.skillService.getSkill(skillName);
|
|
1507
|
-
if (skill)
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1870
|
+
if (skill) {
|
|
1871
|
+
foundSkills.push({
|
|
1872
|
+
name: skill.name,
|
|
1873
|
+
location: skill.basePath,
|
|
1874
|
+
instructions: formatSkillInstructions(skill.name, skill.content)
|
|
1875
|
+
});
|
|
1876
|
+
continue;
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
const promptSkillContent = await this.getPromptSkillContent(skillName);
|
|
1880
|
+
if (promptSkillContent) {
|
|
1881
|
+
foundSkills.push(promptSkillContent);
|
|
1882
|
+
continue;
|
|
1883
|
+
}
|
|
1884
|
+
notFoundItems.push(requestedName);
|
|
1514
1885
|
continue;
|
|
1515
1886
|
}
|
|
1516
1887
|
const { serverName, actualToolName } = parseToolName(requestedName);
|
|
@@ -1539,11 +1910,16 @@ ${skillsSection}`,
|
|
|
1539
1910
|
foundSkills.push({
|
|
1540
1911
|
name: skill.name,
|
|
1541
1912
|
location: skill.basePath,
|
|
1542
|
-
instructions: skill.content
|
|
1913
|
+
instructions: formatSkillInstructions(skill.name, skill.content)
|
|
1543
1914
|
});
|
|
1544
1915
|
continue;
|
|
1545
1916
|
}
|
|
1546
1917
|
}
|
|
1918
|
+
const promptSkillContent = await this.getPromptSkillContent(actualToolName);
|
|
1919
|
+
if (promptSkillContent) {
|
|
1920
|
+
foundSkills.push(promptSkillContent);
|
|
1921
|
+
continue;
|
|
1922
|
+
}
|
|
1547
1923
|
notFoundItems.push(requestedName);
|
|
1548
1924
|
continue;
|
|
1549
1925
|
}
|
|
@@ -1652,7 +2028,7 @@ var UseToolTool = class UseToolTool {
|
|
|
1652
2028
|
getDefinition() {
|
|
1653
2029
|
return {
|
|
1654
2030
|
name: UseToolTool.TOOL_NAME,
|
|
1655
|
-
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:
|
|
2031
|
+
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:
|
|
1656
2032
|
- Provide toolName and toolArgs based on the schema
|
|
1657
2033
|
- If multiple servers provide the same tool, specify serverName
|
|
1658
2034
|
`,
|
|
@@ -1691,6 +2067,37 @@ var UseToolTool = class UseToolTool {
|
|
|
1691
2067
|
}] };
|
|
1692
2068
|
}
|
|
1693
2069
|
/**
|
|
2070
|
+
* Finds a prompt-based skill by name from all connected MCP servers.
|
|
2071
|
+
*
|
|
2072
|
+
* @param skillName - The skill name to search for
|
|
2073
|
+
* @returns PromptSkillMatch if found, undefined otherwise
|
|
2074
|
+
*/
|
|
2075
|
+
findPromptSkill(skillName) {
|
|
2076
|
+
if (!skillName) return void 0;
|
|
2077
|
+
const clients = this.clientManager.getAllClients();
|
|
2078
|
+
for (const client of clients) {
|
|
2079
|
+
if (!client.prompts) continue;
|
|
2080
|
+
for (const [promptName, promptConfig] of Object.entries(client.prompts)) if (promptConfig.skill && promptConfig.skill.name === skillName) return {
|
|
2081
|
+
serverName: client.serverName,
|
|
2082
|
+
promptName,
|
|
2083
|
+
skill: promptConfig.skill
|
|
2084
|
+
};
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
/**
|
|
2088
|
+
* Returns guidance message for prompt-based skill invocation.
|
|
2089
|
+
*
|
|
2090
|
+
* @param promptSkill - The prompt skill match that was found
|
|
2091
|
+
* @returns CallToolResult with guidance message
|
|
2092
|
+
*/
|
|
2093
|
+
executePromptSkill(promptSkill) {
|
|
2094
|
+
const location = promptSkill.skill.folder || `prompt:${promptSkill.serverName}/${promptSkill.promptName}`;
|
|
2095
|
+
return { content: [{
|
|
2096
|
+
type: "text",
|
|
2097
|
+
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.`
|
|
2098
|
+
}] };
|
|
2099
|
+
}
|
|
2100
|
+
/**
|
|
1694
2101
|
* Executes a tool or skill based on the provided input.
|
|
1695
2102
|
*
|
|
1696
2103
|
* Handles three invocation patterns:
|
|
@@ -1711,22 +2118,19 @@ var UseToolTool = class UseToolTool {
|
|
|
1711
2118
|
const { toolName: inputToolName, toolArgs = {} } = input;
|
|
1712
2119
|
if (inputToolName.startsWith(SKILL_PREFIX)) {
|
|
1713
2120
|
const skillName = inputToolName.slice(7);
|
|
1714
|
-
if (
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
const skill = await this.skillService.getSkill(skillName);
|
|
1722
|
-
if (!skill) return {
|
|
2121
|
+
if (this.skillService) {
|
|
2122
|
+
const skill = await this.skillService.getSkill(skillName);
|
|
2123
|
+
if (skill) return this.executeSkill(skill);
|
|
2124
|
+
}
|
|
2125
|
+
const promptSkill = this.findPromptSkill(skillName);
|
|
2126
|
+
if (promptSkill) return this.executePromptSkill(promptSkill);
|
|
2127
|
+
return {
|
|
1723
2128
|
content: [{
|
|
1724
2129
|
type: "text",
|
|
1725
2130
|
text: `Skill "${skillName}" not found. Use describe_tools to see available skills.`
|
|
1726
2131
|
}],
|
|
1727
2132
|
isError: true
|
|
1728
2133
|
};
|
|
1729
|
-
return this.executeSkill(skill);
|
|
1730
2134
|
}
|
|
1731
2135
|
const clients = this.clientManager.getAllClients();
|
|
1732
2136
|
const { serverName, actualToolName } = parseToolName(inputToolName);
|
|
@@ -1774,6 +2178,8 @@ var UseToolTool = class UseToolTool {
|
|
|
1774
2178
|
const skill = await this.skillService.getSkill(actualToolName);
|
|
1775
2179
|
if (skill) return this.executeSkill(skill);
|
|
1776
2180
|
}
|
|
2181
|
+
const promptSkill = this.findPromptSkill(actualToolName);
|
|
2182
|
+
if (promptSkill) return this.executePromptSkill(promptSkill);
|
|
1777
2183
|
return {
|
|
1778
2184
|
content: [{
|
|
1779
2185
|
type: "text",
|
|
@@ -1840,7 +2246,10 @@ async function createServer(options) {
|
|
|
1840
2246
|
const server = new Server({
|
|
1841
2247
|
name: "@agiflowai/one-mcp",
|
|
1842
2248
|
version: "0.1.0"
|
|
1843
|
-
}, { capabilities: {
|
|
2249
|
+
}, { capabilities: {
|
|
2250
|
+
tools: {},
|
|
2251
|
+
prompts: {}
|
|
2252
|
+
} });
|
|
1844
2253
|
const clientManager = new McpClientManagerService();
|
|
1845
2254
|
let configSkills;
|
|
1846
2255
|
if (options?.configFilePath) {
|
|
@@ -1873,9 +2282,16 @@ async function createServer(options) {
|
|
|
1873
2282
|
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(", ")}`);
|
|
1874
2283
|
}
|
|
1875
2284
|
const skillsConfig = options?.skills || configSkills;
|
|
1876
|
-
const
|
|
2285
|
+
const toolsRef = { describeTools: null };
|
|
2286
|
+
const skillService = skillsConfig && skillsConfig.paths.length > 0 ? new SkillService(process.cwd(), skillsConfig.paths, { onCacheInvalidated: () => {
|
|
2287
|
+
toolsRef.describeTools?.clearAutoDetectedSkillsCache();
|
|
2288
|
+
} }) : void 0;
|
|
1877
2289
|
const describeTools = new DescribeToolsTool(clientManager, skillService);
|
|
1878
2290
|
const useTool = new UseToolTool(clientManager, skillService);
|
|
2291
|
+
toolsRef.describeTools = describeTools;
|
|
2292
|
+
if (skillService) skillService.startWatching().catch((error) => {
|
|
2293
|
+
console.error(`[skill-watcher] File watcher failed (non-critical): ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
2294
|
+
});
|
|
1879
2295
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [await describeTools.getDefinition(), useTool.getDefinition()] }));
|
|
1880
2296
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
1881
2297
|
const { name, arguments: args } = request.params;
|
|
@@ -1891,6 +2307,60 @@ async function createServer(options) {
|
|
|
1891
2307
|
}
|
|
1892
2308
|
throw new Error(`Unknown tool: ${name}`);
|
|
1893
2309
|
});
|
|
2310
|
+
server.setRequestHandler(ListPromptsRequestSchema, async () => {
|
|
2311
|
+
const clients = clientManager.getAllClients();
|
|
2312
|
+
const promptToServers = /* @__PURE__ */ new Map();
|
|
2313
|
+
const serverPromptsMap = /* @__PURE__ */ new Map();
|
|
2314
|
+
await Promise.all(clients.map(async (client) => {
|
|
2315
|
+
try {
|
|
2316
|
+
const prompts = await client.listPrompts();
|
|
2317
|
+
serverPromptsMap.set(client.serverName, prompts);
|
|
2318
|
+
for (const prompt of prompts) {
|
|
2319
|
+
if (!promptToServers.has(prompt.name)) promptToServers.set(prompt.name, []);
|
|
2320
|
+
promptToServers.get(prompt.name).push(client.serverName);
|
|
2321
|
+
}
|
|
2322
|
+
} catch (error) {
|
|
2323
|
+
console.error(`Failed to list prompts from ${client.serverName}:`, error);
|
|
2324
|
+
serverPromptsMap.set(client.serverName, []);
|
|
2325
|
+
}
|
|
2326
|
+
}));
|
|
2327
|
+
const aggregatedPrompts = [];
|
|
2328
|
+
for (const client of clients) {
|
|
2329
|
+
const prompts = serverPromptsMap.get(client.serverName) || [];
|
|
2330
|
+
for (const prompt of prompts) {
|
|
2331
|
+
const hasClash = (promptToServers.get(prompt.name) || []).length > 1;
|
|
2332
|
+
aggregatedPrompts.push({
|
|
2333
|
+
name: hasClash ? `${client.serverName}__${prompt.name}` : prompt.name,
|
|
2334
|
+
description: prompt.description,
|
|
2335
|
+
arguments: prompt.arguments
|
|
2336
|
+
});
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
return { prompts: aggregatedPrompts };
|
|
2340
|
+
});
|
|
2341
|
+
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
2342
|
+
const { name, arguments: args } = request.params;
|
|
2343
|
+
const clients = clientManager.getAllClients();
|
|
2344
|
+
const { serverName, actualToolName: actualPromptName } = parseToolName(name);
|
|
2345
|
+
if (serverName) {
|
|
2346
|
+
const client$1 = clientManager.getClient(serverName);
|
|
2347
|
+
if (!client$1) throw new Error(`Server not found: ${serverName}`);
|
|
2348
|
+
return await client$1.getPrompt(actualPromptName, args);
|
|
2349
|
+
}
|
|
2350
|
+
const serversWithPrompt = [];
|
|
2351
|
+
await Promise.all(clients.map(async (client$1) => {
|
|
2352
|
+
try {
|
|
2353
|
+
if ((await client$1.listPrompts()).some((p) => p.name === name)) serversWithPrompt.push(client$1.serverName);
|
|
2354
|
+
} catch (error) {
|
|
2355
|
+
console.error(`Failed to list prompts from ${client$1.serverName}:`, error);
|
|
2356
|
+
}
|
|
2357
|
+
}));
|
|
2358
|
+
if (serversWithPrompt.length === 0) throw new Error(`Prompt not found: ${name}`);
|
|
2359
|
+
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.`);
|
|
2360
|
+
const client = clientManager.getClient(serversWithPrompt[0]);
|
|
2361
|
+
if (!client) throw new Error(`Server not found: ${serversWithPrompt[0]}`);
|
|
2362
|
+
return await client.getPrompt(name, args);
|
|
2363
|
+
});
|
|
1894
2364
|
return server;
|
|
1895
2365
|
}
|
|
1896
2366
|
|
|
@@ -2240,4 +2710,4 @@ var HttpTransportHandler = class {
|
|
|
2240
2710
|
};
|
|
2241
2711
|
|
|
2242
2712
|
//#endregion
|
|
2243
|
-
export {
|
|
2713
|
+
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 };
|