Skip to content

Commit 87a24ff

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 1f55df8 commit 87a24ff

3 files changed

Lines changed: 77 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: 53 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,9 +89,13 @@ export async function cloneRepo(
7589
"--no-checkout",
7690
]);
7791

78-
const repoGit = simpleGit(repoPath);
79-
await repoGit.raw(["sparse-checkout", "set", ...config.sparse]);
92+
const repoGit = simpleGit({ baseDir: repoPath, progress: progressHandler });
93+
await repoGit.raw(["config", "gc.auto", "0"]);
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");
8097
await repoGit.fetch(["origin", config.commit]);
98+
log?.(`${config.name}: Checking out commit`, "debug");
8199
await repoGit.checkout(config.commit);
82100
} else if (config.tag) {
83101
await git.clone(config.url, repoPath, [
@@ -86,21 +104,28 @@ export async function cloneRepo(
86104
"--no-checkout",
87105
]);
88106

89-
const repoGit = simpleGit(repoPath);
90-
await repoGit.raw(["sparse-checkout", "set", ...config.sparse]);
107+
const repoGit = simpleGit({ baseDir: repoPath, progress: progressHandler });
108+
await repoGit.raw(["config", "gc.auto", "0"]);
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");
91112
await repoGit.fetch(["--depth=1", "origin", `refs/tags/${config.tag}:refs/tags/${config.tag}`]);
113+
log?.(`${config.name}: Checking out tag`, "debug");
92114
await repoGit.checkout(config.tag);
93115

94116
// Apply sparse path overrides from different branches
95117
if (config.sparsePathOverrides) {
96118
for (const override of config.sparsePathOverrides) {
119+
log?.(`${config.name}: Fetching override branch ${override.branch}`, "debug");
97120
await repoGit.fetch(["--depth=1", "origin", override.branch]);
98121
try {
122+
log?.(`${config.name}: Checking out override paths from ${override.branch}: ${override.paths.join(", ")}`, "debug");
99123
await repoGit.checkout([`origin/${override.branch}`, "--", ...override.paths]);
100124
} catch (error) {
101125
const repoBase = config.url.replace(/\.git$/, "");
102126
const parentDirs = [...new Set(override.paths.map((p) => p.split("/").slice(0, -1).join("/")))];
103127
const browseLinks = parentDirs.map((d) => `${repoBase}/tree/${override.branch}/${d}`);
128+
log?.(`${config.name}: sparsePathOverrides failed for branch "${override.branch}"`, "error");
104129
throw new Error(
105130
`sparsePathOverrides failed for branch "${override.branch}": could not checkout paths [${override.paths.join(", ")}]. ` +
106131
`Check the actual folder names at: ${browseLinks.join(" , ")}`,
@@ -116,24 +141,31 @@ export async function cloneRepo(
116141
...(config.branch ? ["-b", config.branch] : []),
117142
]);
118143

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

123-
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(", ")})`;
124152
} else {
125153
// Clone for smaller repos
126154
if (config.commit) {
127155
// For commits, clone and checkout specific commit
128156
await git.clone(config.url, repoPath, ["--no-checkout"]);
129-
const repoGit = simpleGit(repoPath);
157+
const repoGit = simpleGit({ baseDir: repoPath, progress: progressHandler });
158+
log?.(`${config.name}: Fetching commit ${config.commit.substring(0, 7)}`, "info");
130159
await repoGit.fetch(["origin", config.commit]);
160+
log?.(`${config.name}: Checking out commit`, "debug");
131161
await repoGit.checkout(config.commit);
132162
} else if (config.tag) {
133163
// Clone and checkout tag
134164
await git.clone(config.url, repoPath, ["--no-checkout"]);
135-
const repoGit = simpleGit(repoPath);
165+
const repoGit = simpleGit({ baseDir: repoPath, progress: progressHandler });
166+
log?.(`${config.name}: Fetching tag ${config.tag}`, "info");
136167
await repoGit.fetch(["--depth=1", "origin", `refs/tags/${config.tag}:refs/tags/${config.tag}`]);
168+
log?.(`${config.name}: Checking out tag`, "debug");
137169
await repoGit.checkout(config.tag);
138170
} else {
139171
await git.clone(config.url, repoPath, [
@@ -142,32 +174,38 @@ export async function cloneRepo(
142174
]);
143175
}
144176

177+
log?.(`${config.name}: Clone complete`, "info");
145178
return `Cloned ${config.name} @ ${ref} (${refType})`;
146179
}
147180
}
148181

149182
/**
150183
* Update an existing repository
151184
*/
152-
export async function updateRepo(repoName: string): Promise<string> {
185+
export async function updateRepo(repoName: string, log?: Logger): Promise<string> {
153186
const repoPath = getRepoPath(repoName);
154187

155188
if (!isRepoCloned(repoName)) {
156189
throw new Error(`Repository ${repoName} is not cloned`);
157190
}
158191

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

161195
try {
162196
await git.fetch(["--depth=1"]);
163197
await git.reset(["--hard", "origin/HEAD"]);
198+
log?.(`${repoName}: Update complete`, "info");
164199
return `Updated ${repoName}`;
165200
} catch (error) {
201+
log?.(`${repoName}: Fetch failed, trying pull`, "warning");
166202
// If fetch fails, try a simple pull
167203
try {
168204
await git.pull();
205+
log?.(`${repoName}: Pull complete`, "info");
169206
return `Updated ${repoName}`;
170207
} catch (pullError) {
208+
log?.(`${repoName}: Update failed: ${pullError}`, "error");
171209
return `Failed to update ${repoName}: ${pullError}`;
172210
}
173211
}

0 commit comments

Comments
 (0)