|
| 1 | +#!/usr/bin/env node |
| 2 | + |
| 3 | +// Adapter that drives the mcpc CLI from the |
| 4 | +// `@modelcontextprotocol/conformance` framework. |
| 5 | +// |
| 6 | +// The framework invokes this script with the test server URL appended as the |
| 7 | +// last positional argument and sets `MCP_CONFORMANCE_SCENARIO` (plus an |
| 8 | +// optional `MCP_CONFORMANCE_CONTEXT` JSON blob) in the environment. Per |
| 9 | +// scenario, we translate the expected behaviour into one or more mcpc |
| 10 | +// sub-commands against a freshly-created session, then tear the session down |
| 11 | +// again. |
| 12 | +// |
| 13 | +// Only a small set of scenarios is wired up today; unsupported ones exit |
| 14 | +// non-zero so the framework records them as failures (track them in |
| 15 | +// `test/conformance/expected-failures.yml` to keep CI green until coverage |
| 16 | +// grows). |
| 17 | + |
| 18 | +import { spawn } from 'node:child_process'; |
| 19 | +import { mkdtemp, rm } from 'node:fs/promises'; |
| 20 | +import { tmpdir } from 'node:os'; |
| 21 | +import { fileURLToPath } from 'node:url'; |
| 22 | +import { dirname, resolve } from 'node:path'; |
| 23 | + |
| 24 | +const scenario = process.env.MCP_CONFORMANCE_SCENARIO; |
| 25 | +const serverUrl = process.argv[process.argv.length - 1]; |
| 26 | + |
| 27 | +if (!scenario) { |
| 28 | + console.error('MCP_CONFORMANCE_SCENARIO environment variable is not set'); |
| 29 | + process.exit(1); |
| 30 | +} |
| 31 | +if (!serverUrl || !/^https?:\/\//i.test(serverUrl)) { |
| 32 | + console.error(`Missing or invalid server URL (got: ${serverUrl ?? ''})`); |
| 33 | + process.exit(1); |
| 34 | +} |
| 35 | + |
| 36 | +const here = dirname(fileURLToPath(import.meta.url)); |
| 37 | +const mcpcBin = resolve(here, '..', '..', 'bin', 'mcpc'); |
| 38 | +const homeDir = await mkdtemp(`${tmpdir()}/mcpc-conformance-`); |
| 39 | +const sessionName = `conformance-${process.pid}`; |
| 40 | +const env = { ...process.env, MCPC_HOME_DIR: homeDir, MCPC_JSON: '1' }; |
| 41 | + |
| 42 | +function runMcpc(args) { |
| 43 | + return new Promise((res, rej) => { |
| 44 | + const child = spawn(mcpcBin, args, { env, stdio: 'inherit' }); |
| 45 | + child.once('error', rej); |
| 46 | + child.once('exit', (code) => { |
| 47 | + if (code === 0) res(); |
| 48 | + else rej(new Error(`mcpc ${args.join(' ')} exited with code ${code}`)); |
| 49 | + }); |
| 50 | + }); |
| 51 | +} |
| 52 | + |
| 53 | +async function cleanup() { |
| 54 | + try { |
| 55 | + await runMcpc([`@${sessionName}`, 'close']); |
| 56 | + } catch { |
| 57 | + // Best effort — the session may never have been created. |
| 58 | + } |
| 59 | + await rm(homeDir, { recursive: true, force: true }).catch(() => {}); |
| 60 | +} |
| 61 | + |
| 62 | +async function main() { |
| 63 | + switch (scenario) { |
| 64 | + case 'initialize': |
| 65 | + // Connecting triggers the full MCP initialize handshake via the |
| 66 | + // bridge process. That is all the conformance server needs to |
| 67 | + // observe for this scenario. |
| 68 | + await runMcpc(['connect', serverUrl, `@${sessionName}`]); |
| 69 | + return; |
| 70 | + |
| 71 | + case 'tools-call': |
| 72 | + await runMcpc(['connect', serverUrl, `@${sessionName}`]); |
| 73 | + await runMcpc([`@${sessionName}`, 'tools-list']); |
| 74 | + return; |
| 75 | + |
| 76 | + default: |
| 77 | + console.error(`Scenario not implemented by mcpc conformance adapter: ${scenario}`); |
| 78 | + process.exit(1); |
| 79 | + } |
| 80 | +} |
| 81 | + |
| 82 | +try { |
| 83 | + await main(); |
| 84 | +} catch (err) { |
| 85 | + console.error(err instanceof Error ? err.message : String(err)); |
| 86 | + process.exitCode = 1; |
| 87 | +} finally { |
| 88 | + await cleanup(); |
| 89 | +} |
0 commit comments