-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgit-status-tool.ts
More file actions
106 lines (97 loc) · 3.42 KB
/
git-status-tool.ts
File metadata and controls
106 lines (97 loc) · 3.42 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
import { join, resolve } from "node:path";
import type { FastMCP } from "fastmcp";
import { z } from "zod";
import { isStrictlyUnderGitTop } from "../repo-paths.js";
import {
asyncPool,
GIT_SUBPROCESS_PARALLELISM,
gitStatusShortBranchAsync,
gitTopLevel,
hasGitMetadata,
parseGitSubmodulePaths,
} from "./git.js";
import { jsonRespond } from "./json.js";
import { requireGitAndRoots } from "./roots.js";
import { WorkspacePickSchema } from "./schemas.js";
export function registerGitStatusTool(server: FastMCP): void {
server.addTool({
name: "git_status",
description: "Read-only `git status --short -b` per root + submodules.",
annotations: {
readOnlyHint: true,
},
parameters: WorkspacePickSchema.extend({
includeSubmodules: z.boolean().optional().default(true),
}),
execute: async (args) => {
const pre = requireGitAndRoots(server, args, undefined);
if (!pre.ok) {
return jsonRespond(pre.error);
}
type RepoRow = { label: string; path: string; statusText: string; ok: boolean };
type Group = { mcpRoot: string; repos: RepoRow[] };
const groups: Group[] = [];
for (const rootInput of pre.roots) {
const repos: RepoRow[] = [];
const top = gitTopLevel(rootInput);
if (!top) {
repos.push({
label: rootInput,
path: rootInput,
statusText: "not a git repository",
ok: false,
});
groups.push({ mcpRoot: rootInput, repos });
continue;
}
const includeSubmodules = args.includeSubmodules !== false;
const meta = await gitStatusShortBranchAsync(top);
repos.push({ label: ".", path: top, statusText: meta.text, ok: meta.ok });
if (includeSubmodules) {
const rels = parseGitSubmodulePaths(top);
const subRows = await asyncPool(rels, GIT_SUBPROCESS_PARALLELISM, async (rel) => {
const subPath = resolve(join(top, rel));
if (!isStrictlyUnderGitTop(subPath, top)) {
return {
label: rel,
path: subPath,
statusText: "(submodule path escapes repository — rejected)",
ok: false,
};
}
if (!hasGitMetadata(subPath)) {
return {
label: rel,
path: subPath,
statusText: "(no .git — submodule not checked out?)",
ok: false,
};
}
const st = await gitStatusShortBranchAsync(subPath);
return { label: rel, path: subPath, statusText: st.text, ok: st.ok };
});
repos.push(...subRows);
}
groups.push({ mcpRoot: rootInput, repos });
}
if (args.format === "json") {
return jsonRespond({ groups });
}
const sections: string[] = [groups.length > 1 ? "# Multi-root git status" : "# Git status"];
for (const g of groups) {
if (groups.length > 1) {
sections.push("", `### MCP root: ${g.mcpRoot}`);
}
for (const row of g.repos) {
const body = row.statusText || "(clean)";
if (body.includes("\n")) {
sections.push("", `## ${row.label} — ${row.path}`, "```text", body, "```");
} else {
sections.push("", `## ${row.label} — ${row.path}`, body);
}
}
}
return sections.join("\n");
},
});
}