Skip to content

Commit 7377b6f

Browse files
authored
Merge pull request #7 from AztecProtocol/feat/sync-logging
feat: add MCP logging, split docs clone, and parallelize sync
2 parents 8db109f + 3560504 commit 7377b6f

File tree

9 files changed

+176
-153
lines changed

9 files changed

+176
-153
lines changed

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+
}).catch(() => {});
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/search.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,12 +73,12 @@ export function searchAztecDocs(options: {
7373
} {
7474
const { query, section, maxResults = 20 } = options;
7575

76-
if (!isRepoCloned("aztec-packages")) {
76+
if (!isRepoCloned("aztec-packages-docs")) {
7777
return {
7878
success: false,
7979
results: [],
8080
message:
81-
"aztec-packages is not cloned. Run aztec_sync_repos first to get documentation.",
81+
"aztec-packages-docs is not cloned. Run aztec_sync_repos first to get documentation.",
8282
};
8383
}
8484

src/tools/sync.ts

Lines changed: 63 additions & 20 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,57 +46,99 @@ export async function syncRepos(options: {
4546
};
4647
}
4748

49+
// Generate synthetic repo configs from sparsePathOverrides
50+
const syntheticRepos: RepoConfig[] = [];
51+
for (const repo of reposToSync) {
52+
if (repo.sparsePathOverrides) {
53+
for (const override of repo.sparsePathOverrides) {
54+
syntheticRepos.push({
55+
name: `${repo.name}-docs`,
56+
url: repo.url,
57+
branch: override.branch,
58+
sparse: override.paths,
59+
description: `${repo.description} (docs from ${override.branch})`,
60+
});
61+
}
62+
}
63+
}
64+
65+
// Include synthetic repos in total count
66+
const totalRepos = reposToSync.length + syntheticRepos.length;
67+
log?.(`Starting sync: ${totalRepos} repos, version=${effectiveVersion}, force=${force}`, "info");
68+
4869
const results: SyncResult["repos"] = [];
4970

50-
async function syncRepo(config: RepoConfig, statusTransform?: (s: string) => string): Promise<void> {
71+
async function syncRepo(
72+
config: RepoConfig,
73+
index: number,
74+
total: number,
75+
statusTransform?: (s: string) => string,
76+
): Promise<void> {
77+
log?.(`Syncing ${index}/${total}: ${config.name}`, "info");
5178
try {
52-
const status = await cloneRepo(config, force);
79+
const status = log ? await cloneRepo(config, force, log) : await cloneRepo(config, force);
5380
results.push({ name: config.name, status: statusTransform ? statusTransform(status) : status });
5481
} catch (error) {
82+
log?.(`${config.name}: Failed: ${error instanceof Error ? error.message : String(error)}`, "error");
5583
results.push({
5684
name: config.name,
5785
status: `Error: ${error instanceof Error ? error.message : String(error)}`,
5886
});
5987
}
6088
}
6189

62-
// Sort repos so aztec-packages is cloned first (needed to determine Noir version)
90+
// Clone aztec-packages first (blocking - needed to determine Noir version)
6391
const aztecPackages = reposToSync.find((r) => r.name === "aztec-packages");
64-
const noirRepos = reposToSync.filter((r) => r.url.includes("noir-lang"));
65-
const otherRepos = reposToSync.filter(
66-
(r) => r.name !== "aztec-packages" && !r.url.includes("noir-lang")
67-
);
68-
69-
// Clone aztec-packages first if present
92+
let nextIndex = 1;
7093
if (aztecPackages) {
71-
await syncRepo(aztecPackages);
94+
await syncRepo(aztecPackages, nextIndex++, totalRepos);
7295
}
7396

7497
// Get the Noir commit from aztec-packages (if available)
7598
const noirCommit = await getNoirCommitFromAztec();
99+
if (noirCommit) {
100+
log?.(`Resolved Noir commit from aztec-packages: ${noirCommit.substring(0, 7)}`, "info");
101+
}
102+
103+
// Build list of all remaining repos to clone in parallel
104+
const parallelBatch: { config: RepoConfig; index: number; statusTransform?: (s: string) => string }[] = [];
105+
106+
const noirRepos = reposToSync.filter((r) => r.url.includes("noir-lang"));
107+
const otherRepos = reposToSync.filter(
108+
(r) => r.name !== "aztec-packages" && !r.url.includes("noir-lang")
109+
);
76110

77-
// Clone Noir repos with the commit from aztec-packages
78111
for (const config of noirRepos) {
79112
const useAztecCommit = config.name === "noir" && noirCommit;
80113
const noirConfig: RepoConfig = useAztecCommit
81114
? { ...config, commit: noirCommit, branch: undefined }
82115
: config;
83-
84-
await syncRepo(
85-
noirConfig,
86-
useAztecCommit ? (s) => s.replace("(commit", "(commit from aztec-packages") : undefined
87-
);
116+
parallelBatch.push({
117+
config: noirConfig,
118+
index: nextIndex++,
119+
statusTransform: useAztecCommit ? (s) => s.replace("(commit", "(commit from aztec-packages") : undefined,
120+
});
88121
}
89122

90-
// Clone other repos
91123
for (const config of otherRepos) {
92-
await syncRepo(config);
124+
parallelBatch.push({ config, index: nextIndex++ });
93125
}
94126

127+
for (const config of syntheticRepos) {
128+
parallelBatch.push({ config, index: nextIndex++ });
129+
}
130+
131+
// Clone all remaining repos in parallel
132+
await Promise.all(
133+
parallelBatch.map((item) => syncRepo(item.config, item.index, totalRepos, item.statusTransform))
134+
);
135+
95136
const allSuccess = results.every(
96137
(r) => !r.status.toLowerCase().includes("error")
97138
);
98139

140+
log?.(`Sync complete: ${results.length} repos, ${allSuccess ? "all succeeded" : "some failed"}`, "info");
141+
99142
return {
100143
success: allSuccess,
101144
message: allSuccess

src/utils/git.ts

Lines changed: 52 additions & 34 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,36 @@ 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

57-
// If already cloned and version matches, just update
61+
// If already cloned and version matches, skip or update
5862
if (isRepoCloned(config.name)) {
59-
return await updateRepo(config.name);
63+
if (config.tag || config.commit) {
64+
log?.(`${config.name}: Already cloned at correct ${config.tag ? "tag" : "commit"}, skipping`, "debug");
65+
return `${config.name} already at ${config.commit || config.tag}`;
66+
}
67+
log?.(`${config.name}: Already cloned, updating`, "debug");
68+
return await updateRepo(config.name, log);
6069
}
6170

62-
const git: SimpleGit = simpleGit();
63-
6471
// Determine ref to checkout: commit > tag > branch
6572
const ref = config.commit || config.tag || config.branch || "default";
6673
const refType = config.commit ? "commit" : config.tag ? "tag" : "branch";
74+
const isSparse = config.sparse && config.sparse.length > 0;
6775

68-
if (config.sparse && config.sparse.length > 0) {
76+
log?.(`${config.name}: Cloning @ ${ref} (${refType}${isSparse ? ", sparse" : ""})`, "info");
77+
78+
const progressHandler = log
79+
? (data: { method: string; stage: string; progress: number }) => {
80+
log(`${config.name}: ${data.method} ${data.stage} ${data.progress}%`, "debug");
81+
}
82+
: undefined;
83+
84+
const git: SimpleGit = simpleGit({ progress: progressHandler });
85+
86+
if (isSparse) {
6987
// Clone with sparse checkout for large repos
7088
if (config.commit) {
7189
// For commits, we need full history to fetch the commit
@@ -75,10 +93,13 @@ export async function cloneRepo(
7593
"--no-checkout",
7694
]);
7795

78-
const repoGit = simpleGit(repoPath);
96+
const repoGit = simpleGit({ baseDir: repoPath, progress: progressHandler });
7997
await repoGit.raw(["config", "gc.auto", "0"]);
80-
await repoGit.raw(["sparse-checkout", "set", ...config.sparse]);
98+
log?.(`${config.name}: Setting sparse checkout paths: ${config.sparse!.join(", ")}`, "debug");
99+
await repoGit.raw(["sparse-checkout", "set", ...config.sparse!]);
100+
log?.(`${config.name}: Fetching commit ${config.commit.substring(0, 7)}`, "info");
81101
await repoGit.fetch(["origin", config.commit]);
102+
log?.(`${config.name}: Checking out commit`, "debug");
82103
await repoGit.checkout(config.commit);
83104
} else if (config.tag) {
84105
await git.clone(config.url, repoPath, [
@@ -87,29 +108,14 @@ export async function cloneRepo(
87108
"--no-checkout",
88109
]);
89110

90-
const repoGit = simpleGit(repoPath);
111+
const repoGit = simpleGit({ baseDir: repoPath, progress: progressHandler });
91112
await repoGit.raw(["config", "gc.auto", "0"]);
92-
await repoGit.raw(["sparse-checkout", "set", ...config.sparse]);
113+
log?.(`${config.name}: Setting sparse checkout paths: ${config.sparse!.join(", ")}`, "debug");
114+
await repoGit.raw(["sparse-checkout", "set", ...config.sparse!]);
115+
log?.(`${config.name}: Fetching tag ${config.tag}`, "info");
93116
await repoGit.fetch(["--depth=1", "origin", `refs/tags/${config.tag}:refs/tags/${config.tag}`]);
117+
log?.(`${config.name}: Checking out tag`, "debug");
94118
await repoGit.checkout(config.tag);
95-
96-
// Apply sparse path overrides from different branches
97-
if (config.sparsePathOverrides) {
98-
for (const override of config.sparsePathOverrides) {
99-
await repoGit.fetch(["--depth=1", "origin", override.branch]);
100-
try {
101-
await repoGit.checkout([`origin/${override.branch}`, "--", ...override.paths]);
102-
} catch (error) {
103-
const repoBase = config.url.replace(/\.git$/, "");
104-
const parentDirs = [...new Set(override.paths.map((p) => p.split("/").slice(0, -1).join("/")))];
105-
const browseLinks = parentDirs.map((d) => `${repoBase}/tree/${override.branch}/${d}`);
106-
throw new Error(
107-
`sparsePathOverrides failed for branch "${override.branch}": could not checkout paths [${override.paths.join(", ")}]. ` +
108-
`Check the actual folder names at: ${browseLinks.join(" , ")}`,
109-
);
110-
}
111-
}
112-
}
113119
} else {
114120
await git.clone(config.url, repoPath, [
115121
"--filter=blob:none",
@@ -118,25 +124,31 @@ export async function cloneRepo(
118124
...(config.branch ? ["-b", config.branch] : []),
119125
]);
120126

121-
const repoGit = simpleGit(repoPath);
127+
const repoGit = simpleGit({ baseDir: repoPath, progress: progressHandler });
122128
await repoGit.raw(["config", "gc.auto", "0"]);
123-
await repoGit.raw(["sparse-checkout", "set", ...config.sparse]);
129+
log?.(`${config.name}: Setting sparse checkout paths: ${config.sparse!.join(", ")}`, "debug");
130+
await repoGit.raw(["sparse-checkout", "set", ...config.sparse!]);
124131
}
125132

126-
return `Cloned ${config.name} @ ${ref} (${refType}, sparse: ${config.sparse.join(", ")})`;
133+
log?.(`${config.name}: Clone complete`, "info");
134+
return `Cloned ${config.name} @ ${ref} (${refType}, sparse: ${config.sparse!.join(", ")})`;
127135
} else {
128136
// Clone for smaller repos
129137
if (config.commit) {
130138
// For commits, clone and checkout specific commit
131139
await git.clone(config.url, repoPath, ["--no-checkout"]);
132-
const repoGit = simpleGit(repoPath);
140+
const repoGit = simpleGit({ baseDir: repoPath, progress: progressHandler });
141+
log?.(`${config.name}: Fetching commit ${config.commit.substring(0, 7)}`, "info");
133142
await repoGit.fetch(["origin", config.commit]);
143+
log?.(`${config.name}: Checking out commit`, "debug");
134144
await repoGit.checkout(config.commit);
135145
} else if (config.tag) {
136146
// Clone and checkout tag
137147
await git.clone(config.url, repoPath, ["--no-checkout"]);
138-
const repoGit = simpleGit(repoPath);
148+
const repoGit = simpleGit({ baseDir: repoPath, progress: progressHandler });
149+
log?.(`${config.name}: Fetching tag ${config.tag}`, "info");
139150
await repoGit.fetch(["--depth=1", "origin", `refs/tags/${config.tag}:refs/tags/${config.tag}`]);
151+
log?.(`${config.name}: Checking out tag`, "debug");
140152
await repoGit.checkout(config.tag);
141153
} else {
142154
await git.clone(config.url, repoPath, [
@@ -145,32 +157,38 @@ export async function cloneRepo(
145157
]);
146158
}
147159

160+
log?.(`${config.name}: Clone complete`, "info");
148161
return `Cloned ${config.name} @ ${ref} (${refType})`;
149162
}
150163
}
151164

152165
/**
153166
* Update an existing repository
154167
*/
155-
export async function updateRepo(repoName: string): Promise<string> {
168+
export async function updateRepo(repoName: string, log?: Logger): Promise<string> {
156169
const repoPath = getRepoPath(repoName);
157170

158171
if (!isRepoCloned(repoName)) {
159172
throw new Error(`Repository ${repoName} is not cloned`);
160173
}
161174

175+
log?.(`${repoName}: Updating`, "info");
162176
const git = simpleGit(repoPath);
163177

164178
try {
165179
await git.fetch(["--depth=1"]);
166180
await git.reset(["--hard", "origin/HEAD"]);
181+
log?.(`${repoName}: Update complete`, "info");
167182
return `Updated ${repoName}`;
168183
} catch (error) {
184+
log?.(`${repoName}: Fetch failed, trying pull`, "warning");
169185
// If fetch fails, try a simple pull
170186
try {
171187
await git.pull();
188+
log?.(`${repoName}: Pull complete`, "info");
172189
return `Updated ${repoName}`;
173190
} catch (pullError) {
191+
log?.(`${repoName}: Update failed: ${pullError}`, "error");
174192
return `Failed to update ${repoName}: ${pullError}`;
175193
}
176194
}

0 commit comments

Comments
 (0)