@agiflowai/one-mcp 0.3.7 → 0.3.9
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 +19 -1
- package/dist/cli.cjs +961 -763
- package/dist/cli.mjs +961 -763
- package/dist/{http-BzTXYfkN.mjs → http-BDeLFFzK.mjs} +7 -7
- package/dist/{http-CKXz9Gp8.cjs → http-pwzeCOpM.cjs} +7 -7
- package/dist/index.cjs +1 -1
- package/dist/index.mjs +1 -1
- package/package.json +3 -3
package/dist/cli.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
const require_http = require('./http-
|
|
2
|
+
const require_http = require('./http-pwzeCOpM.cjs');
|
|
3
3
|
let node_fs_promises = require("node:fs/promises");
|
|
4
4
|
let node_path = require("node:path");
|
|
5
5
|
let liquidjs = require("liquidjs");
|
|
@@ -7,6 +7,84 @@ let commander = require("commander");
|
|
|
7
7
|
let __agiflowai_aicode_utils = require("@agiflowai/aicode-utils");
|
|
8
8
|
let node_child_process = require("node:child_process");
|
|
9
9
|
|
|
10
|
+
//#region src/templates/mcp-config.yaml.liquid?raw
|
|
11
|
+
var mcp_config_yaml_default = "# MCP Server Configuration\n# This file configures the MCP servers that one-mcp will connect to\n#\n# Environment Variable Interpolation:\n# Use ${VAR_NAME} syntax to reference environment variables\n# Example: ${HOME}, ${API_KEY}, ${DATABASE_URL}\n#\n# Instructions:\n# - config.instruction: Server's default instruction (from server documentation)\n# - instruction: User override (optional, takes precedence over config.instruction)\n# - config.toolBlacklist: Array of tool names to hide/block from this server\n# - config.omitToolDescription: Boolean to show only tool names without descriptions (saves tokens)\n\n# Remote Configuration Sources (OPTIONAL)\n# Fetch and merge configurations from remote URLs\n# Remote configs are merged with local configs based on merge strategy\n#\n# SECURITY: SSRF Protection is ENABLED by default\n# - Only HTTPS URLs are allowed (set security.enforceHttps: false to allow HTTP)\n# - Private IPs and localhost are blocked (set security.allowPrivateIPs: true for internal networks)\n# - Blocked ranges: 127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16\nremoteConfigs:\n # Example 1: Basic remote config with default security\n # - url: ${AGIFLOW_URL}/api/v1/mcp-configs\n # headers:\n # Authorization: Bearer ${AGIFLOW_API_KEY}\n # mergeStrategy: local-priority # Options: local-priority (default), remote-priority, merge-deep\n #\n # Example 2: Remote config with custom security settings (for internal networks)\n # - url: ${INTERNAL_URL}/mcp-configs\n # headers:\n # Authorization: Bearer ${INTERNAL_TOKEN}\n # security:\n # allowPrivateIPs: true # Allow internal IPs (default: false)\n # enforceHttps: false # Allow HTTP (default: true, HTTPS only)\n # mergeStrategy: local-priority\n #\n # Example 3: Remote config with additional validation (OPTIONAL)\n # - url: ${AGIFLOW_URL}/api/v1/mcp-configs\n # headers:\n # Authorization: Bearer ${AGIFLOW_API_KEY}\n # X-API-Key: ${AGIFLOW_API_KEY}\n # security:\n # enforceHttps: true # Require HTTPS (default: true)\n # allowPrivateIPs: false # Block private IPs (default: false)\n # validation: # OPTIONAL: Additional regex validation on top of security checks\n # url: ^https://.*\\.agiflow\\.io/.* # OPTIONAL: Regex pattern to validate URL format\n # headers: # OPTIONAL: Regex patterns to validate header values\n # Authorization: ^Bearer [A-Za-z0-9_-]+$\n # X-API-Key: ^[A-Za-z0-9_-]{32,}$\n # mergeStrategy: local-priority\n\nmcpServers:\n{%- if mcpServers %}{% for server in mcpServers %}\n {{ server.name }}:\n command: {{ server.command }}\n args:{% for arg in server.args %}\n - '{{ arg }}'{% endfor %}\n # env:\n # LOG_LEVEL: info\n # # API_KEY: ${MY_API_KEY}\n # config:\n # instruction: Use this server for...\n # # toolBlacklist:\n # # - tool_to_block\n # # omitToolDescription: true\n{% endfor %}\n # Example MCP server using SSE transport\n # remote-server:\n # url: https://example.com/mcp\n # type: sse\n # headers:\n # Authorization: Bearer ${API_KEY}\n # config:\n # instruction: This server provides tools for...\n{% else %}\n # Example MCP server using stdio transport\n example-server:\n command: node\n args:\n - /path/to/mcp-server/build/index.js\n env:\n # Environment variables for the MCP server\n LOG_LEVEL: info\n # You can use environment variable interpolation:\n # DATABASE_URL: ${DATABASE_URL}\n # API_KEY: ${MY_API_KEY}\n config:\n # Server's default instruction (from server documentation)\n instruction: Use this server for...\n # Optional: Block specific tools from being listed or executed\n # toolBlacklist:\n # - dangerous_tool_name\n # - another_blocked_tool\n # Optional: Omit tool descriptions to save tokens (default: false)\n # omitToolDescription: true\n # instruction: Optional user override - takes precedence over config.instruction\n\n # Example MCP server using SSE transport with environment variables\n # remote-server:\n # url: https://example.com/mcp\n # type: sse\n # headers:\n # # Use ${VAR_NAME} to interpolate environment variables\n # Authorization: Bearer ${API_KEY}\n # config:\n # instruction: This server provides tools for...\n # # Optional: Block specific tools from being listed or executed\n # # toolBlacklist:\n # # - tool_to_block\n # # Optional: Omit tool descriptions to save tokens (default: false)\n # # omitToolDescription: true\n # # instruction: Optional user override\n{% endif %}\n";
|
|
12
|
+
|
|
13
|
+
//#endregion
|
|
14
|
+
//#region src/templates/mcp-config.json?raw
|
|
15
|
+
var mcp_config_default = "{\n \"_comment\": \"MCP Server Configuration - Use ${VAR_NAME} syntax for environment variable interpolation\",\n \"_instructions\": \"config.instruction: Server's default instruction | instruction: User override (takes precedence)\",\n \"mcpServers\": {\n \"example-server\": {\n \"command\": \"node\",\n \"args\": [\"/path/to/mcp-server/build/index.js\"],\n \"env\": {\n \"LOG_LEVEL\": \"info\",\n \"_comment\": \"You can use environment variable interpolation:\",\n \"_example_DATABASE_URL\": \"${DATABASE_URL}\",\n \"_example_API_KEY\": \"${MY_API_KEY}\"\n },\n \"config\": {\n \"instruction\": \"Use this server for...\"\n },\n \"_instruction_override\": \"Optional user override - takes precedence over config.instruction\"\n }\n }\n}\n";
|
|
16
|
+
|
|
17
|
+
//#endregion
|
|
18
|
+
//#region src/commands/init.ts
|
|
19
|
+
/**
|
|
20
|
+
* Init Command
|
|
21
|
+
*
|
|
22
|
+
* DESIGN PATTERNS:
|
|
23
|
+
* - Command pattern with Commander for CLI argument parsing
|
|
24
|
+
* - Async/await pattern for asynchronous operations
|
|
25
|
+
* - Error handling pattern with try-catch and proper exit codes
|
|
26
|
+
*
|
|
27
|
+
* CODING STANDARDS:
|
|
28
|
+
* - Use async action handlers for asynchronous operations
|
|
29
|
+
* - Provide clear option descriptions and default values
|
|
30
|
+
* - Handle errors gracefully with process.exit()
|
|
31
|
+
* - Log progress and errors to console
|
|
32
|
+
* - Use Commander's .option() and .argument() for inputs
|
|
33
|
+
*
|
|
34
|
+
* AVOID:
|
|
35
|
+
* - Synchronous blocking operations in action handlers
|
|
36
|
+
* - Missing error handling (always use try-catch)
|
|
37
|
+
* - Hardcoded values (use options or environment variables)
|
|
38
|
+
* - Not exiting with appropriate exit codes on errors
|
|
39
|
+
*/
|
|
40
|
+
/**
|
|
41
|
+
* Initialize MCP configuration file
|
|
42
|
+
*/
|
|
43
|
+
const initCommand = new commander.Command("init").description("Initialize MCP configuration file").option("-o, --output <path>", "Output file path", "mcp-config.yaml").option("--json", "Generate JSON config instead of YAML", false).option("-f, --force", "Overwrite existing config file", false).option("--mcp-servers <json>", "JSON string of MCP servers to add to config (optional)").action(async (options) => {
|
|
44
|
+
try {
|
|
45
|
+
const outputPath = (0, node_path.resolve)(options.output);
|
|
46
|
+
const isYaml = !options.json && (outputPath.endsWith(".yaml") || outputPath.endsWith(".yml"));
|
|
47
|
+
let content;
|
|
48
|
+
if (isYaml) {
|
|
49
|
+
const liquid = new liquidjs.Liquid();
|
|
50
|
+
let mcpServersData = null;
|
|
51
|
+
if (options.mcpServers) try {
|
|
52
|
+
const serversObj = JSON.parse(options.mcpServers);
|
|
53
|
+
mcpServersData = Object.entries(serversObj).map(([name, config]) => ({
|
|
54
|
+
name,
|
|
55
|
+
command: config.command,
|
|
56
|
+
args: config.args
|
|
57
|
+
}));
|
|
58
|
+
} catch (parseError) {
|
|
59
|
+
__agiflowai_aicode_utils.log.error("Failed to parse --mcp-servers JSON:", parseError instanceof Error ? parseError.message : String(parseError));
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
content = await liquid.parseAndRender(mcp_config_yaml_default, { mcpServers: mcpServersData });
|
|
63
|
+
} else content = mcp_config_default;
|
|
64
|
+
try {
|
|
65
|
+
await (0, node_fs_promises.writeFile)(outputPath, content, {
|
|
66
|
+
encoding: "utf-8",
|
|
67
|
+
flag: options.force ? "w" : "wx"
|
|
68
|
+
});
|
|
69
|
+
} catch (error) {
|
|
70
|
+
if (error && typeof error === "object" && "code" in error && error.code === "EEXIST") {
|
|
71
|
+
__agiflowai_aicode_utils.log.error(`Config file already exists: ${outputPath}`);
|
|
72
|
+
__agiflowai_aicode_utils.log.info("Use --force to overwrite");
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
__agiflowai_aicode_utils.log.info(`MCP configuration file created: ${outputPath}`);
|
|
78
|
+
__agiflowai_aicode_utils.log.info("Next steps:");
|
|
79
|
+
__agiflowai_aicode_utils.log.info("1. Edit the configuration file to add your MCP servers");
|
|
80
|
+
__agiflowai_aicode_utils.log.info(`2. Run: one-mcp mcp-serve --config ${outputPath}`);
|
|
81
|
+
} catch (error) {
|
|
82
|
+
__agiflowai_aicode_utils.log.error("Error executing init:", error instanceof Error ? error.message : String(error));
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
//#endregion
|
|
10
88
|
//#region src/types/index.ts
|
|
11
89
|
/**
|
|
12
90
|
* Transport mode constants
|
|
@@ -99,256 +177,387 @@ const mcpServeCommand = new commander.Command("mcp-serve").description("Start MC
|
|
|
99
177
|
});
|
|
100
178
|
|
|
101
179
|
//#endregion
|
|
102
|
-
//#region src/
|
|
180
|
+
//#region src/services/PrefetchService/constants.ts
|
|
103
181
|
/**
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
* DESIGN PATTERNS:
|
|
107
|
-
* - Command pattern with Commander for CLI argument parsing
|
|
108
|
-
* - Async/await pattern for asynchronous operations
|
|
109
|
-
* - Error handling pattern with try-catch and proper exit codes
|
|
110
|
-
*
|
|
111
|
-
* CODING STANDARDS:
|
|
112
|
-
* - Use async action handlers for asynchronous operations
|
|
113
|
-
* - Provide clear option descriptions and default values
|
|
114
|
-
* - Handle errors gracefully with process.exit()
|
|
115
|
-
* - Log progress and errors to console
|
|
116
|
-
* - Use Commander's .option() and .argument() for inputs
|
|
182
|
+
* PrefetchService Constants
|
|
117
183
|
*
|
|
118
|
-
*
|
|
119
|
-
* - Synchronous blocking operations in action handlers
|
|
120
|
-
* - Missing error handling (always use try-catch)
|
|
121
|
-
* - Hardcoded values (use options or environment variables)
|
|
122
|
-
* - Not exiting with appropriate exit codes on errors
|
|
184
|
+
* Constants for package manager commands and process configuration.
|
|
123
185
|
*/
|
|
186
|
+
/** Transport type for stdio-based MCP servers */
|
|
187
|
+
const TRANSPORT_STDIO = "stdio";
|
|
188
|
+
/** npx command name */
|
|
189
|
+
const COMMAND_NPX = "npx";
|
|
190
|
+
/** npm command name */
|
|
191
|
+
const COMMAND_NPM = "npm";
|
|
192
|
+
/** pnpx command name (pnpm's npx equivalent) */
|
|
193
|
+
const COMMAND_PNPX = "pnpx";
|
|
194
|
+
/** pnpm command name */
|
|
195
|
+
const COMMAND_PNPM = "pnpm";
|
|
196
|
+
/** uvx command name */
|
|
197
|
+
const COMMAND_UVX = "uvx";
|
|
198
|
+
/** uv command name */
|
|
199
|
+
const COMMAND_UV = "uv";
|
|
200
|
+
/** Path suffix for npx command */
|
|
201
|
+
const COMMAND_NPX_SUFFIX = "/npx";
|
|
202
|
+
/** Path suffix for pnpx command */
|
|
203
|
+
const COMMAND_PNPX_SUFFIX = "/pnpx";
|
|
204
|
+
/** Path suffix for uvx command */
|
|
205
|
+
const COMMAND_UVX_SUFFIX = "/uvx";
|
|
206
|
+
/** Path suffix for uv command */
|
|
207
|
+
const COMMAND_UV_SUFFIX = "/uv";
|
|
208
|
+
/** Run subcommand for uv */
|
|
209
|
+
const ARG_RUN = "run";
|
|
210
|
+
/** Tool subcommand for uv */
|
|
211
|
+
const ARG_TOOL = "tool";
|
|
212
|
+
/** Install subcommand for uv tool and npm/pnpm */
|
|
213
|
+
const ARG_INSTALL = "install";
|
|
214
|
+
/** Add subcommand for pnpm */
|
|
215
|
+
const ARG_ADD = "add";
|
|
216
|
+
/** Global flag for npm/pnpm install */
|
|
217
|
+
const ARG_GLOBAL = "-g";
|
|
218
|
+
/** Flag prefix for command arguments */
|
|
219
|
+
const FLAG_PREFIX = "-";
|
|
220
|
+
/** npx --package flag (long form) */
|
|
221
|
+
const FLAG_PACKAGE_LONG = "--package";
|
|
222
|
+
/** npx -p flag (short form) */
|
|
223
|
+
const FLAG_PACKAGE_SHORT = "-p";
|
|
224
|
+
/** Equals delimiter used in flag=value patterns */
|
|
225
|
+
const EQUALS_DELIMITER = "=";
|
|
124
226
|
/**
|
|
125
|
-
*
|
|
227
|
+
* Regex pattern for valid package names (npm, pnpm, uvx, uv)
|
|
228
|
+
* Allows: @scope/package-name@version, package-name, package_name
|
|
229
|
+
* Prevents shell metacharacters that could enable command injection
|
|
230
|
+
* @example
|
|
231
|
+
* // Valid: '@scope/package@1.0.0', 'my-package', 'my_package', '@org/pkg'
|
|
232
|
+
* // Invalid: 'pkg; rm -rf /', 'pkg$(cmd)', 'pkg`whoami`', 'pkg|cat /etc/passwd'
|
|
126
233
|
*/
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const connectionPromises = Object.entries(config.mcpServers).map(async ([serverName, serverConfig]) => {
|
|
137
|
-
try {
|
|
138
|
-
await clientManager.connectToServer(serverName, serverConfig);
|
|
139
|
-
if (!options.json) console.error(`✓ Connected to ${serverName}`);
|
|
140
|
-
} catch (error) {
|
|
141
|
-
if (!options.json) console.error(`✗ Failed to connect to ${serverName}:`, error);
|
|
142
|
-
}
|
|
143
|
-
});
|
|
144
|
-
await Promise.all(connectionPromises);
|
|
145
|
-
const clients = options.server ? [clientManager.getClient(options.server)].filter((c) => c !== void 0) : clientManager.getAllClients();
|
|
146
|
-
if (clients.length === 0) {
|
|
147
|
-
console.error("No MCP servers connected");
|
|
148
|
-
process.exit(1);
|
|
149
|
-
}
|
|
150
|
-
const toolsByServer = {};
|
|
151
|
-
const toolResults = await Promise.all(clients.map(async (client) => {
|
|
152
|
-
try {
|
|
153
|
-
const tools = await client.listTools();
|
|
154
|
-
const blacklist = new Set(client.toolBlacklist || []);
|
|
155
|
-
const filteredTools = tools.filter((t) => !blacklist.has(t.name));
|
|
156
|
-
return {
|
|
157
|
-
serverName: client.serverName,
|
|
158
|
-
tools: filteredTools,
|
|
159
|
-
error: null
|
|
160
|
-
};
|
|
161
|
-
} catch (error) {
|
|
162
|
-
return {
|
|
163
|
-
serverName: client.serverName,
|
|
164
|
-
tools: [],
|
|
165
|
-
error
|
|
166
|
-
};
|
|
167
|
-
}
|
|
168
|
-
}));
|
|
169
|
-
for (const { serverName, tools, error } of toolResults) {
|
|
170
|
-
if (error && !options.json) console.error(`Failed to list tools from ${serverName}:`, error);
|
|
171
|
-
toolsByServer[serverName] = tools;
|
|
172
|
-
}
|
|
173
|
-
if (options.json) console.log(JSON.stringify(toolsByServer, null, 2));
|
|
174
|
-
else for (const [serverName, tools] of Object.entries(toolsByServer)) {
|
|
175
|
-
const omitDescription = clients.find((c) => c.serverName === serverName)?.omitToolDescription || false;
|
|
176
|
-
console.log(`\n${serverName}:`);
|
|
177
|
-
if (tools.length === 0) console.log(" No tools available");
|
|
178
|
-
else if (omitDescription) {
|
|
179
|
-
const toolNames = tools.map((t) => t.name).join(", ");
|
|
180
|
-
console.log(` ${toolNames}`);
|
|
181
|
-
} else for (const tool of tools) console.log(` - ${tool.name}: ${tool.description || "No description"}`);
|
|
182
|
-
}
|
|
183
|
-
await clientManager.disconnectAll();
|
|
184
|
-
} catch (error) {
|
|
185
|
-
console.error("Error executing list-tools:", error);
|
|
186
|
-
process.exit(1);
|
|
187
|
-
}
|
|
188
|
-
});
|
|
234
|
+
const VALID_PACKAGE_NAME_PATTERN = /^(@[a-zA-Z0-9_-]+\/)?[a-zA-Z0-9._-]+(@[a-zA-Z0-9._-]+)?$/;
|
|
235
|
+
/** Windows platform identifier */
|
|
236
|
+
const PLATFORM_WIN32 = "win32";
|
|
237
|
+
/** Success exit code */
|
|
238
|
+
const EXIT_CODE_SUCCESS = 0;
|
|
239
|
+
/** Stdio option to ignore stream */
|
|
240
|
+
const STDIO_IGNORE = "ignore";
|
|
241
|
+
/** Stdio option to pipe stream */
|
|
242
|
+
const STDIO_PIPE = "pipe";
|
|
189
243
|
|
|
190
244
|
//#endregion
|
|
191
|
-
//#region src/
|
|
245
|
+
//#region src/services/PrefetchService/PrefetchService.ts
|
|
192
246
|
/**
|
|
193
|
-
*
|
|
247
|
+
* PrefetchService
|
|
194
248
|
*
|
|
195
249
|
* DESIGN PATTERNS:
|
|
196
|
-
* -
|
|
197
|
-
* -
|
|
198
|
-
* - Error handling pattern with try-catch and proper exit codes
|
|
250
|
+
* - Service pattern for business logic encapsulation
|
|
251
|
+
* - Single responsibility principle
|
|
199
252
|
*
|
|
200
253
|
* CODING STANDARDS:
|
|
201
|
-
* - Use async
|
|
202
|
-
* -
|
|
203
|
-
* -
|
|
204
|
-
* -
|
|
205
|
-
* - Use Commander's .option() and .argument() for inputs
|
|
254
|
+
* - Use async/await for asynchronous operations
|
|
255
|
+
* - Throw descriptive errors for error cases
|
|
256
|
+
* - Keep methods focused and well-named
|
|
257
|
+
* - Document complex logic with comments
|
|
206
258
|
*
|
|
207
259
|
* AVOID:
|
|
208
|
-
* -
|
|
209
|
-
* -
|
|
210
|
-
* - Hardcoded values (use options or environment variables)
|
|
211
|
-
* - Not exiting with appropriate exit codes on errors
|
|
260
|
+
* - Mixing concerns (keep focused on single domain)
|
|
261
|
+
* - Direct tool implementation (services should be tool-agnostic)
|
|
212
262
|
*/
|
|
213
263
|
/**
|
|
214
|
-
*
|
|
264
|
+
* Type guard to check if a config object is an McpStdioConfig
|
|
265
|
+
* @param config - Config object to check
|
|
266
|
+
* @returns True if config has required McpStdioConfig properties
|
|
215
267
|
*/
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
error
|
|
258
|
-
};
|
|
259
|
-
}
|
|
260
|
-
}));
|
|
261
|
-
for (const { client, tools, error } of toolResults) {
|
|
262
|
-
if (error) {
|
|
263
|
-
if (!options.json) console.error(`Failed to list tools from ${client.serverName}:`, error);
|
|
264
|
-
continue;
|
|
265
|
-
}
|
|
266
|
-
for (const toolName of toolNames) {
|
|
267
|
-
const tool = tools.find((t) => t.name === toolName);
|
|
268
|
-
if (tool) {
|
|
269
|
-
foundTools.push({
|
|
270
|
-
server: client.serverName,
|
|
271
|
-
name: tool.name,
|
|
272
|
-
description: tool.description,
|
|
273
|
-
inputSchema: tool.inputSchema
|
|
274
|
-
});
|
|
275
|
-
const idx = notFoundTools.indexOf(toolName);
|
|
276
|
-
if (idx > -1) notFoundTools.splice(idx, 1);
|
|
277
|
-
}
|
|
268
|
+
function isMcpStdioConfig(config) {
|
|
269
|
+
return typeof config === "object" && config !== null && "command" in config;
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* PrefetchService handles pre-downloading packages used by MCP servers.
|
|
273
|
+
* Supports npx (Node.js), uvx (Python/uv), and uv run commands.
|
|
274
|
+
*
|
|
275
|
+
* @example
|
|
276
|
+
* ```typescript
|
|
277
|
+
* const service = new PrefetchService({
|
|
278
|
+
* mcpConfig: await configFetcher.fetchConfiguration(),
|
|
279
|
+
* parallel: true,
|
|
280
|
+
* });
|
|
281
|
+
* const packages = service.extractPackages();
|
|
282
|
+
* const summary = await service.prefetch();
|
|
283
|
+
* ```
|
|
284
|
+
*/
|
|
285
|
+
var PrefetchService = class {
|
|
286
|
+
config;
|
|
287
|
+
/**
|
|
288
|
+
* Creates a new PrefetchService instance
|
|
289
|
+
* @param config - Service configuration options
|
|
290
|
+
*/
|
|
291
|
+
constructor(config) {
|
|
292
|
+
this.config = config;
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Extract all prefetchable packages from the MCP configuration
|
|
296
|
+
* @returns Array of package info objects
|
|
297
|
+
*/
|
|
298
|
+
extractPackages() {
|
|
299
|
+
const packages = [];
|
|
300
|
+
const { mcpConfig, filter } = this.config;
|
|
301
|
+
for (const [serverName, serverConfig] of Object.entries(mcpConfig.mcpServers)) {
|
|
302
|
+
if (serverConfig.disabled) continue;
|
|
303
|
+
if (serverConfig.transport !== TRANSPORT_STDIO) continue;
|
|
304
|
+
if (!isMcpStdioConfig(serverConfig.config)) continue;
|
|
305
|
+
const packageInfo = this.extractPackageInfo(serverName, serverConfig.config);
|
|
306
|
+
if (packageInfo) {
|
|
307
|
+
if (filter && packageInfo.packageManager !== filter) continue;
|
|
308
|
+
packages.push(packageInfo);
|
|
278
309
|
}
|
|
279
310
|
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
311
|
+
return packages;
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Prefetch all packages from the configuration
|
|
315
|
+
* @returns Summary of prefetch results
|
|
316
|
+
* @throws Error if prefetch operation fails unexpectedly
|
|
317
|
+
*/
|
|
318
|
+
async prefetch() {
|
|
319
|
+
try {
|
|
320
|
+
const packages = this.extractPackages();
|
|
321
|
+
const results = [];
|
|
322
|
+
if (packages.length === 0) return {
|
|
323
|
+
totalPackages: 0,
|
|
324
|
+
successful: 0,
|
|
325
|
+
failed: 0,
|
|
326
|
+
results: []
|
|
327
|
+
};
|
|
328
|
+
if (this.config.parallel) {
|
|
329
|
+
const promises = packages.map(async (pkg) => this.prefetchPackage(pkg));
|
|
330
|
+
results.push(...await Promise.all(promises));
|
|
331
|
+
} else for (const pkg of packages) {
|
|
332
|
+
const result = await this.prefetchPackage(pkg);
|
|
333
|
+
results.push(result);
|
|
297
334
|
}
|
|
335
|
+
const successful = results.filter((r) => r.success).length;
|
|
336
|
+
const failed = results.filter((r) => !r.success).length;
|
|
337
|
+
return {
|
|
338
|
+
totalPackages: packages.length,
|
|
339
|
+
successful,
|
|
340
|
+
failed,
|
|
341
|
+
results
|
|
342
|
+
};
|
|
343
|
+
} catch (error) {
|
|
344
|
+
throw new Error(`Failed to prefetch packages: ${error instanceof Error ? error.message : String(error)}`);
|
|
298
345
|
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Prefetch a single package
|
|
349
|
+
* @param pkg - Package info to prefetch
|
|
350
|
+
* @returns Result of the prefetch operation
|
|
351
|
+
*/
|
|
352
|
+
async prefetchPackage(pkg) {
|
|
353
|
+
try {
|
|
354
|
+
const [command, ...args] = pkg.fullCommand;
|
|
355
|
+
const result = await this.runCommand(command, args);
|
|
356
|
+
return {
|
|
357
|
+
package: pkg,
|
|
358
|
+
success: result.success,
|
|
359
|
+
output: result.output
|
|
360
|
+
};
|
|
361
|
+
} catch (error) {
|
|
362
|
+
return {
|
|
363
|
+
package: pkg,
|
|
364
|
+
success: false,
|
|
365
|
+
output: error instanceof Error ? error.message : String(error)
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Validate package name to prevent command injection
|
|
371
|
+
* @param packageName - Package name to validate
|
|
372
|
+
* @returns True if package name is safe, false otherwise
|
|
373
|
+
* @remarks Rejects package names containing shell metacharacters
|
|
374
|
+
* @example
|
|
375
|
+
* isValidPackageName('@scope/package') // true
|
|
376
|
+
* isValidPackageName('my-package@1.0.0') // true
|
|
377
|
+
* isValidPackageName('pkg; rm -rf /') // false (shell injection)
|
|
378
|
+
* isValidPackageName('pkg$(whoami)') // false (command substitution)
|
|
379
|
+
*/
|
|
380
|
+
isValidPackageName(packageName) {
|
|
381
|
+
return VALID_PACKAGE_NAME_PATTERN.test(packageName);
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Extract package info from a server's stdio config
|
|
385
|
+
* @param serverName - Name of the MCP server
|
|
386
|
+
* @param config - Stdio configuration for the server
|
|
387
|
+
* @returns Package info if extractable, null otherwise
|
|
388
|
+
*/
|
|
389
|
+
extractPackageInfo(serverName, config) {
|
|
390
|
+
const command = config.command.toLowerCase();
|
|
391
|
+
const args = config.args || [];
|
|
392
|
+
if (command === COMMAND_NPX || command.endsWith(COMMAND_NPX_SUFFIX)) {
|
|
393
|
+
const packageName = this.extractNpxPackage(args);
|
|
394
|
+
if (packageName && this.isValidPackageName(packageName)) return {
|
|
395
|
+
serverName,
|
|
396
|
+
packageManager: COMMAND_NPX,
|
|
397
|
+
packageName,
|
|
398
|
+
fullCommand: [
|
|
399
|
+
COMMAND_NPM,
|
|
400
|
+
ARG_INSTALL,
|
|
401
|
+
ARG_GLOBAL,
|
|
402
|
+
packageName
|
|
403
|
+
]
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
if (command === COMMAND_PNPX || command.endsWith(COMMAND_PNPX_SUFFIX)) {
|
|
407
|
+
const packageName = this.extractNpxPackage(args);
|
|
408
|
+
if (packageName && this.isValidPackageName(packageName)) return {
|
|
409
|
+
serverName,
|
|
410
|
+
packageManager: COMMAND_PNPX,
|
|
411
|
+
packageName,
|
|
412
|
+
fullCommand: [
|
|
413
|
+
COMMAND_PNPM,
|
|
414
|
+
ARG_ADD,
|
|
415
|
+
ARG_GLOBAL,
|
|
416
|
+
packageName
|
|
417
|
+
]
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
if (command === COMMAND_UVX || command.endsWith(COMMAND_UVX_SUFFIX)) {
|
|
421
|
+
const packageName = this.extractUvxPackage(args);
|
|
422
|
+
if (packageName && this.isValidPackageName(packageName)) return {
|
|
423
|
+
serverName,
|
|
424
|
+
packageManager: COMMAND_UVX,
|
|
425
|
+
packageName,
|
|
426
|
+
fullCommand: [COMMAND_UVX, packageName]
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
if ((command === COMMAND_UV || command.endsWith(COMMAND_UV_SUFFIX)) && args.includes(ARG_RUN)) {
|
|
430
|
+
const packageName = this.extractUvRunPackage(args);
|
|
431
|
+
if (packageName && this.isValidPackageName(packageName)) return {
|
|
432
|
+
serverName,
|
|
433
|
+
packageManager: COMMAND_UV,
|
|
434
|
+
packageName,
|
|
435
|
+
fullCommand: [
|
|
436
|
+
COMMAND_UV,
|
|
437
|
+
ARG_TOOL,
|
|
438
|
+
ARG_INSTALL,
|
|
439
|
+
packageName
|
|
440
|
+
]
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
return null;
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Extract package name from npx command args
|
|
447
|
+
* @param args - Command arguments
|
|
448
|
+
* @returns Package name or null
|
|
449
|
+
* @remarks Handles --package=value, --package value, -p value patterns.
|
|
450
|
+
* Falls back to first non-flag argument if no --package/-p flag found.
|
|
451
|
+
* Returns null if flag has no value or is followed by another flag.
|
|
452
|
+
* When multiple --package flags exist, returns the first valid one.
|
|
453
|
+
* @example
|
|
454
|
+
* extractNpxPackage(['--package=@scope/pkg']) // returns '@scope/pkg'
|
|
455
|
+
* extractNpxPackage(['--package', 'pkg-name']) // returns 'pkg-name'
|
|
456
|
+
* extractNpxPackage(['-p', 'pkg']) // returns 'pkg'
|
|
457
|
+
* extractNpxPackage(['-y', 'pkg-name', '--flag']) // returns 'pkg-name' (fallback)
|
|
458
|
+
* extractNpxPackage(['--package=']) // returns null (empty value)
|
|
459
|
+
*/
|
|
460
|
+
extractNpxPackage(args) {
|
|
461
|
+
for (let i = 0; i < args.length; i++) {
|
|
462
|
+
const arg = args[i];
|
|
463
|
+
if (arg.startsWith(FLAG_PACKAGE_LONG + EQUALS_DELIMITER)) return arg.slice(FLAG_PACKAGE_LONG.length + EQUALS_DELIMITER.length) || null;
|
|
464
|
+
if (arg === FLAG_PACKAGE_LONG && i + 1 < args.length) {
|
|
465
|
+
const nextArg = args[i + 1];
|
|
466
|
+
if (!nextArg.startsWith(FLAG_PREFIX)) return nextArg;
|
|
334
467
|
}
|
|
335
|
-
if (
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
process.exit(1);
|
|
468
|
+
if (arg === FLAG_PACKAGE_SHORT && i + 1 < args.length) {
|
|
469
|
+
const nextArg = args[i + 1];
|
|
470
|
+
if (!nextArg.startsWith(FLAG_PREFIX)) return nextArg;
|
|
339
471
|
}
|
|
340
472
|
}
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
473
|
+
for (const arg of args) {
|
|
474
|
+
if (arg.startsWith(FLAG_PREFIX)) continue;
|
|
475
|
+
return arg;
|
|
476
|
+
}
|
|
477
|
+
return null;
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Extract package name from uvx command args
|
|
481
|
+
* @param args - Command arguments
|
|
482
|
+
* @returns Package name or null
|
|
483
|
+
* @remarks Assumes the first non-flag argument is the package name.
|
|
484
|
+
* Handles both single (-) and double (--) dash flags.
|
|
485
|
+
* @example
|
|
486
|
+
* extractUvxPackage(['mcp-server-fetch']) // returns 'mcp-server-fetch'
|
|
487
|
+
* extractUvxPackage(['--quiet', 'pkg-name']) // returns 'pkg-name'
|
|
488
|
+
*/
|
|
489
|
+
extractUvxPackage(args) {
|
|
490
|
+
for (const arg of args) {
|
|
491
|
+
if (arg.startsWith(FLAG_PREFIX)) continue;
|
|
492
|
+
return arg;
|
|
493
|
+
}
|
|
494
|
+
return null;
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* Extract package name from uv run command args
|
|
498
|
+
* @param args - Command arguments
|
|
499
|
+
* @returns Package name or null
|
|
500
|
+
* @remarks Looks for the first non-flag argument after the 'run' subcommand.
|
|
501
|
+
* Returns null if 'run' is not found in args.
|
|
502
|
+
* @example
|
|
503
|
+
* extractUvRunPackage(['run', 'mcp-server']) // returns 'mcp-server'
|
|
504
|
+
* extractUvRunPackage(['run', '--verbose', 'pkg']) // returns 'pkg'
|
|
505
|
+
* extractUvRunPackage(['install', 'pkg']) // returns null (no 'run')
|
|
506
|
+
*/
|
|
507
|
+
extractUvRunPackage(args) {
|
|
508
|
+
const runIndex = args.indexOf(ARG_RUN);
|
|
509
|
+
if (runIndex === -1) return null;
|
|
510
|
+
for (let i = runIndex + 1; i < args.length; i++) {
|
|
511
|
+
const arg = args[i];
|
|
512
|
+
if (arg.startsWith(FLAG_PREFIX)) continue;
|
|
513
|
+
return arg;
|
|
514
|
+
}
|
|
515
|
+
return null;
|
|
345
516
|
}
|
|
346
|
-
|
|
517
|
+
/**
|
|
518
|
+
* Run a shell command and capture output
|
|
519
|
+
* @param command - Command to run
|
|
520
|
+
* @param args - Command arguments
|
|
521
|
+
* @returns Promise with success status and output
|
|
522
|
+
*/
|
|
523
|
+
runCommand(command, args) {
|
|
524
|
+
return new Promise((resolve$1) => {
|
|
525
|
+
const proc = (0, node_child_process.spawn)(command, args, {
|
|
526
|
+
stdio: [
|
|
527
|
+
STDIO_IGNORE,
|
|
528
|
+
STDIO_PIPE,
|
|
529
|
+
STDIO_PIPE
|
|
530
|
+
],
|
|
531
|
+
shell: process.platform === PLATFORM_WIN32
|
|
532
|
+
});
|
|
533
|
+
let stdout = "";
|
|
534
|
+
let stderr = "";
|
|
535
|
+
proc.stdout?.on("data", (data) => {
|
|
536
|
+
stdout += data.toString();
|
|
537
|
+
});
|
|
538
|
+
proc.stderr?.on("data", (data) => {
|
|
539
|
+
stderr += data.toString();
|
|
540
|
+
});
|
|
541
|
+
proc.on("close", (code) => {
|
|
542
|
+
resolve$1({
|
|
543
|
+
success: code === EXIT_CODE_SUCCESS,
|
|
544
|
+
output: stdout || stderr
|
|
545
|
+
});
|
|
546
|
+
});
|
|
547
|
+
proc.on("error", (error) => {
|
|
548
|
+
resolve$1({
|
|
549
|
+
success: false,
|
|
550
|
+
output: error.message
|
|
551
|
+
});
|
|
552
|
+
});
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
};
|
|
347
556
|
|
|
348
557
|
//#endregion
|
|
349
|
-
//#region src/commands/
|
|
558
|
+
//#region src/commands/list-tools.ts
|
|
350
559
|
/**
|
|
351
|
-
*
|
|
560
|
+
* List Tools Command
|
|
352
561
|
*
|
|
353
562
|
* DESIGN PATTERNS:
|
|
354
563
|
* - Command pattern with Commander for CLI argument parsing
|
|
@@ -368,23 +577,16 @@ const describeToolsCommand = new commander.Command("describe-tools").description
|
|
|
368
577
|
* - Hardcoded values (use options or environment variables)
|
|
369
578
|
* - Not exiting with appropriate exit codes on errors
|
|
370
579
|
*/
|
|
580
|
+
function toErrorMessage$2(error) {
|
|
581
|
+
return error instanceof Error ? error.message : String(error);
|
|
582
|
+
}
|
|
371
583
|
/**
|
|
372
|
-
*
|
|
584
|
+
* List all available tools from connected MCP servers
|
|
373
585
|
*/
|
|
374
|
-
const
|
|
586
|
+
const listToolsCommand = new commander.Command("list-tools").description("List all available tools from connected MCP servers").option("-c, --config <path>", "Path to MCP server configuration file").option("-s, --server <name>", "Filter by server name").option("-j, --json", "Output as JSON", false).action(async (options) => {
|
|
375
587
|
try {
|
|
376
588
|
const configFilePath = options.config || require_http.findConfigFile();
|
|
377
|
-
if (!configFilePath)
|
|
378
|
-
console.error("Error: No config file found. Use --config or create mcp-config.yaml");
|
|
379
|
-
process.exit(1);
|
|
380
|
-
}
|
|
381
|
-
let toolArgs = {};
|
|
382
|
-
try {
|
|
383
|
-
toolArgs = JSON.parse(options.args);
|
|
384
|
-
} catch (error) {
|
|
385
|
-
console.error("Error: Invalid JSON in --args");
|
|
386
|
-
process.exit(1);
|
|
387
|
-
}
|
|
589
|
+
if (!configFilePath) throw new Error("No config file found. Use --config or create mcp-config.yaml");
|
|
388
590
|
const config = await new require_http.ConfigFetcherService({ configFilePath }).fetchConfiguration();
|
|
389
591
|
const clientManager = new require_http.McpClientManagerService();
|
|
390
592
|
const connectionPromises = Object.entries(config.mcpServers).map(async ([serverName, serverConfig]) => {
|
|
@@ -392,587 +594,581 @@ const useToolCommand = new commander.Command("use-tool").description("Execute an
|
|
|
392
594
|
await clientManager.connectToServer(serverName, serverConfig);
|
|
393
595
|
if (!options.json) console.error(`✓ Connected to ${serverName}`);
|
|
394
596
|
} catch (error) {
|
|
395
|
-
if (!options.json) console.error(`✗ Failed to connect to ${serverName}
|
|
597
|
+
if (!options.json) console.error(`✗ Failed to connect to ${serverName}: ${toErrorMessage$2(error)}`);
|
|
396
598
|
}
|
|
397
599
|
});
|
|
398
600
|
await Promise.all(connectionPromises);
|
|
399
|
-
const clients = clientManager.getAllClients();
|
|
400
|
-
if (clients.length === 0)
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
}
|
|
404
|
-
if (options.server) {
|
|
405
|
-
const client$1 = clientManager.getClient(options.server);
|
|
406
|
-
if (!client$1) {
|
|
407
|
-
console.error(`Server "${options.server}" not found`);
|
|
408
|
-
process.exit(1);
|
|
409
|
-
}
|
|
410
|
-
try {
|
|
411
|
-
if (!options.json) console.error(`Executing ${toolName} on ${options.server}...`);
|
|
412
|
-
const result = await client$1.callTool(toolName, toolArgs);
|
|
413
|
-
if (options.json) console.log(JSON.stringify(result, null, 2));
|
|
414
|
-
else {
|
|
415
|
-
console.log("\nResult:");
|
|
416
|
-
if (result.content) for (const content of result.content) if (content.type === "text") console.log(content.text);
|
|
417
|
-
else console.log(JSON.stringify(content, null, 2));
|
|
418
|
-
if (result.isError) {
|
|
419
|
-
console.error("\n⚠️ Tool execution returned an error");
|
|
420
|
-
process.exit(1);
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
await clientManager.disconnectAll();
|
|
424
|
-
return;
|
|
425
|
-
} catch (error) {
|
|
426
|
-
console.error(`Failed to execute tool "${toolName}":`, error);
|
|
427
|
-
await clientManager.disconnectAll();
|
|
428
|
-
process.exit(1);
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
const searchResults = await Promise.all(clients.map(async (client$1) => {
|
|
601
|
+
const clients = options.server ? [clientManager.getClient(options.server)].filter((c) => c !== void 0) : clientManager.getAllClients();
|
|
602
|
+
if (clients.length === 0) throw new Error("No MCP servers connected");
|
|
603
|
+
const toolsByServer = {};
|
|
604
|
+
const toolResults = await Promise.all(clients.map(async (client) => {
|
|
432
605
|
try {
|
|
433
|
-
const
|
|
606
|
+
const tools = await client.listTools();
|
|
607
|
+
const blacklist = new Set(client.toolBlacklist || []);
|
|
608
|
+
const filteredTools = tools.filter((t) => !blacklist.has(t.name));
|
|
434
609
|
return {
|
|
435
|
-
serverName: client
|
|
436
|
-
|
|
610
|
+
serverName: client.serverName,
|
|
611
|
+
tools: filteredTools,
|
|
437
612
|
error: null
|
|
438
613
|
};
|
|
439
614
|
} catch (error) {
|
|
440
615
|
return {
|
|
441
|
-
serverName: client
|
|
442
|
-
|
|
616
|
+
serverName: client.serverName,
|
|
617
|
+
tools: [],
|
|
443
618
|
error
|
|
444
619
|
};
|
|
445
620
|
}
|
|
446
621
|
}));
|
|
447
|
-
const
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
if (!options.json) console.error(`Failed to list tools from ${serverName}:`, error);
|
|
451
|
-
continue;
|
|
452
|
-
}
|
|
453
|
-
if (hasTool) matchingServers.push(serverName);
|
|
454
|
-
}
|
|
455
|
-
if (matchingServers.length === 0) {
|
|
456
|
-
const cwd = process.env.PROJECT_PATH || process.cwd();
|
|
457
|
-
const skillPaths = config.skills?.paths || [];
|
|
458
|
-
if (skillPaths.length > 0) try {
|
|
459
|
-
const skillService = new require_http.SkillService(cwd, skillPaths);
|
|
460
|
-
const skillName = toolName.startsWith("skill__") ? toolName.slice(7) : toolName;
|
|
461
|
-
const skill = await skillService.getSkill(skillName);
|
|
462
|
-
if (skill) {
|
|
463
|
-
const result = { content: [{
|
|
464
|
-
type: "text",
|
|
465
|
-
text: skill.content
|
|
466
|
-
}] };
|
|
467
|
-
if (options.json) console.log(JSON.stringify(result, null, 2));
|
|
468
|
-
else {
|
|
469
|
-
console.log("\nSkill content:");
|
|
470
|
-
console.log(skill.content);
|
|
471
|
-
}
|
|
472
|
-
await clientManager.disconnectAll();
|
|
473
|
-
return;
|
|
474
|
-
}
|
|
475
|
-
} catch (error) {
|
|
476
|
-
if (!options.json) console.error(`Failed to lookup skill "${toolName}":`, error);
|
|
477
|
-
}
|
|
478
|
-
console.error(`Tool or skill "${toolName}" not found on any connected server or configured skill paths`);
|
|
479
|
-
await clientManager.disconnectAll();
|
|
480
|
-
process.exit(1);
|
|
481
|
-
}
|
|
482
|
-
if (matchingServers.length > 1) {
|
|
483
|
-
console.error(`Tool "${toolName}" found on multiple servers: ${matchingServers.join(", ")}`);
|
|
484
|
-
console.error("Please specify --server to disambiguate");
|
|
485
|
-
await clientManager.disconnectAll();
|
|
486
|
-
process.exit(1);
|
|
487
|
-
}
|
|
488
|
-
const targetServer = matchingServers[0];
|
|
489
|
-
const client = clientManager.getClient(targetServer);
|
|
490
|
-
if (!client) {
|
|
491
|
-
console.error(`Internal error: Server "${targetServer}" not connected`);
|
|
492
|
-
await clientManager.disconnectAll();
|
|
493
|
-
process.exit(1);
|
|
622
|
+
for (const { serverName, tools, error } of toolResults) {
|
|
623
|
+
if (error && !options.json) console.error(`Failed to list tools from ${serverName}: ${toErrorMessage$2(error)}`);
|
|
624
|
+
toolsByServer[serverName] = tools;
|
|
494
625
|
}
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
console.log("\nResult:");
|
|
501
|
-
if (result.content) for (const content of result.content) if (content.type === "text") console.log(content.text);
|
|
502
|
-
else console.log(JSON.stringify(content, null, 2));
|
|
503
|
-
if (result.isError) {
|
|
504
|
-
console.error("\n⚠️ Tool execution returned an error");
|
|
505
|
-
await clientManager.disconnectAll();
|
|
506
|
-
process.exit(1);
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
await clientManager.disconnectAll();
|
|
626
|
+
const cwd = process.env.PROJECT_PATH || process.cwd();
|
|
627
|
+
const skillPaths = config.skills?.paths || [];
|
|
628
|
+
let skills = [];
|
|
629
|
+
if (skillPaths.length > 0) try {
|
|
630
|
+
skills = await new require_http.SkillService(cwd, skillPaths).getSkills();
|
|
510
631
|
} catch (error) {
|
|
511
|
-
console.error(`Failed to
|
|
512
|
-
await clientManager.disconnectAll();
|
|
513
|
-
process.exit(1);
|
|
632
|
+
if (!options.json) console.error(`Failed to load skills: ${toErrorMessage$2(error)}`);
|
|
514
633
|
}
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
/**
|
|
532
|
-
* Init Command
|
|
533
|
-
*
|
|
534
|
-
* DESIGN PATTERNS:
|
|
535
|
-
* - Command pattern with Commander for CLI argument parsing
|
|
536
|
-
* - Async/await pattern for asynchronous operations
|
|
537
|
-
* - Error handling pattern with try-catch and proper exit codes
|
|
538
|
-
*
|
|
539
|
-
* CODING STANDARDS:
|
|
540
|
-
* - Use async action handlers for asynchronous operations
|
|
541
|
-
* - Provide clear option descriptions and default values
|
|
542
|
-
* - Handle errors gracefully with process.exit()
|
|
543
|
-
* - Log progress and errors to console
|
|
544
|
-
* - Use Commander's .option() and .argument() for inputs
|
|
545
|
-
*
|
|
546
|
-
* AVOID:
|
|
547
|
-
* - Synchronous blocking operations in action handlers
|
|
548
|
-
* - Missing error handling (always use try-catch)
|
|
549
|
-
* - Hardcoded values (use options or environment variables)
|
|
550
|
-
* - Not exiting with appropriate exit codes on errors
|
|
551
|
-
*/
|
|
552
|
-
/**
|
|
553
|
-
* Initialize MCP configuration file
|
|
554
|
-
*/
|
|
555
|
-
const initCommand = new commander.Command("init").description("Initialize MCP configuration file").option("-o, --output <path>", "Output file path", "mcp-config.yaml").option("--json", "Generate JSON config instead of YAML", false).option("-f, --force", "Overwrite existing config file", false).option("--mcp-servers <json>", "JSON string of MCP servers to add to config (optional)").action(async (options) => {
|
|
556
|
-
try {
|
|
557
|
-
const outputPath = (0, node_path.resolve)(options.output);
|
|
558
|
-
const isYaml = !options.json && (outputPath.endsWith(".yaml") || outputPath.endsWith(".yml"));
|
|
559
|
-
let content;
|
|
560
|
-
if (isYaml) {
|
|
561
|
-
const liquid = new liquidjs.Liquid();
|
|
562
|
-
let mcpServersData = null;
|
|
563
|
-
if (options.mcpServers) try {
|
|
564
|
-
const serversObj = JSON.parse(options.mcpServers);
|
|
565
|
-
mcpServersData = Object.entries(serversObj).map(([name, config]) => ({
|
|
566
|
-
name,
|
|
567
|
-
command: config.command,
|
|
568
|
-
args: config.args
|
|
569
|
-
}));
|
|
570
|
-
} catch (parseError) {
|
|
571
|
-
__agiflowai_aicode_utils.log.error("Failed to parse --mcp-servers JSON:", parseError instanceof Error ? parseError.message : String(parseError));
|
|
572
|
-
process.exit(1);
|
|
634
|
+
if (options.json) {
|
|
635
|
+
const output = { ...toolsByServer };
|
|
636
|
+
if (skills.length > 0) output.__skills__ = skills.map((s) => ({
|
|
637
|
+
name: s.name,
|
|
638
|
+
description: s.description
|
|
639
|
+
}));
|
|
640
|
+
console.log(JSON.stringify(output, null, 2));
|
|
641
|
+
} else {
|
|
642
|
+
for (const [serverName, tools] of Object.entries(toolsByServer)) {
|
|
643
|
+
const omitDescription = clients.find((c) => c.serverName === serverName)?.omitToolDescription || false;
|
|
644
|
+
console.log(`\n${serverName}:`);
|
|
645
|
+
if (tools.length === 0) console.log(" No tools available");
|
|
646
|
+
else if (omitDescription) {
|
|
647
|
+
const toolNames = tools.map((t) => t.name).join(", ");
|
|
648
|
+
console.log(` ${toolNames}`);
|
|
649
|
+
} else for (const tool of tools) console.log(` - ${tool.name}: ${tool.description || "No description"}`);
|
|
573
650
|
}
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
await (0, node_fs_promises.writeFile)(outputPath, content, {
|
|
578
|
-
encoding: "utf-8",
|
|
579
|
-
flag: options.force ? "w" : "wx"
|
|
580
|
-
});
|
|
581
|
-
} catch (error) {
|
|
582
|
-
if (error && typeof error === "object" && "code" in error && error.code === "EEXIST") {
|
|
583
|
-
__agiflowai_aicode_utils.log.error(`Config file already exists: ${outputPath}`);
|
|
584
|
-
__agiflowai_aicode_utils.log.info("Use --force to overwrite");
|
|
585
|
-
process.exit(1);
|
|
651
|
+
if (skills.length > 0) {
|
|
652
|
+
console.log("\nskills:");
|
|
653
|
+
for (const skill of skills) console.log(` - ${skill.name}: ${skill.description}`);
|
|
586
654
|
}
|
|
587
|
-
throw error;
|
|
588
655
|
}
|
|
589
|
-
|
|
590
|
-
__agiflowai_aicode_utils.log.info("Next steps:");
|
|
591
|
-
__agiflowai_aicode_utils.log.info("1. Edit the configuration file to add your MCP servers");
|
|
592
|
-
__agiflowai_aicode_utils.log.info(`2. Run: one-mcp mcp-serve --config ${outputPath}`);
|
|
656
|
+
await clientManager.disconnectAll();
|
|
593
657
|
} catch (error) {
|
|
594
|
-
|
|
658
|
+
console.error(`Error executing list-tools: ${toErrorMessage$2(error)}`);
|
|
595
659
|
process.exit(1);
|
|
596
660
|
}
|
|
597
661
|
});
|
|
598
662
|
|
|
599
663
|
//#endregion
|
|
600
|
-
//#region src/
|
|
601
|
-
/**
|
|
602
|
-
* PrefetchService Constants
|
|
603
|
-
*
|
|
604
|
-
* Constants for package manager commands and process configuration.
|
|
605
|
-
*/
|
|
606
|
-
/** Transport type for stdio-based MCP servers */
|
|
607
|
-
const TRANSPORT_STDIO = "stdio";
|
|
608
|
-
/** npx command name */
|
|
609
|
-
const COMMAND_NPX = "npx";
|
|
610
|
-
/** npm command name */
|
|
611
|
-
const COMMAND_NPM = "npm";
|
|
612
|
-
/** pnpx command name (pnpm's npx equivalent) */
|
|
613
|
-
const COMMAND_PNPX = "pnpx";
|
|
614
|
-
/** pnpm command name */
|
|
615
|
-
const COMMAND_PNPM = "pnpm";
|
|
616
|
-
/** uvx command name */
|
|
617
|
-
const COMMAND_UVX = "uvx";
|
|
618
|
-
/** uv command name */
|
|
619
|
-
const COMMAND_UV = "uv";
|
|
620
|
-
/** Path suffix for npx command */
|
|
621
|
-
const COMMAND_NPX_SUFFIX = "/npx";
|
|
622
|
-
/** Path suffix for pnpx command */
|
|
623
|
-
const COMMAND_PNPX_SUFFIX = "/pnpx";
|
|
624
|
-
/** Path suffix for uvx command */
|
|
625
|
-
const COMMAND_UVX_SUFFIX = "/uvx";
|
|
626
|
-
/** Path suffix for uv command */
|
|
627
|
-
const COMMAND_UV_SUFFIX = "/uv";
|
|
628
|
-
/** Run subcommand for uv */
|
|
629
|
-
const ARG_RUN = "run";
|
|
630
|
-
/** Tool subcommand for uv */
|
|
631
|
-
const ARG_TOOL = "tool";
|
|
632
|
-
/** Install subcommand for uv tool and npm/pnpm */
|
|
633
|
-
const ARG_INSTALL = "install";
|
|
634
|
-
/** Add subcommand for pnpm */
|
|
635
|
-
const ARG_ADD = "add";
|
|
636
|
-
/** Global flag for npm/pnpm install */
|
|
637
|
-
const ARG_GLOBAL = "-g";
|
|
638
|
-
/** Flag prefix for command arguments */
|
|
639
|
-
const FLAG_PREFIX = "-";
|
|
640
|
-
/** npx --package flag (long form) */
|
|
641
|
-
const FLAG_PACKAGE_LONG = "--package";
|
|
642
|
-
/** npx -p flag (short form) */
|
|
643
|
-
const FLAG_PACKAGE_SHORT = "-p";
|
|
644
|
-
/** Equals delimiter used in flag=value patterns */
|
|
645
|
-
const EQUALS_DELIMITER = "=";
|
|
646
|
-
/**
|
|
647
|
-
* Regex pattern for valid package names (npm, pnpm, uvx, uv)
|
|
648
|
-
* Allows: @scope/package-name@version, package-name, package_name
|
|
649
|
-
* Prevents shell metacharacters that could enable command injection
|
|
650
|
-
* @example
|
|
651
|
-
* // Valid: '@scope/package@1.0.0', 'my-package', 'my_package', '@org/pkg'
|
|
652
|
-
* // Invalid: 'pkg; rm -rf /', 'pkg$(cmd)', 'pkg`whoami`', 'pkg|cat /etc/passwd'
|
|
653
|
-
*/
|
|
654
|
-
const VALID_PACKAGE_NAME_PATTERN = /^(@[a-zA-Z0-9_-]+\/)?[a-zA-Z0-9._-]+(@[a-zA-Z0-9._-]+)?$/;
|
|
655
|
-
/** Windows platform identifier */
|
|
656
|
-
const PLATFORM_WIN32 = "win32";
|
|
657
|
-
/** Success exit code */
|
|
658
|
-
const EXIT_CODE_SUCCESS = 0;
|
|
659
|
-
/** Stdio option to ignore stream */
|
|
660
|
-
const STDIO_IGNORE = "ignore";
|
|
661
|
-
/** Stdio option to pipe stream */
|
|
662
|
-
const STDIO_PIPE = "pipe";
|
|
663
|
-
|
|
664
|
-
//#endregion
|
|
665
|
-
//#region src/services/PrefetchService/PrefetchService.ts
|
|
664
|
+
//#region src/commands/describe-tools.ts
|
|
666
665
|
/**
|
|
667
|
-
*
|
|
666
|
+
* Describe Tools Command
|
|
668
667
|
*
|
|
669
668
|
* DESIGN PATTERNS:
|
|
670
|
-
* -
|
|
671
|
-
* -
|
|
669
|
+
* - Command pattern with Commander for CLI argument parsing
|
|
670
|
+
* - Async/await pattern for asynchronous operations
|
|
671
|
+
* - Error handling pattern with try-catch and proper exit codes
|
|
672
672
|
*
|
|
673
673
|
* CODING STANDARDS:
|
|
674
|
-
* - Use async
|
|
675
|
-
* -
|
|
676
|
-
* -
|
|
677
|
-
* -
|
|
674
|
+
* - Use async action handlers for asynchronous operations
|
|
675
|
+
* - Provide clear option descriptions and default values
|
|
676
|
+
* - Handle errors gracefully with process.exit()
|
|
677
|
+
* - Log progress and errors to console
|
|
678
|
+
* - Use Commander's .option() and .argument() for inputs
|
|
678
679
|
*
|
|
679
680
|
* AVOID:
|
|
680
|
-
* -
|
|
681
|
-
* -
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
* Type guard to check if a config object is an McpStdioConfig
|
|
685
|
-
* @param config - Config object to check
|
|
686
|
-
* @returns True if config has required McpStdioConfig properties
|
|
681
|
+
* - Synchronous blocking operations in action handlers
|
|
682
|
+
* - Missing error handling (always use try-catch)
|
|
683
|
+
* - Hardcoded values (use options or environment variables)
|
|
684
|
+
* - Not exiting with appropriate exit codes on errors
|
|
687
685
|
*/
|
|
688
|
-
function isMcpStdioConfig(config) {
|
|
689
|
-
return typeof config === "object" && config !== null && "command" in config;
|
|
690
|
-
}
|
|
691
686
|
/**
|
|
692
|
-
*
|
|
693
|
-
* Supports npx (Node.js), uvx (Python/uv), and uv run commands.
|
|
694
|
-
*
|
|
695
|
-
* @example
|
|
696
|
-
* ```typescript
|
|
697
|
-
* const service = new PrefetchService({
|
|
698
|
-
* mcpConfig: await configFetcher.fetchConfiguration(),
|
|
699
|
-
* parallel: true,
|
|
700
|
-
* });
|
|
701
|
-
* const packages = service.extractPackages();
|
|
702
|
-
* const summary = await service.prefetch();
|
|
703
|
-
* ```
|
|
687
|
+
* Describe specific MCP tools
|
|
704
688
|
*/
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
689
|
+
const describeToolsCommand = new commander.Command("describe-tools").description("Describe specific MCP tools").argument("<toolNames...>", "Tool names to describe").option("-c, --config <path>", "Path to MCP server configuration file").option("-s, --server <name>", "Filter by server name").option("-j, --json", "Output as JSON", false).action(async (toolNames, options) => {
|
|
690
|
+
try {
|
|
691
|
+
const configFilePath = options.config || require_http.findConfigFile();
|
|
692
|
+
if (!configFilePath) {
|
|
693
|
+
console.error("Error: No config file found. Use --config or create mcp-config.yaml");
|
|
694
|
+
process.exit(1);
|
|
695
|
+
}
|
|
696
|
+
const config = await new require_http.ConfigFetcherService({ configFilePath }).fetchConfiguration();
|
|
697
|
+
const clientManager = new require_http.McpClientManagerService();
|
|
698
|
+
const connectionPromises = Object.entries(config.mcpServers).map(async ([serverName, serverConfig]) => {
|
|
699
|
+
try {
|
|
700
|
+
await clientManager.connectToServer(serverName, serverConfig);
|
|
701
|
+
if (!options.json) console.error(`✓ Connected to ${serverName}`);
|
|
702
|
+
} catch (error) {
|
|
703
|
+
if (!options.json) console.error(`✗ Failed to connect to ${serverName}:`, error);
|
|
704
|
+
}
|
|
705
|
+
});
|
|
706
|
+
await Promise.all(connectionPromises);
|
|
707
|
+
const clients = clientManager.getAllClients();
|
|
708
|
+
if (clients.length === 0) {
|
|
709
|
+
console.error("No MCP servers connected");
|
|
710
|
+
process.exit(1);
|
|
711
|
+
}
|
|
712
|
+
const cwd = process.env.PROJECT_PATH || process.cwd();
|
|
713
|
+
const skillPaths = config.skills?.paths || [];
|
|
714
|
+
const skillService = skillPaths.length > 0 ? new require_http.SkillService(cwd, skillPaths) : void 0;
|
|
715
|
+
const foundTools = [];
|
|
716
|
+
const foundSkills = [];
|
|
717
|
+
const notFoundTools = [...toolNames];
|
|
718
|
+
const filteredClients = clients.filter((client) => !options.server || client.serverName === options.server);
|
|
719
|
+
const toolResults = await Promise.all(filteredClients.map(async (client) => {
|
|
720
|
+
try {
|
|
721
|
+
return {
|
|
722
|
+
client,
|
|
723
|
+
tools: await client.listTools(),
|
|
724
|
+
error: null
|
|
725
|
+
};
|
|
726
|
+
} catch (error) {
|
|
727
|
+
return {
|
|
728
|
+
client,
|
|
729
|
+
tools: [],
|
|
730
|
+
error
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
}));
|
|
734
|
+
for (const { client, tools, error } of toolResults) {
|
|
735
|
+
if (error) {
|
|
736
|
+
if (!options.json) console.error(`Failed to list tools from ${client.serverName}:`, error);
|
|
737
|
+
continue;
|
|
738
|
+
}
|
|
739
|
+
for (const toolName of toolNames) {
|
|
740
|
+
const tool = tools.find((t) => t.name === toolName);
|
|
741
|
+
if (tool) {
|
|
742
|
+
foundTools.push({
|
|
743
|
+
server: client.serverName,
|
|
744
|
+
name: tool.name,
|
|
745
|
+
description: tool.description,
|
|
746
|
+
inputSchema: tool.inputSchema
|
|
747
|
+
});
|
|
748
|
+
const idx = notFoundTools.indexOf(toolName);
|
|
749
|
+
if (idx > -1) notFoundTools.splice(idx, 1);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
if (skillService && notFoundTools.length > 0) {
|
|
754
|
+
const skillsToCheck = [...notFoundTools];
|
|
755
|
+
const skillResults = await Promise.all(skillsToCheck.map(async (toolName) => {
|
|
756
|
+
const skillName = toolName.startsWith("skill__") ? toolName.slice(7) : toolName;
|
|
757
|
+
return {
|
|
758
|
+
toolName,
|
|
759
|
+
skill: await skillService.getSkill(skillName)
|
|
760
|
+
};
|
|
761
|
+
}));
|
|
762
|
+
for (const { toolName, skill } of skillResults) if (skill) {
|
|
763
|
+
foundSkills.push({
|
|
764
|
+
name: skill.name,
|
|
765
|
+
location: skill.basePath,
|
|
766
|
+
instructions: skill.content
|
|
767
|
+
});
|
|
768
|
+
const idx = notFoundTools.indexOf(toolName);
|
|
769
|
+
if (idx > -1) notFoundTools.splice(idx, 1);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
const nextSteps = [];
|
|
773
|
+
if (foundTools.length > 0) nextSteps.push("For MCP tools: Use the use_tool function with toolName and toolArgs based on the inputSchema above.");
|
|
774
|
+
if (foundSkills.length > 0) nextSteps.push(`For skill, just follow skill's description to continue.`);
|
|
775
|
+
if (options.json) {
|
|
776
|
+
const result = {};
|
|
777
|
+
if (foundTools.length > 0) result.tools = foundTools;
|
|
778
|
+
if (foundSkills.length > 0) result.skills = foundSkills;
|
|
779
|
+
if (nextSteps.length > 0) result.nextSteps = nextSteps;
|
|
780
|
+
if (notFoundTools.length > 0) result.notFound = notFoundTools;
|
|
781
|
+
console.log(JSON.stringify(result, null, 2));
|
|
782
|
+
} else {
|
|
783
|
+
if (foundTools.length > 0) {
|
|
784
|
+
console.log("\nFound tools:\n");
|
|
785
|
+
for (const tool of foundTools) {
|
|
786
|
+
console.log(`Server: ${tool.server}`);
|
|
787
|
+
console.log(`Tool: ${tool.name}`);
|
|
788
|
+
console.log(`Description: ${tool.description || "No description"}`);
|
|
789
|
+
console.log(`Input Schema:`);
|
|
790
|
+
console.log(JSON.stringify(tool.inputSchema, null, 2));
|
|
791
|
+
console.log("");
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
if (foundSkills.length > 0) {
|
|
795
|
+
console.log("\nFound skills:\n");
|
|
796
|
+
for (const skill of foundSkills) {
|
|
797
|
+
console.log(`Skill: ${skill.name}`);
|
|
798
|
+
console.log(`Location: ${skill.location}`);
|
|
799
|
+
console.log(`Instructions:\n${skill.instructions}`);
|
|
800
|
+
console.log("");
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
if (nextSteps.length > 0) {
|
|
804
|
+
console.log("\nNext steps:");
|
|
805
|
+
for (const step of nextSteps) console.log(` • ${step}`);
|
|
806
|
+
console.log("");
|
|
807
|
+
}
|
|
808
|
+
if (notFoundTools.length > 0) console.error(`\nTools/skills not found: ${notFoundTools.join(", ")}`);
|
|
809
|
+
if (foundTools.length === 0 && foundSkills.length === 0) {
|
|
810
|
+
console.error("No tools or skills found");
|
|
811
|
+
process.exit(1);
|
|
729
812
|
}
|
|
730
813
|
}
|
|
731
|
-
|
|
814
|
+
await clientManager.disconnectAll();
|
|
815
|
+
} catch (error) {
|
|
816
|
+
console.error("Error executing describe-tools:", error);
|
|
817
|
+
process.exit(1);
|
|
732
818
|
}
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
//#endregion
|
|
822
|
+
//#region src/commands/use-tool.ts
|
|
823
|
+
/**
|
|
824
|
+
* Use Tool Command
|
|
825
|
+
*
|
|
826
|
+
* DESIGN PATTERNS:
|
|
827
|
+
* - Command pattern with Commander for CLI argument parsing
|
|
828
|
+
* - Async/await pattern for asynchronous operations
|
|
829
|
+
* - Error handling pattern with try-catch and proper exit codes
|
|
830
|
+
*
|
|
831
|
+
* CODING STANDARDS:
|
|
832
|
+
* - Use async action handlers for asynchronous operations
|
|
833
|
+
* - Provide clear option descriptions and default values
|
|
834
|
+
* - Handle errors gracefully with process.exit()
|
|
835
|
+
* - Log progress and errors to console
|
|
836
|
+
* - Use Commander's .option() and .argument() for inputs
|
|
837
|
+
*
|
|
838
|
+
* AVOID:
|
|
839
|
+
* - Synchronous blocking operations in action handlers
|
|
840
|
+
* - Missing error handling (always use try-catch)
|
|
841
|
+
* - Hardcoded values (use options or environment variables)
|
|
842
|
+
* - Not exiting with appropriate exit codes on errors
|
|
843
|
+
*/
|
|
844
|
+
/**
|
|
845
|
+
* Execute an MCP tool with arguments
|
|
846
|
+
*/
|
|
847
|
+
const useToolCommand = new commander.Command("use-tool").description("Execute an MCP tool with arguments").argument("<toolName>", "Tool name to execute").option("-c, --config <path>", "Path to MCP server configuration file").option("-s, --server <name>", "Server name (required if tool exists on multiple servers)").option("-a, --args <json>", "Tool arguments as JSON string", "{}").option("-j, --json", "Output as JSON", false).action(async (toolName, options) => {
|
|
848
|
+
try {
|
|
849
|
+
const configFilePath = options.config || require_http.findConfigFile();
|
|
850
|
+
if (!configFilePath) {
|
|
851
|
+
console.error("Error: No config file found. Use --config or create mcp-config.yaml");
|
|
852
|
+
process.exit(1);
|
|
853
|
+
}
|
|
854
|
+
let toolArgs = {};
|
|
739
855
|
try {
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
results.push(result);
|
|
856
|
+
toolArgs = JSON.parse(options.args);
|
|
857
|
+
} catch (_error) {
|
|
858
|
+
console.error("Error: Invalid JSON in --args");
|
|
859
|
+
process.exit(1);
|
|
860
|
+
}
|
|
861
|
+
const config = await new require_http.ConfigFetcherService({ configFilePath }).fetchConfiguration();
|
|
862
|
+
const clientManager = new require_http.McpClientManagerService();
|
|
863
|
+
const connectionPromises = Object.entries(config.mcpServers).map(async ([serverName, serverConfig]) => {
|
|
864
|
+
try {
|
|
865
|
+
await clientManager.connectToServer(serverName, serverConfig);
|
|
866
|
+
if (!options.json) console.error(`✓ Connected to ${serverName}`);
|
|
867
|
+
} catch (error) {
|
|
868
|
+
if (!options.json) console.error(`✗ Failed to connect to ${serverName}:`, error);
|
|
754
869
|
}
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
results
|
|
762
|
-
};
|
|
763
|
-
} catch (error) {
|
|
764
|
-
throw new Error(`Failed to prefetch packages: ${error instanceof Error ? error.message : String(error)}`);
|
|
870
|
+
});
|
|
871
|
+
await Promise.all(connectionPromises);
|
|
872
|
+
const clients = clientManager.getAllClients();
|
|
873
|
+
if (clients.length === 0) {
|
|
874
|
+
console.error("No MCP servers connected");
|
|
875
|
+
process.exit(1);
|
|
765
876
|
}
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
877
|
+
if (options.server) {
|
|
878
|
+
const client$1 = clientManager.getClient(options.server);
|
|
879
|
+
if (!client$1) {
|
|
880
|
+
console.error(`Server "${options.server}" not found`);
|
|
881
|
+
process.exit(1);
|
|
882
|
+
}
|
|
883
|
+
try {
|
|
884
|
+
if (!options.json) console.error(`Executing ${toolName} on ${options.server}...`);
|
|
885
|
+
const result = await client$1.callTool(toolName, toolArgs);
|
|
886
|
+
if (options.json) console.log(JSON.stringify(result, null, 2));
|
|
887
|
+
else {
|
|
888
|
+
console.log("\nResult:");
|
|
889
|
+
if (result.content) for (const content of result.content) if (content.type === "text") console.log(content.text);
|
|
890
|
+
else console.log(JSON.stringify(content, null, 2));
|
|
891
|
+
if (result.isError) {
|
|
892
|
+
console.error("\n⚠️ Tool execution returned an error");
|
|
893
|
+
process.exit(1);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
await clientManager.disconnectAll();
|
|
897
|
+
return;
|
|
898
|
+
} catch (error) {
|
|
899
|
+
console.error(`Failed to execute tool "${toolName}":`, error);
|
|
900
|
+
await clientManager.disconnectAll();
|
|
901
|
+
process.exit(1);
|
|
902
|
+
}
|
|
787
903
|
}
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
const args = config.args || [];
|
|
812
|
-
if (command === COMMAND_NPX || command.endsWith(COMMAND_NPX_SUFFIX)) {
|
|
813
|
-
const packageName = this.extractNpxPackage(args);
|
|
814
|
-
if (packageName && this.isValidPackageName(packageName)) return {
|
|
815
|
-
serverName,
|
|
816
|
-
packageManager: COMMAND_NPX,
|
|
817
|
-
packageName,
|
|
818
|
-
fullCommand: [
|
|
819
|
-
COMMAND_NPM,
|
|
820
|
-
ARG_INSTALL,
|
|
821
|
-
ARG_GLOBAL,
|
|
822
|
-
packageName
|
|
823
|
-
]
|
|
824
|
-
};
|
|
904
|
+
const searchResults = await Promise.all(clients.map(async (client$1) => {
|
|
905
|
+
try {
|
|
906
|
+
const hasTool = (await client$1.listTools()).some((t) => t.name === toolName);
|
|
907
|
+
return {
|
|
908
|
+
serverName: client$1.serverName,
|
|
909
|
+
hasTool,
|
|
910
|
+
error: null
|
|
911
|
+
};
|
|
912
|
+
} catch (error) {
|
|
913
|
+
return {
|
|
914
|
+
serverName: client$1.serverName,
|
|
915
|
+
hasTool: false,
|
|
916
|
+
error
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
}));
|
|
920
|
+
const matchingServers = [];
|
|
921
|
+
for (const { serverName, hasTool, error } of searchResults) {
|
|
922
|
+
if (error) {
|
|
923
|
+
if (!options.json) console.error(`Failed to list tools from ${serverName}:`, error);
|
|
924
|
+
continue;
|
|
925
|
+
}
|
|
926
|
+
if (hasTool) matchingServers.push(serverName);
|
|
825
927
|
}
|
|
826
|
-
if (
|
|
827
|
-
const
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
928
|
+
if (matchingServers.length === 0) {
|
|
929
|
+
const cwd = process.env.PROJECT_PATH || process.cwd();
|
|
930
|
+
const skillPaths = config.skills?.paths || [];
|
|
931
|
+
if (skillPaths.length > 0) try {
|
|
932
|
+
const skillService = new require_http.SkillService(cwd, skillPaths);
|
|
933
|
+
const skillName = toolName.startsWith("skill__") ? toolName.slice(7) : toolName;
|
|
934
|
+
const skill = await skillService.getSkill(skillName);
|
|
935
|
+
if (skill) {
|
|
936
|
+
const result = { content: [{
|
|
937
|
+
type: "text",
|
|
938
|
+
text: skill.content
|
|
939
|
+
}] };
|
|
940
|
+
if (options.json) console.log(JSON.stringify(result, null, 2));
|
|
941
|
+
else {
|
|
942
|
+
console.log("\nSkill content:");
|
|
943
|
+
console.log(skill.content);
|
|
944
|
+
}
|
|
945
|
+
await clientManager.disconnectAll();
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
948
|
+
} catch (error) {
|
|
949
|
+
if (!options.json) console.error(`Failed to lookup skill "${toolName}":`, error);
|
|
950
|
+
}
|
|
951
|
+
console.error(`Tool or skill "${toolName}" not found on any connected server or configured skill paths`);
|
|
952
|
+
await clientManager.disconnectAll();
|
|
953
|
+
process.exit(1);
|
|
839
954
|
}
|
|
840
|
-
if (
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
955
|
+
if (matchingServers.length > 1) {
|
|
956
|
+
console.error(`Tool "${toolName}" found on multiple servers: ${matchingServers.join(", ")}`);
|
|
957
|
+
console.error("Please specify --server to disambiguate");
|
|
958
|
+
await clientManager.disconnectAll();
|
|
959
|
+
process.exit(1);
|
|
960
|
+
}
|
|
961
|
+
const targetServer = matchingServers[0];
|
|
962
|
+
const client = clientManager.getClient(targetServer);
|
|
963
|
+
if (!client) {
|
|
964
|
+
console.error(`Internal error: Server "${targetServer}" not connected`);
|
|
965
|
+
await clientManager.disconnectAll();
|
|
966
|
+
process.exit(1);
|
|
848
967
|
}
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
968
|
+
try {
|
|
969
|
+
if (!options.json) console.error(`Executing ${toolName} on ${targetServer}...`);
|
|
970
|
+
const result = await client.callTool(toolName, toolArgs);
|
|
971
|
+
if (options.json) console.log(JSON.stringify(result, null, 2));
|
|
972
|
+
else {
|
|
973
|
+
console.log("\nResult:");
|
|
974
|
+
if (result.content) for (const content of result.content) if (content.type === "text") console.log(content.text);
|
|
975
|
+
else console.log(JSON.stringify(content, null, 2));
|
|
976
|
+
if (result.isError) {
|
|
977
|
+
console.error("\n⚠️ Tool execution returned an error");
|
|
978
|
+
await clientManager.disconnectAll();
|
|
979
|
+
process.exit(1);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
await clientManager.disconnectAll();
|
|
983
|
+
} catch (error) {
|
|
984
|
+
console.error(`Failed to execute tool "${toolName}":`, error);
|
|
985
|
+
await clientManager.disconnectAll();
|
|
986
|
+
process.exit(1);
|
|
862
987
|
}
|
|
863
|
-
|
|
988
|
+
} catch (error) {
|
|
989
|
+
console.error("Error executing use-tool:", error);
|
|
990
|
+
process.exit(1);
|
|
864
991
|
}
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
992
|
+
});
|
|
993
|
+
|
|
994
|
+
//#endregion
|
|
995
|
+
//#region src/commands/list-resources.ts
|
|
996
|
+
/**
|
|
997
|
+
* ListResources Command
|
|
998
|
+
*
|
|
999
|
+
* DESIGN PATTERNS:
|
|
1000
|
+
* - Command pattern with Commander for CLI argument parsing
|
|
1001
|
+
* - Async/await pattern for asynchronous operations
|
|
1002
|
+
* - Error handling pattern with try-catch and proper exit codes
|
|
1003
|
+
*
|
|
1004
|
+
* CODING STANDARDS:
|
|
1005
|
+
* - Use async action handlers for asynchronous operations
|
|
1006
|
+
* - Provide clear option descriptions and default values
|
|
1007
|
+
* - Handle errors gracefully with process.exit()
|
|
1008
|
+
* - Log progress and errors to console
|
|
1009
|
+
* - Use Commander's .option() and .argument() for inputs
|
|
1010
|
+
*
|
|
1011
|
+
* AVOID:
|
|
1012
|
+
* - Synchronous blocking operations in action handlers
|
|
1013
|
+
* - Missing error handling (always use try-catch)
|
|
1014
|
+
* - Hardcoded values (use options or environment variables)
|
|
1015
|
+
* - Not exiting with appropriate exit codes on errors
|
|
1016
|
+
*/
|
|
1017
|
+
function toErrorMessage$1(error) {
|
|
1018
|
+
return error instanceof Error ? error.message : String(error);
|
|
1019
|
+
}
|
|
1020
|
+
/**
|
|
1021
|
+
* List all available resources from connected MCP servers
|
|
1022
|
+
*/
|
|
1023
|
+
const listResourcesCommand = new commander.Command("list-resources").description("List all available resources from connected MCP servers").option("-c, --config <path>", "Path to MCP server configuration file").option("-s, --server <name>", "Filter by server name").option("-j, --json", "Output as JSON", false).action(async (options) => {
|
|
1024
|
+
try {
|
|
1025
|
+
const configFilePath = options.config || require_http.findConfigFile();
|
|
1026
|
+
if (!configFilePath) throw new Error("No config file found. Use --config or create mcp-config.yaml");
|
|
1027
|
+
const config = await new require_http.ConfigFetcherService({ configFilePath }).fetchConfiguration();
|
|
1028
|
+
const clientManager = new require_http.McpClientManagerService();
|
|
1029
|
+
await Promise.all(Object.entries(config.mcpServers).map(async ([serverName, serverConfig]) => {
|
|
1030
|
+
try {
|
|
1031
|
+
await clientManager.connectToServer(serverName, serverConfig);
|
|
1032
|
+
if (!options.json) console.error(`✓ Connected to ${serverName}`);
|
|
1033
|
+
} catch (error) {
|
|
1034
|
+
if (!options.json) console.error(`✗ Failed to connect to ${serverName}: ${toErrorMessage$1(error)}`);
|
|
887
1035
|
}
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
1036
|
+
}));
|
|
1037
|
+
const clients = options.server ? [clientManager.getClient(options.server)].filter((c) => c !== void 0) : clientManager.getAllClients();
|
|
1038
|
+
if (clients.length === 0) throw new Error("No MCP servers connected");
|
|
1039
|
+
const resourcesByServer = {};
|
|
1040
|
+
const resourceResults = await Promise.all(clients.map(async (client) => {
|
|
1041
|
+
try {
|
|
1042
|
+
const resources = await client.listResources();
|
|
1043
|
+
return {
|
|
1044
|
+
serverName: client.serverName,
|
|
1045
|
+
resources,
|
|
1046
|
+
error: null
|
|
1047
|
+
};
|
|
1048
|
+
} catch (error) {
|
|
1049
|
+
return {
|
|
1050
|
+
serverName: client.serverName,
|
|
1051
|
+
resources: [],
|
|
1052
|
+
error
|
|
1053
|
+
};
|
|
891
1054
|
}
|
|
1055
|
+
}));
|
|
1056
|
+
for (const { serverName, resources, error } of resourceResults) {
|
|
1057
|
+
if (error && !options.json) console.error(`Failed to list resources from ${serverName}: ${toErrorMessage$1(error)}`);
|
|
1058
|
+
resourcesByServer[serverName] = resources;
|
|
892
1059
|
}
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
1060
|
+
if (options.json) console.log(JSON.stringify(resourcesByServer, null, 2));
|
|
1061
|
+
else for (const [serverName, resources] of Object.entries(resourcesByServer)) {
|
|
1062
|
+
console.log(`\n${serverName}:`);
|
|
1063
|
+
if (resources.length === 0) console.log(" No resources available");
|
|
1064
|
+
else for (const resource of resources) {
|
|
1065
|
+
const label = resource.name ? `${resource.name} (${resource.uri})` : resource.uri;
|
|
1066
|
+
console.log(` - ${label}${resource.description ? `: ${resource.description}` : ""}`);
|
|
1067
|
+
}
|
|
896
1068
|
}
|
|
897
|
-
|
|
1069
|
+
await clientManager.disconnectAll();
|
|
1070
|
+
} catch (error) {
|
|
1071
|
+
console.error(`Error executing list-resources: ${toErrorMessage$1(error)}`);
|
|
1072
|
+
process.exit(1);
|
|
898
1073
|
}
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
1074
|
+
});
|
|
1075
|
+
|
|
1076
|
+
//#endregion
|
|
1077
|
+
//#region src/commands/read-resource.ts
|
|
1078
|
+
/**
|
|
1079
|
+
* ReadResource Command
|
|
1080
|
+
*
|
|
1081
|
+
* DESIGN PATTERNS:
|
|
1082
|
+
* - Command pattern with Commander for CLI argument parsing
|
|
1083
|
+
* - Async/await pattern for asynchronous operations
|
|
1084
|
+
* - Error handling pattern with try-catch and proper exit codes
|
|
1085
|
+
*
|
|
1086
|
+
* CODING STANDARDS:
|
|
1087
|
+
* - Use async action handlers for asynchronous operations
|
|
1088
|
+
* - Provide clear option descriptions and default values
|
|
1089
|
+
* - Handle errors gracefully with process.exit()
|
|
1090
|
+
* - Log progress and errors to console
|
|
1091
|
+
* - Use Commander's .option() and .argument() for inputs
|
|
1092
|
+
*
|
|
1093
|
+
* AVOID:
|
|
1094
|
+
* - Synchronous blocking operations in action handlers
|
|
1095
|
+
* - Missing error handling (always use try-catch)
|
|
1096
|
+
* - Hardcoded values (use options or environment variables)
|
|
1097
|
+
* - Not exiting with appropriate exit codes on errors
|
|
1098
|
+
*/
|
|
1099
|
+
function toErrorMessage(error) {
|
|
1100
|
+
return error instanceof Error ? error.message : String(error);
|
|
1101
|
+
}
|
|
1102
|
+
/**
|
|
1103
|
+
* Read a resource by URI from a connected MCP server
|
|
1104
|
+
*/
|
|
1105
|
+
const readResourceCommand = new commander.Command("read-resource").description("Read a resource by URI from a connected MCP server").argument("<uri>", "Resource URI to read").option("-c, --config <path>", "Path to MCP server configuration file").option("-s, --server <name>", "Server name (required if resource exists on multiple servers)").option("-j, --json", "Output as JSON", false).action(async (uri, options) => {
|
|
1106
|
+
try {
|
|
1107
|
+
const configFilePath = options.config || require_http.findConfigFile();
|
|
1108
|
+
if (!configFilePath) throw new Error("No config file found. Use --config or create mcp-config.yaml");
|
|
1109
|
+
const config = await new require_http.ConfigFetcherService({ configFilePath }).fetchConfiguration();
|
|
1110
|
+
const clientManager = new require_http.McpClientManagerService();
|
|
1111
|
+
await Promise.all(Object.entries(config.mcpServers).map(async ([serverName, serverConfig]) => {
|
|
1112
|
+
try {
|
|
1113
|
+
await clientManager.connectToServer(serverName, serverConfig);
|
|
1114
|
+
if (!options.json) console.error(`✓ Connected to ${serverName}`);
|
|
1115
|
+
} catch (error) {
|
|
1116
|
+
console.error(`✗ Failed to connect to ${serverName}: ${toErrorMessage(error)}`);
|
|
1117
|
+
}
|
|
1118
|
+
}));
|
|
1119
|
+
const clients = clientManager.getAllClients();
|
|
1120
|
+
if (clients.length === 0) throw new Error("No MCP servers connected");
|
|
1121
|
+
if (options.server) {
|
|
1122
|
+
const client$1 = clientManager.getClient(options.server);
|
|
1123
|
+
if (!client$1) throw new Error(`Server "${options.server}" not found`);
|
|
1124
|
+
if (!options.json) console.error(`Reading ${uri} from ${options.server}...`);
|
|
1125
|
+
const result$1 = await client$1.readResource(uri);
|
|
1126
|
+
if (options.json) console.log(JSON.stringify(result$1, null, 2));
|
|
1127
|
+
else for (const content of result$1.contents) if ("text" in content) console.log(content.text);
|
|
1128
|
+
else console.log(JSON.stringify(content, null, 2));
|
|
1129
|
+
await clientManager.disconnectAll();
|
|
1130
|
+
return;
|
|
913
1131
|
}
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
if (
|
|
933
|
-
|
|
1132
|
+
const searchResults = await Promise.all(clients.map(async (client$1) => {
|
|
1133
|
+
try {
|
|
1134
|
+
const hasResource = (await client$1.listResources()).some((r) => r.uri === uri);
|
|
1135
|
+
return {
|
|
1136
|
+
serverName: client$1.serverName,
|
|
1137
|
+
hasResource,
|
|
1138
|
+
error: null
|
|
1139
|
+
};
|
|
1140
|
+
} catch (error) {
|
|
1141
|
+
return {
|
|
1142
|
+
serverName: client$1.serverName,
|
|
1143
|
+
hasResource: false,
|
|
1144
|
+
error
|
|
1145
|
+
};
|
|
1146
|
+
}
|
|
1147
|
+
}));
|
|
1148
|
+
const matchingServers = [];
|
|
1149
|
+
for (const { serverName, hasResource, error } of searchResults) {
|
|
1150
|
+
if (error) {
|
|
1151
|
+
console.error(`Failed to list resources from ${serverName}: ${toErrorMessage(error)}`);
|
|
1152
|
+
continue;
|
|
1153
|
+
}
|
|
1154
|
+
if (hasResource) matchingServers.push(serverName);
|
|
934
1155
|
}
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
STDIO_PIPE
|
|
950
|
-
],
|
|
951
|
-
shell: process.platform === PLATFORM_WIN32
|
|
952
|
-
});
|
|
953
|
-
let stdout = "";
|
|
954
|
-
let stderr = "";
|
|
955
|
-
proc.stdout?.on("data", (data) => {
|
|
956
|
-
stdout += data.toString();
|
|
957
|
-
});
|
|
958
|
-
proc.stderr?.on("data", (data) => {
|
|
959
|
-
stderr += data.toString();
|
|
960
|
-
});
|
|
961
|
-
proc.on("close", (code) => {
|
|
962
|
-
resolve$1({
|
|
963
|
-
success: code === EXIT_CODE_SUCCESS,
|
|
964
|
-
output: stdout || stderr
|
|
965
|
-
});
|
|
966
|
-
});
|
|
967
|
-
proc.on("error", (error) => {
|
|
968
|
-
resolve$1({
|
|
969
|
-
success: false,
|
|
970
|
-
output: error.message
|
|
971
|
-
});
|
|
972
|
-
});
|
|
973
|
-
});
|
|
1156
|
+
if (matchingServers.length === 0) throw new Error(`Resource "${uri}" not found on any connected server`);
|
|
1157
|
+
if (matchingServers.length > 1) throw new Error(`Resource "${uri}" found on multiple servers: ${matchingServers.join(", ")}. Use --server to disambiguate`);
|
|
1158
|
+
const targetServer = matchingServers[0];
|
|
1159
|
+
const client = clientManager.getClient(targetServer);
|
|
1160
|
+
if (!client) throw new Error(`Internal error: Server "${targetServer}" not connected`);
|
|
1161
|
+
if (!options.json) console.error(`Reading ${uri} from ${targetServer}...`);
|
|
1162
|
+
const result = await client.readResource(uri);
|
|
1163
|
+
if (options.json) console.log(JSON.stringify(result, null, 2));
|
|
1164
|
+
else for (const content of result.contents) if ("text" in content) console.log(content.text);
|
|
1165
|
+
else console.log(JSON.stringify(content, null, 2));
|
|
1166
|
+
await clientManager.disconnectAll();
|
|
1167
|
+
} catch (error) {
|
|
1168
|
+
console.error(`Error executing read-resource: ${toErrorMessage(error)}`);
|
|
1169
|
+
process.exit(1);
|
|
974
1170
|
}
|
|
975
|
-
};
|
|
1171
|
+
});
|
|
976
1172
|
|
|
977
1173
|
//#endregion
|
|
978
1174
|
//#region src/commands/prefetch.ts
|
|
@@ -1051,7 +1247,7 @@ const prefetchCommand = new commander.Command("prefetch").description("Pre-downl
|
|
|
1051
1247
|
|
|
1052
1248
|
//#endregion
|
|
1053
1249
|
//#region package.json
|
|
1054
|
-
var version = "0.3.
|
|
1250
|
+
var version = "0.3.8";
|
|
1055
1251
|
|
|
1056
1252
|
//#endregion
|
|
1057
1253
|
//#region src/cli.ts
|
|
@@ -1085,6 +1281,8 @@ async function main() {
|
|
|
1085
1281
|
program.addCommand(listToolsCommand);
|
|
1086
1282
|
program.addCommand(describeToolsCommand);
|
|
1087
1283
|
program.addCommand(useToolCommand);
|
|
1284
|
+
program.addCommand(listResourcesCommand);
|
|
1285
|
+
program.addCommand(readResourceCommand);
|
|
1088
1286
|
program.addCommand(prefetchCommand);
|
|
1089
1287
|
await program.parseAsync(process.argv);
|
|
1090
1288
|
} catch (error) {
|