@rethunk/mcp-multi-root-git 1.0.0 → 2.0.1
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/AGENTS.md +33 -30
- package/HUMANS.md +8 -0
- package/README.md +2 -7
- package/dist/server/git-inventory-tool.js +32 -84
- package/dist/server/git-parity-tool.js +2 -5
- package/dist/server/git-status-tool.js +11 -10
- package/dist/server/git.js +6 -27
- package/dist/server/inventory.js +59 -82
- package/dist/server/json.js +2 -8
- package/dist/server/list-presets-tool.js +1 -1
- package/dist/server/presets.js +6 -5
- package/dist/server/roots.js +7 -21
- package/dist/server/schemas.js +4 -16
- package/docs/mcp-tools.md +20 -8
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -1,53 +1,56 @@
|
|
|
1
|
-
# AGENTS.md — LLM
|
|
1
|
+
# AGENTS.md — LLM + dev onboarding
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
IDEs injecting this as context: do not re-link from rules.
|
|
4
4
|
|
|
5
|
-
**
|
|
5
|
+
**Package:** [`@rethunk/mcp-multi-root-git`](https://www.npmjs.com/package/@rethunk/mcp-multi-root-git). MCP **stdio** server. Entry [`src/server.ts`](src/server.ts) → FastMCP + `registerRethunkGitTools`. Build output [`dist/server.js`](dist/server.js) (publish ships full `dist/`).
|
|
6
6
|
|
|
7
|
-
**
|
|
7
|
+
**Canonical docs — do not duplicate:**
|
|
8
|
+
- Install + per-client wiring → [docs/install.md](docs/install.md)
|
|
9
|
+
- Tools, JSON shape, resources, root resolution → [docs/mcp-tools.md](docs/mcp-tools.md)
|
|
10
|
+
- Presets, dev, CI, publish → [HUMANS.md](HUMANS.md)
|
|
8
11
|
|
|
9
12
|
## Implementation map
|
|
10
13
|
|
|
11
|
-
| File | Symbols
|
|
12
|
-
|
|
14
|
+
| File | Symbols |
|
|
15
|
+
|------|---------|
|
|
13
16
|
| [`src/server.ts`](src/server.ts) | `FastMCP` + `roots: { enabled: true }`; `readMcpServerVersion()`; `registerRethunkGitTools` |
|
|
14
|
-
| [`src/server/json.ts`](src/server/json.ts) | `MCP_JSON_FORMAT_VERSION`, `jsonRespond()
|
|
15
|
-
| [`src/server/git.ts`](src/server/git.ts) | `gateGit
|
|
16
|
-
| [`src/server/roots.ts`](src/server/roots.ts) | `uriToPath`, `listFileRoots`, `pathMatchesWorkspaceRootHint`, `resolveWorkspaceRoots`, `resolveRootsForPreset`, `requireGitAndRoots` — session roots only
|
|
17
|
-
| [`src/server/presets.ts`](src/server/presets.ts) | `PRESET_FILE_PATH`, `splitPresetFileRaw`, `loadPresetsFromGitTop`, `getPresetEntry`, `presetLoadErrorPayload`, `applyPresetNestedRoots`, `applyPresetParityPairs`; Zod
|
|
17
|
+
| [`src/server/json.ts`](src/server/json.ts) | `MCP_JSON_FORMAT_VERSION="2"`, `jsonRespond()` (minified, no envelope), `spreadWhen`, `spreadDefined` |
|
|
18
|
+
| [`src/server/git.ts`](src/server/git.ts) | `gateGit`, `spawnGitAsync`, `asyncPool`, `GIT_SUBPROCESS_PARALLELISM`, `gitTopLevel`, `gitRevParseGitDir`, `gitRevParseHead`, `parseGitSubmodulePaths`, `hasGitMetadata`, `gitStatusSnapshotAsync`, `gitStatusShortBranchAsync`, `fetchAheadBehind`, `isSafeGitUpstreamToken` |
|
|
19
|
+
| [`src/server/roots.ts`](src/server/roots.ts) | `uriToPath`, `listFileRoots`, `pathMatchesWorkspaceRootHint`, `resolveWorkspaceRoots`, `resolveRootsForPreset`, `requireGitAndRoots` — session roots only |
|
|
20
|
+
| [`src/server/presets.ts`](src/server/presets.ts) | `PRESET_FILE_PATH`, `splitPresetFileRaw`, `loadPresetsFromGitTop`, `getPresetEntry`, `presetLoadErrorPayload`, `applyPresetNestedRoots`, `applyPresetParityPairs`; Zod schemas must match [`git-mcp-presets.schema.json`](git-mcp-presets.schema.json) |
|
|
18
21
|
| [`src/server/schemas.ts`](src/server/schemas.ts) | `WorkspacePickSchema`, `MAX_INVENTORY_ROOTS_DEFAULT` |
|
|
19
|
-
| [`src/server/inventory.ts`](src/server/inventory.ts) | `validateRepoPath`, `makeSkipEntry`, `buildInventorySectionMarkdown`, `collectInventoryEntry`
|
|
20
|
-
| [`src/server/tools.ts`](src/server/tools.ts) | `registerRethunkGitTools` —
|
|
21
|
-
| [`src/server/git-status-tool.ts`](src/server/git-status-tool.ts) | `
|
|
22
|
-
| [`src/server/git-inventory-tool.ts`](src/server/git-inventory-tool.ts) | `
|
|
23
|
-
| [`src/server/git-parity-tool.ts`](src/server/git-parity-tool.ts) | `
|
|
24
|
-
| [`src/server/list-presets-tool.ts`](src/server/list-presets-tool.ts) | `
|
|
25
|
-
| [`src/server/presets-resource.ts`](src/server/presets-resource.ts) | `
|
|
22
|
+
| [`src/server/inventory.ts`](src/server/inventory.ts) | `InventoryEntryJson`, `validateRepoPath`, `makeSkipEntry`, `buildInventorySectionMarkdown`, `collectInventoryEntry` |
|
|
23
|
+
| [`src/server/tools.ts`](src/server/tools.ts) | `registerRethunkGitTools` — dispatches to `register*` below |
|
|
24
|
+
| [`src/server/git-status-tool.ts`](src/server/git-status-tool.ts) | `git_status` |
|
|
25
|
+
| [`src/server/git-inventory-tool.ts`](src/server/git-inventory-tool.ts) | `git_inventory` |
|
|
26
|
+
| [`src/server/git-parity-tool.ts`](src/server/git-parity-tool.ts) | `git_parity` |
|
|
27
|
+
| [`src/server/list-presets-tool.ts`](src/server/list-presets-tool.ts) | `list_presets` |
|
|
28
|
+
| [`src/server/presets-resource.ts`](src/server/presets-resource.ts) | `rethunk-git://presets` resource |
|
|
26
29
|
| [`src/repo-paths.ts`](src/repo-paths.ts) | `resolvePathForRepo`, `assertRelativePathUnderTop`, `isStrictlyUnderGitTop`, `realPathOrSelf` |
|
|
27
30
|
|
|
28
31
|
## Changing contracts
|
|
29
32
|
|
|
30
|
-
- **
|
|
31
|
-
- **`
|
|
32
|
-
- **Preset file:** keep
|
|
33
|
-
- **Public tool surface:**
|
|
33
|
+
- **No banner paragraphs** in shipped docs. Use normal titles + cross-links.
|
|
34
|
+
- **`MCP_JSON_FORMAT_VERSION`** (currently `"2"`): bump on incompatible JSON changes (renamed/nested/omitted fields). Document migration here + [docs/mcp-tools.md](docs/mcp-tools.md). v2 removed the `rethunkGitMcp` envelope; payloads are minified; optional fields omitted when empty/null/false.
|
|
35
|
+
- **Preset file:** keep `splitPresetFileRaw` + Zod aligned with [`git-mcp-presets.schema.json`](git-mcp-presets.schema.json).
|
|
36
|
+
- **Public tool surface:** rename/add → update [docs/mcp-tools.md](docs/mcp-tools.md) + [README.md](README.md) (if mentioned). Install/client wiring → [docs/install.md](docs/install.md) only. `.cursor/rules/rethunk-git-mcp.mdc` → only when *MCP-vs-shell* guidance changes.
|
|
34
37
|
|
|
35
|
-
##
|
|
38
|
+
## Validate + CI
|
|
36
39
|
|
|
37
|
-
Local: `bun run build`
|
|
40
|
+
Local: `bun run build` | `bun run check` | `bun run test`. CI ([`ci.yml`](.github/workflows/ci.yml)) runs same on PRs + `main` after `bun install --frozen-lockfile`, uploads prerelease `npm pack` artifact. Tag `v*.*.*` matching `package.json` version → [`release.yml`](.github/workflows/release.yml) publishes to GitHub Packages as `@rethunk-ai/mcp-multi-root-git` + cuts GitHub Release. npmjs publish is manual (see [HUMANS.md](HUMANS.md)).
|
|
38
41
|
|
|
39
|
-
Optional [`.githooks/`](.githooks):
|
|
42
|
+
Optional [`.githooks/`](.githooks): `bun run setup-hooks` once per clone. pre-commit=`check`; pre-push=frozen install + build + check.
|
|
40
43
|
|
|
41
|
-
Path confinement
|
|
44
|
+
Path confinement: [`src/repo-paths.ts`](src/repo-paths.ts) — extend tests when changing.
|
|
42
45
|
|
|
43
|
-
##
|
|
46
|
+
## Repo MCP entry (contributors)
|
|
44
47
|
|
|
45
|
-
|
|
48
|
+
Dogfood from clone: [docs/install.md](docs/install.md) — *From source*.
|
|
46
49
|
|
|
47
|
-
|
|
50
|
+
Repo ships `.cursor/` with alwaysApply rule [`.cursor/rules/rethunk-git-mcp.mdc`](.cursor/rules/rethunk-git-mcp.mdc) covering MCP-vs-shell usage. Rule does not re-link this file (already injected).
|
|
48
51
|
|
|
49
|
-
User-level skills may
|
|
52
|
+
User-level skills may mention README for discovery. Canonical refs: tools/JSON → [docs/mcp-tools.md](docs/mcp-tools.md); install → [docs/install.md](docs/install.md); presets → [HUMANS.md](HUMANS.md).
|
|
50
53
|
|
|
51
54
|
## Commits
|
|
52
55
|
|
|
53
|
-
|
|
56
|
+
Conventional Commits. Small themed commits. Why-focused messages. Stage + commit in one invocation per batch.
|
package/HUMANS.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
Read-only MCP git tools for any workspace. **How the server is installed and wired to clients:** **[docs/install.md](docs/install.md)** only (do not restate that material here).
|
|
4
4
|
|
|
5
|
+
## Badges
|
|
6
|
+
|
|
7
|
+
[](https://github.com/Rethunk-AI/mcp-multi-root-git/actions/workflows/release.yml)
|
|
8
|
+
[](https://github.com/Rethunk-AI/mcp-multi-root-git/releases/latest)
|
|
9
|
+
[](https://www.npmjs.com/package/@rethunk/mcp-multi-root-git)
|
|
10
|
+
[](https://github.com/Rethunk-AI/mcp-multi-root-git/packages)
|
|
11
|
+
[](https://github.com/Rethunk-AI/mcp-multi-root-git/blob/main/package.json)
|
|
12
|
+
|
|
5
13
|
**Implementation map (modules under `src/server/`, entry `src/server.ts`), symbols, and contract bumps** live in **`AGENTS.md`** at the repository root. This guide does not repeat those sections.
|
|
6
14
|
|
|
7
15
|
**Registered tool ids, client naming (`rethunk-git_*`), `format` / JSON envelopes, resource URI, workspace root resolution:** **[docs/mcp-tools.md](docs/mcp-tools.md)** — canonical; not duplicated here.
|
package/README.md
CHANGED
|
@@ -1,17 +1,12 @@
|
|
|
1
1
|
# @rethunk/mcp-multi-root-git
|
|
2
2
|
|
|
3
3
|
[](https://github.com/Rethunk-AI/mcp-multi-root-git/actions/workflows/ci.yml)
|
|
4
|
-
[](https://github.com/Rethunk-AI/mcp-multi-root-git/actions/workflows/release.yml)
|
|
5
|
-
[](https://github.com/Rethunk-AI/mcp-multi-root-git/releases/latest)
|
|
6
4
|
[](https://www.npmjs.com/package/@rethunk/mcp-multi-root-git)
|
|
7
|
-
[](https://www.npmjs.com/package/@rethunk/mcp-multi-root-git)
|
|
8
|
-
[](https://github.com/Rethunk-AI/mcp-multi-root-git/pkgs/npm/mcp-multi-root-git)
|
|
9
5
|
[](LICENSE)
|
|
10
|
-
[](https://github.com/Rethunk-AI/mcp-multi-root-git/blob/main/package.json)
|
|
11
6
|
|
|
12
7
|
Read-only **git** tools over MCP (status, multi-root inventory, `HEAD` parity, presets). **Install and MCP client wiring:** **[docs/install.md](docs/install.md)** only — do not duplicate those steps elsewhere.
|
|
13
8
|
|
|
14
|
-
**Repository:** [github.com/Rethunk-AI/mcp-multi-root-git](https://github.com/Rethunk-AI/mcp-multi-root-git) · **npmjs (manual releases):** [`@rethunk/mcp-multi-root-git`](https://www.npmjs.com/package/@rethunk/mcp-multi-root-git) · **GitHub Packages (CI on each tag):** [`@rethunk-ai/mcp-multi-root-git`](https://github.com/Rethunk-AI/mcp-multi-root-git/
|
|
9
|
+
**Repository:** [github.com/Rethunk-AI/mcp-multi-root-git](https://github.com/Rethunk-AI/mcp-multi-root-git) · **npmjs (manual releases):** [`@rethunk/mcp-multi-root-git`](https://www.npmjs.com/package/@rethunk/mcp-multi-root-git) · **GitHub Packages (CI on each tag):** [`@rethunk-ai/mcp-multi-root-git`](https://github.com/Rethunk-AI/mcp-multi-root-git/packages) — see [docs/install.md](docs/install.md) and [HUMANS.md](HUMANS.md) Publishing.
|
|
15
10
|
|
|
16
11
|
## Documentation
|
|
17
12
|
|
|
@@ -20,6 +15,6 @@ Read-only **git** tools over MCP (status, multi-root inventory, `HEAD` parity, p
|
|
|
20
15
|
| **[docs/install.md](docs/install.md)** | Single source for prerequisites, running the package, and every supported MCP client (plus from-source and troubleshooting) |
|
|
21
16
|
| **[docs/mcp-tools.md](docs/mcp-tools.md)** | Tool ids, client naming, `format` / JSON, resource URI, workspace root resolution (canonical reference) |
|
|
22
17
|
| **[HUMANS.md](HUMANS.md)** | Preset file, dev commands, CI, publishing |
|
|
23
|
-
| **[AGENTS.md](AGENTS.md)** | Contributors: implementation map ([`src/server/`](src/server/) + entry [`src/server.ts`](src/server.ts)), contract bumps, CI
|
|
18
|
+
| **[AGENTS.md](AGENTS.md)** | Contributors: implementation map ([`src/server/`](src/server/) + entry [`src/server.ts`](src/server.ts)), contract bumps, CI |
|
|
24
19
|
|
|
25
20
|
**Tools at a glance:** see the table in **[docs/mcp-tools.md](docs/mcp-tools.md)**.
|
|
@@ -8,66 +8,39 @@ import { MAX_INVENTORY_ROOTS_DEFAULT, WorkspacePickSchema } from "./schemas.js";
|
|
|
8
8
|
export function registerGitInventoryTool(server) {
|
|
9
9
|
server.addTool({
|
|
10
10
|
name: "git_inventory",
|
|
11
|
-
description: "Read-only
|
|
12
|
-
"Uses each repo's configured upstream (`@{u}`) unless both `remote` and `branch` are set. " +
|
|
13
|
-
"Presets from `.rethunk/git-mcp-presets.json`; use `presetMerge` to combine with inline paths.",
|
|
11
|
+
description: "Read-only status + ahead/behind per root. See docs/mcp-tools.md.",
|
|
14
12
|
parameters: WorkspacePickSchema.extend({
|
|
15
|
-
nestedRoots: z
|
|
16
|
-
|
|
17
|
-
.optional()
|
|
18
|
-
.describe("Paths relative to git toplevel. If empty/omitted, only the repo root is listed. With `presetMerge`, merged with preset paths."),
|
|
19
|
-
preset: z
|
|
20
|
-
.string()
|
|
21
|
-
.optional()
|
|
22
|
-
.describe("Named preset from .rethunk/git-mcp-presets.json (at git toplevel)."),
|
|
13
|
+
nestedRoots: z.array(z.string()).optional(),
|
|
14
|
+
preset: z.string().optional(),
|
|
23
15
|
presetMerge: z
|
|
24
16
|
.boolean()
|
|
25
17
|
.optional()
|
|
26
18
|
.default(false)
|
|
27
|
-
.describe("
|
|
28
|
-
remote: z
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
.describe("Fixed upstream remote; must be set together with `branch` to override auto upstream."),
|
|
32
|
-
branch: z
|
|
33
|
-
.string()
|
|
34
|
-
.optional()
|
|
35
|
-
.describe("Fixed upstream branch name; must be set together with `remote`."),
|
|
36
|
-
maxRoots: z
|
|
37
|
-
.number()
|
|
38
|
-
.int()
|
|
39
|
-
.min(1)
|
|
40
|
-
.max(256)
|
|
41
|
-
.optional()
|
|
42
|
-
.default(MAX_INVENTORY_ROOTS_DEFAULT)
|
|
43
|
-
.describe("Max nested roots to process (cap)."),
|
|
19
|
+
.describe("Merge with preset instead of replacing."),
|
|
20
|
+
remote: z.string().optional().describe("Pair with `branch`."),
|
|
21
|
+
branch: z.string().optional().describe("Pair with `remote`."),
|
|
22
|
+
maxRoots: z.number().int().min(1).max(256).optional().default(MAX_INVENTORY_ROOTS_DEFAULT),
|
|
44
23
|
}),
|
|
45
24
|
execute: async (args) => {
|
|
46
25
|
const pre = requireGitAndRoots(server, args, args.preset);
|
|
47
26
|
if (!pre.ok) {
|
|
48
27
|
return jsonRespond(pre.error);
|
|
49
28
|
}
|
|
50
|
-
const
|
|
51
|
-
const
|
|
52
|
-
const hasRemote =
|
|
53
|
-
const hasBranch =
|
|
29
|
+
const rawRemote = args.remote?.trim();
|
|
30
|
+
const rawBranch = args.branch?.trim();
|
|
31
|
+
const hasRemote = rawRemote !== undefined && rawRemote !== "";
|
|
32
|
+
const hasBranch = rawBranch !== undefined && rawBranch !== "";
|
|
54
33
|
if (hasRemote !== hasBranch) {
|
|
55
|
-
return jsonRespond({
|
|
56
|
-
error: "remote_branch_mismatch",
|
|
57
|
-
message: "Set both `remote` and `branch` for fixed upstream, or omit both for auto `@{u}`.",
|
|
58
|
-
});
|
|
34
|
+
return jsonRespond({ error: "remote_branch_mismatch" });
|
|
59
35
|
}
|
|
60
|
-
|
|
61
|
-
if (
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
if (!isSafeGitUpstreamToken(r) || !isSafeGitUpstreamToken(b)) {
|
|
65
|
-
return jsonRespond({
|
|
66
|
-
error: "invalid_remote_or_branch",
|
|
67
|
-
message: "remote and branch must be plain tokens: no whitespace, control characters, `@`, `..`, leading `-`, or git rev metacharacters like `^ : ? * [ ] { } ~ \\`.",
|
|
68
|
-
});
|
|
36
|
+
let upstream = { mode: "auto" };
|
|
37
|
+
if (hasRemote && hasBranch && rawRemote && rawBranch) {
|
|
38
|
+
if (!isSafeGitUpstreamToken(rawRemote) || !isSafeGitUpstreamToken(rawBranch)) {
|
|
39
|
+
return jsonRespond({ error: "invalid_remote_or_branch" });
|
|
69
40
|
}
|
|
41
|
+
upstream = { mode: "fixed", remote: rawRemote, branch: rawBranch };
|
|
70
42
|
}
|
|
43
|
+
const useFixed = upstream.mode === "fixed";
|
|
71
44
|
const allJson = [];
|
|
72
45
|
const mdChunks = [];
|
|
73
46
|
for (const workspaceRoot of pre.roots) {
|
|
@@ -77,31 +50,14 @@ export function registerGitInventoryTool(server) {
|
|
|
77
50
|
if (args.format === "json") {
|
|
78
51
|
allJson.push({
|
|
79
52
|
workspace_root: workspaceRoot,
|
|
80
|
-
upstream: {
|
|
81
|
-
mode: useFixed ? "fixed" : "auto",
|
|
82
|
-
remote: fixedRemote,
|
|
83
|
-
branch: fixedBranch,
|
|
84
|
-
},
|
|
53
|
+
...(upstream.mode === "fixed" ? { upstream } : {}),
|
|
85
54
|
entries: [
|
|
86
|
-
|
|
87
|
-
label: workspaceRoot,
|
|
88
|
-
path: workspaceRoot,
|
|
89
|
-
branchStatus: "",
|
|
90
|
-
shortStatus: "",
|
|
91
|
-
detached: false,
|
|
92
|
-
headAbbrev: "",
|
|
93
|
-
upstreamMode: useFixed ? "fixed" : "auto",
|
|
94
|
-
upstreamRef: null,
|
|
95
|
-
ahead: null,
|
|
96
|
-
behind: null,
|
|
97
|
-
upstreamNote: "",
|
|
98
|
-
skipReason: JSON.stringify(err),
|
|
99
|
-
},
|
|
55
|
+
makeSkipEntry(workspaceRoot, workspaceRoot, upstream.mode, JSON.stringify(err)),
|
|
100
56
|
],
|
|
101
57
|
});
|
|
102
58
|
}
|
|
103
59
|
else {
|
|
104
|
-
mdChunks.push(
|
|
60
|
+
mdChunks.push(`### ${workspaceRoot}\n${jsonRespond(err)}`);
|
|
105
61
|
}
|
|
106
62
|
continue;
|
|
107
63
|
}
|
|
@@ -124,31 +80,31 @@ export function registerGitInventoryTool(server) {
|
|
|
124
80
|
nestedRootsTruncated = true;
|
|
125
81
|
}
|
|
126
82
|
const headerNote = useFixed
|
|
127
|
-
? `
|
|
128
|
-
: "upstream:
|
|
83
|
+
? `upstream (fixed): ${upstream.remote}/${upstream.branch}`
|
|
84
|
+
: "upstream: @{u}";
|
|
129
85
|
const entries = [];
|
|
130
86
|
if (nestedRoots?.length) {
|
|
131
87
|
const jobs = [];
|
|
132
88
|
for (const rel of nestedRoots) {
|
|
133
89
|
const { abs, underTop } = validateRepoPath(rel, top);
|
|
134
90
|
if (!underTop) {
|
|
135
|
-
entries.push(makeSkipEntry(rel, abs,
|
|
91
|
+
entries.push(makeSkipEntry(rel, abs, upstream.mode, "(path escapes git toplevel — rejected)"));
|
|
136
92
|
continue;
|
|
137
93
|
}
|
|
138
94
|
if (!gitRevParseGitDir(abs)) {
|
|
139
|
-
entries.push(makeSkipEntry(rel, abs,
|
|
95
|
+
entries.push(makeSkipEntry(rel, abs, upstream.mode, "(not a git work tree — skip)"));
|
|
140
96
|
continue;
|
|
141
97
|
}
|
|
142
98
|
jobs.push({ label: rel, abs });
|
|
143
99
|
}
|
|
144
|
-
const computed = await asyncPool(jobs, GIT_SUBPROCESS_PARALLELISM, (j) => collectInventoryEntry(j.label, j.abs,
|
|
100
|
+
const computed = await asyncPool(jobs, GIT_SUBPROCESS_PARALLELISM, (j) => collectInventoryEntry(j.label, j.abs, upstream.remote, upstream.branch));
|
|
145
101
|
entries.push(...computed);
|
|
146
102
|
}
|
|
147
103
|
else if (!gitRevParseGitDir(top)) {
|
|
148
|
-
entries.push(makeSkipEntry(".", top,
|
|
104
|
+
entries.push(makeSkipEntry(".", top, upstream.mode, "(not a git work tree — unexpected)"));
|
|
149
105
|
}
|
|
150
106
|
else {
|
|
151
|
-
const one = await collectInventoryEntry(".", top,
|
|
107
|
+
const one = await collectInventoryEntry(".", top, upstream.remote, upstream.branch);
|
|
152
108
|
entries.push(one);
|
|
153
109
|
}
|
|
154
110
|
if (args.format === "json") {
|
|
@@ -159,22 +115,14 @@ export function registerGitInventoryTool(server) {
|
|
|
159
115
|
nestedRootsTruncated: true,
|
|
160
116
|
nestedRootsOmittedCount,
|
|
161
117
|
}),
|
|
162
|
-
upstream:
|
|
163
|
-
? { mode: "fixed", remote: fixedRemote, branch: fixedBranch }
|
|
164
|
-
: { mode: "auto" },
|
|
118
|
+
...(upstream.mode === "fixed" ? { upstream } : {}),
|
|
165
119
|
entries,
|
|
166
120
|
});
|
|
167
121
|
}
|
|
168
122
|
else {
|
|
169
|
-
const sections = [
|
|
170
|
-
"# Git inventory",
|
|
171
|
-
"",
|
|
172
|
-
`workspace_root: ${top}`,
|
|
173
|
-
headerNote,
|
|
174
|
-
"",
|
|
175
|
-
];
|
|
123
|
+
const sections = [`### ${top}`, headerNote];
|
|
176
124
|
if (nestedRootsTruncated) {
|
|
177
|
-
sections.push(`nested_roots_truncated: ${nestedRootsOmittedCount} path(s) not listed (maxRoots=${maxRoots})
|
|
125
|
+
sections.push(`nested_roots_truncated: ${nestedRootsOmittedCount} path(s) not listed (maxRoots=${maxRoots})`);
|
|
178
126
|
}
|
|
179
127
|
for (const e of entries) {
|
|
180
128
|
sections.push(...buildInventorySectionMarkdown(e));
|
|
@@ -185,7 +133,7 @@ export function registerGitInventoryTool(server) {
|
|
|
185
133
|
if (args.format === "json") {
|
|
186
134
|
return jsonRespond({ inventories: allJson });
|
|
187
135
|
}
|
|
188
|
-
return mdChunks.join("\n\n
|
|
136
|
+
return ["# Git inventory", ...mdChunks].join("\n\n");
|
|
189
137
|
},
|
|
190
138
|
});
|
|
191
139
|
}
|
|
@@ -8,7 +8,7 @@ import { WorkspacePickSchema } from "./schemas.js";
|
|
|
8
8
|
export function registerGitParityTool(server) {
|
|
9
9
|
server.addTool({
|
|
10
10
|
name: "git_parity",
|
|
11
|
-
description: "Read-only
|
|
11
|
+
description: "Read-only HEAD parity for path pairs. See docs/mcp-tools.md.",
|
|
12
12
|
parameters: WorkspacePickSchema.extend({
|
|
13
13
|
pairs: z
|
|
14
14
|
.array(z.object({
|
|
@@ -55,10 +55,7 @@ export function registerGitParityTool(server) {
|
|
|
55
55
|
parityPresetSchemaVersion = applied.presetSchemaVersion;
|
|
56
56
|
}
|
|
57
57
|
if (!pairs?.length) {
|
|
58
|
-
return jsonRespond({
|
|
59
|
-
error: "no_pairs",
|
|
60
|
-
message: "Pass `pairs` directly or a `preset` with parityPairs (or presetMerge).",
|
|
61
|
-
});
|
|
58
|
+
return jsonRespond({ error: "no_pairs" });
|
|
62
59
|
}
|
|
63
60
|
let allOk = true;
|
|
64
61
|
const pairResults = [];
|
|
@@ -8,14 +8,9 @@ import { WorkspacePickSchema } from "./schemas.js";
|
|
|
8
8
|
export function registerGitStatusTool(server) {
|
|
9
9
|
server.addTool({
|
|
10
10
|
name: "git_status",
|
|
11
|
-
description: "
|
|
12
|
-
"Read-only. Supports multi-root MCP workspaces via `allWorkspaceRoots` or `rootIndex`.",
|
|
11
|
+
description: "Read-only `git status --short -b` per root + submodules. See docs/mcp-tools.md.",
|
|
13
12
|
parameters: WorkspacePickSchema.extend({
|
|
14
|
-
includeSubmodules: z
|
|
15
|
-
.boolean()
|
|
16
|
-
.optional()
|
|
17
|
-
.default(true)
|
|
18
|
-
.describe("When true (default), include submodule paths listed in .gitmodules."),
|
|
13
|
+
includeSubmodules: z.boolean().optional().default(true),
|
|
19
14
|
}),
|
|
20
15
|
execute: async (args) => {
|
|
21
16
|
const pre = requireGitAndRoots(server, args, undefined);
|
|
@@ -69,13 +64,19 @@ export function registerGitStatusTool(server) {
|
|
|
69
64
|
if (args.format === "json") {
|
|
70
65
|
return jsonRespond({ groups });
|
|
71
66
|
}
|
|
72
|
-
const sections = ["# Multi-root git status"
|
|
67
|
+
const sections = [groups.length > 1 ? "# Multi-root git status" : "# Git status"];
|
|
73
68
|
for (const g of groups) {
|
|
74
69
|
if (groups.length > 1) {
|
|
75
|
-
sections.push(`### MCP root: ${g.mcpRoot}
|
|
70
|
+
sections.push("", `### MCP root: ${g.mcpRoot}`);
|
|
76
71
|
}
|
|
77
72
|
for (const row of g.repos) {
|
|
78
|
-
|
|
73
|
+
const body = row.statusText || "(clean)";
|
|
74
|
+
if (body.includes("\n")) {
|
|
75
|
+
sections.push("", `## ${row.label} — ${row.path}`, "```text", body, "```");
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
sections.push("", `## ${row.label} — ${row.path}`, body);
|
|
79
|
+
}
|
|
79
80
|
}
|
|
80
81
|
}
|
|
81
82
|
return sections.join("\n");
|
package/dist/server/git.js
CHANGED
|
@@ -6,7 +6,6 @@ export const GIT_SUBPROCESS_PARALLELISM = 4;
|
|
|
6
6
|
let gitPathState = "unknown";
|
|
7
7
|
const GIT_NOT_FOUND_BODY = {
|
|
8
8
|
error: "git_not_found",
|
|
9
|
-
message: "The `git` binary was not found on PATH or failed `git --version`. Install Git and ensure it is available to the MCP server process.",
|
|
10
9
|
};
|
|
11
10
|
export function gateGit() {
|
|
12
11
|
if (gitPathState === "ok") {
|
|
@@ -81,22 +80,7 @@ export function isSafeGitUpstreamToken(s) {
|
|
|
81
80
|
return false;
|
|
82
81
|
if (t.includes(".."))
|
|
83
82
|
return false;
|
|
84
|
-
|
|
85
|
-
return false;
|
|
86
|
-
if (t.includes("@"))
|
|
87
|
-
return false;
|
|
88
|
-
const forbidden = new Set("{}[]~^:?*\\".split(""));
|
|
89
|
-
for (let i = 0; i < t.length; i++) {
|
|
90
|
-
const ch = t.charAt(i);
|
|
91
|
-
const c = ch.charCodeAt(0);
|
|
92
|
-
if (c < 32 || c === 127)
|
|
93
|
-
return false;
|
|
94
|
-
if (/\s/.test(ch))
|
|
95
|
-
return false;
|
|
96
|
-
if (forbidden.has(ch))
|
|
97
|
-
return false;
|
|
98
|
-
}
|
|
99
|
-
return true;
|
|
83
|
+
return /^(?!-)[A-Za-z0-9_./+-]+$/.test(t);
|
|
100
84
|
}
|
|
101
85
|
// ---------------------------------------------------------------------------
|
|
102
86
|
// Async pool for parallel git (inventory)
|
|
@@ -140,16 +124,11 @@ function gitStatusFailText(r) {
|
|
|
140
124
|
return (r.stderr || r.stdout || "git status failed").trim();
|
|
141
125
|
}
|
|
142
126
|
export async function gitStatusSnapshotAsync(cwd) {
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
return {
|
|
148
|
-
branchOk: br.ok,
|
|
149
|
-
shortOk: sh.ok,
|
|
150
|
-
branchLine: br.ok ? br.stdout.trimEnd() : gitStatusFailText(br),
|
|
151
|
-
shortLine: sh.ok ? sh.stdout.trimEnd() : gitStatusFailText(sh),
|
|
152
|
-
};
|
|
127
|
+
const r = await spawnGitAsync(cwd, ["status", "--short", "-b"]);
|
|
128
|
+
if (!r.ok) {
|
|
129
|
+
return { branchOk: false, branchLine: gitStatusFailText(r) };
|
|
130
|
+
}
|
|
131
|
+
return { branchOk: true, branchLine: r.stdout.trimEnd() };
|
|
153
132
|
}
|
|
154
133
|
export async function gitStatusShortBranchAsync(cwd) {
|
|
155
134
|
const s = await gitStatusSnapshotAsync(cwd);
|
package/dist/server/inventory.js
CHANGED
|
@@ -6,48 +6,52 @@ export function validateRepoPath(rel, gitTop) {
|
|
|
6
6
|
return { abs, underTop: assertRelativePathUnderTop(rel, abs, gitTop) };
|
|
7
7
|
}
|
|
8
8
|
export function makeSkipEntry(label, abs, upstreamMode, skipReason) {
|
|
9
|
-
return {
|
|
10
|
-
label,
|
|
11
|
-
path: abs,
|
|
12
|
-
branchStatus: "",
|
|
13
|
-
shortStatus: "",
|
|
14
|
-
detached: false,
|
|
15
|
-
headAbbrev: "",
|
|
16
|
-
upstreamMode,
|
|
17
|
-
upstreamRef: null,
|
|
18
|
-
ahead: null,
|
|
19
|
-
behind: null,
|
|
20
|
-
upstreamNote: "",
|
|
21
|
-
skipReason,
|
|
22
|
-
};
|
|
9
|
+
return { label, path: abs, upstreamMode, skipReason };
|
|
23
10
|
}
|
|
24
11
|
export function buildInventorySectionMarkdown(e) {
|
|
12
|
+
const header = `## ${e.label} — ${e.path}`;
|
|
25
13
|
if (e.skipReason) {
|
|
26
|
-
return [
|
|
27
|
-
}
|
|
28
|
-
const lines = [];
|
|
29
|
-
lines.push(e.branchStatus);
|
|
30
|
-
lines.push("");
|
|
31
|
-
lines.push("short:");
|
|
32
|
-
lines.push(e.shortStatus || "(clean)");
|
|
33
|
-
lines.push("");
|
|
34
|
-
if (e.detached) {
|
|
35
|
-
lines.push("branch: (detached HEAD)");
|
|
36
|
-
lines.push("");
|
|
14
|
+
return ["", header, e.skipReason];
|
|
37
15
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
lines.push(
|
|
16
|
+
const lines = [e.branchStatus || "(clean)"];
|
|
17
|
+
if (e.detached)
|
|
18
|
+
lines.push("detached HEAD");
|
|
19
|
+
if (e.ahead !== undefined && e.behind !== undefined && e.upstreamRef) {
|
|
20
|
+
lines.push(`${e.upstreamRef}: ahead ${e.ahead}, behind ${e.behind}`);
|
|
41
21
|
}
|
|
42
|
-
else {
|
|
22
|
+
else if (e.upstreamNote) {
|
|
43
23
|
lines.push(`upstream: ${e.upstreamNote}`);
|
|
44
24
|
}
|
|
45
|
-
|
|
25
|
+
const single = lines.length === 1 ? lines[0] : undefined;
|
|
26
|
+
if (single !== undefined && !single.includes("\n")) {
|
|
27
|
+
return ["", header, single];
|
|
28
|
+
}
|
|
29
|
+
return ["", header, "```text", lines.join("\n"), "```"];
|
|
46
30
|
}
|
|
47
|
-
function upstreamNoteFor(ref,
|
|
48
|
-
return
|
|
49
|
-
|
|
50
|
-
|
|
31
|
+
function upstreamNoteFor(ref, hasCounts) {
|
|
32
|
+
return hasCounts ? `tracking ${ref}` : `upstream ${ref} (counts unreadable)`;
|
|
33
|
+
}
|
|
34
|
+
function buildEntry(params) {
|
|
35
|
+
const out = {
|
|
36
|
+
label: params.label,
|
|
37
|
+
path: params.absPath,
|
|
38
|
+
upstreamMode: params.upstreamMode,
|
|
39
|
+
};
|
|
40
|
+
if (params.branchStatus)
|
|
41
|
+
out.branchStatus = params.branchStatus;
|
|
42
|
+
if (params.detached)
|
|
43
|
+
out.detached = true;
|
|
44
|
+
if (params.headAbbrev)
|
|
45
|
+
out.headAbbrev = params.headAbbrev;
|
|
46
|
+
if (params.upstreamRef !== null)
|
|
47
|
+
out.upstreamRef = params.upstreamRef;
|
|
48
|
+
if (params.ahead !== null)
|
|
49
|
+
out.ahead = params.ahead;
|
|
50
|
+
if (params.behind !== null)
|
|
51
|
+
out.behind = params.behind;
|
|
52
|
+
if (params.upstreamNote)
|
|
53
|
+
out.upstreamNote = params.upstreamNote;
|
|
54
|
+
return out;
|
|
51
55
|
}
|
|
52
56
|
export async function collectInventoryEntry(label, absPath, fixedRemote, fixedBranch) {
|
|
53
57
|
const [snap, headR] = await Promise.all([
|
|
@@ -55,79 +59,52 @@ export async function collectInventoryEntry(label, absPath, fixedRemote, fixedBr
|
|
|
55
59
|
spawnGitAsync(absPath, ["rev-parse", "--abbrev-ref", "HEAD"]),
|
|
56
60
|
]);
|
|
57
61
|
const branchStatus = snap.branchLine;
|
|
58
|
-
const shortStatus = snap.shortLine;
|
|
59
62
|
const headAbbrev = headR.ok ? headR.stdout.trim() : "";
|
|
60
63
|
const detached = !headR.ok || headAbbrev === "HEAD" || headAbbrev.endsWith("/HEAD");
|
|
61
|
-
const
|
|
62
|
-
if (
|
|
63
|
-
const
|
|
64
|
-
const
|
|
65
|
-
const verify = await spawnGitAsync(absPath, ["rev-parse", "--verify", `${remote}/${branch}`]);
|
|
64
|
+
const base = { label, absPath, branchStatus, detached, headAbbrev };
|
|
65
|
+
if (fixedRemote !== undefined && fixedBranch !== undefined) {
|
|
66
|
+
const ref = `${fixedRemote}/${fixedBranch}`;
|
|
67
|
+
const verify = await spawnGitAsync(absPath, ["rev-parse", "--verify", ref]);
|
|
66
68
|
if (!verify.ok) {
|
|
67
|
-
return {
|
|
68
|
-
|
|
69
|
-
path: absPath,
|
|
70
|
-
branchStatus,
|
|
71
|
-
shortStatus,
|
|
72
|
-
detached,
|
|
73
|
-
headAbbrev: headAbbrev || "(unknown)",
|
|
69
|
+
return buildEntry({
|
|
70
|
+
...base,
|
|
74
71
|
upstreamMode: "fixed",
|
|
75
|
-
upstreamRef:
|
|
72
|
+
upstreamRef: ref,
|
|
76
73
|
ahead: null,
|
|
77
74
|
behind: null,
|
|
78
|
-
upstreamNote: `(no local ref ${
|
|
79
|
-
};
|
|
75
|
+
upstreamNote: `(no local ref ${ref} or unreadable)`,
|
|
76
|
+
});
|
|
80
77
|
}
|
|
81
|
-
const ref = `${remote}/${branch}`;
|
|
82
78
|
const { ahead, behind } = await fetchAheadBehind(absPath, ref);
|
|
83
|
-
return {
|
|
84
|
-
|
|
85
|
-
path: absPath,
|
|
86
|
-
branchStatus,
|
|
87
|
-
shortStatus,
|
|
88
|
-
detached,
|
|
89
|
-
headAbbrev: headAbbrev || "(unknown)",
|
|
79
|
+
return buildEntry({
|
|
80
|
+
...base,
|
|
90
81
|
upstreamMode: "fixed",
|
|
91
82
|
upstreamRef: ref,
|
|
92
83
|
ahead,
|
|
93
84
|
behind,
|
|
94
|
-
upstreamNote: upstreamNoteFor(ref, ahead
|
|
95
|
-
};
|
|
85
|
+
upstreamNote: upstreamNoteFor(ref, ahead != null && behind != null),
|
|
86
|
+
});
|
|
96
87
|
}
|
|
97
88
|
const upVerify = await spawnGitAsync(absPath, ["rev-parse", "--verify", "@{u}"]);
|
|
98
89
|
if (!upVerify.ok) {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
note = "detached HEAD — no upstream";
|
|
102
|
-
}
|
|
103
|
-
return {
|
|
104
|
-
label,
|
|
105
|
-
path: absPath,
|
|
106
|
-
branchStatus,
|
|
107
|
-
shortStatus,
|
|
108
|
-
detached,
|
|
109
|
-
headAbbrev: headAbbrev || "(unknown)",
|
|
90
|
+
return buildEntry({
|
|
91
|
+
...base,
|
|
110
92
|
upstreamMode: "auto",
|
|
111
93
|
upstreamRef: null,
|
|
112
94
|
ahead: null,
|
|
113
95
|
behind: null,
|
|
114
|
-
upstreamNote:
|
|
115
|
-
};
|
|
96
|
+
upstreamNote: detached ? "detached HEAD — no upstream" : "no upstream configured",
|
|
97
|
+
});
|
|
116
98
|
}
|
|
117
99
|
const abbrevR = await spawnGitAsync(absPath, ["rev-parse", "--abbrev-ref", "@{u}"]);
|
|
118
100
|
const upstreamRef = abbrevR.ok ? abbrevR.stdout.trim() : "@{u}";
|
|
119
101
|
const { ahead, behind } = await fetchAheadBehind(absPath, "@{u}");
|
|
120
|
-
return {
|
|
121
|
-
|
|
122
|
-
path: absPath,
|
|
123
|
-
branchStatus,
|
|
124
|
-
shortStatus,
|
|
125
|
-
detached,
|
|
126
|
-
headAbbrev: headAbbrev || "(unknown)",
|
|
102
|
+
return buildEntry({
|
|
103
|
+
...base,
|
|
127
104
|
upstreamMode: "auto",
|
|
128
105
|
upstreamRef,
|
|
129
106
|
ahead,
|
|
130
107
|
behind,
|
|
131
|
-
upstreamNote: upstreamNoteFor(upstreamRef, ahead
|
|
132
|
-
};
|
|
108
|
+
upstreamNote: upstreamNoteFor(upstreamRef, ahead != null && behind != null),
|
|
109
|
+
});
|
|
133
110
|
}
|
package/dist/server/json.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
-
export const MCP_JSON_FORMAT_VERSION = "
|
|
4
|
+
export const MCP_JSON_FORMAT_VERSION = "2";
|
|
5
5
|
export function readPackageVersion() {
|
|
6
6
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
7
7
|
const pkgPath = join(here, "..", "..", "package.json");
|
|
@@ -23,13 +23,7 @@ export function readMcpServerVersion() {
|
|
|
23
23
|
return "0.0.0";
|
|
24
24
|
}
|
|
25
25
|
export function jsonRespond(body) {
|
|
26
|
-
return JSON.stringify(
|
|
27
|
-
...body,
|
|
28
|
-
rethunkGitMcp: {
|
|
29
|
-
jsonFormatVersion: MCP_JSON_FORMAT_VERSION,
|
|
30
|
-
packageVersion: readPackageVersion(),
|
|
31
|
-
},
|
|
32
|
-
}, null, 2);
|
|
26
|
+
return JSON.stringify(body);
|
|
33
27
|
}
|
|
34
28
|
/** Spread into an object literal only when `cond` is true; otherwise `{}`. */
|
|
35
29
|
export function spreadWhen(cond, fields) {
|
|
@@ -7,7 +7,7 @@ import { WorkspacePickSchema } from "./schemas.js";
|
|
|
7
7
|
export function registerListPresetsTool(server) {
|
|
8
8
|
server.addTool({
|
|
9
9
|
name: "list_presets",
|
|
10
|
-
description: "List
|
|
10
|
+
description: "List presets from .rethunk/git-mcp-presets.json. See docs/mcp-tools.md.",
|
|
11
11
|
parameters: WorkspacePickSchema.pick({
|
|
12
12
|
workspaceRoot: true,
|
|
13
13
|
rootIndex: true,
|
package/dist/server/presets.js
CHANGED
|
@@ -101,19 +101,20 @@ export function getPresetEntry(gitTop, presetName) {
|
|
|
101
101
|
if (!loaded.ok) {
|
|
102
102
|
if (loaded.reason === "missing") {
|
|
103
103
|
return {
|
|
104
|
+
ok: false,
|
|
104
105
|
error: {
|
|
105
106
|
error: "preset_not_found",
|
|
106
107
|
preset: presetName,
|
|
107
108
|
presetFile: join(gitTop, PRESET_FILE_PATH),
|
|
108
|
-
message: "Preset file missing",
|
|
109
109
|
},
|
|
110
110
|
};
|
|
111
111
|
}
|
|
112
|
-
return { error: presetLoadErrorPayload(gitTop, loaded) };
|
|
112
|
+
return { ok: false, error: presetLoadErrorPayload(gitTop, loaded) };
|
|
113
113
|
}
|
|
114
114
|
const entry = loaded.data[presetName];
|
|
115
115
|
if (!entry) {
|
|
116
116
|
return {
|
|
117
|
+
ok: false,
|
|
117
118
|
error: {
|
|
118
119
|
error: "preset_not_found",
|
|
119
120
|
preset: presetName,
|
|
@@ -121,7 +122,7 @@ export function getPresetEntry(gitTop, presetName) {
|
|
|
121
122
|
},
|
|
122
123
|
};
|
|
123
124
|
}
|
|
124
|
-
return { entry, presetSchemaVersion: loaded.schemaVersion };
|
|
125
|
+
return { ok: true, entry, presetSchemaVersion: loaded.schemaVersion };
|
|
125
126
|
}
|
|
126
127
|
export function mergeNestedRoots(preset, inline) {
|
|
127
128
|
const a = preset ?? [];
|
|
@@ -147,7 +148,7 @@ export function mergePairs(preset, inline) {
|
|
|
147
148
|
}
|
|
148
149
|
export function applyPresetNestedRoots(gitTop, presetName, presetMerge, inlineNestedRoots) {
|
|
149
150
|
const got = getPresetEntry(gitTop, presetName);
|
|
150
|
-
if (
|
|
151
|
+
if (!got.ok)
|
|
151
152
|
return { ok: false, error: got.error };
|
|
152
153
|
const fromPreset = got.entry.nestedRoots;
|
|
153
154
|
let nestedRoots = inlineNestedRoots;
|
|
@@ -161,7 +162,7 @@ export function applyPresetNestedRoots(gitTop, presetName, presetMerge, inlineNe
|
|
|
161
162
|
}
|
|
162
163
|
export function applyPresetParityPairs(gitTop, presetName, presetMerge, inlinePairs) {
|
|
163
164
|
const got = getPresetEntry(gitTop, presetName);
|
|
164
|
-
if (
|
|
165
|
+
if (!got.ok)
|
|
165
166
|
return { ok: false, error: got.error };
|
|
166
167
|
const fromPreset = got.entry.parityPairs;
|
|
167
168
|
let pairs = inlinePairs;
|
package/dist/server/roots.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { basename,
|
|
1
|
+
import { basename, resolve } from "node:path";
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
3
|
import { gateGit, gitTopLevel } from "./git.js";
|
|
4
4
|
import { loadPresetsFromGitTop, presetLoadErrorPayload } from "./presets.js";
|
|
@@ -28,36 +28,25 @@ export function pathMatchesWorkspaceRootHint(rootPath, hint) {
|
|
|
28
28
|
const h = hint.trim();
|
|
29
29
|
if (!h)
|
|
30
30
|
return true;
|
|
31
|
-
if (basename(rootPath) === h)
|
|
32
|
-
return true;
|
|
33
31
|
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
32
|
const normRoot = absRoot.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
44
33
|
const normHint = h.replace(/\\/g, "/").replace(/\/+$/, "").replace(/^\/+/, "");
|
|
45
34
|
if (!normHint)
|
|
46
35
|
return true;
|
|
47
36
|
if (normRoot === normHint)
|
|
48
37
|
return true;
|
|
49
|
-
|
|
38
|
+
if (normRoot.endsWith(`/${normHint}`))
|
|
39
|
+
return true;
|
|
40
|
+
return basename(rootPath) === h;
|
|
50
41
|
}
|
|
51
42
|
export function resolveWorkspaceRoots(server, args) {
|
|
52
43
|
if (args.workspaceRoot?.trim()) {
|
|
53
44
|
return { ok: true, roots: [resolve(args.workspaceRoot.trim())] };
|
|
54
45
|
}
|
|
55
46
|
const fileRoots = listFileRoots(server);
|
|
47
|
+
const fallback = { ok: true, roots: [process.cwd()] };
|
|
56
48
|
if (args.allWorkspaceRoots) {
|
|
57
|
-
|
|
58
|
-
return { ok: true, roots: [process.cwd()] };
|
|
59
|
-
}
|
|
60
|
-
return { ok: true, roots: fileRoots };
|
|
49
|
+
return fileRoots.length === 0 ? fallback : { ok: true, roots: fileRoots };
|
|
61
50
|
}
|
|
62
51
|
if (args.rootIndex != null) {
|
|
63
52
|
const r = fileRoots[args.rootIndex];
|
|
@@ -74,10 +63,7 @@ export function resolveWorkspaceRoots(server, args) {
|
|
|
74
63
|
return { ok: true, roots: [r] };
|
|
75
64
|
}
|
|
76
65
|
const primary = fileRoots[0];
|
|
77
|
-
|
|
78
|
-
return { ok: true, roots: [primary] };
|
|
79
|
-
}
|
|
80
|
-
return { ok: true, roots: [process.cwd()] };
|
|
66
|
+
return primary !== undefined ? { ok: true, roots: [primary] } : fallback;
|
|
81
67
|
}
|
|
82
68
|
/**
|
|
83
69
|
* When a preset name is requested and multiple MCP roots exist, pick the first root
|
package/dist/server/schemas.js
CHANGED
|
@@ -2,21 +2,9 @@ import { z } from "zod";
|
|
|
2
2
|
import { MAX_INVENTORY_ROOTS_DEFAULT } from "./inventory.js";
|
|
3
3
|
const FormatSchema = z.enum(["markdown", "json"]).optional().default("markdown");
|
|
4
4
|
export const WorkspacePickSchema = z.object({
|
|
5
|
-
workspaceRoot: z
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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".'),
|
|
5
|
+
workspaceRoot: z.string().optional().describe("Highest-priority override."),
|
|
6
|
+
rootIndex: z.number().int().min(0).optional(),
|
|
7
|
+
allWorkspaceRoots: z.boolean().optional().default(false),
|
|
8
|
+
format: FormatSchema,
|
|
21
9
|
});
|
|
22
10
|
export { MAX_INVENTORY_ROOTS_DEFAULT };
|
package/docs/mcp-tools.md
CHANGED
|
@@ -20,22 +20,34 @@ Pass **`format: "json"`** on any tool for structured JSON instead of markdown (d
|
|
|
20
20
|
|
|
21
21
|
## JSON responses
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
Tool JSON bodies are minified and contain only the payload — no `rethunkGitMcp` envelope. Current `MCP_JSON_FORMAT_VERSION` is **`"2"`**; server + format version are discoverable via MCP `initialize`. Payload keys (`groups`, `inventories`, `parity`, `roots`) are stable within a given format version. Preset-related responses may include **`presetSchemaVersion`**.
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
- **`packageVersion`**: from the running server’s `package.json`.
|
|
25
|
+
### v2 field omission (consumer contract)
|
|
27
26
|
|
|
28
|
-
|
|
27
|
+
To keep responses compact, **optional fields are omitted when they would be empty, `null`, or `false`** — they are not emitted as `null`. Consumers must test for *presence*, not compare to `null`.
|
|
29
28
|
|
|
30
|
-
|
|
29
|
+
**`git_inventory` → `inventories[*]`**
|
|
31
30
|
|
|
32
|
-
|
|
31
|
+
- Always present: `workspace_root`, `entries`.
|
|
32
|
+
- Omitted when not applicable: `presetSchemaVersion`, `nestedRootsTruncated`, `nestedRootsOmittedCount`, and the whole `upstream` object (emitted only when a fixed `remote`/`branch` pair was supplied; in `auto` mode it is absent).
|
|
33
|
+
|
|
34
|
+
**`git_inventory` → `entries[*]` (`InventoryEntryJson`)**
|
|
35
|
+
|
|
36
|
+
- Always present: `label`, `path`, `upstreamMode` (`"auto"` or `"fixed"`).
|
|
37
|
+
- Optional (omitted when empty/absent): `branchStatus`, `headAbbrev`, `upstreamRef`, `ahead`, `behind`, `upstreamNote`, `detached` (only emitted as `true`), `skipReason` (only on skipped entries).
|
|
38
|
+
- **Removed in v2:** `shortStatus`. The porcelain entries now live inside `branchStatus` (the full `git status --short -b` body — branch header line followed by porcelain lines).
|
|
39
|
+
|
|
40
|
+
**Errors** (any tool)
|
|
41
|
+
|
|
42
|
+
- Error payloads carry an `error` code string and any structured context (e.g. `preset`, `presetFile`). The old free-text `message` field is **removed** for self-describing codes (`git_not_found`, `remote_branch_mismatch`, `invalid_remote_or_branch`, `no_pairs`, `preset_not_found` *missing* case). It is retained only where it carries parse output (the `invalid_json` preset branch).
|
|
43
|
+
|
|
44
|
+
**When to bump `MCP_JSON_FORMAT_VERSION` or change payload shape:** [AGENTS.md](../AGENTS.md) — *Changing contracts*.
|
|
33
45
|
|
|
34
46
|
## Resource
|
|
35
47
|
|
|
36
48
|
| URI | Purpose |
|
|
37
49
|
|-----|---------|
|
|
38
|
-
| `rethunk-git://presets` | JSON snapshot of `.rethunk/git-mcp-presets.json` at the resolved git toplevel (or structured errors
|
|
50
|
+
| `rethunk-git://presets` | JSON snapshot of `.rethunk/git-mcp-presets.json` at the resolved git toplevel (or structured errors). |
|
|
39
51
|
|
|
40
52
|
## Workspace root resolution
|
|
41
53
|
|
|
@@ -43,7 +55,7 @@ Order applied when resolving which directory(ies) tools run against:
|
|
|
43
55
|
|
|
44
56
|
1. Explicit **`workspaceRoot`** on the tool call (highest priority).
|
|
45
57
|
2. **`rootIndex`** (0-based) — one `file://` MCP root when several exist.
|
|
46
|
-
3. **`allWorkspaceRoots`: true** — every `file://` root; markdown
|
|
58
|
+
3. **`allWorkspaceRoots`: true** — every `file://` root; markdown output emits one `# {tool}` header with per-root subsections (`git_inventory` uses `### {gitTop}`; `git_status` uses `### MCP root: ...`), or combined JSON.
|
|
47
59
|
4. **`preset`** set and multiple roots — first root whose git toplevel defines that preset (respecting **`workspaceRootHint`** on the preset entry when present).
|
|
48
60
|
5. Otherwise the first `file://` root from MCP **`initialize`** / **`roots/list_changed`**.
|
|
49
61
|
6. **`process.cwd()`** if no file roots (e.g. CI with explicit `workspaceRoot`).
|
package/package.json
CHANGED