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 <noreply@anthropic.com>
This commit is contained in:
@@ -98,7 +98,7 @@ export function createConfigCommand(deps?: Partial<ConfigCommandDeps>, apiDeps?:
|
|||||||
mcpServers: {
|
mcpServers: {
|
||||||
[opts.project]: {
|
[opts.project]: {
|
||||||
command: 'mcpctl',
|
command: 'mcpctl',
|
||||||
args: ['mcp', '-p', opts.project],
|
args: ['mcp', '--project', opts.project],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -150,8 +150,11 @@ export interface McpCommandDeps {
|
|||||||
export function createMcpCommand(deps: McpCommandDeps): Command {
|
export function createMcpCommand(deps: McpCommandDeps): Command {
|
||||||
const cmd = new Command('mcp')
|
const cmd = new Command('mcp')
|
||||||
.description('MCP STDIO transport bridge — connects stdin/stdout to a project MCP endpoint')
|
.description('MCP STDIO transport bridge — connects stdin/stdout to a project MCP endpoint')
|
||||||
.action(async () => {
|
.passThroughOptions()
|
||||||
const projectName = deps.getProject();
|
.option('-p, --project <name>', '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) {
|
if (!projectName) {
|
||||||
process.stderr.write('Error: --project is required for the mcp command\n');
|
process.stderr.write('Error: --project is required for the mcp command\n');
|
||||||
process.exitCode = 1;
|
process.exitCode = 1;
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ describe('config claude', () => {
|
|||||||
const written = JSON.parse(readFileSync(outPath, 'utf-8'));
|
const written = JSON.parse(readFileSync(outPath, 'utf-8'));
|
||||||
expect(written.mcpServers['homeautomation']).toEqual({
|
expect(written.mcpServers['homeautomation']).toEqual({
|
||||||
command: 'mcpctl',
|
command: 'mcpctl',
|
||||||
args: ['mcp', '-p', 'homeautomation'],
|
args: ['mcp', '--project', 'homeautomation'],
|
||||||
});
|
});
|
||||||
expect(output.join('\n')).toContain('1 server(s)');
|
expect(output.join('\n')).toContain('1 server(s)');
|
||||||
});
|
});
|
||||||
@@ -60,7 +60,7 @@ describe('config claude', () => {
|
|||||||
const parsed = JSON.parse(output[0]);
|
const parsed = JSON.parse(output[0]);
|
||||||
expect(parsed.mcpServers['myproj']).toEqual({
|
expect(parsed.mcpServers['myproj']).toEqual({
|
||||||
command: 'mcpctl',
|
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['existing--server']).toBeDefined();
|
||||||
expect(written.mcpServers['proj-1']).toEqual({
|
expect(written.mcpServers['proj-1']).toEqual({
|
||||||
command: 'mcpctl',
|
command: 'mcpctl',
|
||||||
args: ['mcp', '-p', 'proj-1'],
|
args: ['mcp', '--project', 'proj-1'],
|
||||||
});
|
});
|
||||||
expect(output.join('\n')).toContain('2 server(s)');
|
expect(output.join('\n')).toContain('2 server(s)');
|
||||||
});
|
});
|
||||||
@@ -96,7 +96,7 @@ describe('config claude', () => {
|
|||||||
const written = JSON.parse(readFileSync(outPath, 'utf-8'));
|
const written = JSON.parse(readFileSync(outPath, 'utf-8'));
|
||||||
expect(written.mcpServers['proj-1']).toEqual({
|
expect(written.mcpServers['proj-1']).toEqual({
|
||||||
command: 'mcpctl',
|
command: 'mcpctl',
|
||||||
args: ['mcp', '-p', 'proj-1'],
|
args: ['mcp', '--project', 'proj-1'],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
||||||
import http from 'node:http';
|
import http from 'node:http';
|
||||||
import { Readable, Writable } from 'node:stream';
|
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) ----
|
// ---- Mock MCP server (simulates mcplocal project endpoint) ----
|
||||||
|
|
||||||
@@ -412,3 +412,40 @@ describe('MCP STDIO Bridge', () => {
|
|||||||
expect(post?.url).toBe('/projects/my%20project/mcp');
|
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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user