@rethunk/mcp-multi-root-git 2.3.4 → 2.4.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 (41) hide show
  1. package/AGENTS.md +29 -3
  2. package/CHANGELOG.md +43 -0
  3. package/HUMANS.md +54 -5
  4. package/README.md +15 -1
  5. package/dist/server/batch-commit-tool.js +244 -19
  6. package/dist/server/git-diff-tool.js +132 -0
  7. package/dist/server/git-fetch-tool.js +131 -0
  8. package/dist/server/git-push-tool.js +8 -1
  9. package/dist/server/git-show-tool.js +148 -0
  10. package/dist/server/git-stash-tool.js +131 -0
  11. package/dist/server/git-tag-tool.js +162 -0
  12. package/dist/server/git.js +18 -2
  13. package/dist/server/roots.js +8 -4
  14. package/dist/server/test-harness.js +68 -5
  15. package/dist/server/tool-parameter-schemas.js +21 -1
  16. package/dist/server/tools.js +11 -0
  17. package/docs/install.md +19 -2
  18. package/docs/mcp-tools.md +214 -5
  19. package/package.json +7 -5
  20. package/schemas/batch_commit.json +125 -0
  21. package/schemas/git_cherry_pick.json +63 -0
  22. package/schemas/git_diff.json +62 -0
  23. package/schemas/git_diff_summary.json +75 -0
  24. package/schemas/git_fetch.json +52 -0
  25. package/schemas/git_inventory.json +74 -0
  26. package/schemas/git_log.json +75 -0
  27. package/schemas/git_merge.json +79 -0
  28. package/schemas/git_parity.json +74 -0
  29. package/schemas/git_push.json +50 -0
  30. package/schemas/git_reset_soft.json +42 -0
  31. package/schemas/git_show.json +39 -0
  32. package/schemas/git_stash_apply.json +44 -0
  33. package/schemas/git_stash_list.json +30 -0
  34. package/schemas/git_status.json +49 -0
  35. package/schemas/git_tag.json +50 -0
  36. package/schemas/git_worktree_add.json +52 -0
  37. package/schemas/git_worktree_list.json +30 -0
  38. package/schemas/git_worktree_remove.json +48 -0
  39. package/schemas/index.json +108 -0
  40. package/schemas/list_presets.json +44 -0
  41. package/tool-parameters.schema.json +317 -4
@@ -0,0 +1,162 @@
1
+ import { z } from "zod";
2
+ import { isSafeGitUpstreamToken, spawnGitAsync } from "./git.js";
3
+ import { jsonRespond } from "./json.js";
4
+ import { requireSingleRepo } from "./roots.js";
5
+ import { WorkspacePickSchema } from "./schemas.js";
6
+ // ---------------------------------------------------------------------------
7
+ // Helpers
8
+ // ---------------------------------------------------------------------------
9
+ /**
10
+ * Get the SHA of a given ref (tag, commit, branch, etc).
11
+ */
12
+ async function getRefSha(gitTop, ref) {
13
+ const result = await spawnGitAsync(gitTop, ["rev-parse", ref]);
14
+ if (!result.ok)
15
+ return null;
16
+ return result.stdout.trim();
17
+ }
18
+ /**
19
+ * Check if a tag is annotated or lightweight.
20
+ */
21
+ async function getTagType(gitTop, tag) {
22
+ // For annotated tags, `git cat-file -t <tag>` returns "tag"
23
+ // For lightweight tags, it returns "commit"
24
+ const result = await spawnGitAsync(gitTop, ["cat-file", "-t", tag]);
25
+ if (!result.ok)
26
+ return null;
27
+ const type = result.stdout.trim();
28
+ if (type === "tag")
29
+ return "annotated";
30
+ if (type === "commit")
31
+ return "lightweight";
32
+ return null;
33
+ }
34
+ // ---------------------------------------------------------------------------
35
+ // Tool registration
36
+ // ---------------------------------------------------------------------------
37
+ export function registerGitTagTool(server) {
38
+ server.addTool({
39
+ name: "git_tag",
40
+ description: "Create, delete, or inspect git tags. Create annotated tags (with message) or lightweight tags (ref only). " +
41
+ "Returns tag name, type, and SHA.",
42
+ annotations: {
43
+ readOnlyHint: false,
44
+ destructiveHint: true,
45
+ },
46
+ parameters: WorkspacePickSchema.omit({ absoluteGitRoots: true, allWorkspaceRoots: true })
47
+ .pick({
48
+ workspaceRoot: true,
49
+ rootIndex: true,
50
+ format: true,
51
+ })
52
+ .extend({
53
+ tag: z.string().min(1).describe("Tag name (e.g. 'v1.2.3')."),
54
+ message: z
55
+ .string()
56
+ .optional()
57
+ .describe("If provided, create an annotated tag with this message. If absent, create a lightweight tag."),
58
+ ref: z
59
+ .string()
60
+ .optional()
61
+ .describe("Commit/ref to tag (default: HEAD). Ignored if `delete` is true."),
62
+ delete: z
63
+ .boolean()
64
+ .optional()
65
+ .default(false)
66
+ .describe("If true, delete the named tag instead of creating it."),
67
+ }),
68
+ execute: async (args) => {
69
+ const pre = requireSingleRepo(server, args);
70
+ if (!pre.ok)
71
+ return jsonRespond(pre.error);
72
+ const gitTop = pre.gitTop;
73
+ const tag = args.tag.trim();
74
+ if (!tag) {
75
+ return jsonRespond({ error: "tag_empty" });
76
+ }
77
+ // Validate tag name: no shell metacharacters
78
+ if (!isSafeGitUpstreamToken(tag)) {
79
+ return jsonRespond({ error: "tag_unsafe", tag });
80
+ }
81
+ // Handle deletion
82
+ if (args.delete === true) {
83
+ const delResult = await spawnGitAsync(gitTop, ["tag", "-d", tag]);
84
+ if (!delResult.ok) {
85
+ return jsonRespond({
86
+ error: "tag_delete_failed",
87
+ detail: (delResult.stderr || delResult.stdout).trim(),
88
+ });
89
+ }
90
+ if (args.format === "json") {
91
+ return jsonRespond({
92
+ tag,
93
+ type: "deleted",
94
+ sha: "", // Deleted tags have no SHA
95
+ });
96
+ }
97
+ return `Deleted tag: ${tag}`;
98
+ }
99
+ // Determine the ref to tag (default HEAD)
100
+ const ref = (args.ref ?? "HEAD").trim();
101
+ if (!isSafeGitUpstreamToken(ref)) {
102
+ return jsonRespond({ error: "ref_unsafe", ref });
103
+ }
104
+ // Get the SHA of the ref to tag
105
+ const sha = await getRefSha(gitTop, ref);
106
+ if (!sha) {
107
+ return jsonRespond({
108
+ error: "ref_not_found",
109
+ ref,
110
+ });
111
+ }
112
+ // Create tag (annotated or lightweight)
113
+ const tagArgs = ["tag"];
114
+ if (args.message) {
115
+ // Annotated tag
116
+ tagArgs.push("-a", "-m", args.message);
117
+ }
118
+ else {
119
+ // Lightweight tag (just the tag name and ref)
120
+ }
121
+ tagArgs.push(tag, ref);
122
+ const createResult = await spawnGitAsync(gitTop, tagArgs);
123
+ if (!createResult.ok) {
124
+ return jsonRespond({
125
+ error: "tag_create_failed",
126
+ detail: (createResult.stderr || createResult.stdout).trim(),
127
+ });
128
+ }
129
+ // Verify the tag was created and get its type
130
+ const tagType = await getTagType(gitTop, tag);
131
+ if (!tagType) {
132
+ return jsonRespond({
133
+ error: "tag_verification_failed",
134
+ tag,
135
+ });
136
+ }
137
+ const result = {
138
+ tag,
139
+ type: tagType,
140
+ sha,
141
+ };
142
+ if (args.format === "json") {
143
+ return jsonRespond(result);
144
+ }
145
+ // Markdown output
146
+ const lines = [];
147
+ lines.push(`# Tag: ${tag}`);
148
+ lines.push("");
149
+ lines.push(`**Type:** ${tagType}`);
150
+ lines.push(`**SHA:** \`${sha}\``);
151
+ if (args.message) {
152
+ lines.push("");
153
+ lines.push("**Message:**");
154
+ lines.push("");
155
+ lines.push("```");
156
+ lines.push(args.message);
157
+ lines.push("```");
158
+ }
159
+ return lines.join("\n");
160
+ },
161
+ });
162
+ }
@@ -1,8 +1,24 @@
1
1
  import { spawn, spawnSync } from "node:child_process";
2
2
  import { existsSync, readFileSync } from "node:fs";
3
+ import { cpus } from "node:os";
3
4
  import { join } from "node:path";
4
- /** Parallel git subprocesses for inventory rows and git_status submodule rows. */
5
- export const GIT_SUBPROCESS_PARALLELISM = 4;
5
+ /**
6
+ * Parallel git subprocesses for inventory rows and git_status submodule rows.
7
+ * Reads from GIT_SUBPROCESS_PARALLELISM env var (default 4), clamped to [1, 2×CPU_COUNT].
8
+ */
9
+ function resolveGitSubprocessParallelism() {
10
+ const env = process.env.GIT_SUBPROCESS_PARALLELISM;
11
+ if (env) {
12
+ const n = Number.parseInt(env, 10);
13
+ if (!Number.isNaN(n) && n >= 1) {
14
+ const cpuCount = cpus().length;
15
+ const maxParallel = cpuCount * 2;
16
+ return Math.min(n, maxParallel);
17
+ }
18
+ }
19
+ return 4;
20
+ }
21
+ export const GIT_SUBPROCESS_PARALLELISM = resolveGitSubprocessParallelism();
6
22
  let gitPathState = "unknown";
7
23
  const GIT_NOT_FOUND_BODY = {
8
24
  error: "git_not_found",
@@ -15,12 +15,16 @@ function uriToPath(uri) {
15
15
  }
16
16
  function listFileRoots(server) {
17
17
  const sessions = server.sessions;
18
- const roots = sessions[0]?.roots ?? [];
19
18
  const paths = [];
20
- for (const root of roots) {
21
- const p = uriToPath(root.uri);
22
- if (p)
19
+ const seen = new Set();
20
+ for (const session of sessions) {
21
+ for (const root of session.roots ?? []) {
22
+ const p = uriToPath(root.uri);
23
+ if (!p || seen.has(p))
24
+ continue;
25
+ seen.add(p);
23
26
  paths.push(p);
27
+ }
24
28
  }
25
29
  return paths;
26
30
  }
@@ -16,7 +16,8 @@
16
16
  * const result = await tool({ workspaceRoot: dir, commits: [...] });
17
17
  * // result is string (markdown) or JSON-parseable string
18
18
  */
19
- import { mkdtempSync, rmSync } from "node:fs";
19
+ import { execFileSync } from "node:child_process";
20
+ import { appendFileSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
20
21
  import { tmpdir } from "node:os";
21
22
  import { join } from "node:path";
22
23
  // Stub context — no tool currently uses context
@@ -54,13 +55,20 @@ export function cleanupTmpPaths() {
54
55
  rmSync(p, { recursive: true, force: true });
55
56
  }
56
57
  }
58
+ export function writeTestGitConfig(repo) {
59
+ appendFileSync(join(repo, ".git", "config"), "\n[user]\n\temail = test@example.com\n\tname = Test User\n[commit]\n\tgpgsign = false\n");
60
+ }
57
61
  // ---------------------------------------------------------------------------
58
62
  // Fake server
59
63
  // ---------------------------------------------------------------------------
60
- function makeFakeServer() {
64
+ function makeFakeServer(roots = []) {
61
65
  const tools = [];
62
66
  const server = {
63
- sessions: [],
67
+ sessions: [
68
+ {
69
+ roots: roots.map((uri) => ({ uri })),
70
+ },
71
+ ],
64
72
  addTool(tool) {
65
73
  tools.push({ name: tool.name, parameters: tool.parameters, execute: tool.execute });
66
74
  },
@@ -78,8 +86,8 @@ function makeFakeServer() {
78
86
  * The returned function accepts tool args (always include `workspaceRoot`)
79
87
  * and returns the raw result as a string.
80
88
  */
81
- export function captureTool(register, toolName) {
82
- const { server, tools } = makeFakeServer();
89
+ export function captureTool(register, toolName, roots = []) {
90
+ const { server, tools } = makeFakeServer(roots);
83
91
  register(server);
84
92
  const pick = toolName ? tools.find((t) => t.name === toolName) : tools[0];
85
93
  if (!pick) {
@@ -97,3 +105,58 @@ export function captureToolDefinitions(register) {
97
105
  register(server);
98
106
  return tools;
99
107
  }
108
+ // ---------------------------------------------------------------------------
109
+ // Shared git test helpers (extracted from per-file duplication)
110
+ // ---------------------------------------------------------------------------
111
+ /** Execute git command with standard test environment and encoding. */
112
+ export function gitCmd(cwd, ...args) {
113
+ const opts = {
114
+ cwd,
115
+ encoding: "utf8",
116
+ env: {
117
+ ...process.env,
118
+ GIT_AUTHOR_NAME: "Test User",
119
+ GIT_AUTHOR_EMAIL: "test@example.com",
120
+ GIT_COMMITTER_NAME: "Test User",
121
+ GIT_COMMITTER_EMAIL: "test@example.com",
122
+ GIT_AUTHOR_DATE: "2025-01-01T00:00:00Z",
123
+ GIT_COMMITTER_DATE: "2025-01-01T00:00:00Z",
124
+ },
125
+ };
126
+ return execFileSync("git", args, opts);
127
+ }
128
+ /** Initialize a basic git repo with test config. */
129
+ export function makeRepo(prefix = "mcp-test-repo-") {
130
+ const dir = mkTmpDir(prefix);
131
+ gitCmd(dir, "init", "-b", "main");
132
+ writeTestGitConfig(dir);
133
+ return dir;
134
+ }
135
+ /** Initialize a repo with a seed commit (useful for branch/cherry-pick tests). */
136
+ export function makeRepoWithSeed(prefix = "mcp-test-repo-") {
137
+ const dir = makeRepo(prefix);
138
+ writeFileSync(join(dir, "seed.txt"), "seed\n");
139
+ gitCmd(dir, "add", "seed.txt");
140
+ gitCmd(dir, "commit", "-m", "chore: seed");
141
+ return dir;
142
+ }
143
+ /** Initialize work repo + bare remote with tracking set up. */
144
+ export function makeRepoWithUpstream(workPrefix = "mcp-work-", remotePrefix = "mcp-remote-") {
145
+ const remote = mkTmpDir(remotePrefix);
146
+ gitCmd(remote, "init", "--bare", "-b", "main");
147
+ const work = makeRepoWithSeed(workPrefix);
148
+ gitCmd(work, "remote", "add", "origin", remote);
149
+ gitCmd(work, "push", "-u", "origin", "main");
150
+ return { work, remote };
151
+ }
152
+ /** Initialize a git repo with main branch and test config. */
153
+ export function gitInitMain(dir) {
154
+ gitCmd(dir, "init", "-b", "main");
155
+ writeTestGitConfig(dir);
156
+ }
157
+ /** Add a commit to a repo with specified file content. */
158
+ export function addCommit(dir, file, content, message) {
159
+ writeFileSync(join(dir, file), content);
160
+ gitCmd(dir, "add", file);
161
+ gitCmd(dir, "commit", "-m", message);
162
+ }
@@ -2,13 +2,18 @@ import { z } from "zod";
2
2
  import { registerBatchCommitTool } from "./batch-commit-tool.js";
3
3
  import { registerGitCherryPickTool } from "./git-cherry-pick-tool.js";
4
4
  import { registerGitDiffSummaryTool } from "./git-diff-summary-tool.js";
5
+ import { registerGitDiffTool } from "./git-diff-tool.js";
6
+ import { registerGitFetchTool } from "./git-fetch-tool.js";
5
7
  import { registerGitInventoryTool } from "./git-inventory-tool.js";
6
8
  import { registerGitLogTool } from "./git-log-tool.js";
7
9
  import { registerGitMergeTool } from "./git-merge-tool.js";
8
10
  import { registerGitParityTool } from "./git-parity-tool.js";
9
11
  import { registerGitPushTool } from "./git-push-tool.js";
10
12
  import { registerGitResetSoftTool } from "./git-reset-soft-tool.js";
13
+ import { registerGitShowTool } from "./git-show-tool.js";
14
+ import { registerGitStashApplyTool, registerGitStashListTool } from "./git-stash-tool.js";
11
15
  import { registerGitStatusTool } from "./git-status-tool.js";
16
+ import { registerGitTagTool } from "./git-tag-tool.js";
12
17
  import { registerGitWorktreeAddTool, registerGitWorktreeListTool, registerGitWorktreeRemoveTool, } from "./git-worktree-tool.js";
13
18
  import { registerListPresetsTool } from "./list-presets-tool.js";
14
19
  export const READ_ONLY_ABSOLUTE_ROOT_TOOLS = [
@@ -19,18 +24,27 @@ export const READ_ONLY_ABSOLUTE_ROOT_TOOLS = [
19
24
  "git_log",
20
25
  "git_diff_summary",
21
26
  ];
27
+ export const READ_ONLY_SINGLE_REPO_TOOLS = [
28
+ "git_diff",
29
+ "git_show",
30
+ "git_worktree_list",
31
+ "git_stash_list",
32
+ ];
22
33
  export const MUTATING_TOOLS = [
34
+ "git_fetch",
23
35
  "batch_commit",
24
36
  "git_push",
25
37
  "git_merge",
26
38
  "git_cherry_pick",
27
39
  "git_reset_soft",
40
+ "git_tag",
28
41
  "git_worktree_add",
29
42
  "git_worktree_remove",
43
+ "git_stash_apply",
30
44
  ];
31
45
  export const ALL_PARAMETER_SCHEMA_TOOLS = [
32
46
  ...READ_ONLY_ABSOLUTE_ROOT_TOOLS,
33
- "git_worktree_list",
47
+ ...READ_ONLY_SINGLE_REPO_TOOLS,
34
48
  ...MUTATING_TOOLS,
35
49
  ];
36
50
  function captureParameterTools(register) {
@@ -52,14 +66,20 @@ export function captureToolParameterSchemas() {
52
66
  registerListPresetsTool(server);
53
67
  registerGitLogTool(server);
54
68
  registerGitDiffSummaryTool(server);
69
+ registerGitDiffTool(server);
70
+ registerGitShowTool(server);
55
71
  registerGitWorktreeListTool(server);
72
+ registerGitStashListTool(server);
73
+ registerGitFetchTool(server);
56
74
  registerBatchCommitTool(server);
57
75
  registerGitPushTool(server);
58
76
  registerGitMergeTool(server);
59
77
  registerGitCherryPickTool(server);
60
78
  registerGitResetSoftTool(server);
79
+ registerGitTagTool(server);
61
80
  registerGitWorktreeAddTool(server);
62
81
  registerGitWorktreeRemoveTool(server);
82
+ registerGitStashApplyTool(server);
63
83
  });
64
84
  return Object.fromEntries(tools.map((tool) => [tool.name, z.toJSONSchema(tool.parameters)]));
65
85
  }
@@ -1,13 +1,18 @@
1
1
  import { registerBatchCommitTool } from "./batch-commit-tool.js";
2
2
  import { registerGitCherryPickTool } from "./git-cherry-pick-tool.js";
3
3
  import { registerGitDiffSummaryTool } from "./git-diff-summary-tool.js";
4
+ import { registerGitDiffTool } from "./git-diff-tool.js";
5
+ import { registerGitFetchTool } from "./git-fetch-tool.js";
4
6
  import { registerGitInventoryTool } from "./git-inventory-tool.js";
5
7
  import { registerGitLogTool } from "./git-log-tool.js";
6
8
  import { registerGitMergeTool } from "./git-merge-tool.js";
7
9
  import { registerGitParityTool } from "./git-parity-tool.js";
8
10
  import { registerGitPushTool } from "./git-push-tool.js";
9
11
  import { registerGitResetSoftTool } from "./git-reset-soft-tool.js";
12
+ import { registerGitShowTool } from "./git-show-tool.js";
13
+ import { registerGitStashApplyTool, registerGitStashListTool } from "./git-stash-tool.js";
10
14
  import { registerGitStatusTool } from "./git-status-tool.js";
15
+ import { registerGitTagTool } from "./git-tag-tool.js";
11
16
  import { registerGitWorktreeAddTool, registerGitWorktreeListTool, registerGitWorktreeRemoveTool, } from "./git-worktree-tool.js";
12
17
  import { registerListPresetsTool } from "./list-presets-tool.js";
13
18
  import { registerPresetsResource } from "./presets-resource.js";
@@ -19,15 +24,21 @@ export function registerRethunkGitTools(server) {
19
24
  registerListPresetsTool(server);
20
25
  registerGitLogTool(server);
21
26
  registerGitDiffSummaryTool(server);
27
+ registerGitDiffTool(server);
28
+ registerGitShowTool(server);
22
29
  registerGitWorktreeListTool(server);
30
+ registerGitStashListTool(server);
31
+ registerGitFetchTool(server);
23
32
  // Mutating tools
24
33
  registerBatchCommitTool(server);
25
34
  registerGitPushTool(server);
26
35
  registerGitMergeTool(server);
27
36
  registerGitCherryPickTool(server);
28
37
  registerGitResetSoftTool(server);
38
+ registerGitTagTool(server);
29
39
  registerGitWorktreeAddTool(server);
30
40
  registerGitWorktreeRemoveTool(server);
41
+ registerGitStashApplyTool(server);
31
42
  // Resources
32
43
  registerPresetsResource(server);
33
44
  }
package/docs/install.md CHANGED
@@ -10,6 +10,7 @@ This package is an MCP **stdio** server. The client starts the process and passe
10
10
  - [GitHub Packages](#github-packages)
11
11
  - [Ways to run the binary](#ways-to-run-the-binary)
12
12
  - [Configuration shape (stdio)](#configuration-shape-stdio)
13
+ - [Environment variables](#environment-variables)
13
14
  - [Cursor](#cursor)
14
15
  - [Visual Studio Code (GitHub Copilot)](#visual-studio-code-github-copilot)
15
16
  - [Claude Desktop](#claude-desktop)
@@ -86,11 +87,27 @@ Register the server under a stable name (this documentation uses **`rethunk-git`
86
87
 
87
88
  Omit any `cwd` / `workingDirectory` field unless your client requires it for unrelated reasons. This server resolves repos from MCP **roots**, not from the process cwd.
88
89
 
90
+ ## Environment variables
91
+
92
+ | Variable | Default | Purpose |
93
+ |----------|---------|---------|
94
+ | `GIT_SUBPROCESS_PARALLELISM` | `4` | Number of parallel git subprocesses for inventory operations and `git_status` submodule rows. Valid range: 1 to 2×CPU count (auto-clamped to prevent runaway spawning). Increase on high-core machines to accelerate large fleet scans; decrease if system resources are constrained. |
95
+
96
+ Set these in the environment where the MCP client launches the server (e.g. in your shell, in the MCP client config as `env`, or in a startup script).
97
+
98
+ Example: Running the server with 8 parallel git processes on a 4-core machine:
99
+
100
+ ```bash
101
+ GIT_SUBPROCESS_PARALLELISM=8 npx -y @rethunk/mcp-multi-root-git
102
+ ```
103
+
104
+ On a 4-core machine (CPU count = 4), the max parallelism is clamped to 8 (2×4). The value requested is used if valid.
105
+
89
106
  ## Cursor
90
107
 
91
108
  **User scope (all workspaces):** `~/.cursor/mcp.json` on macOS/Linux, or `%USERPROFILE%\.cursor\mcp.json` on Windows.
92
109
 
93
- **Project scope:** `.cursor/mcp.json` in the workspace (often committed for team dogfooding).
110
+ **Project scope:** `.cursor/mcp.json` in the workspace (use when a team wants repo-local MCP wiring).
94
111
 
95
112
  Cursor uses a top-level **`mcpServers`** object:
96
113
 
@@ -185,7 +202,7 @@ For contributors working inside a clone of [mcp-multi-root-git](https://github.c
185
202
  1. **Dependencies, build, and CI parity:** **[HUMANS.md](../HUMANS.md)** — *Development* (`bun install`, `bun run build`, `bun run check`). Do not duplicate that workflow here.
186
203
  2. **Run the dev server** (no `dist/` required): from the repo root, **`bun src/server.ts`** (stdio MCP).
187
204
 
188
- **MCP registration for a local checkout:** point your client at that command (or at **`dist/server.js`** via `node` after `bun run build` — see HUMANS). **Cursor:** this repo may ship an example **`.cursor/mcp.json`** using `bun` + `["src/server.ts"]`; open the workspace at the repository root so relative args resolve.
205
+ **MCP registration for a local checkout:** point your client at that command (or at **`dist/server.js`** via `node` after `bun run build` — see HUMANS). **Cursor:** add the command to either your user-scope or project-scope `mcp.json`, and open the workspace at the repository root so relative args resolve.
189
206
 
190
207
  **Reload** the MCP connection after changing server code.
191
208