Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
<!-- toc -->

- [[Unreleased]](#unreleased)
- [[2.0.2] - 2026-09-15](#202---2026-09-15)
- [[2.0.1] - 2026-09-09](#201---2026-09-09)
- [[2.0.0] - 2026-09-09](#200---2026-09-09)
- [[2.0.0-rc.2] - 2026-09-07](#200-rc2---2026-09-07)
Expand All @@ -32,6 +33,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [2.0.2] - 2026-09-15

### Fixed

- The dump wizard now launches the generated command without a shell, removing
Node.js `[DEP0190]` warnings and preserving user-supplied arguments without
shell interpretation.

## [2.0.1] - 2026-09-09

### Changed
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
The `mcpcontract` CLI dumps capabilities from live MCP servers, and lets you create changelogs, detect breaking changes, and generate documentation.

[![License: Apache-2.0](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](LICENSE)
[![Status: release](https://img.shields.io/badge/status-2.0.1-brightgreen.svg)](CHANGELOG.md)
[![Status: release](https://img.shields.io/badge/status-2.0.2-brightgreen.svg)](CHANGELOG.md)
[![Node.js: >=22.x](https://img.shields.io/badge/Node.js-%3E%3D22.x-brightgreen.svg)](https://nodejs.org/)
[![mcpdesc: 0.8](https://img.shields.io/badge/mcpdesc-0.8-blue.svg)](https://mcpdesc.org)

Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@cisco_open/mcptoolkit-contract",
"version": "2.0.1",
"version": "2.0.2",
"description": "CLI toolkit for MCP server contract management: dumps capabilities, generates changelogs and renders documentation",
"type": "module",
"main": "./build/index.js",
Expand Down
58 changes: 40 additions & 18 deletions src/commands/wizard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const YELLOW = '\x1b[33m';
const RESET = '\x1b[0m';
const BOLD = '\x1b[1m';

interface WizardAnswers {
export interface WizardAnswers {
transport: 'stdio' | 'http' | 'sse';
serverName: string;
format: 'json' | 'yaml';
Expand Down Expand Up @@ -181,7 +181,7 @@ export async function runWizardInteractive(): Promise<void> {
console.log(`${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}\n`);

const cmdArgs = buildCommandArgs(answers);
const commandLine = `mcpcontract dump ${cmdArgs.join(' ')}`;
const commandLine = formatCommand(cmdArgs);

console.log(`${BOLD}Generated Command:${RESET}`);
console.log(`${YELLOW}${commandLine}${RESET}\n`);
Expand All @@ -206,59 +206,81 @@ export async function runWizardInteractive(): Promise<void> {
/**
* Build command arguments from wizard answers
*/
function buildCommandArgs(answers: WizardAnswers): string[] {
export function buildCommandArgs(answers: WizardAnswers): string[] {
const args: string[] = [
'--server-name', `"${answers.serverName}"`,
'--server-name', answers.serverName,
'--transport', answers.transport === 'http' ? 'streamable-http' : answers.transport,
'--verbose'
];

if (answers.transport === 'stdio') {
args.push('--command', `"${answers.command}"`);
args.push('--command', answers.command ?? '');

if (answers.args) {
// Split args properly - support both comma and space separation
const argParts = answers.args.split(/[\s,]+/).filter(a => a.length > 0);
if (argParts.length > 0) {
args.push('--args');
argParts.forEach(arg => args.push(`"${arg}"`));
args.push(...argParts);
}
}

if (answers.env) {
args.push('--env', `"${answers.env}"`);
args.push('--env', answers.env);
}
} else {
args.push('--url', `"${answers.url}"`);
args.push('--url', answers.url ?? '');
args.push('--auth', answers.auth || 'none');

if (answers.headers) {
const headerList = answers.headers.split('|||');
headerList.forEach(header => {
args.push('-H', `"${header}"`);
args.push('-H', header);
});
}
}

args.push('--format', answers.format);
args.push('--output', `"${answers.output}"`);
args.push('--output', answers.output);

return args;
}

export function formatCommand(args: string[]): string {
const quote = (arg: string): string => {
if (/^[A-Za-z0-9_./:@%+=,-]+$/.test(arg)) {
return arg;
}

return `'${arg.replace(/'/g, `'\\''`)}'`;
};

return ['mcpcontract', 'dump', ...args].map(quote).join(' ');
}

export function buildCommandInvocation(
args: string[],
cliPath = process.argv[1],
nodePath = process.execPath
): { command: string; args: string[]; options: { stdio: 'inherit' } } {
if (!cliPath) {
throw new Error('Unable to determine the mcpcontract executable path');
}

return {
command: nodePath,
args: [cliPath, 'dump', ...args],
options: { stdio: 'inherit' }
};
}

/**
* Execute the dump command
*/
async function executeCommand(args: string[]): Promise<void> {
return new Promise((resolve, reject) => {
// Get the path to the mcpcontract binary
const cliPath = process.argv[1]; // Path to current executable

// Spawn the dump command
const child = spawn(cliPath, ['dump', ...args], {
stdio: 'inherit',
shell: true
});
const invocation = buildCommandInvocation(args);
const child = spawn(invocation.command, invocation.args, invocation.options);

child.on('close', (code) => {
if (code === 0) {
Expand Down
53 changes: 53 additions & 0 deletions tests/unit/wizard.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Copyright 2026 Cisco Systems, Inc. and its affiliates
//
// SPDX-License-Identifier: Apache-2.0

import { describe, expect, it } from '@jest/globals';
import {
buildCommandArgs,
buildCommandInvocation,
formatCommand,
type WizardAnswers
} from '../../src/commands/wizard.js';

describe('dump wizard command execution', () => {
it('keeps user input as raw arguments and quotes only the displayed command', () => {
const answers: WizardAnswers = {
transport: 'http',
serverName: 'Webflow production',
url: 'https://mcp.webflow.com/mcp?source=one&mode=two',
auth: 'auto',
format: 'yaml',
output: 'webflow contract.yaml',
headers: 'Authorization: Bearer token with spaces'
};

const args = buildCommandArgs(answers);

expect(args).toContain('Webflow production');
expect(args).toContain('Authorization: Bearer token with spaces');
expect(args).not.toContain('"Webflow production"');
expect(formatCommand(args)).toContain("'Webflow production'");
expect(formatCommand(args)).toContain("'webflow contract.yaml'");
});

it('spawns Node directly without a shell', () => {
const invocation = buildCommandInvocation(
['--server-name', 'name; echo unsafe'],
'/opt/mcp contract/build/index.js',
'/usr/bin/node'
);

expect(invocation).toEqual({
command: '/usr/bin/node',
args: [
'/opt/mcp contract/build/index.js',
'dump',
'--server-name',
'name; echo unsafe'
],
options: { stdio: 'inherit' }
});
expect(invocation.options).not.toHaveProperty('shell');
});
});