Skip to content

Commit b340300

Browse files
critesjoshclaude
andcommitted
feat: add MCP logging to sync operations for real-time progress visibility
Adds structured logging throughout the clone/update lifecycle so MCP clients can surface progress messages (repo-by-repo status, git fetch/checkout stages, sparse-checkout paths, and error details). Also includes the gc.auto=0 fix for blobless clones to prevent the race condition. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 8db109f commit b340300

3 files changed

Lines changed: 74 additions & 18 deletions

File tree

src/index.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ const server = new Server(
4141
{
4242
capabilities: {
4343
tools: {},
44+
logging: {},
4445
},
4546
}
4647
);
@@ -197,10 +198,18 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
197198
try {
198199
switch (name) {
199200
case "aztec_sync_repos": {
201+
const log = (message: string, level: string = "info") => {
202+
server.sendLoggingMessage({
203+
level: level as "info" | "debug" | "warning" | "error",
204+
logger: "aztec-sync",
205+
data: message,
206+
});
207+
};
200208
const result = await syncRepos({
201209
version: args?.version as string | undefined,
202210
force: args?.force as boolean | undefined,
203211
repos: args?.repos as string[] | undefined,
212+
log,
204213
});
205214
return {
206215
content: [

src/tools/sync.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
*/
44

55
import { AZTEC_REPOS, getAztecRepos, DEFAULT_AZTEC_VERSION, RepoConfig } from "../repos/config.js";
6-
import { cloneRepo, getReposStatus, getNoirCommitFromAztec, REPOS_DIR } from "../utils/git.js";
6+
import { cloneRepo, getReposStatus, getNoirCommitFromAztec, REPOS_DIR, Logger } from "../utils/git.js";
77

88
export interface SyncResult {
99
success: boolean;
@@ -24,8 +24,9 @@ export async function syncRepos(options: {
2424
force?: boolean;
2525
repos?: string[];
2626
version?: string;
27+
log?: Logger;
2728
}): Promise<SyncResult> {
28-
const { force = false, repos: repoNames, version } = options;
29+
const { force = false, repos: repoNames, version, log } = options;
2930

3031
// Get repos configured for the specified version
3132
const configuredRepos = version ? getAztecRepos(version) : AZTEC_REPOS;
@@ -45,13 +46,19 @@ export async function syncRepos(options: {
4546
};
4647
}
4748

49+
log?.(`Starting sync: ${reposToSync.length} repos, version=${effectiveVersion}, force=${force}`, "info");
50+
4851
const results: SyncResult["repos"] = [];
52+
let syncIndex = 0;
4953

5054
async function syncRepo(config: RepoConfig, statusTransform?: (s: string) => string): Promise<void> {
55+
syncIndex++;
56+
log?.(`Syncing ${syncIndex}/${reposToSync.length}: ${config.name}`, "info");
5157
try {
52-
const status = await cloneRepo(config, force);
58+
const status = await cloneRepo(config, force, log);
5359
results.push({ name: config.name, status: statusTransform ? statusTransform(status) : status });
5460
} catch (error) {
61+
log?.(`${config.name}: Failed: ${error instanceof Error ? error.message : String(error)}`, "error");
5562
results.push({
5663
name: config.name,
5764
status: `Error: ${error instanceof Error ? error.message : String(error)}`,
@@ -73,6 +80,9 @@ export async function syncRepos(options: {
7380

7481
// Get the Noir commit from aztec-packages (if available)
7582
const noirCommit = await getNoirCommitFromAztec();
83+
if (noirCommit) {
84+
log?.(`Resolved Noir commit from aztec-packages: ${noirCommit.substring(0, 7)}`, "info");
85+
}
7686

7787
// Clone Noir repos with the commit from aztec-packages
7888
for (const config of noirRepos) {
@@ -96,6 +106,8 @@ export async function syncRepos(options: {
96106
(r) => !r.status.toLowerCase().includes("error")
97107
);
98108

109+
log?.(`Sync complete: ${results.length} repos, ${allSuccess ? "all succeeded" : "some failed"}`, "info");
110+
99111
return {
100112
success: allSuccess,
101113
message: allSuccess

src/utils/git.ts

Lines changed: 50 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import { join } from "path";
88
import { homedir } from "os";
99
import { RepoConfig } from "../repos/config.js";
1010

11+
export type Logger = (message: string, level?: "info" | "debug" | "warning" | "error") => void;
12+
1113
/** Base directory for cloned repos */
1214
export const REPOS_DIR = join(
1315
process.env.AZTEC_MCP_REPOS_DIR || join(homedir(), ".aztec-mcp"),
@@ -41,7 +43,8 @@ export function isRepoCloned(repoName: string): boolean {
4143
*/
4244
export async function cloneRepo(
4345
config: RepoConfig,
44-
force: boolean = false
46+
force: boolean = false,
47+
log?: Logger
4548
): Promise<string> {
4649
ensureReposDir();
4750
const repoPath = getRepoPath(config.name);
@@ -51,21 +54,32 @@ export async function cloneRepo(
5154

5255
// Remove existing if force is set or version changed
5356
if ((force || versionMismatch) && existsSync(repoPath)) {
57+
log?.(`${config.name}: Removing existing clone (force=${force}, versionMismatch=${versionMismatch})`, "debug");
5458
rmSync(repoPath, { recursive: true, force: true });
5559
}
5660

5761
// If already cloned and version matches, just update
5862
if (isRepoCloned(config.name)) {
59-
return await updateRepo(config.name);
63+
log?.(`${config.name}: Already cloned, updating`, "debug");
64+
return await updateRepo(config.name, log);
6065
}
6166

62-
const git: SimpleGit = simpleGit();
63-
6467
// Determine ref to checkout: commit > tag > branch
6568
const ref = config.commit || config.tag || config.branch || "default";
6669
const refType = config.commit ? "commit" : config.tag ? "tag" : "branch";
70+
const isSparse = config.sparse && config.sparse.length > 0;
71+
72+
log?.(`${config.name}: Cloning @ ${ref} (${refType}${isSparse ? ", sparse" : ""})`, "info");
73+
74+
const progressHandler = log
75+
? (data: { method: string; stage: string; progress: number }) => {
76+
log(`${config.name}: ${data.method} ${data.stage} ${data.progress}%`, "debug");
77+
}
78+
: undefined;
79+
80+
const git: SimpleGit = simpleGit({ progress: progressHandler });
6781

68-
if (config.sparse && config.sparse.length > 0) {
82+
if (isSparse) {
6983
// Clone with sparse checkout for large repos
7084
if (config.commit) {
7185
// For commits, we need full history to fetch the commit
@@ -75,10 +89,13 @@ export async function cloneRepo(
7589
"--no-checkout",
7690
]);
7791

78-
const repoGit = simpleGit(repoPath);
92+
const repoGit = simpleGit({ baseDir: repoPath, progress: progressHandler });
7993
await repoGit.raw(["config", "gc.auto", "0"]);
80-
await repoGit.raw(["sparse-checkout", "set", ...config.sparse]);
94+
log?.(`${config.name}: Setting sparse checkout paths: ${config.sparse!.join(", ")}`, "debug");
95+
await repoGit.raw(["sparse-checkout", "set", ...config.sparse!]);
96+
log?.(`${config.name}: Fetching commit ${config.commit.substring(0, 7)}`, "info");
8197
await repoGit.fetch(["origin", config.commit]);
98+
log?.(`${config.name}: Checking out commit`, "debug");
8299
await repoGit.checkout(config.commit);
83100
} else if (config.tag) {
84101
await git.clone(config.url, repoPath, [
@@ -87,22 +104,28 @@ export async function cloneRepo(
87104
"--no-checkout",
88105
]);
89106

90-
const repoGit = simpleGit(repoPath);
107+
const repoGit = simpleGit({ baseDir: repoPath, progress: progressHandler });
91108
await repoGit.raw(["config", "gc.auto", "0"]);
92-
await repoGit.raw(["sparse-checkout", "set", ...config.sparse]);
109+
log?.(`${config.name}: Setting sparse checkout paths: ${config.sparse!.join(", ")}`, "debug");
110+
await repoGit.raw(["sparse-checkout", "set", ...config.sparse!]);
111+
log?.(`${config.name}: Fetching tag ${config.tag}`, "info");
93112
await repoGit.fetch(["--depth=1", "origin", `refs/tags/${config.tag}:refs/tags/${config.tag}`]);
113+
log?.(`${config.name}: Checking out tag`, "debug");
94114
await repoGit.checkout(config.tag);
95115

96116
// Apply sparse path overrides from different branches
97117
if (config.sparsePathOverrides) {
98118
for (const override of config.sparsePathOverrides) {
119+
log?.(`${config.name}: Fetching override branch ${override.branch}`, "debug");
99120
await repoGit.fetch(["--depth=1", "origin", override.branch]);
100121
try {
122+
log?.(`${config.name}: Checking out override paths from ${override.branch}: ${override.paths.join(", ")}`, "debug");
101123
await repoGit.checkout([`origin/${override.branch}`, "--", ...override.paths]);
102124
} catch (error) {
103125
const repoBase = config.url.replace(/\.git$/, "");
104126
const parentDirs = [...new Set(override.paths.map((p) => p.split("/").slice(0, -1).join("/")))];
105127
const browseLinks = parentDirs.map((d) => `${repoBase}/tree/${override.branch}/${d}`);
128+
log?.(`${config.name}: sparsePathOverrides failed for branch "${override.branch}"`, "error");
106129
throw new Error(
107130
`sparsePathOverrides failed for branch "${override.branch}": could not checkout paths [${override.paths.join(", ")}]. ` +
108131
`Check the actual folder names at: ${browseLinks.join(" , ")}`,
@@ -118,25 +141,31 @@ export async function cloneRepo(
118141
...(config.branch ? ["-b", config.branch] : []),
119142
]);
120143

121-
const repoGit = simpleGit(repoPath);
144+
const repoGit = simpleGit({ baseDir: repoPath, progress: progressHandler });
122145
await repoGit.raw(["config", "gc.auto", "0"]);
123-
await repoGit.raw(["sparse-checkout", "set", ...config.sparse]);
146+
log?.(`${config.name}: Setting sparse checkout paths: ${config.sparse!.join(", ")}`, "debug");
147+
await repoGit.raw(["sparse-checkout", "set", ...config.sparse!]);
124148
}
125149

126-
return `Cloned ${config.name} @ ${ref} (${refType}, sparse: ${config.sparse.join(", ")})`;
150+
log?.(`${config.name}: Clone complete`, "info");
151+
return `Cloned ${config.name} @ ${ref} (${refType}, sparse: ${config.sparse!.join(", ")})`;
127152
} else {
128153
// Clone for smaller repos
129154
if (config.commit) {
130155
// For commits, clone and checkout specific commit
131156
await git.clone(config.url, repoPath, ["--no-checkout"]);
132-
const repoGit = simpleGit(repoPath);
157+
const repoGit = simpleGit({ baseDir: repoPath, progress: progressHandler });
158+
log?.(`${config.name}: Fetching commit ${config.commit.substring(0, 7)}`, "info");
133159
await repoGit.fetch(["origin", config.commit]);
160+
log?.(`${config.name}: Checking out commit`, "debug");
134161
await repoGit.checkout(config.commit);
135162
} else if (config.tag) {
136163
// Clone and checkout tag
137164
await git.clone(config.url, repoPath, ["--no-checkout"]);
138-
const repoGit = simpleGit(repoPath);
165+
const repoGit = simpleGit({ baseDir: repoPath, progress: progressHandler });
166+
log?.(`${config.name}: Fetching tag ${config.tag}`, "info");
139167
await repoGit.fetch(["--depth=1", "origin", `refs/tags/${config.tag}:refs/tags/${config.tag}`]);
168+
log?.(`${config.name}: Checking out tag`, "debug");
140169
await repoGit.checkout(config.tag);
141170
} else {
142171
await git.clone(config.url, repoPath, [
@@ -145,32 +174,38 @@ export async function cloneRepo(
145174
]);
146175
}
147176

177+
log?.(`${config.name}: Clone complete`, "info");
148178
return `Cloned ${config.name} @ ${ref} (${refType})`;
149179
}
150180
}
151181

152182
/**
153183
* Update an existing repository
154184
*/
155-
export async function updateRepo(repoName: string): Promise<string> {
185+
export async function updateRepo(repoName: string, log?: Logger): Promise<string> {
156186
const repoPath = getRepoPath(repoName);
157187

158188
if (!isRepoCloned(repoName)) {
159189
throw new Error(`Repository ${repoName} is not cloned`);
160190
}
161191

192+
log?.(`${repoName}: Updating`, "info");
162193
const git = simpleGit(repoPath);
163194

164195
try {
165196
await git.fetch(["--depth=1"]);
166197
await git.reset(["--hard", "origin/HEAD"]);
198+
log?.(`${repoName}: Update complete`, "info");
167199
return `Updated ${repoName}`;
168200
} catch (error) {
201+
log?.(`${repoName}: Fetch failed, trying pull`, "warning");
169202
// If fetch fails, try a simple pull
170203
try {
171204
await git.pull();
205+
log?.(`${repoName}: Pull complete`, "info");
172206
return `Updated ${repoName}`;
173207
} catch (pullError) {
208+
log?.(`${repoName}: Update failed: ${pullError}`, "error");
174209
return `Failed to update ${repoName}: ${pullError}`;
175210
}
176211
}

0 commit comments

Comments
 (0)