@rethunk/mcp-multi-root-git 3.1.0 → 4.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 (66) hide show
  1. package/AGENTS.md +21 -15
  2. package/CHANGELOG.md +114 -39
  3. package/HUMANS.md +20 -39
  4. package/README.md +30 -13
  5. package/dist/repo-paths.js +57 -13
  6. package/dist/server/batch-commit-tool.js +187 -46
  7. package/dist/server/error-codes.js +25 -7
  8. package/dist/server/git-blame-tool.js +6 -3
  9. package/dist/server/git-branch-tool.js +151 -0
  10. package/dist/server/git-cherry-pick-tool.js +220 -11
  11. package/dist/server/git-conflicts-tool.js +230 -0
  12. package/dist/server/git-diff-summary-tool.js +36 -25
  13. package/dist/server/git-diff-tool.js +55 -27
  14. package/dist/server/git-grep-tool.js +188 -0
  15. package/dist/server/git-inventory-tool.js +26 -6
  16. package/dist/server/git-log-tool.js +35 -4
  17. package/dist/server/git-merge-tool.js +70 -12
  18. package/dist/server/git-parity-tool.js +13 -4
  19. package/dist/server/git-push-tool.js +3 -1
  20. package/dist/server/git-refs.js +48 -17
  21. package/dist/server/git-revert-tool.js +160 -0
  22. package/dist/server/git-show-tool.js +7 -3
  23. package/dist/server/git-stash-tool.js +110 -67
  24. package/dist/server/git-tag-tool.js +7 -6
  25. package/dist/server/git-worktree-tool.js +65 -53
  26. package/dist/server/git.js +116 -24
  27. package/dist/server/inventory.js +90 -32
  28. package/dist/server/presets-resource.js +37 -20
  29. package/dist/server/presets.js +87 -15
  30. package/dist/server/roots.js +13 -1
  31. package/dist/server/schemas.js +9 -2
  32. package/dist/server/test-harness.js +11 -4
  33. package/dist/server/tool-parameter-schemas.js +44 -55
  34. package/dist/server/tools.js +36 -17
  35. package/dist/server.js +1 -1
  36. package/docs/install.md +52 -5
  37. package/docs/mcp-tools.md +385 -178
  38. package/package.json +6 -6
  39. package/schemas/batch_commit.json +2 -2
  40. package/schemas/git_blame.json +1 -1
  41. package/schemas/git_branch.json +54 -0
  42. package/schemas/git_cherry_pick.json +12 -2
  43. package/schemas/git_cherry_pick_continue.json +34 -0
  44. package/schemas/{git_reflog.json → git_conflicts.json} +12 -12
  45. package/schemas/git_diff.json +11 -3
  46. package/schemas/git_grep.json +85 -0
  47. package/schemas/git_inventory.json +19 -5
  48. package/schemas/git_log.json +7 -6
  49. package/schemas/git_merge.json +2 -2
  50. package/schemas/git_parity.json +0 -5
  51. package/schemas/git_revert.json +47 -0
  52. package/schemas/git_show.json +1 -1
  53. package/schemas/git_stash_push.json +47 -0
  54. package/schemas/git_status.json +0 -5
  55. package/schemas/git_worktree_add.json +1 -1
  56. package/schemas/git_worktree_remove.json +1 -1
  57. package/schemas/index.json +28 -23
  58. package/schemas/list_presets.json +0 -5
  59. package/tool-parameters.schema.json +326 -167
  60. package/dist/server/git-branch-list-tool.js +0 -132
  61. package/dist/server/git-fetch-tool.js +0 -257
  62. package/dist/server/git-reflog-tool.js +0 -120
  63. package/schemas/git_branch_list.json +0 -30
  64. package/schemas/git_fetch.json +0 -46
  65. package/schemas/git_stash_list.json +0 -24
  66. package/schemas/git_worktree_list.json +0 -24
@@ -16,6 +16,7 @@
16
16
  * const result = await tool({ workspaceRoot: dir, commits: [...] });
17
17
  * // result is string (markdown) or JSON-parseable string
18
18
  */
19
+ import { afterEach } from "bun:test";
19
20
  import { execFileSync } from "node:child_process";
20
21
  import { appendFileSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
21
22
  import { tmpdir } from "node:os";
@@ -33,12 +34,18 @@ const STUB_CONTEXT = {
33
34
  };
34
35
  // ---------------------------------------------------------------------------
35
36
  // Tmp-dir lifecycle — prevents accumulating thousands of leaked test dirs.
36
- // Each test file must register the hook itself:
37
- // afterEach(cleanupTmpPaths);
38
- // Module-scope afterEach(...) would only register once (first-importer wins)
39
- // because the module is cached across test files in the same bun test run.
37
+ // Each test file that calls mkTmpDir / makeRepo / trackTmpPath must register
38
+ // cleanup once at module scope:
39
+ // registerTmpCleanup();
40
+ // (equivalent to afterEach(cleanupTmpPaths) but documents the contract.)
41
+ // Module-scope afterEach(...) in this file would only register once
42
+ // (first-importer wins) because the module is cached across test files.
40
43
  // ---------------------------------------------------------------------------
41
44
  const tmpPaths = [];
45
+ /** Register afterEach cleanup for dirs created via mkTmpDir / makeRepo. Call once per test file. */
46
+ export function registerTmpCleanup() {
47
+ afterEach(cleanupTmpPaths);
48
+ }
42
49
  export function mkTmpDir(prefix) {
43
50
  const dir = mkdtempSync(join(tmpdir(), prefix));
44
51
  tmpPaths.push(dir);
@@ -1,95 +1,84 @@
1
1
  import { z } from "zod";
2
- import { registerBatchCommitTool } from "./batch-commit-tool.js";
3
- import { registerGitBlameTool } from "./git-blame-tool.js";
4
- import { registerGitBranchListTool } from "./git-branch-list-tool.js";
5
- import { registerGitCherryPickTool } from "./git-cherry-pick-tool.js";
6
- import { registerGitDiffSummaryTool } from "./git-diff-summary-tool.js";
7
- import { registerGitDiffTool } from "./git-diff-tool.js";
8
- import { registerGitFetchTool } from "./git-fetch-tool.js";
9
- import { registerGitInventoryTool } from "./git-inventory-tool.js";
10
- import { registerGitLogTool } from "./git-log-tool.js";
11
- import { registerGitMergeTool } from "./git-merge-tool.js";
12
- import { registerGitParityTool } from "./git-parity-tool.js";
13
- import { registerGitPushTool } from "./git-push-tool.js";
14
- import { registerGitReflogTool } from "./git-reflog-tool.js";
15
- import { registerGitResetSoftTool } from "./git-reset-soft-tool.js";
16
- import { registerGitShowTool } from "./git-show-tool.js";
17
- import { registerGitStashApplyTool, registerGitStashListTool } from "./git-stash-tool.js";
18
- import { registerGitStatusTool } from "./git-status-tool.js";
19
- import { registerGitTagTool } from "./git-tag-tool.js";
20
- import { registerGitWorktreeAddTool, registerGitWorktreeListTool, registerGitWorktreeRemoveTool, } from "./git-worktree-tool.js";
21
- import { registerListPresetsTool } from "./list-presets-tool.js";
2
+ import { registerRethunkGitTools } from "./tools.js";
3
+ /**
4
+ * Fan-out read tools: polymorphic `root` routing (string | string[] | "*").
5
+ * Must stay in sync with the tools that use RootPickSchema — asserted against
6
+ * live `registerRethunkGitTools` capture in tests.
7
+ */
22
8
  export const FAN_OUT_ROOT_TOOLS = [
23
9
  "git_status",
24
10
  "git_inventory",
25
11
  "git_parity",
26
12
  "list_presets",
27
13
  "git_log",
14
+ "git_grep",
28
15
  ];
16
+ /**
17
+ * Read-only single-repo tools: `workspaceRoot` routing only.
18
+ */
29
19
  export const READ_ONLY_SINGLE_REPO_TOOLS = [
30
20
  "git_diff_summary",
31
21
  "git_diff",
32
22
  "git_show",
33
- "git_worktree_list",
34
- "git_stash_list",
23
+ "git_conflicts",
35
24
  "git_blame",
36
- "git_branch_list",
37
- "git_reflog",
38
25
  ];
26
+ /**
27
+ * Mutating tools: `workspaceRoot` routing only.
28
+ */
39
29
  export const MUTATING_TOOLS = [
40
- "git_fetch",
41
30
  "batch_commit",
42
31
  "git_push",
43
32
  "git_merge",
44
33
  "git_cherry_pick",
34
+ "git_cherry_pick_continue",
45
35
  "git_reset_soft",
36
+ "git_revert",
46
37
  "git_tag",
38
+ "git_branch",
47
39
  "git_worktree_add",
48
40
  "git_worktree_remove",
49
41
  "git_stash_apply",
42
+ "git_stash_push",
50
43
  ];
44
+ /** Category union used for routing assertions; must equal live registrar names. */
51
45
  export const ALL_PARAMETER_SCHEMA_TOOLS = [
52
46
  ...FAN_OUT_ROOT_TOOLS,
53
47
  ...READ_ONLY_SINGLE_REPO_TOOLS,
54
48
  ...MUTATING_TOOLS,
55
49
  ];
56
- function captureParameterTools(register) {
50
+ /**
51
+ * Capture parameter Zod schemas by driving the same registrar path as the live
52
+ * server (`registerRethunkGitTools`), so adding a tool only in `tools.ts` is
53
+ * enough for schema capture — no parallel register* list here.
54
+ *
55
+ * Clears `RETHUNK_GIT_TOOLS` for the duration so allowlist filtering cannot
56
+ * silently omit tools from published artifacts.
57
+ */
58
+ export function captureToolParameterSchemas() {
57
59
  const tools = [];
58
60
  const server = {
59
61
  sessions: [],
60
62
  addTool(tool) {
61
63
  tools.push({ name: tool.name, parameters: tool.parameters });
62
64
  },
65
+ addResource() {
66
+ // Presets resource is always registered; capture only needs tools.
67
+ },
63
68
  };
64
- register(server);
65
- return tools;
66
- }
67
- export function captureToolParameterSchemas() {
68
- const tools = captureParameterTools((server) => {
69
- registerGitStatusTool(server);
70
- registerGitInventoryTool(server);
71
- registerGitParityTool(server);
72
- registerListPresetsTool(server);
73
- registerGitLogTool(server);
74
- registerGitDiffSummaryTool(server);
75
- registerGitDiffTool(server);
76
- registerGitShowTool(server);
77
- registerGitWorktreeListTool(server);
78
- registerGitStashListTool(server);
79
- registerGitFetchTool(server);
80
- registerGitBlameTool(server);
81
- registerGitBranchListTool(server);
82
- registerGitReflogTool(server);
83
- registerBatchCommitTool(server);
84
- registerGitPushTool(server);
85
- registerGitMergeTool(server);
86
- registerGitCherryPickTool(server);
87
- registerGitResetSoftTool(server);
88
- registerGitTagTool(server);
89
- registerGitWorktreeAddTool(server);
90
- registerGitWorktreeRemoveTool(server);
91
- registerGitStashApplyTool(server);
92
- });
69
+ const prev = process.env.RETHUNK_GIT_TOOLS;
70
+ delete process.env.RETHUNK_GIT_TOOLS;
71
+ try {
72
+ registerRethunkGitTools(server);
73
+ }
74
+ finally {
75
+ if (prev === undefined) {
76
+ delete process.env.RETHUNK_GIT_TOOLS;
77
+ }
78
+ else {
79
+ process.env.RETHUNK_GIT_TOOLS = prev;
80
+ }
81
+ }
93
82
  return Object.fromEntries(tools.map((tool) => [tool.name, z.toJSONSchema(tool.parameters)]));
94
83
  }
95
84
  export function buildToolParameterSchemaDocument() {
@@ -1,26 +1,27 @@
1
1
  import { registerBatchCommitTool } from "./batch-commit-tool.js";
2
2
  import { registerGitBlameTool } from "./git-blame-tool.js";
3
- import { registerGitBranchListTool } from "./git-branch-list-tool.js";
4
- import { registerGitCherryPickTool } from "./git-cherry-pick-tool.js";
3
+ import { registerGitBranchTool } from "./git-branch-tool.js";
4
+ import { registerGitCherryPickContinueTool, registerGitCherryPickTool, } from "./git-cherry-pick-tool.js";
5
+ import { registerGitConflictsTool } from "./git-conflicts-tool.js";
5
6
  import { registerGitDiffSummaryTool } from "./git-diff-summary-tool.js";
6
7
  import { registerGitDiffTool } from "./git-diff-tool.js";
7
- import { registerGitFetchTool } from "./git-fetch-tool.js";
8
+ import { registerGitGrepTool } from "./git-grep-tool.js";
8
9
  import { registerGitInventoryTool } from "./git-inventory-tool.js";
9
10
  import { registerGitLogTool } from "./git-log-tool.js";
10
11
  import { registerGitMergeTool } from "./git-merge-tool.js";
11
12
  import { registerGitParityTool } from "./git-parity-tool.js";
12
13
  import { registerGitPushTool } from "./git-push-tool.js";
13
- import { registerGitReflogTool } from "./git-reflog-tool.js";
14
14
  import { registerGitResetSoftTool } from "./git-reset-soft-tool.js";
15
+ import { registerGitRevertTool } from "./git-revert-tool.js";
15
16
  import { registerGitShowTool } from "./git-show-tool.js";
16
- import { registerGitStashApplyTool, registerGitStashListTool } from "./git-stash-tool.js";
17
+ import { registerGitStashApplyTool, registerGitStashPushTool } from "./git-stash-tool.js";
17
18
  import { registerGitStatusTool } from "./git-status-tool.js";
18
19
  import { registerGitTagTool } from "./git-tag-tool.js";
19
- import { registerGitWorktreeAddTool, registerGitWorktreeListTool, registerGitWorktreeRemoveTool, } from "./git-worktree-tool.js";
20
+ import { registerGitWorktreeAddTool, registerGitWorktreeRemoveTool } from "./git-worktree-tool.js";
20
21
  import { registerListPresetsTool } from "./list-presets-tool.js";
21
22
  import { registerPresetsResource } from "./presets-resource.js";
22
23
  /**
23
- * Ordered registry of all 23 MCP tools. Registration order is preserved for
24
+ * Ordered registry of all 24 MCP tools. Registration order is preserved for
24
25
  * both full and filtered (RETHUNK_GIT_TOOLS) subsets.
25
26
  */
26
27
  const TOOL_REGISTRARS = [
@@ -30,30 +31,36 @@ const TOOL_REGISTRARS = [
30
31
  { name: "git_parity", register: registerGitParityTool },
31
32
  { name: "list_presets", register: registerListPresetsTool },
32
33
  { name: "git_log", register: registerGitLogTool },
34
+ { name: "git_grep", register: registerGitGrepTool },
33
35
  { name: "git_diff_summary", register: registerGitDiffSummaryTool },
34
36
  { name: "git_diff", register: registerGitDiffTool },
35
37
  { name: "git_show", register: registerGitShowTool },
36
- { name: "git_worktree_list", register: registerGitWorktreeListTool },
37
- { name: "git_stash_list", register: registerGitStashListTool },
38
- { name: "git_fetch", register: registerGitFetchTool },
38
+ { name: "git_conflicts", register: registerGitConflictsTool },
39
39
  { name: "git_blame", register: registerGitBlameTool },
40
- { name: "git_branch_list", register: registerGitBranchListTool },
41
- { name: "git_reflog", register: registerGitReflogTool },
42
40
  // Mutating tools
43
41
  { name: "batch_commit", register: registerBatchCommitTool },
44
42
  { name: "git_push", register: registerGitPushTool },
45
43
  { name: "git_merge", register: registerGitMergeTool },
46
44
  { name: "git_cherry_pick", register: registerGitCherryPickTool },
45
+ { name: "git_cherry_pick_continue", register: registerGitCherryPickContinueTool },
47
46
  { name: "git_reset_soft", register: registerGitResetSoftTool },
47
+ { name: "git_revert", register: registerGitRevertTool },
48
48
  { name: "git_tag", register: registerGitTagTool },
49
+ { name: "git_branch", register: registerGitBranchTool },
49
50
  { name: "git_worktree_add", register: registerGitWorktreeAddTool },
50
51
  { name: "git_worktree_remove", register: registerGitWorktreeRemoveTool },
51
52
  { name: "git_stash_apply", register: registerGitStashApplyTool },
53
+ { name: "git_stash_push", register: registerGitStashPushTool },
52
54
  ];
53
55
  /**
54
56
  * Parse the RETHUNK_GIT_TOOLS env var and return the matching subset of
55
57
  * registrars plus any unrecognized token names.
56
58
  *
59
+ * Semantics:
60
+ * - unset / empty / whitespace-only → all tools
61
+ * - bare `*` (sole non-empty token) → all tools (all-tools sentinel; not an empty selection)
62
+ * - otherwise → exact name match (case-sensitive), canonical order, duplicates ignored
63
+ *
57
64
  * @param envValue Raw value of process.env.RETHUNK_GIT_TOOLS (may be undefined).
58
65
  * @param registrars Full ordered registrar list (injectable for tests).
59
66
  */
@@ -62,13 +69,25 @@ export function selectToolRegistrars(envValue, registrars) {
62
69
  .split(",")
63
70
  .map((t) => t.trim())
64
71
  .filter((t) => t.length > 0);
65
- // Unset, empty, or whitespace-only → register all tools.
66
- if (tokens.length === 0) {
67
- return { selected: registrars, unknown: [] };
72
+ // Unset, empty, whitespace-only, or bare "*" → register all tools.
73
+ // Bare "*" is an intentional all-tools sentinel (operators used to root="*"
74
+ // fan-out may set RETHUNK_GIT_TOOLS=* expecting the full surface — treating
75
+ // it as an unrecognized name that empties the allowlist is a footgun).
76
+ if (tokens.length === 0 || (tokens.length === 1 && tokens[0] === "*")) {
77
+ // Shallow copy so callers cannot mutate the shared TOOL_REGISTRARS array.
78
+ return { selected: [...registrars], unknown: [] };
68
79
  }
69
80
  const knownNames = new Set(registrars.map((r) => r.name));
70
81
  const requested = new Set(tokens);
71
- const unknown = tokens.filter((t) => !knownNames.has(t));
82
+ // Deduplicate unknown tokens while preserving first-seen order.
83
+ const unknownSeen = new Set();
84
+ const unknown = [];
85
+ for (const t of tokens) {
86
+ if (!knownNames.has(t) && !unknownSeen.has(t)) {
87
+ unknownSeen.add(t);
88
+ unknown.push(t);
89
+ }
90
+ }
72
91
  // Preserve canonical order; deduplicate duplicate tokens automatically.
73
92
  const selected = registrars.filter((r) => requested.has(r.name));
74
93
  return { selected, unknown };
@@ -81,7 +100,7 @@ export function registerRethunkGitTools(server) {
81
100
  }
82
101
  if (selected.length === 0 && (env ?? "").trim().length > 0) {
83
102
  process.stderr.write(`[rethunk-git] RETHUNK_GIT_TOOLS: every listed name was unrecognized — registering NO tools. ` +
84
- `Set RETHUNK_GIT_TOOLS to a comma-separated list of valid tool names, or unset it to register all tools.\n`);
103
+ `Set RETHUNK_GIT_TOOLS to a comma-separated list of valid tool names, bare "*" for all tools, or unset it to register all tools.\n`);
85
104
  }
86
105
  for (const { register } of selected) {
87
106
  register(server);
package/dist/server.js CHANGED
@@ -7,7 +7,7 @@ import { registerRethunkGitTools } from "./server/tools.js";
7
7
  * (renamed/nested/omitted fields). Surfaced via the FastMCP `instructions`
8
8
  * field below, so it is discoverable in the MCP `initialize` response.
9
9
  */
10
- export const MCP_JSON_FORMAT_VERSION = "5";
10
+ export const MCP_JSON_FORMAT_VERSION = "6";
11
11
  const server = new FastMCP({
12
12
  name: "rethunk-git",
13
13
  version: readMcpServerVersion(),
package/docs/install.md CHANGED
@@ -91,11 +91,30 @@ Omit any `cwd` / `workingDirectory` field unless your client requires it for unr
91
91
 
92
92
  | Variable | Default | Purpose |
93
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
- | `RETHUNK_GIT_TOOLS` | _(unset)_ | Comma-separated list of tool names to register. When unset or empty, all 23 tools are registered (default). When set, only the listed tools are exposed — unknown names are warned and ignored. If every name is unknown, **zero** tools are registered and a loud warning is emitted (the restriction is honored literally). The presets resource (`rethunk-git://presets`) is always registered regardless of this setting. Example: `RETHUNK_GIT_TOOLS=git_status,git_diff_summary,git_diff,git_log,batch_commit,git_push`. Full tool-name list: `git_status`, `git_inventory`, `git_parity`, `list_presets`, `git_log`, `git_diff_summary`, `git_diff`, `git_show`, `git_worktree_list`, `git_stash_list`, `git_fetch`, `git_blame`, `git_branch_list`, `git_reflog`, `batch_commit`, `git_push`, `git_merge`, `git_cherry_pick`, `git_reset_soft`, `git_tag`, `git_worktree_add`, `git_worktree_remove`, `git_stash_apply`. |
94
+ | `GIT_SUBPROCESS_PARALLELISM` | `4` | Max concurrent git subprocesses for `git_inventory` rows, `git_status` submodule rows, and multi-root fan-out in `git_log` and `git_grep`. Valid range: 1 to 2×CPU count (auto-clamped). Increase on high-core machines to accelerate large fleet scans; decrease if system resources are constrained. |
95
+ | `GIT_SUBPROCESS_TIMEOUT_MS` | `120000` | Per-subprocess timeout in milliseconds for async git calls. On expiry the child receives SIGTERM and the call fails. Set `0` (or negative) to disable timeout for intentionally unbounded operations. |
96
+ | `RETHUNK_GIT_TOOLS` | _(unset)_ | Comma-separated list of tool names to register. When unset or empty, all 24 tools are registered (default). When set, only the listed tools are exposed — unknown names are warned and ignored. If every name is unknown, **zero** tools are registered and a loud warning is emitted (the restriction is honored literally). The presets resource (`rethunk-git://presets`) is always registered regardless of this setting. Example: `RETHUNK_GIT_TOOLS=git_status,git_diff_summary,git_diff,git_log,batch_commit,git_push`. Full tool-name list: `git_status`, `git_inventory`, `git_parity`, `list_presets`, `git_log`, `git_grep`, `git_diff_summary`, `git_diff`, `git_show`, `git_conflicts`, `git_blame`, `batch_commit`, `git_push`, `git_merge`, `git_cherry_pick`, `git_cherry_pick_continue`, `git_reset_soft`, `git_revert`, `git_tag`, `git_branch`, `git_worktree_add`, `git_worktree_remove`, `git_stash_apply`, `git_stash_push`. |
96
97
 
97
98
  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).
98
99
 
100
+ **MCP client `env` block** (Cursor / Claude Desktop `mcpServers`; VS Code adds `"env"` beside `"command"` / `"args"`):
101
+
102
+ ```json
103
+ {
104
+ "mcpServers": {
105
+ "rethunk-git": {
106
+ "command": "npx",
107
+ "args": ["-y", "@rethunk/mcp-multi-root-git"],
108
+ "env": {
109
+ "GIT_SUBPROCESS_PARALLELISM": "8",
110
+ "GIT_SUBPROCESS_TIMEOUT_MS": "120000",
111
+ "RETHUNK_GIT_TOOLS": "git_status,git_log,batch_commit,git_push"
112
+ }
113
+ }
114
+ }
115
+ }
116
+ ```
117
+
99
118
  Example: Running the server with 8 parallel git processes on a 4-core machine:
100
119
 
101
120
  ```bash
@@ -200,12 +219,40 @@ Official protocol overview: [modelcontextprotocol.io](https://modelcontextprotoc
200
219
 
201
220
  For contributors working inside a clone of [mcp-multi-root-git](https://github.com/Rethunk-AI/mcp-multi-root-git):
202
221
 
203
- 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.
222
+ 1. **Dependencies, build, and CI parity:** [CONTRIBUTING.md](../CONTRIBUTING.md) — *Development setup* (`bun install`, `bun run build`, `bun run ci`).
204
223
  2. **Run the dev server** (no `dist/` required): from the repo root, **`bun src/server.ts`** (stdio MCP).
205
224
 
206
- **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.
225
+ **MCP registration for a local checkout** set `cwd` to the repository root (exception to the no-`cwd` rule for published packages) so relative `args` resolve, or pass absolute paths in `args`. Open the workspace at the clone root in your client.
226
+
227
+ **Bun dev server** (no `dist/` build):
228
+
229
+ ```json
230
+ {
231
+ "mcpServers": {
232
+ "rethunk-git": {
233
+ "command": "bun",
234
+ "args": ["src/server.ts"],
235
+ "cwd": "/path/to/mcp-multi-root-git"
236
+ }
237
+ }
238
+ }
239
+ ```
240
+
241
+ **Built entrypoint** (after `bun run build`):
242
+
243
+ ```json
244
+ {
245
+ "mcpServers": {
246
+ "rethunk-git": {
247
+ "command": "node",
248
+ "args": ["dist/server.js"],
249
+ "cwd": "/path/to/mcp-multi-root-git"
250
+ }
251
+ }
252
+ }
253
+ ```
207
254
 
208
- **Reload** the MCP connection after changing server code.
255
+ **Cursor:** add either block to user-scope (`~/.cursor/mcp.json`) or project-scope (`.cursor/mcp.json`). **Reload** the MCP connection after changing server code.
209
256
 
210
257
  ## Troubleshooting
211
258