This directory contains the test suite for mcpc. There are two kinds of tests: unit and end-to-end (e2e). Unit tests are self-contained and don't need any configurations. However, e2e tests need a local setup - see E2E Setup for details.
npm test # Run all tests (unit + e2e)
npm run test:unit # Run unit tests only
npm run test:e2e # Run e2e tests only
npm run test:coverage # Run all tests with coverage and merge reports
npm run test:coverage:unit # Run unit tests with coverage
npm run test:coverage:e2e # Run e2e tests with coverageUnit tests use Vitest with TypeScript and live in test/unit/. They test individual modules in isolation with mocked dependencies. Vitest's API is Jest-compatible (describe/it/expect/vi.fn/vi.mock) and runs ESM natively, so there are no transformIgnorePatterns for pure-ESM dependencies like chalk and ora.
npm run test:unit # Run once
npm run test:watch # Watch mode - rerun on changes
npm run test:coverage:unit # Generate coverage reportCoverage reports are generated to test/coverage/unit/:
test/coverage/unit/lcov-report/index.html- HTML report (open in browser)test/coverage/unit/lcov.info- LCOV format (for CI integration)
Coverage thresholds are enforced at 70% for branches, functions, lines, and statements.
Unit test coverage measures the TypeScript source files directly via Vitest's v8 coverage provider (@vitest/coverage-v8).
Create files in test/unit/ matching the source structure with .test.ts extension:
// test/unit/lib/utils.test.ts
import { someFunction } from '../../../src/lib/utils.js';
describe('someFunction', () => {
it('should handle normal input', () => {
expect(someFunction('input')).toBe('expected');
});
it('should throw on invalid input', () => {
expect(() => someFunction(null)).toThrow('Invalid input');
});
});test/unit/
├── cli/ # CLI module tests
│ ├── index.test.ts
│ ├── output.test.ts
│ ├── parser.test.ts
│ └── shell.test.ts
├── core/ # Core module tests
│ ├── factory.test.ts
│ └── transports.test.ts
└── lib/ # Library tests
├── config.test.ts
├── errors.test.ts
├── logger.test.ts
└── utils.test.ts
End-to-end tests verify mcpc behavior from the command line, testing real MCP connections with session management, bridge processes, and various transports.
In order to test OAuth with real-world, remote MCP server, you need to run the following commands locally:
mcpc mcp.apify.com login --profile e2e-test1mcpc mcp.apify.com login --profile e2e-test2
To do that, you'll need to create a free Apify account - you can use the same one for both profiles. The test runner should not take any destructive action in your Apify account.
# Run all tests (parallel by default)
./test/e2e/run.sh
# Run a specific test suite
./test/e2e/run.sh basic/
./test/e2e/run.sh sessions/
# Run a specific test
./test/e2e/run.sh basic/help.test.sh
# Options
./test/e2e/run.sh -p, --parallel N # Max parallel tests (default: 16)
./test/e2e/run.sh -i, --isolated # Isolated home dirs for all tests (troubleshooting)
./test/e2e/run.sh -c, --coverage # Collect code coverage
./test/e2e/run.sh -b, --no-build # Skip building mcpc (assumes dist/ is up to date)
./test/e2e/run.sh -v, --verbose # Verbose - show output as tests run
./test/e2e/run.sh -k, --keep # Keep test run directory after tests
./test/e2e/run.sh -l, --list # List available tests
./test/e2e/run.sh -h, --help # Show helpE2E tests can also collect code coverage using Node.js V8 coverage:
npm run test:coverage:e2e # Run E2E tests with coverage
./test/e2e/run.sh --coverage # Same as aboveCoverage reports are generated to test/coverage/e2e/:
test/coverage/e2e/index.html- HTML reporttest/coverage/e2e/lcov.info- LCOV format
E2E coverage uses V8's built-in coverage (via c8), which traces the compiled JavaScript and maps back to TypeScript via source maps. This provides real coverage data from actual CLI usage, complementing unit test coverage.
To generate a merged coverage report from both unit and E2E tests:
# Run all tests with coverage and merge
npm run test:coverage
# Or run individually then merge
npm run test:coverage:unit # Unit tests
npm run test:coverage:e2e # E2E tests
npm run test:coverage:merge # Merge reportsMerged coverage is saved to test/coverage/merged/:
test/coverage/merged/index.html- Combined HTML report (nyc-style)test/coverage/merged/lcov.info- Combined LCOV data (for CI tools like Codecov)
By default, tests share a common home directory per test run to exercise file locking and concurrent access:
- Shared home directory: Tests share
MCPC_HOME_DIRto test file synchronization - Unique session names: Generated using
session_name "suffix"to avoid conflicts - Automatic cleanup: Sessions are closed and shared home cleaned after test run
- Parallel-safe: Tests run concurrently thanks to file locking in mcpc
For tests that directly manipulate home directory files, use isolated mode:
# In test file:
test_init "suite/test-name" --isolated # This test gets its own home directoryTo force all tests to use isolated home directories (for troubleshooting):
./test/e2e/run.sh --isolated # All tests get their own home directories
./test/e2e/run.sh --isolated -p 1 # Isolated + sequential for debuggingThe framework enforces these invariants via run_xmcpc:
--verboseonly adds to stderr, never changes stdout (checked for both bare and--jsonmodes)--jsonalways returns valid JSON (on success to stdout, otherwise to stderr)
Note that the invariants don't apply on invalid argument errors, as those take precedence.
- Create a file in the appropriate suite directory with
.test.shextension:
#!/bin/bash
# Test: Description of what this test covers
source "$(dirname "$0")/../../lib/framework.sh"
test_init "suite/test-name"
# Test case 1
test_case "description of what we're testing"
run_mcpc --help
assert_success
assert_contains "$STDOUT" "Usage:"
test_pass
# Test case 2 with session
test_case "create and use a session"
SESSION=$(session_name "mysession")
run_mcpc "$TEST_SERVER_URL" session "$SESSION"
assert_success
_SESSIONS_CREATED+=("$SESSION") # Track for cleanup
run_mcpc "$SESSION" tools-list
assert_success
test_pass
test_done-
Make it executable:
chmod +x test/e2e/suites/mysuite/mytest.test.sh -
Run it:
./test/e2e/run.sh mysuite/mytest.test.sh
test_init "suite/test-name" # Initialize test environment
test_case "description" # Start a test case
test_pass # Mark current case as passed
test_fail "reason" # Mark current case as failed
test_skip "reason" # Skip current case
test_done # Print summary and exitrun_xmcpc <args> # Preferred: run with invariant checks
run_mcpc <args> # Use only for special cases (see below)Always prefer run_xmcpc - it runs the caller's exact command and returns those results,
but also runs all 4 combinations of --json/--verbose to verify invariants:
- bare args
--verboseargs--jsonargs--json --verboseargs
Use run_mcpc only when:
- Command has side effects and cannot be called multiple times (e.g.,
session,close) - Command doesn't support
--jsonoutput (e.g.,--help) - CLI argument errors handled by Commander.js (plain text, not JSON)
- Output contains non-deterministic data (e.g., temp file names, timestamps)
SESSION=$(session_name "suffix") # Generate unique session name
SESSION=$(create_session "$target" "suffix") # Create session and track itassert_success # Exit code should be 0
assert_failure # Exit code should be non-zero
assert_exit_code 2 # Exit code should be exactly 2
assert_contains "$STDOUT" "expected" # String contains substring
assert_not_contains "$STDOUT" "unexpected" # String doesn't contain substring
assert_eq "$actual" "$expected" # Values are equal
assert_not_empty "$value" # Value is not empty
assert_empty "$value" # Value is empty
assert_json_valid "$STDOUT" # Valid JSON
assert_json "$STDOUT" '.field' # JSON field exists/is truthy
assert_json_eq "$STDOUT" '.name' "expected" # JSON field equals value
assert_file_exists "$path" # File exists
assert_file_not_exists "$path" # File doesn't exist
assert_stdout_empty # $STDOUT is empty
assert_stderr_empty # $STDERR is emptystart_test_server # Start test MCP server, sets $TEST_SERVER_URL
start_test_server LATENCY_MS=100 # Start with custom env vars
stop_test_server # Stop test server (automatic on exit)
# Server control endpoints
server_fail_next 3 # Make next 3 requests fail
server_expire_session # Expire current session
server_reset # Reset server stateCONFIG=$(create_fs_config "$path") # Create config for filesystem server
CONFIG=$(create_stdio_config "name" "cmd" "arg1" "arg2") # Create custom stdio configjson_get ".field.path" # Extract field from $STDOUT
wait_for "command" 10 # Wait up to 10s for command to succeed
is_macos # Check if running on macOS
is_linux # Check if running on Linuxtest/
├── e2e/
│ ├── run.sh # Test runner
│ ├── lib/
│ │ └── framework.sh # Testing framework
│ ├── server/
│ │ └── index.ts # Test MCP server
│ └── suites/ # Test suites
│ ├── basic/ # Basic CLI tests
│ │ ├── help.test.sh
│ │ ├── errors.test.sh
│ │ └── output-invariants.test.sh
│ ├── sessions/ # Session management tests
│ │ ├── lifecycle.test.sh
│ │ └── failover.test.sh
│ └── stdio/ # Stdio transport tests
│ └── filesystem.test.sh
└── runs/ # Test run artifacts (auto-cleaned on success)
Each test run creates a timestamped directory with all test artifacts:
test/runs/<run-id>/
├── _shared_home/ # Shared mcpc home directory
│ ├── sessions.json
│ └── profiles.json
├── basic/ # Test suite
│ └── help/ # Individual test
│ ├── result # Exit code (0 = pass)
│ ├── output.log # Test stdout/stderr
│ ├── commands.log # Detailed mcpc command logs
│ ├── server.log # Test server logs (if started)
│ ├── tmp/ # Temp artifacts
│ └── _home/ # Isolated home (if --isolated)
└── v8-coverage/ # Coverage data (if --coverage)
E2E_RUN_ID- Unique ID for the test run (auto-generated)E2E_RUNS_DIR- Directory for test run artifactsMCPC_HOME_DIR- mcpc home directory (set per-test)TEST_TMP- Temp directory for test artifactsTEST_SERVER_URL- URL of test MCP server (when started)TEST_SERVER_PORT- Port for test server (0 = random)