@testivai/mcp 0.2.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.
- package/LICENSE +21 -0
- package/README.md +55 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/lib.d.ts +44 -0
- package/dist/lib.d.ts.map +1 -0
- package/dist/lib.js +102 -0
- package/dist/lib.js.map +1 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +129 -0
- package/dist/server.js.map +1 -0
- package/package.json +51 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 TestivAI
|
|
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,55 @@
|
|
|
1
|
+
# @testivai/mcp
|
|
2
|
+
|
|
3
|
+
MCP (Model Context Protocol) server that gives AI coding agents eyes on your
|
|
4
|
+
visual regression results. The agent changes UI code, runs your test suite,
|
|
5
|
+
then uses these tools to find out **what actually changed on screen** — and
|
|
6
|
+
whether it's real or just render noise.
|
|
7
|
+
|
|
8
|
+
## Tools
|
|
9
|
+
|
|
10
|
+
| Tool | What it does |
|
|
11
|
+
|---|---|
|
|
12
|
+
| `get_visual_results` | Reads `visual-report/results.json`, returns a per-snapshot verdict: passed / likely render noise (DOM identical) / real structural change (with DOM summary) |
|
|
13
|
+
| `get_snapshot_diff` | Returns the baseline, current, and diff **images** for one snapshot, so the agent can see the change |
|
|
14
|
+
| `list_baselines` | Lists the committed baselines under `.testivai/baselines/` |
|
|
15
|
+
|
|
16
|
+
By design there is **no approve tool**: promoting a baseline is a human
|
|
17
|
+
decision. The verdicts tell the agent to suggest `/testivai approve <name>`
|
|
18
|
+
on the PR instead.
|
|
19
|
+
|
|
20
|
+
## Setup
|
|
21
|
+
|
|
22
|
+
### Claude Code
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
claude mcp add testivai -- npx -y @testivai/mcp
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Cursor / other MCP clients
|
|
29
|
+
|
|
30
|
+
```jsonc
|
|
31
|
+
// .cursor/mcp.json (or your client's equivalent)
|
|
32
|
+
{
|
|
33
|
+
"mcpServers": {
|
|
34
|
+
"testivai": { "command": "npx", "args": ["-y", "@testivai/mcp"] }
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The server reads the project from its working directory (pass `--root <path>`
|
|
40
|
+
to override) and respects `reportDir` from `.testivai/config.json`.
|
|
41
|
+
|
|
42
|
+
## Typical agent flow
|
|
43
|
+
|
|
44
|
+
1. Agent edits UI code.
|
|
45
|
+
2. Agent runs `npx playwright test` (the TestivAI reporter captures + diffs).
|
|
46
|
+
3. Agent calls `get_visual_results` → sees `homepage: changed (4.2%) and the
|
|
47
|
+
DOM changed (2 added, 1 removed)`.
|
|
48
|
+
4. Agent calls `get_snapshot_diff homepage` → looks at the images, confirms
|
|
49
|
+
the change matches the task (or fixes its own regression).
|
|
50
|
+
5. Agent reports to the human: what changed, whether it looks intended, and
|
|
51
|
+
which snapshots need `/testivai approve`.
|
|
52
|
+
|
|
53
|
+
Local mode only — no account, no API key, nothing leaves the machine.
|
|
54
|
+
|
|
55
|
+
Full integration guide (instructions-file level, MCP level, zero-test-suite apps, real transcript): [docs/guides/ai-agents.md](https://github.com/mcbuddy/testivai-oss/blob/main/docs/guides/ai-agents.md)
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AAC3F,YAAY,EAAE,WAAW,EAAE,cAAc,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,OAAO,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.listBaselines = exports.resolveImage = exports.verdictFor = exports.readResults = exports.resolvePaths = void 0;
|
|
4
|
+
var lib_1 = require("./lib");
|
|
5
|
+
Object.defineProperty(exports, "resolvePaths", { enumerable: true, get: function () { return lib_1.resolvePaths; } });
|
|
6
|
+
Object.defineProperty(exports, "readResults", { enumerable: true, get: function () { return lib_1.readResults; } });
|
|
7
|
+
Object.defineProperty(exports, "verdictFor", { enumerable: true, get: function () { return lib_1.verdictFor; } });
|
|
8
|
+
Object.defineProperty(exports, "resolveImage", { enumerable: true, get: function () { return lib_1.resolveImage; } });
|
|
9
|
+
Object.defineProperty(exports, "listBaselines", { enumerable: true, get: function () { return lib_1.listBaselines; } });
|
|
10
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,6BAA2F;AAAlF,mGAAA,YAAY,OAAA;AAAE,kGAAA,WAAW,OAAA;AAAE,iGAAA,UAAU,OAAA;AAAE,mGAAA,YAAY,OAAA;AAAE,oGAAA,aAAa,OAAA"}
|
package/dist/lib.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** Subset of the semver-governed results.json contract this server reads. */
|
|
2
|
+
export interface DomInfo {
|
|
3
|
+
changed: boolean;
|
|
4
|
+
noiseHint: boolean;
|
|
5
|
+
summary: {
|
|
6
|
+
added: number;
|
|
7
|
+
removed: number;
|
|
8
|
+
attributeChanges: number;
|
|
9
|
+
textChanges?: number;
|
|
10
|
+
} | null;
|
|
11
|
+
}
|
|
12
|
+
export interface SnapshotResult {
|
|
13
|
+
name: string;
|
|
14
|
+
status: 'passed' | 'changed' | 'new';
|
|
15
|
+
diffPercent?: number;
|
|
16
|
+
baselinePath?: string;
|
|
17
|
+
currentPath?: string;
|
|
18
|
+
diffPath?: string;
|
|
19
|
+
dom?: DomInfo;
|
|
20
|
+
}
|
|
21
|
+
export interface ResultsFile {
|
|
22
|
+
version: string;
|
|
23
|
+
timestamp: string;
|
|
24
|
+
summary: {
|
|
25
|
+
total: number;
|
|
26
|
+
passed: number;
|
|
27
|
+
changed: number;
|
|
28
|
+
newSnapshots: number;
|
|
29
|
+
};
|
|
30
|
+
snapshots: SnapshotResult[];
|
|
31
|
+
}
|
|
32
|
+
export interface ProjectPaths {
|
|
33
|
+
root: string;
|
|
34
|
+
reportDir: string;
|
|
35
|
+
}
|
|
36
|
+
/** Resolve the report dir from .testivai/config.json (reportDir key), default visual-report/. */
|
|
37
|
+
export declare function resolvePaths(root: string): ProjectPaths;
|
|
38
|
+
export declare function readResults(paths: ProjectPaths): ResultsFile | null;
|
|
39
|
+
/** One-line, agent-oriented verdict for a snapshot. */
|
|
40
|
+
export declare function verdictFor(snapshot: SnapshotResult): string;
|
|
41
|
+
/** Resolve a report-relative image path safely inside the report dir. */
|
|
42
|
+
export declare function resolveImage(paths: ProjectPaths, relativePath: string): string | null;
|
|
43
|
+
export declare function listBaselines(root: string): string[];
|
|
44
|
+
//# sourceMappingURL=lib.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lib.d.ts","sourceRoot":"","sources":["../src/lib.ts"],"names":[],"mappings":"AAGA,6EAA6E;AAC7E,MAAM,WAAW,OAAO;IACtB,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,gBAAgB,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;CACpG;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,QAAQ,GAAG,SAAS,GAAG,KAAK,CAAC;IACrC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,GAAG,CAAC,EAAE,OAAO,CAAC;CACf;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAA;KAAE,CAAC;IAClF,SAAS,EAAE,cAAc,EAAE,CAAC;CAC7B;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,iGAAiG;AACjG,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,CAcvD;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,YAAY,GAAG,WAAW,GAAG,IAAI,CAInE;AAED,uDAAuD;AACvD,wBAAgB,UAAU,CAAC,QAAQ,EAAE,cAAc,GAAG,MAAM,CAe3D;AAED,yEAAyE;AACzE,wBAAgB,YAAY,CAAC,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAIrF;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAQpD"}
|
package/dist/lib.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.resolvePaths = resolvePaths;
|
|
37
|
+
exports.readResults = readResults;
|
|
38
|
+
exports.verdictFor = verdictFor;
|
|
39
|
+
exports.resolveImage = resolveImage;
|
|
40
|
+
exports.listBaselines = listBaselines;
|
|
41
|
+
const fs = __importStar(require("fs"));
|
|
42
|
+
const path = __importStar(require("path"));
|
|
43
|
+
/** Resolve the report dir from .testivai/config.json (reportDir key), default visual-report/. */
|
|
44
|
+
function resolvePaths(root) {
|
|
45
|
+
let reportDir = 'visual-report';
|
|
46
|
+
const configPath = path.join(root, '.testivai', 'config.json');
|
|
47
|
+
if (fs.existsSync(configPath)) {
|
|
48
|
+
try {
|
|
49
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
50
|
+
if (typeof config.reportDir === 'string' && config.reportDir.length > 0) {
|
|
51
|
+
reportDir = config.reportDir;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// unreadable config falls back to the default report dir
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return { root, reportDir: path.join(root, reportDir) };
|
|
59
|
+
}
|
|
60
|
+
function readResults(paths) {
|
|
61
|
+
const file = path.join(paths.reportDir, 'results.json');
|
|
62
|
+
if (!fs.existsSync(file))
|
|
63
|
+
return null;
|
|
64
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
65
|
+
}
|
|
66
|
+
/** One-line, agent-oriented verdict for a snapshot. */
|
|
67
|
+
function verdictFor(snapshot) {
|
|
68
|
+
if (snapshot.status === 'passed')
|
|
69
|
+
return 'passed — no visual change';
|
|
70
|
+
if (snapshot.status === 'new')
|
|
71
|
+
return 'new snapshot — no baseline yet; a human should review and approve it';
|
|
72
|
+
const pct = snapshot.diffPercent !== undefined ? `${snapshot.diffPercent.toFixed(2)}% pixels differ` : 'pixels differ';
|
|
73
|
+
if (snapshot.dom?.noiseHint) {
|
|
74
|
+
return `changed (${pct}) but DOM is structurally identical — likely render noise (font hinting, anti-aliasing); mention it, don't block`;
|
|
75
|
+
}
|
|
76
|
+
if (snapshot.dom?.changed) {
|
|
77
|
+
const s = snapshot.dom.summary;
|
|
78
|
+
const detail = s
|
|
79
|
+
? ` (${s.added} added, ${s.removed} removed, ${s.attributeChanges} attribute changes${s.textChanges ? `, ${s.textChanges} text changes` : ''})`
|
|
80
|
+
: '';
|
|
81
|
+
return `changed (${pct}) and the DOM changed${detail} — a real structural change; confirm it is intended before approving`;
|
|
82
|
+
}
|
|
83
|
+
return `changed (${pct}) — no DOM data; treat as needing human review`;
|
|
84
|
+
}
|
|
85
|
+
/** Resolve a report-relative image path safely inside the report dir. */
|
|
86
|
+
function resolveImage(paths, relativePath) {
|
|
87
|
+
const abs = path.resolve(paths.reportDir, relativePath);
|
|
88
|
+
if (!abs.startsWith(path.resolve(paths.reportDir) + path.sep))
|
|
89
|
+
return null; // no traversal
|
|
90
|
+
return fs.existsSync(abs) ? abs : null;
|
|
91
|
+
}
|
|
92
|
+
function listBaselines(root) {
|
|
93
|
+
const dir = path.join(root, '.testivai', 'baselines');
|
|
94
|
+
if (!fs.existsSync(dir))
|
|
95
|
+
return [];
|
|
96
|
+
return fs
|
|
97
|
+
.readdirSync(dir, { withFileTypes: true })
|
|
98
|
+
.filter((e) => e.isDirectory() && !e.name.startsWith('.'))
|
|
99
|
+
.map((e) => e.name)
|
|
100
|
+
.sort();
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=lib.js.map
|
package/dist/lib.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lib.js","sourceRoot":"","sources":["../src/lib.ts"],"names":[],"mappings":"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"}
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":""}
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k;
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
+
}
|
|
9
|
+
Object.defineProperty(o, k2, desc);
|
|
10
|
+
}) : (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
o[k2] = m[k];
|
|
13
|
+
}));
|
|
14
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
+
}) : function(o, v) {
|
|
17
|
+
o["default"] = v;
|
|
18
|
+
});
|
|
19
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
20
|
+
var ownKeys = function(o) {
|
|
21
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
22
|
+
var ar = [];
|
|
23
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
24
|
+
return ar;
|
|
25
|
+
};
|
|
26
|
+
return ownKeys(o);
|
|
27
|
+
};
|
|
28
|
+
return function (mod) {
|
|
29
|
+
if (mod && mod.__esModule) return mod;
|
|
30
|
+
var result = {};
|
|
31
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
32
|
+
__setModuleDefault(result, mod);
|
|
33
|
+
return result;
|
|
34
|
+
};
|
|
35
|
+
})();
|
|
36
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const zod_1 = require("zod");
|
|
39
|
+
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
40
|
+
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
41
|
+
const lib_1 = require("./lib");
|
|
42
|
+
const packageJson = require('../package.json');
|
|
43
|
+
// Project root: --root <path> flag or cwd (MCP clients set cwd to the workspace).
|
|
44
|
+
const rootFlag = process.argv.indexOf('--root');
|
|
45
|
+
const projectRoot = rootFlag !== -1 && process.argv[rootFlag + 1] ? process.argv[rootFlag + 1] : process.cwd();
|
|
46
|
+
const server = new mcp_js_1.McpServer({ name: 'testivai', version: packageJson.version });
|
|
47
|
+
server.registerTool('get_visual_results', {
|
|
48
|
+
title: 'Get visual test results',
|
|
49
|
+
description: 'Read the latest TestivAI visual regression results (visual-report/results.json). ' +
|
|
50
|
+
'Returns a per-snapshot verdict combining the pixel diff and the DOM signal: ' +
|
|
51
|
+
'DOM-identical diffs are likely render noise; DOM changes are real and need human review. ' +
|
|
52
|
+
'Run the test suite first (e.g. `npx playwright test`) if results are stale or missing.',
|
|
53
|
+
inputSchema: {},
|
|
54
|
+
}, async () => {
|
|
55
|
+
const paths = (0, lib_1.resolvePaths)(projectRoot);
|
|
56
|
+
const results = (0, lib_1.readResults)(paths);
|
|
57
|
+
if (!results) {
|
|
58
|
+
return {
|
|
59
|
+
content: [
|
|
60
|
+
{
|
|
61
|
+
type: 'text',
|
|
62
|
+
text: `No results found at ${paths.reportDir}/results.json. Run the visual tests first (e.g. npx playwright test).`,
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
const lines = [
|
|
68
|
+
`Run: ${results.timestamp} — ${results.summary.total} snapshots: ${results.summary.passed} passed, ${results.summary.changed} changed, ${results.summary.newSnapshots} new.`,
|
|
69
|
+
'',
|
|
70
|
+
...results.snapshots.map((s) => `- ${s.name}: ${(0, lib_1.verdictFor)(s)}`),
|
|
71
|
+
'',
|
|
72
|
+
'Baseline approval is a human decision: suggest `/testivai approve <name>` on the PR (or `npx testivai approve` locally); do not approve autonomously.',
|
|
73
|
+
];
|
|
74
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
75
|
+
});
|
|
76
|
+
// registerTool is called through an untyped alias: zod@3.25 + TS 6 blow the
|
|
77
|
+
// type-depth limit (TS2589) when inferring the schema generics. Runtime
|
|
78
|
+
// validation of the input schema is unaffected.
|
|
79
|
+
server.registerTool('get_snapshot_diff', {
|
|
80
|
+
title: 'View snapshot diff images',
|
|
81
|
+
description: 'Return the baseline, current, and diff images for one changed snapshot so you can see what changed visually. ' +
|
|
82
|
+
'Use get_visual_results first to find snapshot names.',
|
|
83
|
+
inputSchema: { name: zod_1.z.string().describe('Snapshot name from get_visual_results') },
|
|
84
|
+
}, (async ({ name }) => {
|
|
85
|
+
const paths = (0, lib_1.resolvePaths)(projectRoot);
|
|
86
|
+
const results = (0, lib_1.readResults)(paths);
|
|
87
|
+
const snapshot = results?.snapshots.find((s) => s.name === name);
|
|
88
|
+
if (!results || !snapshot) {
|
|
89
|
+
return { content: [{ type: 'text', text: `No snapshot named "${name}" in the latest results.` }] };
|
|
90
|
+
}
|
|
91
|
+
const content = [
|
|
92
|
+
{ type: 'text', text: `${snapshot.name}: ${(0, lib_1.verdictFor)(snapshot)}` },
|
|
93
|
+
];
|
|
94
|
+
for (const [label, rel] of [
|
|
95
|
+
['baseline', snapshot.baselinePath],
|
|
96
|
+
['current', snapshot.currentPath],
|
|
97
|
+
['diff', snapshot.diffPath],
|
|
98
|
+
]) {
|
|
99
|
+
const abs = rel ? (0, lib_1.resolveImage)(paths, rel) : null;
|
|
100
|
+
if (abs) {
|
|
101
|
+
content.push({ type: 'text', text: `${label}:` });
|
|
102
|
+
content.push({ type: 'image', data: fs.readFileSync(abs).toString('base64'), mimeType: 'image/png' });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return { content };
|
|
106
|
+
}));
|
|
107
|
+
server.registerTool('list_baselines', {
|
|
108
|
+
title: 'List committed baselines',
|
|
109
|
+
description: 'List the snapshot baselines committed under .testivai/baselines/.',
|
|
110
|
+
inputSchema: {},
|
|
111
|
+
}, async () => {
|
|
112
|
+
const names = (0, lib_1.listBaselines)(projectRoot);
|
|
113
|
+
return {
|
|
114
|
+
content: [
|
|
115
|
+
{
|
|
116
|
+
type: 'text',
|
|
117
|
+
text: names.length ? `Baselines (${names.length}):\n${names.map((n) => `- ${n}`).join('\n')}` : 'No baselines yet — the first test run creates them.',
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
};
|
|
121
|
+
});
|
|
122
|
+
async function main() {
|
|
123
|
+
await server.connect(new stdio_js_1.StdioServerTransport());
|
|
124
|
+
}
|
|
125
|
+
main().catch((err) => {
|
|
126
|
+
console.error('testivai-mcp failed to start:', err);
|
|
127
|
+
process.exit(1);
|
|
128
|
+
});
|
|
129
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@testivai/mcp",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "MCP server exposing TestivAI visual regression results to AI coding agents",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"bin": {
|
|
8
|
+
"testivai-mcp": "dist/server.js"
|
|
9
|
+
},
|
|
10
|
+
"keywords": [
|
|
11
|
+
"mcp",
|
|
12
|
+
"model-context-protocol",
|
|
13
|
+
"visual-regression",
|
|
14
|
+
"testing",
|
|
15
|
+
"testivai",
|
|
16
|
+
"ai-agents"
|
|
17
|
+
],
|
|
18
|
+
"author": "TestivAI",
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/mcbuddy/testivai-oss.git",
|
|
23
|
+
"directory": "packages/mcp"
|
|
24
|
+
},
|
|
25
|
+
"bugs": {
|
|
26
|
+
"url": "https://github.com/mcbuddy/testivai-oss/issues"
|
|
27
|
+
},
|
|
28
|
+
"homepage": "https://github.com/mcbuddy/testivai-oss/tree/main/packages/mcp#readme",
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public"
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"dist",
|
|
34
|
+
"README.md"
|
|
35
|
+
],
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@modelcontextprotocol/sdk": "^1.17.0",
|
|
38
|
+
"zod": "^3.25.0"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@types/jest": "^30.0.0",
|
|
42
|
+
"@types/node": "^25.6.1",
|
|
43
|
+
"jest": "^30.4.2",
|
|
44
|
+
"ts-jest": "^29.4.0",
|
|
45
|
+
"typescript": "^6.0.3"
|
|
46
|
+
},
|
|
47
|
+
"scripts": {
|
|
48
|
+
"build": "tsc",
|
|
49
|
+
"test": "jest"
|
|
50
|
+
}
|
|
51
|
+
}
|