From 4cf213190597a07d8bb35fc37711f978d9e69358 Mon Sep 17 00:00:00 2001 From: yao <63141491+yaonyan@users.noreply.github.com> Date: Wed, 12 Nov 2025 20:09:45 +0800 Subject: [PATCH] feat(cli): bump @mcpc/core to 0.3.6 and overhaul CLI UX MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Upgraded @mcpc/core from 0.3.4 → 0.3.6 for latest fixes/features - Replaced legacy --proxy/--transport-type flags with intuitive --wrap/--add - Added --mcp-stdio/--mcp-http/--mcp-sse helpers for quick server wrapping - Streamlined README with new quick-start, persistent config, and one-shot usage - Default config now auto-loaded from ~/.mcpc/config.json --- deno.lock | 2 +- packages/cli/README.md | 185 ++++++++++++---- packages/cli/deno.json | 4 +- packages/cli/src/config/loader.ts | 298 +++++++++++++++++++------- packages/cli/tests/proxy_mode_test.ts | 57 ----- packages/cli/tests/wrap_mode_test.ts | 148 +++++++++++++ packages/core/deno.json | 2 +- packages/core/src/compose.ts | 27 ++- 8 files changed, 532 insertions(+), 191 deletions(-) delete mode 100644 packages/cli/tests/proxy_mode_test.ts create mode 100644 packages/cli/tests/wrap_mode_test.ts diff --git a/deno.lock b/deno.lock index cf7d92f..5ab3034 100644 --- a/deno.lock +++ b/deno.lock @@ -1382,7 +1382,7 @@ }, "packages/cli": { "dependencies": [ - "jsr:@mcpc/core@~0.3.4", + "jsr:@mcpc/core@~0.3.6", "jsr:@mcpc/utils@~0.2.2", "jsr:@std/assert@^1.0.14", "jsr:@std/http@^1.0.14", diff --git a/packages/cli/README.md b/packages/cli/README.md index 4681d4a..4df181d 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -5,79 +5,108 @@ CLI server for MCPC with configuration support. -> **Note:** This package is published as `@mcpc-tech/cli` on npm and `@mcpc/cli` -> on JSR. +> **Note:** Published as `@mcpc-tech/cli` on npm and `@mcpc/cli` on JSR. ## Quick Start ```bash -# Using npm -npx -y @mcpc-tech/cli --help +# Install globally (or use npx -y @mcpc-tech/cli instead of mcpc) +npm install -g @mcpc-tech/cli -# Using JSR -npx -y deno run -A jsr:@mcpc/cli/bin --help +# Wrap an existing MCP server and run it immediately +mcpc --wrap --name "file-manager" \ + --mcp-stdio "npx -y @wonderwhy-er/desktop-commander" -# Load configuration from URL -npx -y deno run -A jsr:@mcpc/cli/bin --config-url \ - "https://raw.githubusercontent.com/mcpc-tech/mcpc/main/packages/cli/examples/configs/codex-fork.json" -``` - -## Configuration +# Add MCP servers to config, then run separately +mcpc --add --mcp-stdio "npx -y @wonderwhy-er/desktop-commander" +mcpc # Loads ~/.mcpc/config.json automatically -Load configuration using command-line arguments: +# Show help +mcpc --help +``` -- `--help`, `-h` - Show help message -- `--config ` - Inline JSON configuration string -- `--config-url ` - Fetch from URL (e.g., GitHub raw) -- `--config-file ` - Path to configuration file -- `--request-headers
`, `-H
` - Add custom HTTP header for URL - fetching (can be used multiple times) -- No arguments - Uses `./mcpc.config.json` if available +## Wrapping MCP Servers -## Usage +The simplest way to use MCPC is to wrap existing MCP servers with custom +execution modes: -**Show help:** +### One-time Run (no config saved) ```bash -npx -y deno run -A jsr:@mcpc/cli/bin --help +# Wrap and run a single MCP server +mcpc --wrap --name "my-file-manager-agent" \ + --mcp-stdio "npx -y @wonderwhy-er/desktop-commander" + +# Wrap multiple servers with different protocols and execution mode +mcpc --wrap --name "file-and-github-agent" --mode code_execution \ + --mcp-stdio "npx -y @wonderwhy-er/desktop-commander" \ + --mcp-http "https://api.github.com/mcp" ``` -**Inline JSON config:** +### Persistent Config (save and reuse) ```bash -npx -y deno run -A jsr:@mcpc/cli/bin --config '[{"name":"my-agent","description":"..."}]' -``` +# Step 1: Add servers to config +mcpc --add --mcp-stdio "npx -y @wonderwhy-er/desktop-commander" -**From URL:** +# Step 2: (Optional) Edit ~/.mcpc/config.json to add headers, env vars, etc. -```bash -npx -y deno run -A jsr:@mcpc/cli/bin --config-url https://example.com/config.json +# Step 3: Run with saved config +mcpc # Automatically loads ~/.mcpc/config.json ``` -**From URL with custom headers:** +The config file lets you add custom headers, environment variables, and other +settings: -```bash -npx -y deno run -A jsr:@mcpc/cli/bin \ - --config-url https://api.example.com/config.json \ - -H "Authorization: Bearer token123" \ - -H "X-Custom-Header: value" +```json +{ + "agents": [{ + "deps": { + "mcpServers": { + "github": { + "command": "https://api.github.com/mcp", + "transportType": "streamable-http", + "headers": { + "Authorization": "Bearer YOUR_TOKEN" + } + } + } + } + }] +} ``` -**From file:** +## Configuration Files + +Load config from different sources: ```bash -npx -y deno run -A jsr:@mcpc/cli/bin --config-file ./my-config.json -``` +# From a specific file +mcpc --config-file ./my-config.json -**Default (uses ./mcpc.config.json):** +# From a URL +mcpc --config-url https://example.com/config.json -```bash -npx -y deno run -A jsr:@mcpc/cli/bin +# From URL with custom headers +mcpc --config-url https://api.example.com/config.json \ + -H "Authorization: Bearer token123" + +# Inline JSON +mcpc --config '[{"name":"my-agent","description":"..."}]' ``` -**Environment variable substitution:** +### Config Priority Order + +1. `--config` (inline JSON) +2. `MCPC_CONFIG` environment variable +3. `--config-url` or `MCPC_CONFIG_URL` +4. `--config-file` or `MCPC_CONFIG_FILE` +5. `~/.mcpc/config.json` (user config) +6. `./mcpc.config.json` (local config) + +### Environment Variables -Config files support `$ENV_VAR_NAME` syntax: +Use `$VAR_NAME` syntax in config files: ```json { @@ -86,7 +115,7 @@ Config files support `$ENV_VAR_NAME` syntax: "mcpServers": { "github": { "headers": { - "Authorization": "Bearer $GITHUB_PERSONAL_ACCESS_TOKEN" + "Authorization": "Bearer $GITHUB_TOKEN" } } } @@ -95,14 +124,78 @@ Config files support `$ENV_VAR_NAME` syntax: } ``` -**HTTP server:** +## HTTP Server + +Run as an HTTP server instead of stdio: + +```bash +deno run -A jsr:@mcpc/cli/server --config-file ./my-config.json +``` + +## Command Reference + +### Main Options + +- `--help`, `-h` - Show help message +- `--add` - Add MCP servers to `~/.mcpc/config.json` and exit +- `--wrap` - Wrap and run MCP servers immediately (no config saved) +- `--mcp-stdio ` - Add stdio MCP server +- `--mcp-http ` - Add HTTP MCP server +- `--mcp-sse ` - Add SSE MCP server +- `--name ` - Custom agent name (default: auto-generated from server + names) +- `--mode ` - Execution mode (default: `agentic`) + +### Execution Modes (`--mode`) + +MCPC supports different execution modes that control how the agent processes and +executes tools: + +#### `agentic` (default) + +Interactive tool execution where the AI agent decides which tools to call and +when. The agent can make multiple tool calls in a conversation-like flow. + +```bash +mcpc --wrap --mode agentic --name "smart-assistant" \ + --mcp-stdio "npx -y @wonderwhy-er/desktop-commander" +``` + +#### `agentic_workflow` + +Structured execution with predefined or runtime-generated steps. The agent +follows a workflow pattern with specific actions at each step. ```bash -npx -y deno run -A jsr:@mcpc/cli/server --config-file ./my-config.json +mcpc --wrap --mode agentic_workflow --name "workflow-processor" \ + --mcp-stdio "npx -y @wonderwhy-er/desktop-commander" ``` +#### `code_execution` + +Enables code execution capabilities for running code snippets and scripts +through the agent. + +```bash +mcpc --wrap --mode code_execution --name "code-runner" \ + --mcp-stdio "npx -y @wonderwhy-er/desktop-commander" +``` + +> **Note:** Different modes may require specific plugins to be available. The +> `agentic` mode is always available by default. + +### Config Options + +- `--config ` - Inline JSON config +- `--config-url ` - Fetch config from URL +- `--config-file ` - Load config from file +- `--request-headers
`, `-H
` - Add HTTP header for URL fetching + ## Examples +See the [examples directory](examples/) for complete working examples using the +Codex Fork configuration. + ### Required Environment Variables When using the Codex Fork configuration: diff --git a/packages/cli/deno.json b/packages/cli/deno.json index b67fbc4..8dedbae 100644 --- a/packages/cli/deno.json +++ b/packages/cli/deno.json @@ -1,6 +1,6 @@ { "name": "@mcpc/cli", - "version": "0.1.16", + "version": "0.1.17", "repository": { "type": "git", "url": "git+https://github.com/mcpc-tech/mcpc.git" @@ -13,7 +13,7 @@ }, "imports": { "@mcpc-tech/plugin-code-execution": "npm:@mcpc-tech/plugin-code-execution@^0.0.6", - "@mcpc/core": "jsr:@mcpc/core@^0.3.4", + "@mcpc/core": "jsr:@mcpc/core@^0.3.6", "@mcpc/utils": "jsr:@mcpc/utils@^0.2.2", "@modelcontextprotocol/sdk": "npm:@modelcontextprotocol/sdk@^1.8.0", "@mcpc-tech/ripgrep-napi": "npm:@mcpc-tech/ripgrep-napi@^0.0.4", diff --git a/packages/cli/src/config/loader.ts b/packages/cli/src/config/loader.ts index fec4ca5..203591a 100644 --- a/packages/cli/src/config/loader.ts +++ b/packages/cli/src/config/loader.ts @@ -19,7 +19,8 @@ * 2. MCPC_CONFIG environment variable * 3. --config-url or MCPC_CONFIG_URL * 4. --config-file or MCPC_CONFIG_FILE - * 5. ./mcpc.config.json (default) + * 5. ~/.mcpc/config.json (user config) + * 6. ./mcpc.config.json (local config) * * @example * ```bash @@ -44,8 +45,9 @@ */ import type { ComposeDefinition } from "@mcpc/core"; -import { readFile } from "node:fs/promises"; -import { resolve } from "node:path"; +import { access, mkdir, readFile, writeFile } from "node:fs/promises"; +import { homedir } from "node:os"; +import { dirname, join, resolve } from "node:path"; import process from "node:process"; export interface MCPCConfig { @@ -92,49 +94,131 @@ function extractServerName(command: string, commandArgs: string[]): string { return name || "agentic-tool"; } +interface ServerSpec { + command: string; + args: string[]; + transportType: "stdio" | "streamable-http" | "sse"; +} + +/** + * Get the path to the user's config directory (~/.mcpc) + */ +function getUserConfigDir(): string { + return join(homedir(), ".mcpc"); +} + +/** + * Get the path to the user's saved config file (~/.mcpc/config.json) + */ +function getUserConfigPath(): string { + return join(getUserConfigDir(), "config.json"); +} + +/** + * Save configuration to user's config file (~/.mcpc/config.json) + */ +async function saveUserConfig(config: MCPCConfig): Promise { + const configPath = getUserConfigPath(); + const configDir = dirname(configPath); + + try { + // Check if config file already exists + let exists = false; + try { + await access(configPath); + exists = true; + } catch { + // File doesn't exist, which is fine + } + + if (exists) { + console.error(` +⚠ Configuration file already exists: ${configPath} + + To avoid overwriting your customized settings: + 1. Use a different output path with --config-file + 2. Or manually merge the new servers into your existing config + 3. Or delete the file first: rm ${configPath} + + Skipping save to preserve your existing configuration. +`); + return; + } + + // Create directory if it doesn't exist + await mkdir(configDir, { recursive: true }); + + // Write config file with pretty formatting + await writeFile(configPath, JSON.stringify(config, null, 2), "utf-8"); + + console.error(` +✓ Configuration saved to: ${configPath} + + Next steps: + 1. (Optional) Edit the config to add headers, env vars, etc. + Examples: + - Add headers: "headers": {"Authorization": "Bearer \${YOUR_TOKEN}"} + - Add env vars: "env": {"API_KEY": "\${API_KEY}"} + + 2. Run the server: + mcpc + + The config will be loaded automatically from ${configPath} +`); + } catch (error) { + console.error(`Warning: Failed to save config to ${configPath}:`, error); + } +} + /** - * Create proxy configuration from command-line arguments - * This generates an MCPC config that wraps an existing MCP server + * Create wrap configuration from command-line arguments + * This generates an MCPC config that wraps one or more existing MCP servers */ -function createProxyConfig(args: { - transportType?: string; - proxyCommand?: string[]; +async function createWrapConfig(args: { + mcpServers?: ServerSpec[]; mode?: string; name?: string; -}): MCPCConfig { - if (!args.proxyCommand || args.proxyCommand.length === 0) { - console.error("Error: --proxy requires a command after --"); + saveConfig?: boolean; +}): Promise { + if (!args.mcpServers || args.mcpServers.length === 0) { + console.error("Error: --wrap/--add requires at least one MCP server"); console.error( - "Example: mcpc --proxy --transport-type stdio -- npx -y @wonderwhy-er/desktop-commander", + "Example: mcpc --wrap --mcp-stdio 'npx -y @wonderwhy-er/desktop-commander'", ); - process.exit(1); - } - - if (!args.transportType) { - console.error("Error: --proxy requires --transport-type to be specified"); - console.error("Supported types: stdio, streamable-http, sse"); console.error( - "Example: mcpc --proxy --transport-type stdio -- npx -y @wonderwhy-er/desktop-commander", + "Multiple: mcpc --add --mcp-stdio 'npx -y server1' --mcp-http 'https://api.example.com'", ); process.exit(1); } - const validTransports = ["stdio", "streamable-http", "sse"]; - if (!validTransports.includes(args.transportType)) { - console.error(`Error: Invalid transport type '${args.transportType}'`); - console.error(`Supported types: ${validTransports.join(", ")}`); - process.exit(1); - } + // Build MCP servers configuration + const mcpServers: Record = {}; + const serverNames: string[] = []; + const refs: string[] = []; - const command = args.proxyCommand[0]; - const commandArgs = args.proxyCommand.slice(1); + for (const spec of args.mcpServers) { + const serverName = extractServerName(spec.command, spec.args); + + mcpServers[serverName] = { + command: spec.command, + args: spec.args, + transportType: spec.transportType, + }; - // Use custom name if provided, otherwise extract from command - const serverName = args.name || extractServerName(command, commandArgs); + serverNames.push(serverName); + refs.push(``); + + console.error(`Added MCP server: ${serverName} + Transport: ${spec.transportType} + Command: ${spec.command} ${spec.args.join(" ")}`); + } + + // Use custom name if provided, otherwise use merged server names + const agentName = args.name || `${serverNames.join("__")}--orchestrator`; // Create configuration const config: MCPCConfig = { - name: `${serverName}-proxy`, + name: "mcpc-wrap-config", version: "0.1.0", capabilities: { tools: {}, @@ -142,35 +226,28 @@ function createProxyConfig(args: { }, agents: [ { - name: serverName, - description: `Orchestrate ${serverName} MCP server tools`, + name: agentName, + description: `Orchestrate ${ + serverNames.length === 1 ? serverNames[0] : serverNames.join(", ") + } MCP server tools`, deps: { - mcpServers: { - [serverName]: { - command: command, - args: commandArgs, - transportType: args.transportType as - | "stdio" - | "streamable-http" - | "sse", - }, - }, + mcpServers: mcpServers, }, options: { - mode: (args.mode || "agentic"), - refs: [ - ``, - ], + mode: args.mode || "agentic", + refs: refs as any, }, }, ], }; - console.error(`Created proxy configuration for ${serverName}`); - console.error(`Transport: ${args.transportType}`); - console.error(`Command: ${command} ${commandArgs.join(" ")}`); - if (args.mode) { - console.error(`Mode: ${args.mode}`); + const modeInfo = args.mode ? `\nMode: ${args.mode}` : ""; + console.error(` +Created wrap configuration for ${serverNames.length} MCP server(s)${modeInfo}`); + + // Save configuration to user's config file if requested + if (args.saveConfig) { + await saveUserConfig(config); } return config; @@ -202,12 +279,18 @@ OPTIONS: - agentic_sampling: Autonomous sampling mode for agentic execution - agentic_workflow_sampling: Autonomous sampling mode for workflow execution - code_execution: Code execution mode for most efficient token usage - --proxy Proxy mode: automatically configure MCPC to wrap an MCP server - Use with --transport-type to specify the transport - Example: --proxy --transport-type stdio -- npx -y @wonderwhy-er/desktop-commander - --transport-type Transport type for proxy mode - Supported types: stdio, streamable-http, sse - --name Custom server name for proxy mode (overrides auto-detection) + --add Add MCP servers to ~/.mcpc/config.json and exit + Then run 'mcpc' to start the server with saved config + Use --mcp-stdio, --mcp-http, or --mcp-sse to specify servers + --wrap Wrap and run MCP servers immediately without saving config + Use --mcp-stdio, --mcp-http, or --mcp-sse to specify servers + --mcp-stdio Add an MCP server with stdio transport + Example: --mcp-stdio "npx -y @wonderwhy-er/desktop-commander" + --mcp-http Add an MCP server with streamable-http transport + Example: --mcp-http "https://api.github.com/mcp" + --mcp-sse Add an MCP server with SSE transport + Example: --mcp-sse "https://api.example.com/sse" + --name Custom agent name for wrap mode (overrides auto-detection) ENVIRONMENT VARIABLES: MCPC_CONFIG Inline JSON configuration (same as --config) @@ -218,17 +301,23 @@ EXAMPLES: # Show help mcpc --help - # Proxy mode - wrap an existing MCP server (stdio) - mcpc --proxy --transport-type stdio -- npx -y @wonderwhy-er/desktop-commander + # Add MCP servers to config and save to ~/.mcpc/config.json + mcpc --add --mcp-stdio "npx -y @wonderwhy-er/desktop-commander" + # Edit ~/.mcpc/config.json if needed (add headers, etc.) + mcpc # Loads config from ~/.mcpc/config.json automatically - # Proxy mode with custom server name - mcpc --proxy --transport-type stdio --name my-server -- npx shadcn@latest mcp + # Wrap and run immediately (one-time use, no config saved) + mcpc --wrap --mcp-stdio "npx -y @wonderwhy-er/desktop-commander" - # Proxy mode - wrap an MCP server (streamable-http) - mcpc --proxy --transport-type streamable-http -- https://api.example.com/mcp + # Multiple servers with different transports + mcpc --add \ + --mcp-stdio "npx -y @wonderwhy-er/desktop-commander" \ + --mcp-http "https://api.github.com/mcp" \ + --mcp-sse "https://api.example.com/sse" - # Proxy mode - wrap an MCP server (sse) - mcpc --proxy --transport-type sse -- https://api.example.com/sse + # Custom agent name + mcpc --add --name my-agent --mcp-stdio "npx shadcn@latest mcp" + mcpc --wrap --name my-agent --mcp-stdio "npx shadcn@latest mcp" # Load from URL mcpc --config-url \\ @@ -276,9 +365,9 @@ function parseArgs(): { configFile?: string; requestHeaders?: Record; help?: boolean; - proxy?: boolean; - transportType?: string; - proxyCommand?: string[]; + add?: boolean; + wrap?: boolean; + mcpServers?: ServerSpec[]; mode?: string; name?: string; } { @@ -289,9 +378,9 @@ function parseArgs(): { configFile?: string; requestHeaders?: Record; help?: boolean; - proxy?: boolean; - transportType?: string; - proxyCommand?: string[]; + add?: boolean; + wrap?: boolean; + mcpServers?: ServerSpec[]; mode?: string; name?: string; } = {}; @@ -326,18 +415,41 @@ function parseArgs(): { } } else if (arg === "--help" || arg === "-h") { result.help = true; - } else if (arg === "--proxy") { - result.proxy = true; - } else if (arg === "--transport-type" && i + 1 < args.length) { - result.transportType = args[++i]; + } else if (arg === "--add") { + result.add = true; + } else if (arg === "--wrap") { + result.wrap = true; + } else if ( + (arg === "--mcp-stdio" || arg === "--mcp-http" || arg === "--mcp-sse") && + i + 1 < args.length + ) { + // Parse MCP server specification + const cmdString = args[++i]; + const cmdParts = cmdString.split(/\s+/); + const command = cmdParts[0]; + const cmdArgs = cmdParts.slice(1); + + let transportType: "stdio" | "streamable-http" | "sse"; + if (arg === "--mcp-stdio") { + transportType = "stdio"; + } else if (arg === "--mcp-http") { + transportType = "streamable-http"; + } else { + transportType = "sse"; + } + + if (!result.mcpServers) { + result.mcpServers = []; + } + result.mcpServers.push({ + command, + args: cmdArgs, + transportType, + }); } else if (arg === "--mode" && i + 1 < args.length) { result.mode = args[++i]; } else if (arg === "--name" && i + 1 < args.length) { result.name = args[++i]; - } else if (arg === "--") { - // Everything after -- is the proxy command - result.proxyCommand = args.slice(i + 1); - break; } } @@ -357,9 +469,15 @@ export async function loadConfig(): Promise { process.exit(0); } - // Handle --proxy mode - if (args.proxy) { - return createProxyConfig(args); + // Handle --add mode - generate config, save, and exit + if (args.add) { + await createWrapConfig({ ...args, saveConfig: true }); + process.exit(0); + } + + // Handle --wrap mode - generate config and run immediately (no save) + if (args.wrap) { + return await createWrapConfig({ ...args, saveConfig: false }); } // Priority 1: --config (inline JSON string) @@ -423,7 +541,21 @@ export async function loadConfig(): Promise { } } - // Priority 5: ./mcpc.config.json in current directory + // Priority 5: ~/.mcpc/config.json (user config directory) + const userConfigPath = getUserConfigPath(); + try { + const content = await readFile(userConfigPath, "utf-8"); + const parsed = JSON.parse(content); + return applyModeOverride(normalizeConfig(parsed), args.mode); + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== "ENOENT") { + console.error(`Failed to load config from ${userConfigPath}:`, error); + throw error; + } + // File doesn't exist, continue to next option + } + + // Priority 6: ./mcpc.config.json in current directory const defaultConfigPath = resolve(process.cwd(), "mcpc.config.json"); try { const content = await readFile(defaultConfigPath, "utf-8"); diff --git a/packages/cli/tests/proxy_mode_test.ts b/packages/cli/tests/proxy_mode_test.ts deleted file mode 100644 index 037dc55..0000000 --- a/packages/cli/tests/proxy_mode_test.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { assertEquals } from "@std/assert"; -import { loadConfig } from "../src/config/loader.ts"; - -Deno.test("proxy mode - parse command correctly", async () => { - // Save original argv - const originalArgv = Deno.args; - - try { - // Mock process.argv for proxy mode - const mockArgv = [ - "--proxy", - "--transport-type", - "stdio", - "--", - "npx", - "-y", - "@wonderwhy-er/desktop-commander", - ]; - - // Set process.argv - Object.defineProperty(globalThis, "process", { - value: { - ...globalThis.process, - argv: ["deno", "run", ...mockArgv], - env: {}, - cwd: () => Deno.cwd(), - exit: (code: number) => { - throw new Error(`Process exited with code ${code}`); - }, - }, - configurable: true, - }); - - const config = await loadConfig(); - - assertEquals(config?.name, "desktop-commander-proxy"); - assertEquals(config?.agents.length, 1); - assertEquals(config?.agents[0].name, "desktop-commander"); - assertEquals( - config?.agents[0].deps?.mcpServers?.["desktop-commander"]?.command, - "npx", - ); - assertEquals( - config?.agents[0].deps?.mcpServers?.["desktop-commander"]?.args, - ["-y", "@wonderwhy-er/desktop-commander"], - ); - assertEquals( - config?.agents[0].deps?.mcpServers?.["desktop-commander"]?.transportType, - "stdio", - ); - } finally { - // Restore original argv - Object.defineProperty(globalThis, "Deno", { - value: { ...Deno, args: originalArgv }, - }); - } -}); diff --git a/packages/cli/tests/wrap_mode_test.ts b/packages/cli/tests/wrap_mode_test.ts new file mode 100644 index 0000000..911798d --- /dev/null +++ b/packages/cli/tests/wrap_mode_test.ts @@ -0,0 +1,148 @@ +import { assertEquals, assertExists } from "@std/assert"; +import { loadConfig } from "../src/config/loader.ts"; +import process from "node:process"; + +Deno.test("wrap mode - parse single server command correctly", async () => { + // Save original argv + const originalArgv = process.argv; + + try { + // Mock process.argv for wrap mode + const mockArgv = [ + "deno", + "run", + "--wrap", + "--mcp-stdio", + "npx -y @wonderwhy-er/desktop-commander", + ]; + + // Set process.argv + Object.defineProperty(process, "argv", { + value: mockArgv, + configurable: true, + writable: true, + }); + + const config = await loadConfig(); + + assertExists(config); + assertEquals(config?.name, "mcpc-wrap-config"); + assertEquals(config?.agents.length, 1); + assertEquals( + config?.agents[0].name, + "_wonderwhy-er_desktop-commander--orchestrator", + ); + assertEquals( + config?.agents[0].deps?.mcpServers?.["_wonderwhy-er_desktop-commander"] + ?.command, + "npx", + ); + assertEquals( + config?.agents[0].deps?.mcpServers?.["_wonderwhy-er_desktop-commander"] + ?.args, + ["-y", "@wonderwhy-er/desktop-commander"], + ); + assertEquals( + config?.agents[0].deps?.mcpServers?.["_wonderwhy-er_desktop-commander"] + ?.transportType, + "stdio", + ); + } finally { + // Restore original argv + Object.defineProperty(process, "argv", { + value: originalArgv, + configurable: true, + writable: true, + }); + } +}); + +Deno.test("wrap mode - parse multiple servers with different transports", async () => { + // Save original argv + const originalArgv = process.argv; + + try { + // Mock process.argv for wrap mode with multiple servers + const mockArgv = [ + "deno", + "run", + "--wrap", + "--mcp-stdio", + "npx -y @wonderwhy-er/desktop-commander", + "--mcp-http", + "https://api.github.com/mcp", + "--mcp-sse", + "https://api.example.com/sse", + ]; + + // Set process.argv + Object.defineProperty(process, "argv", { + value: mockArgv, + configurable: true, + writable: true, + }); + + const config = await loadConfig(); + + assertExists(config); + // Should create a multi-server wrapper config + assertEquals(config?.name, "mcpc-wrap-config"); + assertEquals(config?.agents.length, 1); + assertEquals( + config?.agents[0].name, + "_wonderwhy-er_desktop-commander__https___api_github_com_mcp__https___api_example_com_sse--orchestrator", + ); + + // Check that all three servers are configured + const mcpServers = config?.agents[0].deps?.mcpServers; + assertExists(mcpServers); + assertEquals(Object.keys(mcpServers || {}).length, 3); + + // Check first server (stdio) + assertEquals( + mcpServers?.["_wonderwhy-er_desktop-commander"]?.command, + "npx", + ); + assertEquals(mcpServers?.["_wonderwhy-er_desktop-commander"]?.args, [ + "-y", + "@wonderwhy-er/desktop-commander", + ]); + assertEquals( + mcpServers?.["_wonderwhy-er_desktop-commander"]?.transportType, + "stdio", + ); + + // Check second server (http) + assertEquals( + mcpServers?.["https___api_github_com_mcp"]?.command, + "https://api.github.com/mcp", + ); + assertEquals(mcpServers?.["https___api_github_com_mcp"]?.args, []); + assertEquals( + mcpServers?.["https___api_github_com_mcp"]?.transportType, + "streamable-http", + ); + + // Check third server (sse) + assertEquals( + mcpServers?.["https___api_example_com_sse"]?.command, + "https://api.example.com/sse", + ); + assertEquals(mcpServers?.["https___api_example_com_sse"]?.args, []); + assertEquals( + mcpServers?.["https___api_example_com_sse"]?.transportType, + "sse", + ); + + // Check that refs include all servers + const refs = config?.agents[0].options?.refs || []; + assertEquals(refs.length, 3); + } finally { + // Restore original argv + Object.defineProperty(process, "argv", { + value: originalArgv, + configurable: true, + writable: true, + }); + } +}); diff --git a/packages/core/deno.json b/packages/core/deno.json index c596110..ced9d1b 100644 --- a/packages/core/deno.json +++ b/packages/core/deno.json @@ -1,6 +1,6 @@ { "name": "@mcpc/core", - "version": "0.3.5", + "version": "0.3.6", "repository": { "type": "git", "url": "git+https://github.com/mcpc-tech/mcpc.git" diff --git a/packages/core/src/compose.ts b/packages/core/src/compose.ts index 50c4e70..257b7b4 100644 --- a/packages/core/src/compose.ts +++ b/packages/core/src/compose.ts @@ -404,11 +404,22 @@ export class ComposableMCPServer extends Server { const toolNameToIdMapping = new Map(); const requestedToolNames = new Set(); const availableToolNames = new Set(); + const allPlaceholderUsages: string[] = []; // Collect all requested tool names from XML tags tagToResults.tool.forEach((tool: any) => { if (tool.attribs.name) { - requestedToolNames.add(sanitizePropertyKey(tool.attribs.name)); + const originalName = tool.attribs.name; + const toolName = sanitizePropertyKey(originalName); + // Track __ALL__ placeholder usages separately for special warning + if ( + toolName.endsWith(`_${ALL_TOOLS_PLACEHOLDER}`) || + toolName === ALL_TOOLS_PLACEHOLDER + ) { + allPlaceholderUsages.push(originalName); + } else { + requestedToolNames.add(toolName); + } } }); @@ -485,6 +496,20 @@ export class ComposableMCPServer extends Server { availableToolNames.add(toolName); }); + // Warn about __ALL__ placeholder usages - suggest using specific tool names + if (allPlaceholderUsages.length > 0) { + await this.logger.warning( + `Found ${allPlaceholderUsages.length} __ALL__ placeholder(s) for agent "${name}":`, + ); + allPlaceholderUsages.forEach((usage) => { + this.logger.warning( + ` • "${usage}" - consider using specific tool names`, + ); + }); + const available = Array.from(availableToolNames).sort().join(", "); + await this.logger.warning(` Available tools: ${available}`); + } + // Warn about unmatched tool names against final tool set const unmatchedTools = Array.from(requestedToolNames).filter( (toolName) => !allTools[toolName],