From 4faf478b1587ef282213a5347e505fdb21da3f85 Mon Sep 17 00:00:00 2001 From: Steve Sfartz Date: Tue, 15 Sep 2026 07:16:25 +0200 Subject: [PATCH] Release v2.0.2 Signed-off-by: Steve Sfartz --- CHANGELOG.md | 9 ++++++ README.md | 2 +- package-lock.json | 4 +-- package.json | 2 +- src/commands/wizard.ts | 58 +++++++++++++++++++++++++++------------ tests/unit/wizard.test.ts | 53 +++++++++++++++++++++++++++++++++++ 6 files changed, 106 insertions(+), 22 deletions(-) create mode 100644 tests/unit/wizard.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 59e21c6..46e2255 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [[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) @@ -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 diff --git a/README.md b/README.md index 498666f..eaa61e6 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/package-lock.json b/package-lock.json index 098657e..61a0e12 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@cisco_open/mcptoolkit-contract", - "version": "2.0.1", + "version": "2.0.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@cisco_open/mcptoolkit-contract", - "version": "2.0.1", + "version": "2.0.2", "license": "Apache-2.0", "dependencies": { "@inquirer/prompts": "^8.5.2", diff --git a/package.json b/package.json index d7feff6..c106b66 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/commands/wizard.ts b/src/commands/wizard.ts index fad908c..7372169 100644 --- a/src/commands/wizard.ts +++ b/src/commands/wizard.ts @@ -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'; @@ -181,7 +181,7 @@ export async function runWizardInteractive(): Promise { 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`); @@ -206,59 +206,81 @@ export async function runWizardInteractive(): Promise { /** * 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 { 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) { diff --git a/tests/unit/wizard.test.ts b/tests/unit/wizard.test.ts new file mode 100644 index 0000000..fff172c --- /dev/null +++ b/tests/unit/wizard.test.ts @@ -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'); + }); +}); \ No newline at end of file