evidence-diff-mcp 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.
- package/README.md +41 -0
- package/dist/evidence.d.ts +4 -0
- package/dist/evidence.js +82 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +11 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +24 -0
- package/package.json +45 -0
- package/server.json +20 -0
package/README.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# evidence-diff-mcp
|
|
2
|
+
|
|
3
|
+
Evidence Diff is a local MCP tool for explaining coarse changes in a Git project’s test and build evidence. It can tell an agent that a worktree changed, that uncommitted evidence exists, or that test-like artifacts appear without build-like artifacts, without exposing source diffs.
|
|
4
|
+
|
|
5
|
+
## Tool
|
|
6
|
+
|
|
7
|
+
- `explain_change_evidence`: inspect local Git state and bounded top-level evidence names.
|
|
8
|
+
|
|
9
|
+
## Safety
|
|
10
|
+
|
|
11
|
+
- Local paths only, bounded by `EVIDENCE_DIFF_ROOT` and realpath checks.
|
|
12
|
+
- Returns only counts, coarse categories, coarse branch state, and commit dates.
|
|
13
|
+
- Never returns source diffs, evidence filenames, branch names, file paths from Git output, commit subjects, authors, command output, file contents, or secrets.
|
|
14
|
+
- Does not execute tests, builds, or arbitrary project commands.
|
|
15
|
+
- This is an evidence explanation aid, not a code review or CI oracle.
|
|
16
|
+
|
|
17
|
+
## Run
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install
|
|
21
|
+
npm run build
|
|
22
|
+
node dist/index.js
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Quick start
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm install
|
|
29
|
+
npm run build
|
|
30
|
+
node dist/index.js
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The server uses stdio, so it can be connected to Claude Desktop, Cursor, VS Code, MCP Inspector, or another compatible MCP client.
|
|
34
|
+
|
|
35
|
+
## Tools at a glance
|
|
36
|
+
|
|
37
|
+
- `explain_change_evidence`: Explain coarse changes in local Git and test-build evidence without returning source diffs, file contents, or secrets.
|
|
38
|
+
|
|
39
|
+
## Try it
|
|
40
|
+
|
|
41
|
+
After building, connect the server through your MCP client. The repository root also contains `smoke-test.mjs` for projects covered by the shared harness. A typical tool call starts with `explain_change_evidence`.
|
package/dist/evidence.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import { readdir, realpath } from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
const exec = promisify(execFile);
|
|
6
|
+
const MAX_OUTPUT = 14000;
|
|
7
|
+
const MAX_ENTRIES = 120;
|
|
8
|
+
const MAX_LOG_BYTES = 2 * 1024 * 1024;
|
|
9
|
+
const TEST_NAMES = /(?:test|spec|e2e|playwright|cypress|vitest|jest|pytest|cargo test)/i;
|
|
10
|
+
const BUILD_NAMES = /(?:dist|build|coverage|artifact|report|junit|test-results)/i;
|
|
11
|
+
export class EvidenceError extends Error {
|
|
12
|
+
}
|
|
13
|
+
async function safeRoot(input) {
|
|
14
|
+
const configured = process.env.EVIDENCE_DIFF_ROOT ?? path.join(process.cwd(), "..");
|
|
15
|
+
const allowed = await realpath(path.resolve(configured)).catch(() => path.resolve(configured));
|
|
16
|
+
const requested = path.isAbsolute(input) ? input : path.resolve(allowed, input || ".");
|
|
17
|
+
const target = await realpath(requested).catch(() => requested);
|
|
18
|
+
const relative = path.relative(allowed, target);
|
|
19
|
+
if (relative.startsWith("..") || path.isAbsolute(relative))
|
|
20
|
+
throw new EvidenceError("Project path must stay inside the configured local workspace");
|
|
21
|
+
return target;
|
|
22
|
+
}
|
|
23
|
+
async function git(cwd, args) {
|
|
24
|
+
try {
|
|
25
|
+
const result = await exec("git", args, { cwd, timeout: 15000, maxBuffer: MAX_LOG_BYTES });
|
|
26
|
+
return result.stdout.trim();
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
throw new EvidenceError("Git evidence unavailable for this local project");
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
async function topLevel(root) {
|
|
33
|
+
try {
|
|
34
|
+
const entries = await readdir(root, { withFileTypes: true });
|
|
35
|
+
return entries.filter((entry) => entry.name !== ".git" && entry.name !== "node_modules").slice(0, MAX_ENTRIES).map((entry) => entry.name).sort();
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
throw new EvidenceError("Local project evidence could not be read");
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function classify(name) { if (TEST_NAMES.test(name))
|
|
42
|
+
return "test-evidence"; if (BUILD_NAMES.test(name))
|
|
43
|
+
return "build-evidence"; return "other"; }
|
|
44
|
+
export async function explainEvidence(input) {
|
|
45
|
+
const root = await safeRoot(input);
|
|
46
|
+
const inside = await git(root, ["rev-parse", "--is-inside-work-tree"]);
|
|
47
|
+
if (inside !== "true")
|
|
48
|
+
throw new EvidenceError("Project is not a Git work tree");
|
|
49
|
+
const status = await git(root, ["status", "--porcelain=v1"]);
|
|
50
|
+
const recent = await git(root, ["log", "-5", "--date=short", "--format=%ad%x09%s"]);
|
|
51
|
+
const changed = await git(root, ["diff", "--name-status", "HEAD"]);
|
|
52
|
+
const staged = await git(root, ["diff", "--cached", "--name-status"]);
|
|
53
|
+
const entries = await topLevel(root);
|
|
54
|
+
const evidenceKinds = entries.filter((name) => TEST_NAMES.test(name) || BUILD_NAMES.test(name)).map(classify);
|
|
55
|
+
const testEvidenceCount = evidenceKinds.filter((kind) => kind === "test-evidence").length;
|
|
56
|
+
const buildEvidenceCount = evidenceKinds.filter((kind) => kind === "build-evidence").length;
|
|
57
|
+
const concerns = [];
|
|
58
|
+
if (status)
|
|
59
|
+
concerns.push("working-tree-not-clean");
|
|
60
|
+
if (changed || staged)
|
|
61
|
+
concerns.push("uncommitted-change-evidence");
|
|
62
|
+
if (testEvidenceCount > 0 && buildEvidenceCount === 0)
|
|
63
|
+
concerns.push("test-evidence-without-build-evidence");
|
|
64
|
+
return {
|
|
65
|
+
root: "<local-project>",
|
|
66
|
+
branch: (await git(root, ["branch", "--show-current"])) ? "named-branch" : "detached",
|
|
67
|
+
workingTree: status ? "changed" : "clean",
|
|
68
|
+
recentCommitCount: recent ? recent.split("\n").length : 0,
|
|
69
|
+
changedPathCount: (changed ? changed.split("\n").length : 0) + (staged ? staged.split("\n").length : 0),
|
|
70
|
+
evidenceKinds: [...new Set(evidenceKinds)].sort(),
|
|
71
|
+
testEvidenceCount,
|
|
72
|
+
buildEvidenceCount,
|
|
73
|
+
concerns,
|
|
74
|
+
explanation: concerns.length ? "The available local evidence suggests a change or an incomplete test-build evidence trail; inspect the project directly for details." : "No coarse evidence contradiction was detected; this does not prove the build or tests are correct.",
|
|
75
|
+
valueFree: true,
|
|
76
|
+
warning: "Only counts, coarse categories, branch state, and commit dates are returned. Source diffs, paths, subjects, authors, file contents, command output, and secrets are never emitted."
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
export function format(value) {
|
|
80
|
+
const output = JSON.stringify(value, null, 2);
|
|
81
|
+
return output.length <= MAX_OUTPUT ? output : JSON.stringify({ error: "Result exceeded the output budget and was reduced.", valueFree: true, truncated: true });
|
|
82
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
2
|
+
import { createServer } from "./server.js";
|
|
3
|
+
async function main() {
|
|
4
|
+
const server = createServer();
|
|
5
|
+
await server.connect(new StdioServerTransport());
|
|
6
|
+
console.error("MCP server running on stdio");
|
|
7
|
+
}
|
|
8
|
+
main().catch((error) => {
|
|
9
|
+
console.error("Fatal error:", error);
|
|
10
|
+
process.exit(1);
|
|
11
|
+
});
|
package/dist/server.d.ts
ADDED
package/dist/server.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { explainEvidence, format } from "./evidence.js";
|
|
4
|
+
const text = (value) => ({ content: [{ type: "text", text: value }] });
|
|
5
|
+
const textError = (t) => ({ content: [{ type: "text", text: t }], isError: true });
|
|
6
|
+
const READ_ONLY = { readOnlyHint: true, openWorldHint: true };
|
|
7
|
+
const errorText = (error) => text(`Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
8
|
+
export function createServer() {
|
|
9
|
+
const server = new McpServer({ name: "evidence-diff-mcp", version: "1.0.0" });
|
|
10
|
+
server.registerTool("explain_change_evidence", {
|
|
11
|
+
title: "Explain change evidence",
|
|
12
|
+
description: "Explain coarse changes in local Git and test-build evidence without returning source diffs, file contents, or secrets.",
|
|
13
|
+
inputSchema: z.object({ project: z.string().min(1).max(1000).describe("Local Git project directory; no URLs") }),
|
|
14
|
+
annotations: READ_ONLY,
|
|
15
|
+
}, async ({ project }) => {
|
|
16
|
+
try {
|
|
17
|
+
return text(format(await explainEvidence(project)));
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
return errorText(error);
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
return server;
|
|
24
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "evidence-diff-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Explain coarse Git and test-build evidence changes without source diff contents. Tools include explain change evidence",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"mcpName": "io.github.mrfentmen/evidence-diff-mcp",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/mrfentmen/evidence-diff-mcp.git"
|
|
10
|
+
},
|
|
11
|
+
"bin": {
|
|
12
|
+
"evidence-diff-mcp": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"main": "./dist/index.js",
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"server.json",
|
|
18
|
+
"README.md"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsc -p tsconfig.json",
|
|
22
|
+
"start": "node dist/index.js",
|
|
23
|
+
"dev": "npm run build && node dist/index.js"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"mcp",
|
|
27
|
+
"git",
|
|
28
|
+
"ci",
|
|
29
|
+
"test",
|
|
30
|
+
"build",
|
|
31
|
+
"local-first"
|
|
32
|
+
],
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@modelcontextprotocol/sdk": "^1.0.4",
|
|
36
|
+
"zod": "^3.23.8"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@types/node": "^22.0.0",
|
|
40
|
+
"typescript": "^5.6.0"
|
|
41
|
+
},
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=20"
|
|
44
|
+
}
|
|
45
|
+
}
|
package/server.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
|
+
"name": "io.github.mrfentmen/evidence-diff-mcp",
|
|
4
|
+
"description": "Explain coarse Git and test-build evidence changes without source diff contents. Tools include...",
|
|
5
|
+
"repository": {
|
|
6
|
+
"url": "https://github.com/mrfentmen/evidence-diff-mcp",
|
|
7
|
+
"source": "github"
|
|
8
|
+
},
|
|
9
|
+
"version": "1.0.0",
|
|
10
|
+
"packages": [
|
|
11
|
+
{
|
|
12
|
+
"registryType": "npm",
|
|
13
|
+
"identifier": "evidence-diff-mcp",
|
|
14
|
+
"version": "1.0.0",
|
|
15
|
+
"transport": {
|
|
16
|
+
"type": "stdio"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
}
|