mcpdrift-cli 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +428 -0
  3. package/dist/approver.d.ts +8 -0
  4. package/dist/approver.d.ts.map +1 -0
  5. package/dist/approver.js +90 -0
  6. package/dist/approver.js.map +1 -0
  7. package/dist/cli.d.ts +3 -0
  8. package/dist/cli.d.ts.map +1 -0
  9. package/dist/cli.js +103 -0
  10. package/dist/cli.js.map +1 -0
  11. package/dist/client.d.ts +23 -0
  12. package/dist/client.d.ts.map +1 -0
  13. package/dist/client.js +148 -0
  14. package/dist/client.js.map +1 -0
  15. package/dist/config.d.ts +12 -0
  16. package/dist/config.d.ts.map +1 -0
  17. package/dist/config.js +123 -0
  18. package/dist/config.js.map +1 -0
  19. package/dist/differ.d.ts +11 -0
  20. package/dist/differ.d.ts.map +1 -0
  21. package/dist/differ.js +57 -0
  22. package/dist/differ.js.map +1 -0
  23. package/dist/fsUtil.d.ts +15 -0
  24. package/dist/fsUtil.d.ts.map +1 -0
  25. package/dist/fsUtil.js +70 -0
  26. package/dist/fsUtil.js.map +1 -0
  27. package/dist/index.d.ts +10 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +10 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/initializer.d.ts +10 -0
  32. package/dist/initializer.d.ts.map +1 -0
  33. package/dist/initializer.js +150 -0
  34. package/dist/initializer.js.map +1 -0
  35. package/dist/jsonPointer.d.ts +31 -0
  36. package/dist/jsonPointer.d.ts.map +1 -0
  37. package/dist/jsonPointer.js +116 -0
  38. package/dist/jsonPointer.js.map +1 -0
  39. package/dist/runner.d.ts +13 -0
  40. package/dist/runner.d.ts.map +1 -0
  41. package/dist/runner.js +283 -0
  42. package/dist/runner.js.map +1 -0
  43. package/dist/types.d.ts +66 -0
  44. package/dist/types.d.ts.map +1 -0
  45. package/dist/types.js +2 -0
  46. package/dist/types.js.map +1 -0
  47. package/package.json +58 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,428 @@
1
+ # mcpdrift ๐Ÿงช
2
+
3
+ > Dependable CI/CD regression tester and snapshot assertion tool for Model Context Protocol (MCP) servers.
4
+
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
+ [![Node.js](https://img.shields.io/badge/node-%3E%3D18.0.0-brightgreen.svg)](https://nodejs.org)
7
+
8
+ `mcpdrift` prevents unintended breaking changes and response drift in MCP servers. It executes defined tool calls over `stdio` via the official `@modelcontextprotocol/sdk`, captures response snapshots, and fails your CI pipeline with clear, colorized line-by-line diffs when unexpected changes occur.
9
+
10
+ ---
11
+
12
+ ## Table of Contents
13
+
14
+ - [What is mcpdrift?](#what-is-mcpdrift)
15
+ - [Important Concepts & Guarantees](#important-concepts--guarantees)
16
+ - [Quickstart (5-Minute Walkthrough)](#quickstart-5-minute-walkthrough)
17
+ - [Configuration Reference (`mcpdrift.config.json`)](#configuration-reference-mcpdriftconfigjson)
18
+ - [Multiple Test Cases Per Tool](#multiple-test-cases-per-tool)
19
+ - [Targeted Field Ignoring (RFC 6901 JSON Pointer)](#targeted-field-ignoring-rfc-6901-json-pointer)
20
+ - [CLI Command Reference](#cli-command-reference)
21
+ - [`mcpdrift run`](#mcpdrift-run)
22
+ - [`mcpdrift approve`](#mcpdrift-approve)
23
+ - [`mcpdrift init`](#mcpdrift-init)
24
+ - [CI/CD Integration](#cicd-integration)
25
+ - [Machine-Readable Reports](#machine-readable-reports)
26
+ - [Migration Guide: Single-Tool to Multi-Case IDs](#migration-guide-single-tool-to-multi-case-ids)
27
+ - [Known Limitations](#known-limitations)
28
+ - [License](#license)
29
+
30
+ ---
31
+
32
+ ## What is mcpdrift?
33
+
34
+ Model Context Protocol (MCP) servers expose tools to AI agents and LLMs. When developers refactor server code, update dependencies, or adjust prompt/response serialization, tool response schemas can inadvertently drift.
35
+
36
+ `mcpdrift` acts like Jest/Vitest snapshot testing, but specifically tailored for MCP servers:
37
+ 1. Spawns your server as a child process via `serverCommand`.
38
+ 2. Connects over `stdio` using `@modelcontextprotocol/sdk`.
39
+ 3. Calls each configured tool with deterministic test inputs.
40
+ 4. Compares responses against committed baseline snapshots under `.mcpdrift/snapshots/<case-id>.json`.
41
+ 5. Exits `0` when identical, or exits `1` with a line-by-line diff if unexpected changes or errors occur.
42
+
43
+ ---
44
+
45
+ ## Important Concepts & Guarantees
46
+
47
+ - **Differences do not automatically mean bugs**: A detected drift indicates the server response changed from the saved baseline. If the change was deliberate, review the diff and run `mcpdrift approve <case-id>` to accept it.
48
+ - **Passing snapshots do not prove correctness**: A snapshot only proves that the current output matches the baseline. If your baseline contained a defect, `mcpdrift` will preserve that defect until updated.
49
+ - **Dynamic data requires controlled fixtures or narrow ignore rules**: Fields like timestamps, UUIDs, trace tokens, or ephemeral counters will drift every run. Use targeted `ignore` rules (RFC 6901 JSON Pointers) or configure deterministic server test seeds.
50
+ - **MCP tools may have side effects**: Tools that delete files, invoke external APIs, or charge accounts will trigger those actions during testing. **Always point `mcpdrift` to local, sandboxed test servers or read-only mock environments.**
51
+ - **Approval executes tools again**: When you run `mcpdrift approve`, `mcpdrift` re-runs the tool against your server and overwrites the snapshot file with the newly returned output. If the tool fails or returns `isError: true`, approval is automatically aborted and your existing baseline is preserved.
52
+ - **CI checks block PRs by policy**: `mcpdrift run --ci` returns exit code `1` on drift, missing baselines, or tool failures. Configure your repository branch protection rules to require this check before merging.
53
+
54
+ ---
55
+
56
+ ## Quickstart
57
+
58
+ ### A. Consumer Quickstart (For Your MCP Server Project)
59
+
60
+ In your MCP project directory:
61
+
62
+ #### 1. Installation
63
+
64
+ Run directly without installation, or add it to your project:
65
+
66
+ ```bash
67
+ # Run directly:
68
+ npx mcpdrift-cli --help
69
+
70
+ # Or install as a project devDependency:
71
+ npm install --save-dev mcpdrift-cli
72
+ ```
73
+
74
+ #### 2. Initialize Configuration
75
+ Connect to your running MCP server to inspect its available tools. To prevent unintended execution of tools with side effects, explicit tool selection is required:
76
+
77
+ ```bash
78
+ # Explicitly select tools by name:
79
+ npx mcpdrift-cli init --server "node dist/server.js" --tools getWeather,searchDocs
80
+
81
+ # Or explicitly include all discovered tools:
82
+ npx mcpdrift-cli init --server "node dist/server.js" --all-tools
83
+
84
+ # Or generate a standalone starter template without connecting to a server:
85
+ npx mcpdrift-cli init --template
86
+ ```
87
+
88
+ This creates `mcpdrift.config.json` in your project root with inferred parameter placeholders.
89
+
90
+ #### 3. Review and Customize Inputs
91
+ Open `mcpdrift.config.json` and adjust inputs to provide deterministic, safe test values:
92
+
93
+ ```json
94
+ {
95
+ "serverCommand": "node dist/server.js",
96
+ "testCases": [
97
+ {
98
+ "id": "weather_addis",
99
+ "tool": "getWeather",
100
+ "input": { "city": "Addis Ababa" }
101
+ },
102
+ {
103
+ "id": "docs_pricing",
104
+ "tool": "searchDocs",
105
+ "input": { "query": "pricing" }
106
+ }
107
+ ]
108
+ }
109
+ ```
110
+
111
+ #### 4. Create Local Baseline Snapshots
112
+ Run tests in local development mode:
113
+
114
+ ```bash
115
+ npx mcpdrift-cli run
116
+ ```
117
+
118
+ This creates baseline response snapshots under `.mcpdrift/snapshots/<case-id>.json`. Commit `.mcpdrift/snapshots/` to Git.
119
+
120
+ #### 5. Run Strict CI Checks
121
+ In CI pipelines (or with `--ci`), `mcpdrift` asserts that all snapshots exist and match:
122
+
123
+ ```bash
124
+ npx mcpdrift-cli run --ci
125
+ ```
126
+
127
+
128
+ ---
129
+
130
+ ### B. Contributor Quickstart (Within the mcpdrift Repository)
131
+
132
+ If you are developing or testing `mcpdrift` directly from source:
133
+
134
+ 1. Clone the repository and build:
135
+ ```bash
136
+ git clone <repo-url>
137
+ cd mcpdrift
138
+ npm ci
139
+ npm run build
140
+ ```
141
+
142
+ 2. Test against the bundled fixture server:
143
+ ```bash
144
+ node dist/cli.js init --server "node examples/sample-server/server.js" --all-tools --output my-test.config.json
145
+ node dist/cli.js run -c my-test.config.json
146
+ ```
147
+
148
+ 3. Run the automated offline test suite:
149
+ ```bash
150
+ npm test
151
+ ```
152
+
153
+ > **Network Access Note**:
154
+ > The automated test suite (`npm test`, covering diffing, pointer rules, config validation, safety, and CLI execution) is **100% offline and deterministic**; it never calls external APIs or npm registries. Installing `mcpdrift` as a consumer via `npm install` requires normal npm registry network access to fetch declared dependencies.
155
+
156
+ โœจ All MCP regression checks passed!
157
+ ```
158
+
159
+ ---
160
+
161
+ ## Configuration Reference (`mcpdrift.config.json`)
162
+
163
+ ```json
164
+ {
165
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
166
+ "serverCommand": "node dist/server.js",
167
+ "timeout": 15000,
168
+ "snapshotDir": ".mcpdrift/snapshots",
169
+ "env": {
170
+ "NODE_ENV": "test"
171
+ },
172
+ "testCases": [
173
+ {
174
+ "id": "weather_cold",
175
+ "tool": "getWeather",
176
+ "input": { "city": "Oslo" },
177
+ "timeout": 5000,
178
+ "ignore": [
179
+ "/meta/timestamp",
180
+ "/meta/traceId"
181
+ ]
182
+ },
183
+ {
184
+ "id": "weather_warm",
185
+ "tool": "getWeather",
186
+ "input": { "city": "Nairobi" }
187
+ }
188
+ ]
189
+ }
190
+ ```
191
+
192
+ ### Multiple Test Cases Per Tool
193
+
194
+ You can test the same tool with varying inputs, edge cases, or error parameters. To avoid snapshot collisions, each test case must specify a unique, filesystem-safe `id`.
195
+
196
+ - Valid characters for `id`: alphanumeric, `_`, `-`, `.`.
197
+ - Path traversal sequences (`..`, `/`, `\`) and special characters (`< > : " | ? *`) are strictly prohibited and rejected by config validation.
198
+
199
+ ### Targeted Field Ignoring (RFC 6901 JSON Pointer)
200
+
201
+ When responses contain dynamic values (e.g., timestamps, session IDs, tokens), specify an array of RFC 6901 JSON Pointers in `ignore`:
202
+
203
+ ```json
204
+ {
205
+ "id": "user_profile",
206
+ "tool": "getUser",
207
+ "input": { "userId": 42 },
208
+ "ignore": [
209
+ "/lastLoginAt",
210
+ "/session/token",
211
+ "/tags/0/createdAt"
212
+ ]
213
+ }
214
+ ```
215
+
216
+ - **Standards-compliant**: Uses standard JSON Pointer syntax starting with `/`. Tokens with `/` are escaped as `~1`, and `~` as `~0`.
217
+ - **Immutable normalization**: `mcpdrift` normalizes copies of both baseline and actual responses without mutating the original output.
218
+ - **Array order preserved**: Array lengths and index positions are preserved.
219
+ - **Missing path safety**: If an ignored path does not exist in a given response, it is safely skipped without failing.
220
+ - **Meaningful changes still caught**: Any modification to unignored fields will immediately trigger a drift failure.
221
+
222
+ ---
223
+
224
+ ## CLI Command Reference
225
+
226
+ ### `mcpdrift run`
227
+
228
+ Runs all configured test cases against the MCP server.
229
+
230
+ ```bash
231
+ mcpdrift run [options]
232
+
233
+ Options:
234
+ -c, --config <path> Path to config file (default: "mcpdrift.config.json")
235
+ --ci Strict CI mode: fails if any baseline is missing, never writes snapshots
236
+ --case <id> Run only a specific test case by ID
237
+ -t, --tool <name> Run all test cases configured for a specific tool
238
+ --report <path> Write a machine-readable JSON report
239
+ --json-report <path> Alias for --report
240
+ --timeout <ms> Override tool execution timeout in milliseconds
241
+ -h, --help Display command help
242
+ ```
243
+
244
+ ### `mcpdrift approve`
245
+
246
+ Overwrites baseline snapshot(s) with the latest server response.
247
+
248
+ ```bash
249
+ # Approve a specific test case by ID:
250
+ mcpdrift approve weather_addis
251
+
252
+ # Approve only a specific test case explicitly:
253
+ mcpdrift approve --case weather_addis
254
+
255
+ # Approve all test cases for a specific tool:
256
+ mcpdrift approve --tool getWeather
257
+
258
+ # Approve all test cases in the entire suite:
259
+ mcpdrift approve --all
260
+ ```
261
+
262
+ > **Safety Guarantee**: If the tool call throws an exception, times out, or returns `isError: true`, the approval operation is aborted and your existing baseline snapshot remains untouched.
263
+
264
+ ### `mcpdrift init`
265
+
266
+ Inspects a running MCP server and generates a starter `mcpdrift.config.json`.
267
+
268
+ ```bash
269
+ # Inspect running server:
270
+ mcpdrift init --server "node dist/server.js"
271
+
272
+ # Filter to specific tools:
273
+ mcpdrift init --server "node dist/server.js" --tools ping,getWeather
274
+
275
+ # Overwrite existing config:
276
+ mcpdrift init --server "node dist/server.js" --force
277
+
278
+ # Generate standalone template without connecting to a server:
279
+ mcpdrift init --template
280
+ ```
281
+
282
+ > **Discovery Safety**: `mcpdrift init` issues only `tools/list` over stdio and **never** calls `tools/call`. Inferred inputs are placeholders and must be reviewed.
283
+
284
+ ---
285
+
286
+ ## CI/CD Integration
287
+
288
+ `mcpdrift` is designed from the ground up for automated pipelines.
289
+
290
+ ### Automatic CI Detection
291
+
292
+ `mcpdrift` automatically activates strict CI mode when:
293
+ - Passed the `--ci` CLI flag, OR
294
+ - The `CI=true` or `CI=1` environment variable is detected (standard in GitHub Actions, GitLab CI, CircleCI, Jenkins).
295
+
296
+ In strict CI mode:
297
+ - Any missing baseline snapshot causes an immediate failure (`exit code 1`).
298
+ - No snapshots are written to disk during the CI run.
299
+ - Changed responses or tool errors fail the build with line-by-line diffs.
300
+
301
+ ### Example GitHub Actions Workflow
302
+
303
+ Create `.github/workflows/mcpdrift.yml`:
304
+
305
+ ```yaml
306
+ name: MCP Server Regression Tests
307
+
308
+ on:
309
+ pull_request:
310
+ branches: [main, master]
311
+ push:
312
+ branches: [main, master]
313
+
314
+ jobs:
315
+ test:
316
+ name: MCP Regression (${{ matrix.os }})
317
+ runs-on: ${{ matrix.os }}
318
+ strategy:
319
+ matrix:
320
+ os: [ubuntu-latest, windows-latest]
321
+
322
+ steps:
323
+ - name: Checkout repository
324
+ uses: actions/checkout@v4
325
+
326
+ - name: Setup Node.js
327
+ uses: actions/setup-node@v4
328
+ with:
329
+ node-version: '20'
330
+ cache: 'npm'
331
+
332
+ - name: Install dependencies
333
+ run: npm ci
334
+
335
+ - name: Build server
336
+ run: npm run build --if-present
337
+
338
+ - name: Run strict regression check
339
+ run: npx mcpdrift-cli run --ci
340
+ ```
341
+
342
+ ---
343
+
344
+ ## Machine-Readable Reports
345
+
346
+ Generate machine-readable JSON reports for CI dashboard integration, test summarizers, or compliance logs:
347
+
348
+ ```bash
349
+ npx mcpdrift-cli run --report .mcpdrift/report.json
350
+ ```
351
+
352
+
353
+ Sample JSON report output (clean JSON, no ANSI escape codes, no leaked secrets):
354
+
355
+ ```json
356
+ {
357
+ "timestamp": "2026-09-12T18:35:00.000Z",
358
+ "serverCommand": "node dist/server.js",
359
+ "ci": true,
360
+ "summary": {
361
+ "total": 2,
362
+ "passed": 1,
363
+ "changed": 1,
364
+ "failed": 0,
365
+ "baselineCreated": 0,
366
+ "missingBaseline": 0,
367
+ "success": false
368
+ },
369
+ "results": [
370
+ {
371
+ "id": "weather_addis",
372
+ "tool": "getWeather",
373
+ "status": "PASS",
374
+ "durationMs": 12,
375
+ "snapshotPath": "C:\\repo\\.mcpdrift\\snapshots\\weather_addis.json"
376
+ },
377
+ {
378
+ "id": "docs_pricing",
379
+ "tool": "searchDocs",
380
+ "status": "CHANGED",
381
+ "failureCategory": "SNAPSHOT_MISMATCH",
382
+ "durationMs": 15,
383
+ "snapshotPath": "C:\\repo\\.mcpdrift\\snapshots\\docs_pricing.json"
384
+ }
385
+ ]
386
+ }
387
+ ```
388
+
389
+ ---
390
+
391
+ ## Migration Guide: Single-Tool to Multi-Case IDs
392
+
393
+ In earlier versions of `mcpdrift`, test cases did not require an `id` property, and snapshots were saved as `.mcpdrift/snapshots/<tool-name>.json`.
394
+
395
+ ### Backward Compatibility
396
+ If your configuration contains only **one** test case per tool name, `mcpdrift` maintains 100% backward compatibility:
397
+ - The test case automatically inherits `id: "<tool-name>"`.
398
+ - Existing snapshots at `.mcpdrift/snapshots/<tool-name>.json` will load and pass without change.
399
+
400
+ ### Upgrading to Multiple Cases
401
+ When you want multiple test cases for the same tool:
402
+ 1. Add an explicit, unique `id` to each test case in `mcpdrift.config.json`:
403
+ ```json
404
+ {
405
+ "testCases": [
406
+ { "id": "getWeather_city_1", "tool": "getWeather", "input": { "city": "Addis Ababa" } },
407
+ { "id": "getWeather_city_2", "tool": "getWeather", "input": { "city": "Tokyo" } }
408
+ ]
409
+ }
410
+ ```
411
+ 2. Rename your existing snapshot file to match the new `id`:
412
+ - e.g., rename `.mcpdrift/snapshots/getWeather.json` to `.mcpdrift/snapshots/getWeather_city_1.json`.
413
+ 3. Run `npx mcpdrift-cli run` to generate the new baseline for `getWeather_city_2.json`.
414
+ 4. Commit the new snapshots. `mcpdrift` never silently deletes or ambiguously migrates old snapshot files.
415
+
416
+ ---
417
+
418
+ ## Known Limitations
419
+
420
+ 1. **Stdio Only**: `mcpdrift` currently connects to MCP servers over standard I/O (`stdio`). SSE (Server-Sent Events) and custom HTTP transports are planned for future releases.
421
+ 2. **Deterministic Inputs Required**: If your MCP server relies on external third-party live APIs that return uncontrolled live data, responses will drift unless mocked or covered by `ignore` rules.
422
+ 3. **Re-execution on Approval**: `mcpdrift approve` re-executes the tool against the active server to capture the latest response rather than re-using the cached run failure.
423
+
424
+ ---
425
+
426
+ ## License
427
+
428
+ [MIT](LICENSE)
@@ -0,0 +1,8 @@
1
+ export interface ApproveOptions {
2
+ config?: string;
3
+ caseId?: string;
4
+ tool?: string;
5
+ all?: boolean;
6
+ }
7
+ export declare function approveTool(target?: string, options?: ApproveOptions): Promise<void>;
8
+ //# sourceMappingURL=approver.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"approver.d.ts","sourceRoot":"","sources":["../src/approver.ts"],"names":[],"mappings":"AASA,MAAM,WAAW,cAAc;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,OAAO,CAAC;CACf;AAED,wBAAsB,WAAW,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,OAAO,GAAE,cAAmB,GAAG,OAAO,CAAC,IAAI,CAAC,CAwG9F"}
@@ -0,0 +1,90 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import chalk from 'chalk';
4
+ import { loadConfig, getSnapshotPath } from './config.js';
5
+ import { createMcpSession, executeToolWithTimeout } from './client.js';
6
+ import { formatJson } from './differ.js';
7
+ import { writeAtomic } from './fsUtil.js';
8
+ export async function approveTool(target, options = {}) {
9
+ const { config, resolvedPath } = loadConfig(options.config);
10
+ const snapshotDir = path.resolve(path.dirname(resolvedPath), config.snapshotDir || path.join('.mcpdrift', 'snapshots'));
11
+ if (!fs.existsSync(snapshotDir)) {
12
+ fs.mkdirSync(snapshotDir, { recursive: true });
13
+ }
14
+ let casesToApprove = [];
15
+ if (options.all) {
16
+ casesToApprove = config.testCases;
17
+ }
18
+ else if (options.caseId) {
19
+ casesToApprove = config.testCases.filter((tc) => (tc.id || tc.tool) === options.caseId);
20
+ if (casesToApprove.length === 0) {
21
+ throw new Error(`Test case with ID "${options.caseId}" was not found in ${resolvedPath}.`);
22
+ }
23
+ }
24
+ else if (options.tool) {
25
+ casesToApprove = config.testCases.filter((tc) => tc.tool === options.tool);
26
+ if (casesToApprove.length === 0) {
27
+ throw new Error(`No test cases found for tool "${options.tool}" in ${resolvedPath}.`);
28
+ }
29
+ }
30
+ else if (target) {
31
+ // Target can match either case ID or tool name
32
+ const matchById = config.testCases.filter((tc) => (tc.id || tc.tool) === target);
33
+ if (matchById.length > 0) {
34
+ casesToApprove = matchById;
35
+ }
36
+ else {
37
+ const matchByTool = config.testCases.filter((tc) => tc.tool === target);
38
+ if (matchByTool.length > 0) {
39
+ casesToApprove = matchByTool;
40
+ }
41
+ else {
42
+ throw new Error(`Target "${target}" matched no test case ID or tool name in ${resolvedPath}.`);
43
+ }
44
+ }
45
+ }
46
+ else {
47
+ throw new Error('Please specify a case ID or tool name to approve (e.g. `mcpdrift approve <case-id>`) or pass `--all`.');
48
+ }
49
+ console.log(chalk.bold(`\n๐Ÿ“ mcpdrift approve: Updating snapshots`));
50
+ console.log(chalk.dim(` Config: ${resolvedPath}`));
51
+ console.log(chalk.dim(` Server: ${config.serverCommand}`));
52
+ console.log(chalk.dim(` Cases to approve: ${casesToApprove.length}\n`));
53
+ const session = await createMcpSession(config.serverCommand, config.env, config.timeout || 15000);
54
+ try {
55
+ for (const testCase of casesToApprove) {
56
+ const caseId = testCase.id || testCase.tool;
57
+ const tool = testCase.tool;
58
+ const snapshotPath = getSnapshotPath(snapshotDir, caseId);
59
+ const testTimeout = testCase.timeout || config.timeout || 15000;
60
+ console.log(chalk.dim(`Calling tool "${tool}" for case "${caseId}" to capture latest response...`));
61
+ let latestResponse;
62
+ try {
63
+ latestResponse = await executeToolWithTimeout(session.client, tool, testCase.input || {}, testTimeout);
64
+ }
65
+ catch (err) {
66
+ console.error(chalk.red.bold(`\nโŒ Failed to approve "${caseId}": Tool threw an execution error.`));
67
+ console.error(chalk.red(` Error: ${err.message}`));
68
+ console.error(chalk.yellow(` Existing baseline snapshot was preserved: ${snapshotPath}\n`));
69
+ throw new Error(`Approval aborted: Tool "${tool}" threw an execution error.`);
70
+ }
71
+ // Check if tool returned error result (isError: true)
72
+ if (latestResponse !== null &&
73
+ typeof latestResponse === 'object' &&
74
+ latestResponse.isError === true) {
75
+ console.error(chalk.red.bold(`\nโŒ Failed to approve "${caseId}": Tool returned an error result (isError: true).`));
76
+ console.error(chalk.red(` Response:\n${formatJson(latestResponse)}`));
77
+ console.error(chalk.yellow(` Existing baseline snapshot was preserved: ${snapshotPath}\n`));
78
+ throw new Error(`Approval aborted: Tool "${tool}" returned an error result (isError: true).`);
79
+ }
80
+ // Write snapshot atomically
81
+ writeAtomic(snapshotPath, formatJson(latestResponse));
82
+ console.log(`${chalk.green.bold('โœ… APPROVED:')} Snapshot for "${chalk.bold(caseId)}" updated at ${chalk.cyan(path.relative(process.cwd(), snapshotPath))}\n`);
83
+ }
84
+ }
85
+ finally {
86
+ await session.close();
87
+ }
88
+ console.log(chalk.green.bold('๐ŸŽ‰ All requested snapshots have been approved!'));
89
+ }
90
+ //# sourceMappingURL=approver.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"approver.js","sourceRoot":"","sources":["../src/approver.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC1D,OAAO,EAAE,gBAAgB,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AACvE,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAU1C,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,MAAe,EAAE,UAA0B,EAAE;IAC7E,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,GAAG,UAAU,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IAE5D,MAAM,WAAW,GAAG,IAAI,CAAC,OAAO,CAC9B,IAAI,CAAC,OAAO,CAAC,YAAY,CAAC,EAC1B,MAAM,CAAC,WAAW,IAAI,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,WAAW,CAAC,CAC1D,CAAC;IACF,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,WAAW,CAAC,EAAE,CAAC;QAChC,EAAE,CAAC,SAAS,CAAC,WAAW,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACjD,CAAC;IAED,IAAI,cAAc,GAAuB,EAAE,CAAC;IAE5C,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC;QAChB,cAAc,GAAG,MAAM,CAAC,SAAS,CAAC;IACpC,CAAC;SAAM,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;QAC1B,cAAc,GAAG,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,IAAI,CAAC,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;QACxF,IAAI,cAAc,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAChC,MAAM,IAAI,KAAK,CAAC,sBAAsB,OAAO,CAAC,MAAM,sBAAsB,YAAY,GAAG,CAAC,CAAC;QAC7F,CAAC;IACH,CAAC;SAAM,IAAI,OAAO,CAAC,IAAI,EAAE,CAAC;QACxB,cAAc,GAAG,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,IAAI,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;QAC3E,IAAI,cAAc,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAChC,MAAM,IAAI,KAAK,CAAC,iCAAiC,OAAO,CAAC,IAAI,QAAQ,YAAY,GAAG,CAAC,CAAC;QACxF,CAAC;IACH,CAAC;SAAM,IAAI,MAAM,EAAE,CAAC;QAClB,+CAA+C;QAC/C,MAAM,SAAS,GAAG,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,IAAI,CAAC,KAAK,MAAM,CAAC,CAAC;QACjF,IAAI,SAAS,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACzB,cAAc,GAAG,SAAS,CAAC;QAC7B,CAAC;aAAM,CAAC;YACN,MAAM,WAAW,GAAG,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC;YACxE,IAAI,WAAW,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAC3B,cAAc,GAAG,WAAW,CAAC;YAC/B,CAAC;iBAAM,CAAC;gBACN,MAAM,IAAI,KAAK,CAAC,WAAW,MAAM,6CAA6C,YAAY,GAAG,CAAC,CAAC;YACjG,CAAC;QACH,CAAC;IACH,CAAC;SAAM,CAAC;QACN,MAAM,IAAI,KAAK,CACb,uGAAuG,CACxG,CAAC;IACJ,CAAC;IAED,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,2CAA2C,CAAC,CAAC,CAAC;IACrE,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,cAAc,YAAY,EAAE,CAAC,CAAC,CAAC;IACrD,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,cAAc,MAAM,CAAC,aAAa,EAAE,CAAC,CAAC,CAAC;IAC7D,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,wBAAwB,cAAc,CAAC,MAAM,IAAI,CAAC,CAAC,CAAC;IAE1E,MAAM,OAAO,GAAG,MAAM,gBAAgB,CAAC,MAAM,CAAC,aAAa,EAAE,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,OAAO,IAAI,KAAK,CAAC,CAAC;IAElG,IAAI,CAAC;QACH,KAAK,MAAM,QAAQ,IAAI,cAAc,EAAE,CAAC;YACtC,MAAM,MAAM,GAAG,QAAQ,CAAC,EAAE,IAAI,QAAQ,CAAC,IAAI,CAAC;YAC5C,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAI,CAAC;YAC3B,MAAM,YAAY,GAAG,eAAe,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC;YAC1D,MAAM,WAAW,GAAG,QAAQ,CAAC,OAAO,IAAI,MAAM,CAAC,OAAO,IAAI,KAAK,CAAC;YAEhE,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,iBAAiB,IAAI,eAAe,MAAM,iCAAiC,CAAC,CAAC,CAAC;YAEpG,IAAI,cAAuB,CAAC;YAC5B,IAAI,CAAC;gBACH,cAAc,GAAG,MAAM,sBAAsB,CAC3C,OAAO,CAAC,MAAM,EACd,IAAI,EACJ,QAAQ,CAAC,KAAK,IAAI,EAAE,EACpB,WAAW,CACZ,CAAC;YACJ,CAAC;YAAC,OAAO,GAAQ,EAAE,CAAC;gBAClB,OAAO,CAAC,KAAK,CACX,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,0BAA0B,MAAM,mCAAmC,CAAC,CACpF,CAAC;gBACF,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,aAAa,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;gBACrD,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,gDAAgD,YAAY,IAAI,CAAC,CAAC,CAAC;gBAC9F,MAAM,IAAI,KAAK,CAAC,2BAA2B,IAAI,6BAA6B,CAAC,CAAC;YAChF,CAAC;YAED,sDAAsD;YACtD,IACE,cAAc,KAAK,IAAI;gBACvB,OAAO,cAAc,KAAK,QAAQ;gBACjC,cAAsB,CAAC,OAAO,KAAK,IAAI,EACxC,CAAC;gBACD,OAAO,CAAC,KAAK,CACX,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,0BAA0B,MAAM,mDAAmD,CAAC,CACpG,CAAC;gBACF,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,iBAAiB,UAAU,CAAC,cAAc,CAAC,EAAE,CAAC,CAAC,CAAC;gBACxE,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,gDAAgD,YAAY,IAAI,CAAC,CAAC,CAAC;gBAC9F,MAAM,IAAI,KAAK,CAAC,2BAA2B,IAAI,6CAA6C,CAAC,CAAC;YAChG,CAAC;YAED,4BAA4B;YAC5B,WAAW,CAAC,YAAY,EAAE,UAAU,CAAC,cAAc,CAAC,CAAC,CAAC;YACtD,OAAO,CAAC,GAAG,CACT,GAAG,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,aAAa,CAAC,kBAAkB,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,gBAAgB,KAAK,CAAC,IAAI,CAC9F,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,YAAY,CAAC,CAC3C,IAAI,CACN,CAAC;QACJ,CAAC;IACH,CAAC;YAAS,CAAC;QACT,MAAM,OAAO,CAAC,KAAK,EAAE,CAAC;IACxB,CAAC;IAED,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,gDAAgD,CAAC,CAAC,CAAC;AAClF,CAAC"}
package/dist/cli.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
package/dist/cli.js ADDED
@@ -0,0 +1,103 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import chalk from 'chalk';
4
+ import { runTests } from './runner.js';
5
+ import { approveTool } from './approver.js';
6
+ import { initConfig } from './initializer.js';
7
+ const program = new Command();
8
+ program
9
+ .name('mcpdrift')
10
+ .description('CI/CD regression and snapshot tester for Model Context Protocol (MCP) servers')
11
+ .version('1.0.0');
12
+ program
13
+ .command('run')
14
+ .description('Run regression tests against the MCP server tools and compare with baseline snapshots')
15
+ .option('-c, --config <path>', 'path to mcpdrift.config.json')
16
+ .option('-t, --tool <name>', 'run tests only for a specific tool')
17
+ .option('--case <id>', 'run tests only for a specific test case ID')
18
+ .option('--ci', 'strict CI mode: fail if any baseline snapshot is missing, never write baselines')
19
+ .option('--report <path>', 'output machine-readable JSON report to the specified file path')
20
+ .option('--json-report <path>', 'alias for --report')
21
+ .option('--timeout <ms>', 'timeout in milliseconds per tool call', (val) => parseInt(val, 10))
22
+ .action(async (options) => {
23
+ try {
24
+ const summary = await runTests({
25
+ config: options.config,
26
+ tool: options.tool,
27
+ caseId: options.case,
28
+ ci: options.ci,
29
+ report: options.report || options.jsonReport,
30
+ timeout: options.timeout,
31
+ });
32
+ if (!summary.success) {
33
+ process.exit(1);
34
+ }
35
+ process.exit(0);
36
+ }
37
+ catch (err) {
38
+ console.error(chalk.red.bold(`\nโŒ Error: ${err.message}\n`));
39
+ process.exit(1);
40
+ }
41
+ });
42
+ program
43
+ .command('approve [target]')
44
+ .description('Overwrite baseline snapshot(s) with the latest server response')
45
+ .option('-c, --config <path>', 'path to mcpdrift.config.json')
46
+ .option('--case <id>', 'approve only a specific test case ID')
47
+ .option('-t, --tool <name>', 'approve all test cases for a specific tool')
48
+ .option('-a, --all', 'approve all tools configured in mcpdrift.config.json')
49
+ .action(async (target, options) => {
50
+ try {
51
+ await approveTool(target, {
52
+ config: options.config,
53
+ caseId: options.case,
54
+ tool: options.tool,
55
+ all: options.all,
56
+ });
57
+ process.exit(0);
58
+ }
59
+ catch (err) {
60
+ console.error(chalk.red.bold(`\nโŒ Approve failed: ${err.message}\n`));
61
+ process.exit(1);
62
+ }
63
+ });
64
+ program
65
+ .command('init')
66
+ .description('Inspect a running MCP server and generate a starter mcpdrift.config.json')
67
+ .option('-s, --server <command>', 'server command to start MCP server (default: "node dist/server.js")')
68
+ .option('--tools <list>', 'comma-separated list of tools to include in config')
69
+ .option('--all-tools', 'explicitly include all discovered tools in generated test cases')
70
+ .option('--template', 'generate a starter template config without connecting to a server')
71
+ .option('-o, --output <path>', 'output config file path (default: "mcpdrift.config.json")')
72
+ .option('-f, --force', 'overwrite existing config file if present')
73
+ .action(async (options) => {
74
+ try {
75
+ const toolList = options.tools
76
+ ? options.tools.split(',').map((t) => t.trim()).filter(Boolean)
77
+ : undefined;
78
+ await initConfig({
79
+ server: options.server,
80
+ tools: toolList,
81
+ allTools: options.allTools,
82
+ template: options.template,
83
+ output: options.output,
84
+ force: options.force,
85
+ });
86
+ process.exit(0);
87
+ }
88
+ catch (err) {
89
+ console.error(chalk.red.bold(`\nโŒ Init failed: ${err.message}\n`));
90
+ process.exit(1);
91
+ }
92
+ });
93
+ // If no subcommands provided, default to help
94
+ if (process.argv.length <= 2) {
95
+ program.help();
96
+ }
97
+ else {
98
+ program.parseAsync(process.argv).catch((err) => {
99
+ console.error(chalk.red.bold(`Unexpected error: ${err.message}`));
100
+ process.exit(1);
101
+ });
102
+ }
103
+ //# sourceMappingURL=cli.js.map