@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/dist/cli.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- const require_http = require('./http-CKXz9Gp8.cjs');
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/commands/list-tools.ts
180
+ //#region src/services/PrefetchService/constants.ts
103
181
  /**
104
- * List Tools Command
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
- * AVOID:
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
- * List all available tools from connected MCP servers
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 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) => {
128
- try {
129
- const configFilePath = options.config || require_http.findConfigFile();
130
- if (!configFilePath) {
131
- console.error("Error: No config file found. Use --config or create mcp-config.yaml");
132
- process.exit(1);
133
- }
134
- const config = await new require_http.ConfigFetcherService({ configFilePath }).fetchConfiguration();
135
- const clientManager = new require_http.McpClientManagerService();
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/commands/describe-tools.ts
245
+ //#region src/services/PrefetchService/PrefetchService.ts
192
246
  /**
193
- * Describe Tools Command
247
+ * PrefetchService
194
248
  *
195
249
  * DESIGN PATTERNS:
196
- * - Command pattern with Commander for CLI argument parsing
197
- * - Async/await pattern for asynchronous operations
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 action handlers for asynchronous operations
202
- * - Provide clear option descriptions and default values
203
- * - Handle errors gracefully with process.exit()
204
- * - Log progress and errors to console
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
- * - Synchronous blocking operations in action handlers
209
- * - Missing error handling (always use try-catch)
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
- * Describe specific MCP tools
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
- 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) => {
217
- try {
218
- const configFilePath = options.config || require_http.findConfigFile();
219
- if (!configFilePath) {
220
- console.error("Error: No config file found. Use --config or create mcp-config.yaml");
221
- process.exit(1);
222
- }
223
- const config = await new require_http.ConfigFetcherService({ configFilePath }).fetchConfiguration();
224
- const clientManager = new require_http.McpClientManagerService();
225
- const connectionPromises = Object.entries(config.mcpServers).map(async ([serverName, serverConfig]) => {
226
- try {
227
- await clientManager.connectToServer(serverName, serverConfig);
228
- if (!options.json) console.error(`✓ Connected to ${serverName}`);
229
- } catch (error) {
230
- if (!options.json) console.error(`✗ Failed to connect to ${serverName}:`, error);
231
- }
232
- });
233
- await Promise.all(connectionPromises);
234
- const clients = clientManager.getAllClients();
235
- if (clients.length === 0) {
236
- console.error("No MCP servers connected");
237
- process.exit(1);
238
- }
239
- const cwd = process.env.PROJECT_PATH || process.cwd();
240
- const skillPaths = config.skills?.paths || [];
241
- const skillService = skillPaths.length > 0 ? new require_http.SkillService(cwd, skillPaths) : void 0;
242
- const foundTools = [];
243
- const foundSkills = [];
244
- const notFoundTools = [...toolNames];
245
- const filteredClients = clients.filter((client) => !options.server || client.serverName === options.server);
246
- const toolResults = await Promise.all(filteredClients.map(async (client) => {
247
- try {
248
- return {
249
- client,
250
- tools: await client.listTools(),
251
- error: null
252
- };
253
- } catch (error) {
254
- return {
255
- client,
256
- tools: [],
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
- if (skillService && notFoundTools.length > 0) {
281
- const skillsToCheck = [...notFoundTools];
282
- const skillResults = await Promise.all(skillsToCheck.map(async (toolName) => {
283
- const skillName = toolName.startsWith("skill__") ? toolName.slice(7) : toolName;
284
- return {
285
- toolName,
286
- skill: await skillService.getSkill(skillName)
287
- };
288
- }));
289
- for (const { toolName, skill } of skillResults) if (skill) {
290
- foundSkills.push({
291
- name: skill.name,
292
- location: skill.basePath,
293
- instructions: skill.content
294
- });
295
- const idx = notFoundTools.indexOf(toolName);
296
- if (idx > -1) notFoundTools.splice(idx, 1);
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
- const nextSteps = [];
300
- if (foundTools.length > 0) nextSteps.push("For MCP tools: Use the use_tool function with toolName and toolArgs based on the inputSchema above.");
301
- if (foundSkills.length > 0) nextSteps.push(`For skill, just follow skill's description to continue.`);
302
- if (options.json) {
303
- const result = {};
304
- if (foundTools.length > 0) result.tools = foundTools;
305
- if (foundSkills.length > 0) result.skills = foundSkills;
306
- if (nextSteps.length > 0) result.nextSteps = nextSteps;
307
- if (notFoundTools.length > 0) result.notFound = notFoundTools;
308
- console.log(JSON.stringify(result, null, 2));
309
- } else {
310
- if (foundTools.length > 0) {
311
- console.log("\nFound tools:\n");
312
- for (const tool of foundTools) {
313
- console.log(`Server: ${tool.server}`);
314
- console.log(`Tool: ${tool.name}`);
315
- console.log(`Description: ${tool.description || "No description"}`);
316
- console.log(`Input Schema:`);
317
- console.log(JSON.stringify(tool.inputSchema, null, 2));
318
- console.log("");
319
- }
320
- }
321
- if (foundSkills.length > 0) {
322
- console.log("\nFound skills:\n");
323
- for (const skill of foundSkills) {
324
- console.log(`Skill: ${skill.name}`);
325
- console.log(`Location: ${skill.location}`);
326
- console.log(`Instructions:\n${skill.instructions}`);
327
- console.log("");
328
- }
329
- }
330
- if (nextSteps.length > 0) {
331
- console.log("\nNext steps:");
332
- for (const step of nextSteps) console.log(` • ${step}`);
333
- console.log("");
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 (notFoundTools.length > 0) console.error(`\nTools/skills not found: ${notFoundTools.join(", ")}`);
336
- if (foundTools.length === 0 && foundSkills.length === 0) {
337
- console.error("No tools or skills found");
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
- await clientManager.disconnectAll();
342
- } catch (error) {
343
- console.error("Error executing describe-tools:", error);
344
- process.exit(1);
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/use-tool.ts
558
+ //#region src/commands/list-tools.ts
350
559
  /**
351
- * Use Tool Command
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
- * Execute an MCP tool with arguments
584
+ * List all available tools from connected MCP servers
373
585
  */
374
- 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) => {
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}:`, error);
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
- console.error("No MCP servers connected");
402
- process.exit(1);
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 hasTool = (await client$1.listTools()).some((t) => t.name === toolName);
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$1.serverName,
436
- hasTool,
610
+ serverName: client.serverName,
611
+ tools: filteredTools,
437
612
  error: null
438
613
  };
439
614
  } catch (error) {
440
615
  return {
441
- serverName: client$1.serverName,
442
- hasTool: false,
616
+ serverName: client.serverName,
617
+ tools: [],
443
618
  error
444
619
  };
445
620
  }
446
621
  }));
447
- const matchingServers = [];
448
- for (const { serverName, hasTool, error } of searchResults) {
449
- if (error) {
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
- try {
496
- if (!options.json) console.error(`Executing ${toolName} on ${targetServer}...`);
497
- const result = await client.callTool(toolName, toolArgs);
498
- if (options.json) console.log(JSON.stringify(result, null, 2));
499
- else {
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 execute tool "${toolName}":`, error);
512
- await clientManager.disconnectAll();
513
- process.exit(1);
632
+ if (!options.json) console.error(`Failed to load skills: ${toErrorMessage$2(error)}`);
514
633
  }
515
- } catch (error) {
516
- console.error("Error executing use-tool:", error);
517
- process.exit(1);
518
- }
519
- });
520
-
521
- //#endregion
522
- //#region src/templates/mcp-config.yaml.liquid?raw
523
- 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";
524
-
525
- //#endregion
526
- //#region src/templates/mcp-config.json?raw
527
- 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\": [\n \"/path/to/mcp-server/build/index.js\"\n ],\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";
528
-
529
- //#endregion
530
- //#region src/commands/init.ts
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
- content = await liquid.parseAndRender(mcp_config_yaml_default, { mcpServers: mcpServersData });
575
- } else content = mcp_config_default;
576
- try {
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
- __agiflowai_aicode_utils.log.info(`MCP configuration file created: ${outputPath}`);
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
- __agiflowai_aicode_utils.log.error("Error executing init:", error instanceof Error ? error.message : String(error));
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/services/PrefetchService/constants.ts
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
- * PrefetchService
666
+ * Describe Tools Command
668
667
  *
669
668
  * DESIGN PATTERNS:
670
- * - Service pattern for business logic encapsulation
671
- * - Single responsibility principle
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/await for asynchronous operations
675
- * - Throw descriptive errors for error cases
676
- * - Keep methods focused and well-named
677
- * - Document complex logic with comments
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
- * - Mixing concerns (keep focused on single domain)
681
- * - Direct tool implementation (services should be tool-agnostic)
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
- * PrefetchService handles pre-downloading packages used by MCP servers.
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
- var PrefetchService = class {
706
- config;
707
- /**
708
- * Creates a new PrefetchService instance
709
- * @param config - Service configuration options
710
- */
711
- constructor(config) {
712
- this.config = config;
713
- }
714
- /**
715
- * Extract all prefetchable packages from the MCP configuration
716
- * @returns Array of package info objects
717
- */
718
- extractPackages() {
719
- const packages = [];
720
- const { mcpConfig, filter } = this.config;
721
- for (const [serverName, serverConfig] of Object.entries(mcpConfig.mcpServers)) {
722
- if (serverConfig.disabled) continue;
723
- if (serverConfig.transport !== TRANSPORT_STDIO) continue;
724
- if (!isMcpStdioConfig(serverConfig.config)) continue;
725
- const packageInfo = this.extractPackageInfo(serverName, serverConfig.config);
726
- if (packageInfo) {
727
- if (filter && packageInfo.packageManager !== filter) continue;
728
- packages.push(packageInfo);
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
- return packages;
814
+ await clientManager.disconnectAll();
815
+ } catch (error) {
816
+ console.error("Error executing describe-tools:", error);
817
+ process.exit(1);
732
818
  }
733
- /**
734
- * Prefetch all packages from the configuration
735
- * @returns Summary of prefetch results
736
- * @throws Error if prefetch operation fails unexpectedly
737
- */
738
- async prefetch() {
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
- const packages = this.extractPackages();
741
- const results = [];
742
- if (packages.length === 0) return {
743
- totalPackages: 0,
744
- successful: 0,
745
- failed: 0,
746
- results: []
747
- };
748
- if (this.config.parallel) {
749
- const promises = packages.map(async (pkg) => this.prefetchPackage(pkg));
750
- results.push(...await Promise.all(promises));
751
- } else for (const pkg of packages) {
752
- const result = await this.prefetchPackage(pkg);
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
- const successful = results.filter((r) => r.success).length;
756
- const failed = results.filter((r) => !r.success).length;
757
- return {
758
- totalPackages: packages.length,
759
- successful,
760
- failed,
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
- * Prefetch a single package
769
- * @param pkg - Package info to prefetch
770
- * @returns Result of the prefetch operation
771
- */
772
- async prefetchPackage(pkg) {
773
- try {
774
- const [command, ...args] = pkg.fullCommand;
775
- const result = await this.runCommand(command, args);
776
- return {
777
- package: pkg,
778
- success: result.success,
779
- output: result.output
780
- };
781
- } catch (error) {
782
- return {
783
- package: pkg,
784
- success: false,
785
- output: error instanceof Error ? error.message : String(error)
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
- * Validate package name to prevent command injection
791
- * @param packageName - Package name to validate
792
- * @returns True if package name is safe, false otherwise
793
- * @remarks Rejects package names containing shell metacharacters
794
- * @example
795
- * isValidPackageName('@scope/package') // true
796
- * isValidPackageName('my-package@1.0.0') // true
797
- * isValidPackageName('pkg; rm -rf /') // false (shell injection)
798
- * isValidPackageName('pkg$(whoami)') // false (command substitution)
799
- */
800
- isValidPackageName(packageName) {
801
- return VALID_PACKAGE_NAME_PATTERN.test(packageName);
802
- }
803
- /**
804
- * Extract package info from a server's stdio config
805
- * @param serverName - Name of the MCP server
806
- * @param config - Stdio configuration for the server
807
- * @returns Package info if extractable, null otherwise
808
- */
809
- extractPackageInfo(serverName, config) {
810
- const command = config.command.toLowerCase();
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 (command === COMMAND_PNPX || command.endsWith(COMMAND_PNPX_SUFFIX)) {
827
- const packageName = this.extractNpxPackage(args);
828
- if (packageName && this.isValidPackageName(packageName)) return {
829
- serverName,
830
- packageManager: COMMAND_PNPX,
831
- packageName,
832
- fullCommand: [
833
- COMMAND_PNPM,
834
- ARG_ADD,
835
- ARG_GLOBAL,
836
- packageName
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 (command === COMMAND_UVX || command.endsWith(COMMAND_UVX_SUFFIX)) {
841
- const packageName = this.extractUvxPackage(args);
842
- if (packageName && this.isValidPackageName(packageName)) return {
843
- serverName,
844
- packageManager: COMMAND_UVX,
845
- packageName,
846
- fullCommand: [COMMAND_UVX, packageName]
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
- if ((command === COMMAND_UV || command.endsWith(COMMAND_UV_SUFFIX)) && args.includes(ARG_RUN)) {
850
- const packageName = this.extractUvRunPackage(args);
851
- if (packageName && this.isValidPackageName(packageName)) return {
852
- serverName,
853
- packageManager: COMMAND_UV,
854
- packageName,
855
- fullCommand: [
856
- COMMAND_UV,
857
- ARG_TOOL,
858
- ARG_INSTALL,
859
- packageName
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
- return null;
988
+ } catch (error) {
989
+ console.error("Error executing use-tool:", error);
990
+ process.exit(1);
864
991
  }
865
- /**
866
- * Extract package name from npx command args
867
- * @param args - Command arguments
868
- * @returns Package name or null
869
- * @remarks Handles --package=value, --package value, -p value patterns.
870
- * Falls back to first non-flag argument if no --package/-p flag found.
871
- * Returns null if flag has no value or is followed by another flag.
872
- * When multiple --package flags exist, returns the first valid one.
873
- * @example
874
- * extractNpxPackage(['--package=@scope/pkg']) // returns '@scope/pkg'
875
- * extractNpxPackage(['--package', 'pkg-name']) // returns 'pkg-name'
876
- * extractNpxPackage(['-p', 'pkg']) // returns 'pkg'
877
- * extractNpxPackage(['-y', 'pkg-name', '--flag']) // returns 'pkg-name' (fallback)
878
- * extractNpxPackage(['--package=']) // returns null (empty value)
879
- */
880
- extractNpxPackage(args) {
881
- for (let i = 0; i < args.length; i++) {
882
- const arg = args[i];
883
- if (arg.startsWith(FLAG_PACKAGE_LONG + EQUALS_DELIMITER)) return arg.slice(FLAG_PACKAGE_LONG.length + EQUALS_DELIMITER.length) || null;
884
- if (arg === FLAG_PACKAGE_LONG && i + 1 < args.length) {
885
- const nextArg = args[i + 1];
886
- if (!nextArg.startsWith(FLAG_PREFIX)) return nextArg;
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
- if (arg === FLAG_PACKAGE_SHORT && i + 1 < args.length) {
889
- const nextArg = args[i + 1];
890
- if (!nextArg.startsWith(FLAG_PREFIX)) return nextArg;
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
- for (const arg of args) {
894
- if (arg.startsWith(FLAG_PREFIX)) continue;
895
- return arg;
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
- return null;
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
- * Extract package name from uvx command args
901
- * @param args - Command arguments
902
- * @returns Package name or null
903
- * @remarks Assumes the first non-flag argument is the package name.
904
- * Handles both single (-) and double (--) dash flags.
905
- * @example
906
- * extractUvxPackage(['mcp-server-fetch']) // returns 'mcp-server-fetch'
907
- * extractUvxPackage(['--quiet', 'pkg-name']) // returns 'pkg-name'
908
- */
909
- extractUvxPackage(args) {
910
- for (const arg of args) {
911
- if (arg.startsWith(FLAG_PREFIX)) continue;
912
- return arg;
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
- return null;
915
- }
916
- /**
917
- * Extract package name from uv run command args
918
- * @param args - Command arguments
919
- * @returns Package name or null
920
- * @remarks Looks for the first non-flag argument after the 'run' subcommand.
921
- * Returns null if 'run' is not found in args.
922
- * @example
923
- * extractUvRunPackage(['run', 'mcp-server']) // returns 'mcp-server'
924
- * extractUvRunPackage(['run', '--verbose', 'pkg']) // returns 'pkg'
925
- * extractUvRunPackage(['install', 'pkg']) // returns null (no 'run')
926
- */
927
- extractUvRunPackage(args) {
928
- const runIndex = args.indexOf(ARG_RUN);
929
- if (runIndex === -1) return null;
930
- for (let i = runIndex + 1; i < args.length; i++) {
931
- const arg = args[i];
932
- if (arg.startsWith(FLAG_PREFIX)) continue;
933
- return arg;
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
- return null;
936
- }
937
- /**
938
- * Run a shell command and capture output
939
- * @param command - Command to run
940
- * @param args - Command arguments
941
- * @returns Promise with success status and output
942
- */
943
- runCommand(command, args) {
944
- return new Promise((resolve$1) => {
945
- const proc = (0, node_child_process.spawn)(command, args, {
946
- stdio: [
947
- STDIO_IGNORE,
948
- STDIO_PIPE,
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.6";
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) {