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