@rethunk/mcp-multi-root-git 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.
@@ -0,0 +1,175 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { z } from "zod";
4
+ /**
5
+ * Schema for `.rethunk/git-mcp-presets.json` at the workspace root.
6
+ * Each named entry defines roots for `git_inventory` and/or pairs for `git_parity`.
7
+ */
8
+ const PresetEntrySchema = z.object({
9
+ nestedRoots: z.array(z.string()).optional(),
10
+ parityPairs: z
11
+ .array(z.object({
12
+ left: z.string(),
13
+ right: z.string(),
14
+ label: z.string().optional(),
15
+ }))
16
+ .optional(),
17
+ /** When multiple MCP file roots exist, prefer one whose path basename or suffix matches this hint. */
18
+ workspaceRootHint: z.string().optional(),
19
+ });
20
+ const PresetFileSchema = z.record(z.string(), PresetEntrySchema);
21
+ export const PRESET_FILE_PATH = ".rethunk/git-mcp-presets.json";
22
+ /**
23
+ * Supports:
24
+ * - Wrapped: `{ "schemaVersion": "1", "presets": { "name": { ... } } }`
25
+ * - Legacy: `{ "name": { ... }, ... }` with optional top-level `schemaVersion` / `$schema` (editor hints).
26
+ */
27
+ export function splitPresetFileRaw(raw) {
28
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
29
+ throw new Error("invalid_root");
30
+ }
31
+ const o = raw;
32
+ if ("presets" in o &&
33
+ o.presets !== null &&
34
+ typeof o.presets === "object" &&
35
+ !Array.isArray(o.presets)) {
36
+ const sv = o.schemaVersion;
37
+ return {
38
+ mapRaw: o.presets,
39
+ schemaVersion: typeof sv === "string" ? sv : undefined,
40
+ };
41
+ }
42
+ const rest = { ...o };
43
+ const sv = rest.schemaVersion;
44
+ delete rest.schemaVersion;
45
+ delete rest.$schema;
46
+ return {
47
+ mapRaw: rest,
48
+ schemaVersion: typeof sv === "string" ? sv : undefined,
49
+ };
50
+ }
51
+ export function loadPresetsFromGitTop(gitTop) {
52
+ const presetPath = join(gitTop, PRESET_FILE_PATH);
53
+ if (!existsSync(presetPath)) {
54
+ return { ok: false, reason: "missing" };
55
+ }
56
+ let raw;
57
+ try {
58
+ raw = JSON.parse(readFileSync(presetPath, "utf8"));
59
+ }
60
+ catch (e) {
61
+ const message = e instanceof Error ? e.message : String(e);
62
+ return { ok: false, reason: "invalid_json", message };
63
+ }
64
+ let mapRaw;
65
+ let schemaVersion;
66
+ try {
67
+ const s = splitPresetFileRaw(raw);
68
+ mapRaw = s.mapRaw;
69
+ schemaVersion = s.schemaVersion;
70
+ }
71
+ catch {
72
+ return {
73
+ ok: false,
74
+ reason: "invalid_json",
75
+ message: "Preset file root must be a JSON object",
76
+ };
77
+ }
78
+ const parsed = PresetFileSchema.safeParse(mapRaw);
79
+ if (!parsed.success) {
80
+ return { ok: false, reason: "schema", issues: parsed.error.issues };
81
+ }
82
+ return { ok: true, data: parsed.data, schemaVersion };
83
+ }
84
+ export function presetLoadErrorPayload(gitTop, fail) {
85
+ const presetFile = join(gitTop, PRESET_FILE_PATH);
86
+ if (fail.reason === "invalid_json") {
87
+ return {
88
+ error: "preset_file_invalid",
89
+ kind: "invalid_json",
90
+ presetFile,
91
+ message: fail.message,
92
+ };
93
+ }
94
+ if (fail.reason === "schema") {
95
+ return { error: "preset_file_invalid", kind: "schema", presetFile, issues: fail.issues };
96
+ }
97
+ return { error: "preset_file_invalid", presetFile };
98
+ }
99
+ export function getPresetEntry(gitTop, presetName) {
100
+ const loaded = loadPresetsFromGitTop(gitTop);
101
+ if (!loaded.ok) {
102
+ if (loaded.reason === "missing") {
103
+ return {
104
+ error: {
105
+ error: "preset_not_found",
106
+ preset: presetName,
107
+ presetFile: join(gitTop, PRESET_FILE_PATH),
108
+ message: "Preset file missing",
109
+ },
110
+ };
111
+ }
112
+ return { error: presetLoadErrorPayload(gitTop, loaded) };
113
+ }
114
+ const entry = loaded.data[presetName];
115
+ if (!entry) {
116
+ return {
117
+ error: {
118
+ error: "preset_not_found",
119
+ preset: presetName,
120
+ presetFile: join(gitTop, PRESET_FILE_PATH),
121
+ },
122
+ };
123
+ }
124
+ return { entry, presetSchemaVersion: loaded.schemaVersion };
125
+ }
126
+ export function mergeNestedRoots(preset, inline) {
127
+ const a = preset ?? [];
128
+ const b = inline ?? [];
129
+ if (a.length === 0 && b.length === 0)
130
+ return undefined;
131
+ const seen = new Set();
132
+ const out = [];
133
+ for (const x of [...a, ...b]) {
134
+ if (!seen.has(x)) {
135
+ seen.add(x);
136
+ out.push(x);
137
+ }
138
+ }
139
+ return out;
140
+ }
141
+ export function mergePairs(preset, inline) {
142
+ const a = preset ?? [];
143
+ const b = inline ?? [];
144
+ if (a.length === 0 && b.length === 0)
145
+ return undefined;
146
+ return [...a, ...b];
147
+ }
148
+ export function applyPresetNestedRoots(gitTop, presetName, presetMerge, inlineNestedRoots) {
149
+ const got = getPresetEntry(gitTop, presetName);
150
+ if ("error" in got)
151
+ return { ok: false, error: got.error };
152
+ const fromPreset = got.entry.nestedRoots;
153
+ let nestedRoots = inlineNestedRoots;
154
+ if (presetMerge) {
155
+ nestedRoots = mergeNestedRoots(fromPreset, nestedRoots);
156
+ }
157
+ else if (!nestedRoots?.length) {
158
+ nestedRoots = fromPreset;
159
+ }
160
+ return { ok: true, nestedRoots, presetSchemaVersion: got.presetSchemaVersion };
161
+ }
162
+ export function applyPresetParityPairs(gitTop, presetName, presetMerge, inlinePairs) {
163
+ const got = getPresetEntry(gitTop, presetName);
164
+ if ("error" in got)
165
+ return { ok: false, error: got.error };
166
+ const fromPreset = got.entry.parityPairs;
167
+ let pairs = inlinePairs;
168
+ if (presetMerge) {
169
+ pairs = mergePairs(fromPreset, pairs);
170
+ }
171
+ else if (!pairs?.length) {
172
+ pairs = fromPreset;
173
+ }
174
+ return { ok: true, pairs, presetSchemaVersion: got.presetSchemaVersion };
175
+ }
@@ -0,0 +1,134 @@
1
+ import { basename, isAbsolute, resolve } from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ import { gateGit, gitTopLevel } from "./git.js";
4
+ import { loadPresetsFromGitTop, presetLoadErrorPayload } from "./presets.js";
5
+ export function uriToPath(uri) {
6
+ if (!uri.startsWith("file://"))
7
+ return null;
8
+ try {
9
+ return fileURLToPath(uri);
10
+ }
11
+ catch {
12
+ return null;
13
+ }
14
+ }
15
+ export function listFileRoots(server) {
16
+ const sessions = server.sessions;
17
+ const roots = sessions[0]?.roots ?? [];
18
+ const paths = [];
19
+ for (const root of roots) {
20
+ const p = uriToPath(root.uri);
21
+ if (p)
22
+ paths.push(p);
23
+ }
24
+ return paths;
25
+ }
26
+ /** Basename or trailing path segment; compares using normalized slashes so Windows backslashes match. */
27
+ export function pathMatchesWorkspaceRootHint(rootPath, hint) {
28
+ const h = hint.trim();
29
+ if (!h)
30
+ return true;
31
+ if (basename(rootPath) === h)
32
+ return true;
33
+ const absRoot = resolve(rootPath);
34
+ if (absRoot === h)
35
+ return true;
36
+ try {
37
+ if (isAbsolute(h) && resolve(h) === absRoot)
38
+ return true;
39
+ }
40
+ catch {
41
+ /* invalid absolute hint */
42
+ }
43
+ const normRoot = absRoot.replace(/\\/g, "/").replace(/\/+$/, "");
44
+ const normHint = h.replace(/\\/g, "/").replace(/\/+$/, "").replace(/^\/+/, "");
45
+ if (!normHint)
46
+ return true;
47
+ if (normRoot === normHint)
48
+ return true;
49
+ return normRoot.endsWith(`/${normHint}`);
50
+ }
51
+ export function resolveWorkspaceRoots(server, args) {
52
+ if (args.workspaceRoot?.trim()) {
53
+ return { ok: true, roots: [resolve(args.workspaceRoot.trim())] };
54
+ }
55
+ const fileRoots = listFileRoots(server);
56
+ if (args.allWorkspaceRoots) {
57
+ if (fileRoots.length === 0) {
58
+ return { ok: true, roots: [process.cwd()] };
59
+ }
60
+ return { ok: true, roots: fileRoots };
61
+ }
62
+ if (args.rootIndex != null) {
63
+ const r = fileRoots[args.rootIndex];
64
+ if (!r) {
65
+ return {
66
+ ok: false,
67
+ error: {
68
+ error: "root_index_out_of_range",
69
+ rootIndex: args.rootIndex,
70
+ rootCount: fileRoots.length,
71
+ },
72
+ };
73
+ }
74
+ return { ok: true, roots: [r] };
75
+ }
76
+ const primary = fileRoots[0];
77
+ if (primary !== undefined) {
78
+ return { ok: true, roots: [primary] };
79
+ }
80
+ return { ok: true, roots: [process.cwd()] };
81
+ }
82
+ /**
83
+ * When a preset name is requested and multiple MCP roots exist, pick the first root
84
+ * whose git toplevel loads a preset file containing that name.
85
+ */
86
+ export function resolveRootsForPreset(server, args, presetName) {
87
+ if (args.workspaceRoot?.trim() || args.allWorkspaceRoots || args.rootIndex != null) {
88
+ return resolveWorkspaceRoots(server, args);
89
+ }
90
+ const fileRoots = listFileRoots(server);
91
+ if (fileRoots.length <= 1) {
92
+ return resolveWorkspaceRoots(server, args);
93
+ }
94
+ const matches = [];
95
+ for (const r of fileRoots) {
96
+ const top = gitTopLevel(r);
97
+ if (!top)
98
+ continue;
99
+ const loaded = loadPresetsFromGitTop(top);
100
+ if (!loaded.ok && loaded.reason !== "missing") {
101
+ return { ok: false, error: presetLoadErrorPayload(top, loaded) };
102
+ }
103
+ if (!loaded.ok)
104
+ continue;
105
+ const entry = loaded.data[presetName];
106
+ if (!entry)
107
+ continue;
108
+ const hint = entry.workspaceRootHint?.trim();
109
+ if (hint) {
110
+ if (!pathMatchesWorkspaceRootHint(r, hint))
111
+ continue;
112
+ }
113
+ matches.push(r);
114
+ }
115
+ const pick = matches[0];
116
+ if (pick !== undefined) {
117
+ return { ok: true, roots: [pick] };
118
+ }
119
+ return resolveWorkspaceRoots(server, args);
120
+ }
121
+ /** `gateGit` plus workspace / preset root resolution; shared tool and resource prelude. */
122
+ export function requireGitAndRoots(server, args, presetName) {
123
+ const gg = gateGit();
124
+ if (!gg.ok) {
125
+ return { ok: false, error: gg.body };
126
+ }
127
+ const rootsRes = presetName
128
+ ? resolveRootsForPreset(server, args, presetName)
129
+ : resolveWorkspaceRoots(server, args);
130
+ if (!rootsRes.ok) {
131
+ return { ok: false, error: rootsRes.error };
132
+ }
133
+ return { ok: true, roots: rootsRes.roots };
134
+ }
@@ -0,0 +1,22 @@
1
+ import { z } from "zod";
2
+ import { MAX_INVENTORY_ROOTS_DEFAULT } from "./inventory.js";
3
+ const FormatSchema = z.enum(["markdown", "json"]).optional().default("markdown");
4
+ export const WorkspacePickSchema = z.object({
5
+ workspaceRoot: z
6
+ .string()
7
+ .optional()
8
+ .describe("Override workspace path. Wins over MCP roots, rootIndex, and allWorkspaceRoots."),
9
+ rootIndex: z
10
+ .number()
11
+ .int()
12
+ .min(0)
13
+ .optional()
14
+ .describe("Use the Nth file:// MCP root (0-based) when multiple workspace roots exist."),
15
+ allWorkspaceRoots: z
16
+ .boolean()
17
+ .optional()
18
+ .default(false)
19
+ .describe("Run against every file:// MCP root and aggregate results."),
20
+ format: FormatSchema.describe('Return "markdown" (default) or structured "json".'),
21
+ });
22
+ export { MAX_INVENTORY_ROOTS_DEFAULT };
@@ -0,0 +1,12 @@
1
+ import { registerGitInventoryTool } from "./git-inventory-tool.js";
2
+ import { registerGitParityTool } from "./git-parity-tool.js";
3
+ import { registerGitStatusTool } from "./git-status-tool.js";
4
+ import { registerListPresetsTool } from "./list-presets-tool.js";
5
+ import { registerPresetsResource } from "./presets-resource.js";
6
+ export function registerRethunkGitTools(server) {
7
+ registerGitStatusTool(server);
8
+ registerGitInventoryTool(server);
9
+ registerGitParityTool(server);
10
+ registerListPresetsTool(server);
11
+ registerPresetsResource(server);
12
+ }
package/dist/server.js ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ import { FastMCP } from "fastmcp";
3
+ import { readMcpServerVersion } from "./server/json.js";
4
+ import { registerRethunkGitTools } from "./server/tools.js";
5
+ const server = new FastMCP({
6
+ name: "rethunk-git",
7
+ version: readMcpServerVersion(),
8
+ roots: { enabled: true },
9
+ });
10
+ registerRethunkGitTools(server);
11
+ void server.start({ transportType: "stdio" });
@@ -0,0 +1,199 @@
1
+ # Installing @rethunk/mcp-multi-root-git
2
+
3
+ This package is an MCP **stdio** server. The client starts the process and passes **workspace roots** at `initialize` — **do not** set a fixed working-directory (`cwd`) in the server launch config; roots come from the client session.
4
+
5
+ **See also:** [mcp-tools.md](mcp-tools.md) (tool ids, JSON, resources, workspace root *resolution*), [HUMANS.md](../HUMANS.md) (preset file, dev, CI, publishing), [AGENTS.md](../AGENTS.md) (contributors).
6
+
7
+ ## Table of contents
8
+
9
+ - [Prerequisites](#prerequisites)
10
+ - [GitHub Packages](#github-packages)
11
+ - [Ways to run the binary](#ways-to-run-the-binary)
12
+ - [Configuration shape (stdio)](#configuration-shape-stdio)
13
+ - [Cursor](#cursor)
14
+ - [Visual Studio Code (GitHub Copilot)](#visual-studio-code-github-copilot)
15
+ - [Claude Desktop](#claude-desktop)
16
+ - [Zed](#zed)
17
+ - [Other clients and CLIs](#other-clients-and-clis)
18
+ - [From source (this repository)](#from-source-this-repository)
19
+ - [Troubleshooting](#troubleshooting)
20
+
21
+ ## Prerequisites
22
+
23
+ - **Git** on `PATH` (`git --version`). The server shells out to `git`; if it is missing, tools return `git_not_found`.
24
+ - **Node.js ≥ 22** if you use **`npx`**, or **Bun** if you use **`bunx`** / **`bun`** (see `package.json` `engines` / `packageManager`).
25
+
26
+ ## GitHub Packages
27
+
28
+ Every **version tag** on this repo is published to the **GitHub npm registry** as **`@rethunk-ai/mcp-multi-root-git`** (scope matches the GitHub org). The **npmjs** package name **`@rethunk/mcp-multi-root-git`** is updated **manually** by maintainers and may lag; prefer GitHub Packages for CI-aligned installs.
29
+
30
+ 1. Create a [GitHub personal access token](https://github.com/settings/tokens) with at least **`read:packages`** (and **`repo`** if the package were private).
31
+ 2. In **`~/.npmrc`** or the project **`.npmrc`** (do not commit secrets):
32
+
33
+ ```ini
34
+ @rethunk-ai:registry=https://npm.pkg.github.com
35
+ //npm.pkg.github.com/:_authToken=YOUR_TOKEN_HERE
36
+ ```
37
+
38
+ 3. Install or run, for example:
39
+
40
+ ```bash
41
+ npx -y @rethunk-ai/mcp-multi-root-git
42
+ ```
43
+
44
+ Or **`bunx @rethunk-ai/mcp-multi-root-git`** with the same registry configuration for that scope.
45
+
46
+ **`$schema` in preset JSON:** point at the copy under **`node_modules/@rethunk-ai/mcp-multi-root-git/git-mcp-presets.schema.json`** when you install from GitHub Packages; use **`@rethunk/mcp-multi-root-git/...`** when installing from npmjs.
47
+
48
+ ## Ways to run the binary
49
+
50
+ Use any of these from a terminal to confirm the package runs (each starts the stdio server until EOF). **npmjs** name:
51
+
52
+ ```bash
53
+ npx -y @rethunk/mcp-multi-root-git
54
+ bunx @rethunk/mcp-multi-root-git
55
+ npm install -g @rethunk/mcp-multi-root-git && mcp-multi-root-git
56
+ ```
57
+
58
+ **GitHub Packages** name (after configuring **`.npmrc`** as in [GitHub Packages](#github-packages)):
59
+
60
+ ```bash
61
+ npx -y @rethunk-ai/mcp-multi-root-git
62
+ bunx @rethunk-ai/mcp-multi-root-git
63
+ ```
64
+
65
+ Published entrypoint: **`dist/server.js`** (see `bin` / `exports`). Clients typically invoke **`npx`** or **`bunx`** so a global install is optional.
66
+
67
+ ## Configuration shape (stdio)
68
+
69
+ Across clients you always provide:
70
+
71
+ - A **command** (e.g. `npx`, `bunx`, `bun`, `node`).
72
+ - **Arguments** that resolve to this package’s server (e.g. `["-y", "@rethunk/mcp-multi-root-git"]` for `npx`).
73
+
74
+ Register the server under a stable name (this documentation uses **`rethunk-git`**). Tools appear as `{serverName}_{toolName}` (e.g. `rethunk-git_git_status`).
75
+
76
+ **Bun instead of npx**
77
+
78
+ ```json
79
+ {
80
+ "command": "bunx",
81
+ "args": ["@rethunk/mcp-multi-root-git"]
82
+ }
83
+ ```
84
+
85
+ **No `cwd`**
86
+
87
+ 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
+ ## Cursor
90
+
91
+ **User scope (all workspaces):** `~/.cursor/mcp.json` on macOS/Linux, or `%USERPROFILE%\.cursor\mcp.json` on Windows.
92
+
93
+ **Project scope:** `.cursor/mcp.json` in the workspace (often committed for team dogfooding).
94
+
95
+ Cursor uses a top-level **`mcpServers`** object:
96
+
97
+ ```json
98
+ {
99
+ "mcpServers": {
100
+ "rethunk-git": {
101
+ "command": "npx",
102
+ "args": ["-y", "@rethunk/mcp-multi-root-git"]
103
+ }
104
+ }
105
+ }
106
+ ```
107
+
108
+ After editing, reload MCP (Command Palette: reload / restart MCP). If tools are missing, ensure **`npx`** or **`bun`** is on `PATH`.
109
+
110
+ ## Visual Studio Code (GitHub Copilot)
111
+
112
+ MCP config lives in **`mcp.json`**: either **`.vscode/mcp.json`** in the workspace or the **user** MCP file (Command Palette: **MCP: Open User Configuration**). See the official [MCP configuration reference](https://code.visualstudio.com/docs/copilot/reference/mcp-configuration).
113
+
114
+ VS Code expects a **`servers`** object. For a stdio server set **`type`** to **`stdio`**, then **`command`** / **`args`**:
115
+
116
+ ```json
117
+ {
118
+ "servers": {
119
+ "rethunk-git": {
120
+ "type": "stdio",
121
+ "command": "npx",
122
+ "args": ["-y", "@rethunk/mcp-multi-root-git"]
123
+ }
124
+ }
125
+ }
126
+ ```
127
+
128
+ Use **MCP: List Servers** / **MCP: Reset Cached Tools** if tools do not update after a package upgrade.
129
+
130
+ ## Claude Desktop
131
+
132
+ Config file (create if missing):
133
+
134
+ | OS | Path |
135
+ |----|------|
136
+ | macOS | `~/Library/Application Support/Claude/claude_desktop_config.json` |
137
+ | Windows | `%APPDATA%\Claude\claude_desktop_config.json` |
138
+ | Linux | `~/.config/Claude/claude_desktop_config.json` |
139
+
140
+ Top-level key **`mcpServers`** (stdio by default):
141
+
142
+ ```json
143
+ {
144
+ "mcpServers": {
145
+ "rethunk-git": {
146
+ "command": "npx",
147
+ "args": ["-y", "@rethunk/mcp-multi-root-git"]
148
+ }
149
+ }
150
+ }
151
+ ```
152
+
153
+ Restart Claude Desktop after saving; invalid JSON often fails silently.
154
+
155
+ ## Zed
156
+
157
+ Configure **context servers** in Zed **`settings.json`** (e.g. `~/.config/zed/settings.json` on macOS/Linux). See [Model Context Protocol in Zed](https://zed.dev/docs/ai/mcp.html).
158
+
159
+ ```json
160
+ {
161
+ "context_servers": {
162
+ "rethunk-git": {
163
+ "command": "npx",
164
+ "args": ["-y", "@rethunk/mcp-multi-root-git"]
165
+ }
166
+ }
167
+ }
168
+ ```
169
+
170
+ Use the Agent Panel server status indicator to confirm the server is active.
171
+
172
+ ## Other clients and CLIs
173
+
174
+ Any MCP host that supports **stdio** and workspace roots can use the same **`command` / `args`** as above. Examples include other editors, team-internal launchers, and **Claude Code**-style environments — follow that product’s MCP docs and map:
175
+
176
+ - **Command:** `npx` (or `bunx`, `node`, etc.)
177
+ - **Args:** e.g. `["-y", "@rethunk/mcp-multi-root-git"]` for `npx`
178
+
179
+ Official protocol overview: [modelcontextprotocol.io](https://modelcontextprotocol.io/).
180
+
181
+ ## From source (this repository)
182
+
183
+ For contributors working inside a clone of [mcp-multi-root-git](https://github.com/Rethunk-AI/mcp-multi-root-git):
184
+
185
+ 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
+ 2. **Run the dev server** (no `dist/` required): from the repo root, **`bun src/server.ts`** (stdio MCP).
187
+
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.
189
+
190
+ **Reload** the MCP connection after changing server code.
191
+
192
+ ## Troubleshooting
193
+
194
+ | Issue | What to try |
195
+ |-------|-------------|
196
+ | `git_not_found` | Install Git and ensure it is on `PATH` in the environment that launches the MCP server. |
197
+ | Tools missing / stale | Restart the MCP host or use its “reload MCP / reset tools” action; in VS Code try **MCP: Reset Cached Tools**. |
198
+ | `npx` / `bun` not found | Install Node ≥ 22 or Bun; use full paths to the executable in config if `PATH` is minimal. |
199
+ | Wrong repo / root | Ensure the client opened the intended workspace so MCP **file roots** match your git work trees; see [mcp-tools.md](mcp-tools.md) — *Workspace root resolution*. |
@@ -0,0 +1,51 @@
1
+ # MCP tools and resources (canonical reference)
2
+
3
+ Single source of truth for **registered tool ids**, **client naming**, **JSON output shape**, **resource URI**, and **workspace root resolution**.
4
+ **Install and MCP clients (only canonical location):** [install.md](install.md). **Preset file, dev, CI, publishing:** [HUMANS.md](../HUMANS.md). **Implementation layout (`src/server/` + entry [`server.ts`](../src/server.ts)), contract bumps:** [AGENTS.md](../AGENTS.md).
5
+
6
+ ## Naming
7
+
8
+ MCP clients expose tools as `{serverName}_{toolName}`. With the server registered as **`rethunk-git`**, examples use the prefix **`rethunk-git_`**.
9
+
10
+ ## Tools
11
+
12
+ | Short id | Client id (server `rethunk-git`) | Purpose |
13
+ |----------|-----------------------------------|---------|
14
+ | `git_status` | `rethunk-git_git_status` | `git status --short -b` per MCP root and optional submodules (`includeSubmodules`); parallel submodule status. Args include `allWorkspaceRoots`, `rootIndex`, `workspaceRoot`, `format`. |
15
+ | `git_inventory` | `rethunk-git_git_inventory` | Status + ahead/behind per path; default upstream each repo’s `@{u}`; pass **both** `remote` and `branch` for fixed tracking. `nestedRoots`, `preset`, `presetMerge`, `maxRoots`, `format`, plus workspace pick args. |
16
+ | `git_parity` | `rethunk-git_git_parity` | Compare `git rev-parse HEAD` for path pairs. `pairs`, `preset`, `presetMerge`, `format`, plus workspace pick args. |
17
+ | `list_presets` | `rethunk-git_list_presets` | List preset names/counts from `.rethunk/git-mcp-presets.json`; invalid JSON/schema surface as errors. Workspace pick + `format` only. |
18
+
19
+ Pass **`format: "json"`** on any tool for structured JSON instead of markdown (default).
20
+
21
+ ## JSON responses
22
+
23
+ Every JSON string returned by a tool (including errors) ends with a **`rethunkGitMcp`** object:
24
+
25
+ - **`jsonFormatVersion`**: `"1"` for the current response shape.
26
+ - **`packageVersion`**: from the running server’s `package.json`.
27
+
28
+ Payload keys (e.g. `groups`, `inventories`, `parity`, `roots`) are stable within a given `jsonFormatVersion`. Preset-related responses may include **`presetSchemaVersion`**.
29
+
30
+ For **`git_inventory`** with `format: "json"`, each object inside **`inventories`** may include **`nestedRootsTruncated`** (boolean) and **`nestedRootsOmittedCount`** (number) when **`nestedRoots`** was longer than **`maxRoots`**.
31
+
32
+ **When to bump `jsonFormatVersion` or change payload shape:** [AGENTS.md](../AGENTS.md) — *Changing contracts*.
33
+
34
+ ## Resource
35
+
36
+ | URI | Purpose |
37
+ |-----|---------|
38
+ | `rethunk-git://presets` | JSON snapshot of `.rethunk/git-mcp-presets.json` at the resolved git toplevel (or structured errors; same `rethunkGitMcp` envelope pattern as tools). |
39
+
40
+ ## Workspace root resolution
41
+
42
+ Order applied when resolving which directory(ies) tools run against:
43
+
44
+ 1. Explicit **`workspaceRoot`** on the tool call (highest priority).
45
+ 2. **`rootIndex`** (0-based) — one `file://` MCP root when several exist.
46
+ 3. **`allWorkspaceRoots`: true** — every `file://` root; markdown sections separated by `---`, or combined JSON.
47
+ 4. **`preset`** set and multiple roots — first root whose git toplevel defines that preset (respecting **`workspaceRootHint`** on the preset entry when present).
48
+ 5. Otherwise the first `file://` root from MCP **`initialize`** / **`roots/list_changed`**.
49
+ 6. **`process.cwd()`** if no file roots (e.g. CI with explicit `workspaceRoot`).
50
+
51
+ Roots come from the MCP session (**`FastMCP` with `roots: { enabled: true }`** in code); there is no fixed `cwd` in server config.
@@ -0,0 +1,79 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://raw.githubusercontent.com/Rethunk-AI/mcp-multi-root-git/main/git-mcp-presets.schema.json",
4
+ "title": "rethunk-git MCP preset file",
5
+ "description": "Schema for .rethunk/git-mcp-presets.json (at the git repository root). Supports a wrapped layout with an explicit presets object, or a legacy flat map of preset name to entry.",
6
+ "oneOf": [
7
+ {
8
+ "type": "object",
9
+ "additionalProperties": false,
10
+ "properties": {
11
+ "$schema": {
12
+ "type": "string",
13
+ "description": "JSON Schema $schema pointer for editor validation."
14
+ },
15
+ "schemaVersion": {
16
+ "type": "string",
17
+ "const": "1",
18
+ "description": "File format version; use 1 for this schema."
19
+ },
20
+ "presets": {
21
+ "type": "object",
22
+ "additionalProperties": {
23
+ "$ref": "#/$defs/presetEntry"
24
+ },
25
+ "description": "Named preset entries for git_inventory / git_parity."
26
+ }
27
+ },
28
+ "required": ["presets"]
29
+ },
30
+ {
31
+ "type": "object",
32
+ "properties": {
33
+ "$schema": {
34
+ "type": "string"
35
+ },
36
+ "schemaVersion": {
37
+ "type": "string",
38
+ "const": "1",
39
+ "description": "Optional file format version when using the flat preset map."
40
+ }
41
+ },
42
+ "additionalProperties": {
43
+ "$ref": "#/$defs/presetEntry"
44
+ },
45
+ "minProperties": 1
46
+ }
47
+ ],
48
+ "$defs": {
49
+ "presetEntry": {
50
+ "type": "object",
51
+ "additionalProperties": false,
52
+ "properties": {
53
+ "nestedRoots": {
54
+ "type": "array",
55
+ "items": { "type": "string" },
56
+ "description": "Paths relative to the git toplevel, in order, for git_inventory."
57
+ },
58
+ "parityPairs": {
59
+ "type": "array",
60
+ "items": {
61
+ "type": "object",
62
+ "additionalProperties": false,
63
+ "properties": {
64
+ "left": { "type": "string" },
65
+ "right": { "type": "string" },
66
+ "label": { "type": "string" }
67
+ },
68
+ "required": ["left", "right"]
69
+ },
70
+ "description": "Path pairs for git_parity (paths relative to git toplevel unless absolute)."
71
+ },
72
+ "workspaceRootHint": {
73
+ "type": "string",
74
+ "description": "When multiple MCP file roots exist, restrict preset resolution to a root matching this basename or path suffix."
75
+ }
76
+ }
77
+ }
78
+ }
79
+ }