From eb49ede7324cb6d855f9592eb22c6e0e09d6befb Mon Sep 17 00:00:00 2001 From: Michal Date: Tue, 24 Feb 2026 10:14:16 +0000 Subject: [PATCH] fix: mcp command accepts --project directly for Claude spawned processes The mcp subcommand now has its own -p/--project option with passThroughOptions(), so `mcpctl mcp --project NAME` works when Claude spawns the process. Updated config claude to generate args: ['mcp', '--project', project] and added Commander-level tests. Co-Authored-By: Claude Opus 4.6 --- src/cli/src/commands/config.ts | 2 +- src/cli/src/commands/mcp.ts | 7 +++-- src/cli/tests/commands/claude.test.ts | 8 +++--- src/cli/tests/commands/mcp.test.ts | 39 ++++++++++++++++++++++++++- 4 files changed, 48 insertions(+), 8 deletions(-) diff --git a/src/cli/src/commands/config.ts b/src/cli/src/commands/config.ts index 7524776..b57f114 100644 --- a/src/cli/src/commands/config.ts +++ b/src/cli/src/commands/config.ts @@ -98,7 +98,7 @@ export function createConfigCommand(deps?: Partial, apiDeps?: mcpServers: { [opts.project]: { command: 'mcpctl', - args: ['mcp', '-p', opts.project], + args: ['mcp', '--project', opts.project], }, }, }; diff --git a/src/cli/src/commands/mcp.ts b/src/cli/src/commands/mcp.ts index ddf410c..d3d21b0 100644 --- a/src/cli/src/commands/mcp.ts +++ b/src/cli/src/commands/mcp.ts @@ -150,8 +150,11 @@ export interface McpCommandDeps { export function createMcpCommand(deps: McpCommandDeps): Command { const cmd = new Command('mcp') .description('MCP STDIO transport bridge — connects stdin/stdout to a project MCP endpoint') - .action(async () => { - const projectName = deps.getProject(); + .passThroughOptions() + .option('-p, --project ', 'Project name') + .action(async (opts: { project?: string }) => { + // Accept -p/--project on the command itself, or fall back to global --project + const projectName = opts.project ?? deps.getProject(); if (!projectName) { process.stderr.write('Error: --project is required for the mcp command\n'); process.exitCode = 1; diff --git a/src/cli/tests/commands/claude.test.ts b/src/cli/tests/commands/claude.test.ts index ef9ae93..4b55e56 100644 --- a/src/cli/tests/commands/claude.test.ts +++ b/src/cli/tests/commands/claude.test.ts @@ -45,7 +45,7 @@ describe('config claude', () => { const written = JSON.parse(readFileSync(outPath, 'utf-8')); expect(written.mcpServers['homeautomation']).toEqual({ command: 'mcpctl', - args: ['mcp', '-p', 'homeautomation'], + args: ['mcp', '--project', 'homeautomation'], }); expect(output.join('\n')).toContain('1 server(s)'); }); @@ -60,7 +60,7 @@ describe('config claude', () => { const parsed = JSON.parse(output[0]); expect(parsed.mcpServers['myproj']).toEqual({ command: 'mcpctl', - args: ['mcp', '-p', 'myproj'], + args: ['mcp', '--project', 'myproj'], }); }); @@ -80,7 +80,7 @@ describe('config claude', () => { expect(written.mcpServers['existing--server']).toBeDefined(); expect(written.mcpServers['proj-1']).toEqual({ command: 'mcpctl', - args: ['mcp', '-p', 'proj-1'], + args: ['mcp', '--project', 'proj-1'], }); expect(output.join('\n')).toContain('2 server(s)'); }); @@ -96,7 +96,7 @@ describe('config claude', () => { const written = JSON.parse(readFileSync(outPath, 'utf-8')); expect(written.mcpServers['proj-1']).toEqual({ command: 'mcpctl', - args: ['mcp', '-p', 'proj-1'], + args: ['mcp', '--project', 'proj-1'], }); }); diff --git a/src/cli/tests/commands/mcp.test.ts b/src/cli/tests/commands/mcp.test.ts index 588706a..b53f89b 100644 --- a/src/cli/tests/commands/mcp.test.ts +++ b/src/cli/tests/commands/mcp.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import http from 'node:http'; import { Readable, Writable } from 'node:stream'; -import { runMcpBridge } from '../../src/commands/mcp.js'; +import { runMcpBridge, createMcpCommand } from '../../src/commands/mcp.js'; // ---- Mock MCP server (simulates mcplocal project endpoint) ---- @@ -412,3 +412,40 @@ describe('MCP STDIO Bridge', () => { expect(post?.url).toBe('/projects/my%20project/mcp'); }); }); + +describe('createMcpCommand', () => { + it('accepts --project option directly', () => { + const cmd = createMcpCommand({ + getProject: () => undefined, + configLoader: () => ({ mcplocalUrl: 'http://localhost:3200' }), + credentialsLoader: () => null, + }); + const opt = cmd.options.find((o) => o.long === '--project'); + expect(opt).toBeDefined(); + expect(opt!.short).toBe('-p'); + }); + + it('parses --project from command args', async () => { + let capturedProject: string | undefined; + const cmd = createMcpCommand({ + getProject: () => undefined, + configLoader: () => ({ mcplocalUrl: `http://localhost:${mockPort}` }), + credentialsLoader: () => null, + }); + // Override the action to capture what project was parsed + // We test by checking the option parsing works, not by running the full bridge + const parsed = cmd.parse(['--project', 'test-proj'], { from: 'user' }); + capturedProject = parsed.opts().project; + expect(capturedProject).toBe('test-proj'); + }); + + it('parses -p shorthand from command args', () => { + const cmd = createMcpCommand({ + getProject: () => undefined, + configLoader: () => ({ mcplocalUrl: `http://localhost:${mockPort}` }), + credentialsLoader: () => null, + }); + const parsed = cmd.parse(['-p', 'my-project'], { from: 'user' }); + expect(parsed.opts().project).toBe('my-project'); + }); +});