pastcode 0.1.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 (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +118 -0
  3. package/index.js +286 -0
  4. package/package.json +41 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) [YEAR] [YOUR NAME]
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,118 @@
1
+ # pastcode
2
+
3
+ A minimal MCP server for git history, git blame, and debugging the moment a bug was introduced.
4
+
5
+ [![npm version](https://img.shields.io/npm/v/pastcode.svg)](https://www.npmjs.com/package/pastcode)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
7
+ [![Node.js](https://img.shields.io/badge/Node.js-%3E%3D18-43853d.svg)](https://nodejs.org/)
8
+
9
+ AI coding agents can read the current snapshot of a repo, but they usually cannot answer the question that matters most when debugging: when did this bug get introduced, who changed it, and what did the file look like before the breakage?
10
+
11
+ pastcode gives any MCP-compatible AI agent read access to your git history so it can investigate bugs the way a senior engineer would: by walking back through time, checking blame, diffing commits, and narrowing the culprit with git bisect.
12
+
13
+ ## Quick start
14
+
15
+ Add a server entry like this to your MCP client configuration:
16
+
17
+ ### Claude Desktop
18
+
19
+ ```json
20
+ {
21
+ "mcpServers": {
22
+ "pastcode": {
23
+ "command": "npx",
24
+ "args": ["-y", "pastcode"]
25
+ }
26
+ }
27
+ }
28
+ ```
29
+
30
+ This works the same way in Claude Code, Cursor, Windsurf, or any other MCP-compatible client that launches stdio-based MCP servers. There are no environment variables to set up, no repo config to install, and no global state to manage. The repo path is passed per tool call, so one running server can work across unlimited repositories.
31
+
32
+ Not published yet, or testing locally? Use the local path variant above instead.
33
+
34
+ ```json
35
+ {
36
+ "mcpServers": {
37
+ "pastcode": {
38
+ "command": "node",
39
+ "args": ["/absolute/path/to/pastcode/index.js"]
40
+ }
41
+ }
42
+ }
43
+ ```
44
+
45
+ ## Available tools
46
+
47
+ | Tool | What it does | Example prompt |
48
+ | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
49
+ | `get_file_at_commit` | Returns the contents of a file at a specific commit or ref. Useful when an AI agent wants to inspect older code without checking out the repo. | “Show me the version of src/auth.js from commit abc123 and compare it to the current file.” |
50
+ | `list_commits_for_file` | Lists the recent history of a file using `git log --follow`, including commit hashes, authors, dates, and messages. | “Which commits touched src/api/users.ts, and when were they introduced?” |
51
+ | `blame_line_range` | Shows who last changed a line range and which commit introduced it. Great for narrowing suspect edits. | “Who changed the validateEmail function and which commit owns this block?” |
52
+ | `diff_between_commits` | Compares two refs, optionally scoped to a file, to show exactly what changed between them. | “What changed between release-1.2 and release-1.3 in src/checkout.ts?” |
53
+ | `find_introducing_commit` | Runs a git bisect in a temporary worktree using a supplied test command to locate the first bad commit. | “When did the isValidEmail function start returning false for valid emails?” |
54
+
55
+ ## How agents actually use this
56
+
57
+ ### 1) “When did this bug get introduced?”
58
+
59
+ A user asks: “The login form breaks for valid email addresses after the last deploy. When did this start?”
60
+
61
+ The AI may call:
62
+
63
+ - `list_commits_for_file` on the file that owns email validation
64
+ - `blame_line_range` on the suspicious lines
65
+ - `get_file_at_commit` on historical revisions to inspect behavior before and after the regression
66
+ - `find_introducing_commit` with a minimal test command to confirm the exact first bad commit
67
+
68
+ ### 2) “Who changed this logic and why?”
69
+
70
+ A user asks: “I want to know why the pricing calculation changed in the checkout flow.”
71
+
72
+ The AI may call:
73
+
74
+ - `blame_line_range` on the calculation block
75
+ - `diff_between_commits` between the relevant revisions
76
+ - `get_file_at_commit` to inspect the code as it existed before the refactor or hotfix
77
+
78
+ ### 3) “What did this file look like before the refactor?”
79
+
80
+ A user asks: “The refactor in the auth service looks risky. Show me the pre-refactor implementation.”
81
+
82
+ The AI may call:
83
+
84
+ - `list_commits_for_file` to locate the refactor commit
85
+ - `get_file_at_commit` at the parent ref or historical tag
86
+ - `diff_between_commits` between the refactor and its parent to highlight the exact behavioral change
87
+
88
+ This is not a CLI tool you run manually. The AI agent drives it as part of a normal debugging workflow.
89
+
90
+ ## Security note
91
+
92
+ > ⚠️ `find_introducing_commit` executes an arbitrary shell command (`testCommand`) during bisect. Only use it against trusted repositories and trusted test commands, just as you would with any AI-driven script execution or automated debugging workflow.
93
+
94
+ ## Contributing
95
+
96
+ This project is intentionally small and dependency-light. The goal is to stay easy to reason about and easy to wire into AI tools without over-engineering the stack.
97
+
98
+ To work on it locally:
99
+
100
+ ```bash
101
+ git clone https://github.com/USERNAME/pastcode.git
102
+ cd pastcode
103
+ npm install
104
+ ```
105
+
106
+ For manual inspection of the MCP server, you can run it via the official MCP inspector:
107
+
108
+ ```bash
109
+ npx @modelcontextprotocol/inspector node index.js
110
+ ```
111
+
112
+ Then connect the inspector to the local server entry and inspect tool calls in real time.
113
+
114
+ If you want to propose a new tool, open an issue first to discuss scope and fit before sending a PR. This project keeps the surface area intentionally small, and it’s best to align on the workflow before adding new capabilities.
115
+
116
+ ## License
117
+
118
+ This project is licensed under the [MIT License](LICENSE).
package/index.js ADDED
@@ -0,0 +1,286 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { execFile } from "node:child_process";
6
+ import { promisify } from "node:util";
7
+
8
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
9
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
10
+ import {
11
+ CallToolRequestSchema,
12
+ ListToolsRequestSchema,
13
+ } from "@modelcontextprotocol/sdk/types.js";
14
+
15
+ const execFileAsync = promisify(execFile);
16
+
17
+ function ensureRepo(repoPath) {
18
+ if (!repoPath || typeof repoPath !== "string") {
19
+ throw new Error("repoPath is required");
20
+ }
21
+
22
+ if (!fs.existsSync(repoPath) || !fs.existsSync(path.join(repoPath, ".git"))) {
23
+ throw new Error(`Not a git repo: ${repoPath}`);
24
+ }
25
+ }
26
+
27
+ async function runGit(repoPath, args, stdoutOnly = false) {
28
+ ensureRepo(repoPath);
29
+
30
+ try {
31
+ const { stdout, stderr } = await execFileAsync(
32
+ "git",
33
+ ["-C", repoPath, ...args],
34
+ {
35
+ maxBuffer: 20 * 1024 * 1024,
36
+ },
37
+ );
38
+ if (stdoutOnly) {
39
+ return (stdout || "").trim();
40
+ }
41
+ return `${stdout || ""}${stderr ? `\n${stderr}` : ""}`.trim();
42
+ } catch (error) {
43
+ const message = [error.stderr, error.stdout, error.message]
44
+ .filter(Boolean)
45
+ .join("\n");
46
+ if (stdoutOnly) {
47
+ throw new Error(
48
+ (error.stderr || error.message || "git command failed").trim(),
49
+ );
50
+ }
51
+ throw new Error(message.trim() || "git command failed");
52
+ }
53
+ }
54
+
55
+ async function getFileAtCommit(repoPath, filePath, ref) {
56
+ return runGit(repoPath, ["show", `${ref}:${filePath}`], true);
57
+ }
58
+
59
+ async function listCommitsForFile(repoPath, filePath, maxCount) {
60
+ const args = ["log", "--follow"];
61
+ if (maxCount != null && Number.isFinite(Number(maxCount))) {
62
+ args.push("-n", String(Number(maxCount)));
63
+ }
64
+ args.push("--format=%H%x09%an%x09%ad%x09%s", "--date=short", "--", filePath);
65
+ return runGit(repoPath, args);
66
+ }
67
+
68
+ async function blameLineRange(repoPath, filePath, startLine, endLine, ref) {
69
+ const args = ["blame"];
70
+ if (ref) {
71
+ args.push(ref);
72
+ }
73
+ args.push("-L", `${startLine},${endLine}`, "--", filePath);
74
+ return runGit(repoPath, args);
75
+ }
76
+
77
+ async function diffBetweenCommits(repoPath, refA, refB, filePath) {
78
+ const args = ["diff", refA, refB];
79
+ if (filePath) {
80
+ args.push("--", filePath);
81
+ }
82
+ return runGit(repoPath, args);
83
+ }
84
+
85
+ async function findIntroducingCommit(
86
+ repoPath,
87
+ filePath,
88
+ testCommand,
89
+ goodRef,
90
+ badRef,
91
+ ) {
92
+ ensureRepo(repoPath);
93
+
94
+ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "pastcode-bisect-"));
95
+ const worktreePath = path.join(tempRoot, "repo");
96
+
97
+ try {
98
+ await runGit(repoPath, [
99
+ "worktree",
100
+ "add",
101
+ "--detach",
102
+ worktreePath,
103
+ goodRef,
104
+ ]);
105
+ await runGit(worktreePath, ["bisect", "start"]);
106
+ await runGit(worktreePath, ["bisect", "bad", badRef]);
107
+ await runGit(worktreePath, ["bisect", "good", goodRef]);
108
+
109
+ const bisectOutput = await execFileAsync(
110
+ "git",
111
+ ["-C", worktreePath, "bisect", "run", "bash", "-lc", testCommand],
112
+ { maxBuffer: 20 * 1024 * 1024 },
113
+ );
114
+
115
+ const head = await runGit(worktreePath, ["rev-parse", "HEAD"]);
116
+ const summary = [
117
+ `Introduced by commit: ${head}`,
118
+ `File: ${filePath || "(not restricted)"}`,
119
+ "",
120
+ bisectOutput.stdout || bisectOutput.stderr || "",
121
+ ].join("\n");
122
+
123
+ return summary.trim();
124
+ } catch (error) {
125
+ const msg = [error.stderr, error.stdout, error.message]
126
+ .filter(Boolean)
127
+ .join("\n");
128
+ throw new Error(msg.trim() || "bisect failed");
129
+ } finally {
130
+ try {
131
+ if (fs.existsSync(worktreePath)) {
132
+ try {
133
+ await runGit(worktreePath, ["bisect", "reset"]);
134
+ } catch (cleanupError) {
135
+ // ignore bisect reset failures so the worktree can still be removed
136
+ }
137
+ await runGit(repoPath, ["worktree", "remove", "--force", worktreePath]);
138
+ }
139
+ } catch (cleanupError) {
140
+ // ignore cleanup errors during short-lived tooling
141
+ }
142
+ fs.rmSync(tempRoot, { recursive: true, force: true });
143
+ }
144
+ }
145
+
146
+ const server = new Server(
147
+ { name: "pastcode", version: "0.1.0" },
148
+ { capabilities: { tools: {} } },
149
+ );
150
+
151
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
152
+ tools: [
153
+ {
154
+ name: "get_file_at_commit",
155
+ description: "Return the contents of a file at a specific commit or ref.",
156
+ inputSchema: {
157
+ type: "object",
158
+ properties: {
159
+ repoPath: { type: "string" },
160
+ path: { type: "string" },
161
+ ref: { type: "string" },
162
+ },
163
+ required: ["repoPath", "path", "ref"],
164
+ },
165
+ },
166
+ {
167
+ name: "list_commits_for_file",
168
+ description: "List recent commits affecting a file.",
169
+ inputSchema: {
170
+ type: "object",
171
+ properties: {
172
+ repoPath: { type: "string" },
173
+ path: { type: "string" },
174
+ maxCount: { type: "number" },
175
+ },
176
+ required: ["repoPath", "path"],
177
+ },
178
+ },
179
+ {
180
+ name: "blame_line_range",
181
+ description: "Blame a line range in a file.",
182
+ inputSchema: {
183
+ type: "object",
184
+ properties: {
185
+ repoPath: { type: "string" },
186
+ path: { type: "string" },
187
+ startLine: { type: "number" },
188
+ endLine: { type: "number" },
189
+ ref: { type: "string" },
190
+ },
191
+ required: ["repoPath", "path", "startLine", "endLine"],
192
+ },
193
+ },
194
+ {
195
+ name: "diff_between_commits",
196
+ description: "Show the diff between two refs for a file or repo.",
197
+ inputSchema: {
198
+ type: "object",
199
+ properties: {
200
+ repoPath: { type: "string" },
201
+ refA: { type: "string" },
202
+ refB: { type: "string" },
203
+ path: { type: "string" },
204
+ },
205
+ required: ["repoPath", "refA", "refB"],
206
+ },
207
+ },
208
+ {
209
+ name: "find_introducing_commit",
210
+ description:
211
+ "Use git bisect in a temporary worktree to find the introducing commit for a bug.",
212
+ inputSchema: {
213
+ type: "object",
214
+ properties: {
215
+ repoPath: { type: "string" },
216
+ path: { type: "string" },
217
+ testCommand: { type: "string" },
218
+ goodRef: { type: "string" },
219
+ badRef: { type: "string" },
220
+ },
221
+ required: ["repoPath", "path", "testCommand", "goodRef", "badRef"],
222
+ },
223
+ },
224
+ ],
225
+ }));
226
+
227
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
228
+ const { name, arguments: args = {} } = request.params;
229
+
230
+ try {
231
+ let result;
232
+
233
+ switch (name) {
234
+ case "get_file_at_commit":
235
+ result = await getFileAtCommit(args.repoPath, args.path, args.ref);
236
+ break;
237
+ case "list_commits_for_file":
238
+ result = await listCommitsForFile(
239
+ args.repoPath,
240
+ args.path,
241
+ args.maxCount,
242
+ );
243
+ break;
244
+ case "blame_line_range":
245
+ result = await blameLineRange(
246
+ args.repoPath,
247
+ args.path,
248
+ args.startLine,
249
+ args.endLine,
250
+ args.ref,
251
+ );
252
+ break;
253
+ case "diff_between_commits":
254
+ result = await diffBetweenCommits(
255
+ args.repoPath,
256
+ args.refA,
257
+ args.refB,
258
+ args.path,
259
+ );
260
+ break;
261
+ case "find_introducing_commit":
262
+ result = await findIntroducingCommit(
263
+ args.repoPath,
264
+ args.path,
265
+ args.testCommand,
266
+ args.goodRef,
267
+ args.badRef,
268
+ );
269
+ break;
270
+ default:
271
+ throw new Error(`Unknown tool: ${name}`);
272
+ }
273
+
274
+ return {
275
+ content: [{ type: "text", text: String(result) }],
276
+ };
277
+ } catch (error) {
278
+ return {
279
+ content: [{ type: "text", text: String(error.message || error) }],
280
+ isError: true,
281
+ };
282
+ }
283
+ });
284
+
285
+ const transport = new StdioServerTransport();
286
+ await server.connect(transport);
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "pastcode",
3
+ "version": "0.1.0",
4
+ "description": "MCP server for git history inspection, blame, diffs, and bisect-driven bug investigation for AI coding agents.",
5
+ "type": "module",
6
+ "bin": {
7
+ "pastcode": "./index.js"
8
+ },
9
+ "files": [
10
+ "index.js",
11
+ "README.md",
12
+ "LICENSE"
13
+ ],
14
+ "keywords": [
15
+ "mcp",
16
+ "model-context-protocol",
17
+ "git",
18
+ "git-history",
19
+ "git-blame",
20
+ "ai-agent",
21
+ "claude",
22
+ "debugging",
23
+ "bug-tracing",
24
+ "developer-tools"
25
+ ],
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "git+https://github.com/USERNAME/pastcode.git"
29
+ },
30
+ "homepage": "https://github.com/USERNAME/pastcode",
31
+ "bugs": {
32
+ "url": "https://github.com/USERNAME/pastcode/issues"
33
+ },
34
+ "license": "MIT",
35
+ "engines": {
36
+ "node": ">=18"
37
+ },
38
+ "dependencies": {
39
+ "@modelcontextprotocol/sdk": "^1.17.0"
40
+ }
41
+ }