Skip to content

Commit a3d1731

Browse files
committed
refactor(server): tighten git helpers and inventory shape
- isSafeGitUpstreamToken: allowlist regex over per-char loop - gitStatusSnapshotAsync: one `status -b` instead of two subprocesses; derive short output from the same payload - collectInventoryEntry: extract buildEntry helper, dedupe four return paths, drop ~40 lines - upstreamNoteFor: take hasCounts boolean, signature no longer lies - getPresetEntry: return discriminated {ok} union to match PresetLoadResult convention used elsewhere in the file - pathMatchesWorkspaceRootHint: drop redundant pre-normalized branches - resolveWorkspaceRoots: hoist cwd fallback - git-inventory-tool: reuse makeSkipEntry for not_a_git_repository branch; drop dead String().trim() noise after non-empty guard Behavior preserved; halves the subprocess count on the status path.
1 parent 93b7c67 commit a3d1731

File tree

5 files changed

+79
-126
lines changed

5 files changed

+79
-126
lines changed

src/server/git-inventory-tool.ts

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,7 @@ export function registerGitInventoryTool(server: FastMCP): void {
8181
}
8282
const useFixed = hasRemote && hasBranch;
8383
if (useFixed) {
84-
const r = String(fixedRemote).trim();
85-
const b = String(fixedBranch).trim();
86-
if (!isSafeGitUpstreamToken(r) || !isSafeGitUpstreamToken(b)) {
84+
if (!isSafeGitUpstreamToken(fixedRemote!.trim()) || !isSafeGitUpstreamToken(fixedBranch!.trim())) {
8785
return jsonRespond({
8886
error: "invalid_remote_or_branch",
8987
message:
@@ -113,22 +111,7 @@ export function registerGitInventoryTool(server: FastMCP): void {
113111
remote: fixedRemote,
114112
branch: fixedBranch,
115113
},
116-
entries: [
117-
{
118-
label: workspaceRoot,
119-
path: workspaceRoot,
120-
branchStatus: "",
121-
shortStatus: "",
122-
detached: false,
123-
headAbbrev: "",
124-
upstreamMode: useFixed ? "fixed" : "auto",
125-
upstreamRef: null,
126-
ahead: null,
127-
behind: null,
128-
upstreamNote: "",
129-
skipReason: JSON.stringify(err),
130-
},
131-
],
114+
entries: [makeSkipEntry(workspaceRoot, workspaceRoot, useFixed ? "fixed" : "auto", JSON.stringify(err))],
132115
});
133116
} else {
134117
mdChunks.push(`# Git inventory`, "", jsonRespond(err), "");

src/server/git.ts

Lines changed: 12 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -94,17 +94,7 @@ export function isSafeGitUpstreamToken(s: string): boolean {
9494
const t = s.trim();
9595
if (t.length === 0 || t.length > 256) return false;
9696
if (t.includes("..")) return false;
97-
if (t.startsWith("-")) return false;
98-
if (t.includes("@")) return false;
99-
const forbidden = new Set("{}[]~^:?*\\".split(""));
100-
for (let i = 0; i < t.length; i++) {
101-
const ch = t.charAt(i);
102-
const c = ch.charCodeAt(0);
103-
if (c < 32 || c === 127) return false;
104-
if (/\s/.test(ch)) return false;
105-
if (forbidden.has(ch)) return false;
106-
}
107-
return true;
97+
return /^(?!-)[A-Za-z0-9_./+-]+$/.test(t);
10898
}
10999

110100
// ---------------------------------------------------------------------------
@@ -122,9 +112,7 @@ export async function asyncPool<T, R>(
122112
for (;;) {
123113
const i = next++;
124114
if (i >= items.length) break;
125-
const item = items[i];
126-
if (item === undefined) break;
127-
results[i] = await fn(item);
115+
results[i] = await fn(items[i]!);
128116
}
129117
}
130118
const n = Math.min(Math.max(1, concurrency), Math.max(1, items.length));
@@ -163,16 +151,16 @@ export async function gitStatusSnapshotAsync(cwd: string): Promise<{
163151
branchOk: boolean;
164152
shortOk: boolean;
165153
}> {
166-
const [br, sh] = await Promise.all([
167-
spawnGitAsync(cwd, ["status", "--short", "-b"]),
168-
spawnGitAsync(cwd, ["status", "--short"]),
169-
]);
170-
return {
171-
branchOk: br.ok,
172-
shortOk: sh.ok,
173-
branchLine: br.ok ? br.stdout.trimEnd() : gitStatusFailText(br),
174-
shortLine: sh.ok ? sh.stdout.trimEnd() : gitStatusFailText(sh),
175-
};
154+
const r = await spawnGitAsync(cwd, ["status", "--short", "-b"]);
155+
if (!r.ok) {
156+
const text = gitStatusFailText(r);
157+
return { branchOk: false, shortOk: false, branchLine: text, shortLine: text };
158+
}
159+
const full = r.stdout.trimEnd();
160+
const nl = full.indexOf("\n");
161+
const branchLine = full;
162+
const shortLine = nl >= 0 ? full.slice(nl + 1) : "";
163+
return { branchOk: true, shortOk: true, branchLine, shortLine };
176164
}
177165

178166
export async function gitStatusShortBranchAsync(

src/server/inventory.ts

Lines changed: 51 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -68,10 +68,36 @@ export function buildInventorySectionMarkdown(e: InventoryEntryJson): string[] {
6868
return [`## ${e.label}`, `path: ${e.path}`, "```text", lines.join("\n"), "```", ``];
6969
}
7070

71-
function upstreamNoteFor(ref: string, ahead: string | null, behind: string | null): string {
72-
return ahead != null && behind != null
73-
? `tracking ${ref}`
74-
: `upstream ${ref} (counts unreadable)`;
71+
function upstreamNoteFor(ref: string, hasCounts: boolean): string {
72+
return hasCounts ? `tracking ${ref}` : `upstream ${ref} (counts unreadable)`;
73+
}
74+
75+
function buildEntry(params: {
76+
label: string;
77+
absPath: string;
78+
branchStatus: string;
79+
shortStatus: string;
80+
detached: boolean;
81+
headAbbrev: string;
82+
upstreamMode: "auto" | "fixed";
83+
upstreamRef: string | null;
84+
ahead: string | null;
85+
behind: string | null;
86+
upstreamNote: string;
87+
}): InventoryEntryJson {
88+
return {
89+
label: params.label,
90+
path: params.absPath,
91+
branchStatus: params.branchStatus,
92+
shortStatus: params.shortStatus,
93+
detached: params.detached,
94+
headAbbrev: params.headAbbrev || "(unknown)",
95+
upstreamMode: params.upstreamMode,
96+
upstreamRef: params.upstreamRef,
97+
ahead: params.ahead,
98+
behind: params.behind,
99+
upstreamNote: params.upstreamNote,
100+
};
75101
}
76102

77103
export async function collectInventoryEntry(
@@ -89,81 +115,53 @@ export async function collectInventoryEntry(
89115
const shortStatus = snap.shortLine;
90116
const headAbbrev = headR.ok ? headR.stdout.trim() : "";
91117
const detached = !headR.ok || headAbbrev === "HEAD" || headAbbrev.endsWith("/HEAD");
118+
const base = { label, absPath, branchStatus, shortStatus, detached, headAbbrev };
92119

93-
const useFixed = fixedRemote !== undefined && fixedBranch !== undefined;
94-
95-
if (useFixed) {
96-
const remote = fixedRemote;
97-
const branch = fixedBranch;
98-
const verify = await spawnGitAsync(absPath, ["rev-parse", "--verify", `${remote}/${branch}`]);
120+
if (fixedRemote !== undefined && fixedBranch !== undefined) {
121+
const ref = `${fixedRemote}/${fixedBranch}`;
122+
const verify = await spawnGitAsync(absPath, ["rev-parse", "--verify", ref]);
99123
if (!verify.ok) {
100-
return {
101-
label,
102-
path: absPath,
103-
branchStatus,
104-
shortStatus,
105-
detached,
106-
headAbbrev: headAbbrev || "(unknown)",
124+
return buildEntry({
125+
...base,
107126
upstreamMode: "fixed",
108-
upstreamRef: `${remote}/${branch}`,
127+
upstreamRef: ref,
109128
ahead: null,
110129
behind: null,
111-
upstreamNote: `(no local ref ${remote}/${branch} or unreadable)`,
112-
};
130+
upstreamNote: `(no local ref ${ref} or unreadable)`,
131+
});
113132
}
114-
const ref = `${remote}/${branch}`;
115133
const { ahead, behind } = await fetchAheadBehind(absPath, ref);
116-
return {
117-
label,
118-
path: absPath,
119-
branchStatus,
120-
shortStatus,
121-
detached,
122-
headAbbrev: headAbbrev || "(unknown)",
134+
return buildEntry({
135+
...base,
123136
upstreamMode: "fixed",
124137
upstreamRef: ref,
125138
ahead,
126139
behind,
127-
upstreamNote: upstreamNoteFor(ref, ahead, behind),
128-
};
140+
upstreamNote: upstreamNoteFor(ref, ahead != null && behind != null),
141+
});
129142
}
130143

131144
const upVerify = await spawnGitAsync(absPath, ["rev-parse", "--verify", "@{u}"]);
132145
if (!upVerify.ok) {
133-
let note = "no upstream configured";
134-
if (detached) {
135-
note = "detached HEAD — no upstream";
136-
}
137-
return {
138-
label,
139-
path: absPath,
140-
branchStatus,
141-
shortStatus,
142-
detached,
143-
headAbbrev: headAbbrev || "(unknown)",
146+
return buildEntry({
147+
...base,
144148
upstreamMode: "auto",
145149
upstreamRef: null,
146150
ahead: null,
147151
behind: null,
148-
upstreamNote: note,
149-
};
152+
upstreamNote: detached ? "detached HEAD — no upstream" : "no upstream configured",
153+
});
150154
}
151155

152156
const abbrevR = await spawnGitAsync(absPath, ["rev-parse", "--abbrev-ref", "@{u}"]);
153157
const upstreamRef = abbrevR.ok ? abbrevR.stdout.trim() : "@{u}";
154158
const { ahead, behind } = await fetchAheadBehind(absPath, "@{u}");
155-
156-
return {
157-
label,
158-
path: absPath,
159-
branchStatus,
160-
shortStatus,
161-
detached,
162-
headAbbrev: headAbbrev || "(unknown)",
159+
return buildEntry({
160+
...base,
163161
upstreamMode: "auto",
164162
upstreamRef,
165163
ahead,
166164
behind,
167-
upstreamNote: upstreamNoteFor(upstreamRef, ahead, behind),
168-
};
165+
upstreamNote: upstreamNoteFor(upstreamRef, ahead != null && behind != null),
166+
});
169167
}

src/server/presets.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -124,11 +124,12 @@ export function presetLoadErrorPayload(
124124
export function getPresetEntry(
125125
gitTop: string,
126126
presetName: string,
127-
): { entry: PresetEntry; presetSchemaVersion?: string } | { error: Record<string, unknown> } {
127+
): { ok: true; entry: PresetEntry; presetSchemaVersion?: string } | { ok: false; error: Record<string, unknown> } {
128128
const loaded = loadPresetsFromGitTop(gitTop);
129129
if (!loaded.ok) {
130130
if (loaded.reason === "missing") {
131131
return {
132+
ok: false,
132133
error: {
133134
error: "preset_not_found",
134135
preset: presetName,
@@ -137,19 +138,20 @@ export function getPresetEntry(
137138
},
138139
};
139140
}
140-
return { error: presetLoadErrorPayload(gitTop, loaded) };
141+
return { ok: false, error: presetLoadErrorPayload(gitTop, loaded) };
141142
}
142143
const entry = loaded.data[presetName];
143144
if (!entry) {
144145
return {
146+
ok: false,
145147
error: {
146148
error: "preset_not_found",
147149
preset: presetName,
148150
presetFile: join(gitTop, PRESET_FILE_PATH),
149151
},
150152
};
151153
}
152-
return { entry, presetSchemaVersion: loaded.schemaVersion };
154+
return { ok: true, entry, presetSchemaVersion: loaded.schemaVersion };
153155
}
154156

155157
export function mergeNestedRoots(
@@ -191,7 +193,7 @@ export function applyPresetNestedRoots(
191193
| { ok: true; nestedRoots: string[] | undefined; presetSchemaVersion?: string }
192194
| { ok: false; error: Record<string, unknown> } {
193195
const got = getPresetEntry(gitTop, presetName);
194-
if ("error" in got) return { ok: false, error: got.error };
196+
if (!got.ok) return { ok: false, error: got.error };
195197
const fromPreset = got.entry.nestedRoots;
196198
let nestedRoots: string[] | undefined = inlineNestedRoots;
197199
if (presetMerge) {
@@ -211,7 +213,7 @@ export function applyPresetParityPairs(
211213
| { ok: true; pairs: ParityPair[] | undefined; presetSchemaVersion?: string }
212214
| { ok: false; error: Record<string, unknown> } {
213215
const got = getPresetEntry(gitTop, presetName);
214-
if ("error" in got) return { ok: false, error: got.error };
216+
if (!got.ok) return { ok: false, error: got.error };
215217
const fromPreset = got.entry.parityPairs;
216218
let pairs: ParityPair[] | undefined = inlinePairs;
217219
if (presetMerge) {

src/server/roots.ts

Lines changed: 7 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { basename, isAbsolute, resolve } from "node:path";
1+
import { basename, resolve } from "node:path";
22
import { fileURLToPath } from "node:url";
33

44
import type { FastMCP } from "fastmcp";
@@ -30,19 +30,13 @@ export function listFileRoots(server: FastMCP): string[] {
3030
export function pathMatchesWorkspaceRootHint(rootPath: string, hint: string): boolean {
3131
const h = hint.trim();
3232
if (!h) return true;
33-
if (basename(rootPath) === h) return true;
3433
const absRoot = resolve(rootPath);
35-
if (absRoot === h) return true;
36-
try {
37-
if (isAbsolute(h) && resolve(h) === absRoot) return true;
38-
} catch {
39-
/* invalid absolute hint */
40-
}
4134
const normRoot = absRoot.replace(/\\/g, "/").replace(/\/+$/, "");
4235
const normHint = h.replace(/\\/g, "/").replace(/\/+$/, "").replace(/^\/+/, "");
4336
if (!normHint) return true;
4437
if (normRoot === normHint) return true;
45-
return normRoot.endsWith(`/${normHint}`);
38+
if (normRoot.endsWith(`/${normHint}`)) return true;
39+
return basename(rootPath) === h;
4640
}
4741

4842
export type RootPick = {
@@ -60,31 +54,19 @@ export function resolveWorkspaceRoots(server: FastMCP, args: RootPick): ResolveR
6054
return { ok: true, roots: [resolve(args.workspaceRoot.trim())] };
6155
}
6256
const fileRoots = listFileRoots(server);
57+
const fallback: ResolveRootsResult = { ok: true, roots: [process.cwd()] };
6358
if (args.allWorkspaceRoots) {
64-
if (fileRoots.length === 0) {
65-
return { ok: true, roots: [process.cwd()] };
66-
}
67-
return { ok: true, roots: fileRoots };
59+
return fileRoots.length === 0 ? fallback : { ok: true, roots: fileRoots };
6860
}
6961
if (args.rootIndex != null) {
7062
const r = fileRoots[args.rootIndex];
7163
if (!r) {
72-
return {
73-
ok: false,
74-
error: {
75-
error: "root_index_out_of_range",
76-
rootIndex: args.rootIndex,
77-
rootCount: fileRoots.length,
78-
},
79-
};
64+
return { ok: false, error: { error: "root_index_out_of_range", rootIndex: args.rootIndex, rootCount: fileRoots.length } };
8065
}
8166
return { ok: true, roots: [r] };
8267
}
8368
const primary = fileRoots[0];
84-
if (primary !== undefined) {
85-
return { ok: true, roots: [primary] };
86-
}
87-
return { ok: true, roots: [process.cwd()] };
69+
return primary !== undefined ? { ok: true, roots: [primary] } : fallback;
8870
}
8971

9072
/**

0 commit comments

Comments
 (0)