Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ const server = new Server(
{
capabilities: {
tools: {},
logging: {},
},
}
);
Expand Down Expand Up @@ -197,10 +198,18 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
try {
switch (name) {
case "aztec_sync_repos": {
const log = (message: string, level: string = "info") => {
server.sendLoggingMessage({
level: level as "info" | "debug" | "warning" | "error",
logger: "aztec-sync",
data: message,
}).catch(() => {});
};
const result = await syncRepos({
version: args?.version as string | undefined,
force: args?.force as boolean | undefined,
repos: args?.repos as string[] | undefined,
log,
});
return {
content: [
Expand Down
4 changes: 2 additions & 2 deletions src/tools/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,12 +73,12 @@ export function searchAztecDocs(options: {
} {
const { query, section, maxResults = 20 } = options;

if (!isRepoCloned("aztec-packages")) {
if (!isRepoCloned("aztec-packages-docs")) {
return {
success: false,
results: [],
message:
"aztec-packages is not cloned. Run aztec_sync_repos first to get documentation.",
"aztec-packages-docs is not cloned. Run aztec_sync_repos first to get documentation.",
};
}

Expand Down
83 changes: 63 additions & 20 deletions src/tools/sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*/

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

export interface SyncResult {
success: boolean;
Expand All @@ -24,8 +24,9 @@ export async function syncRepos(options: {
force?: boolean;
repos?: string[];
version?: string;
log?: Logger;
}): Promise<SyncResult> {
const { force = false, repos: repoNames, version } = options;
const { force = false, repos: repoNames, version, log } = options;

// Get repos configured for the specified version
const configuredRepos = version ? getAztecRepos(version) : AZTEC_REPOS;
Expand All @@ -45,57 +46,99 @@ export async function syncRepos(options: {
};
}

// Generate synthetic repo configs from sparsePathOverrides
const syntheticRepos: RepoConfig[] = [];
for (const repo of reposToSync) {
if (repo.sparsePathOverrides) {
for (const override of repo.sparsePathOverrides) {
syntheticRepos.push({
name: `${repo.name}-docs`,
url: repo.url,
branch: override.branch,
sparse: override.paths,
description: `${repo.description} (docs from ${override.branch})`,
});
}
}
}

// Include synthetic repos in total count
const totalRepos = reposToSync.length + syntheticRepos.length;
log?.(`Starting sync: ${totalRepos} repos, version=${effectiveVersion}, force=${force}`, "info");

const results: SyncResult["repos"] = [];

async function syncRepo(config: RepoConfig, statusTransform?: (s: string) => string): Promise<void> {
async function syncRepo(
config: RepoConfig,
index: number,
total: number,
statusTransform?: (s: string) => string,
): Promise<void> {
log?.(`Syncing ${index}/${total}: ${config.name}`, "info");
try {
const status = await cloneRepo(config, force);
const status = log ? await cloneRepo(config, force, log) : await cloneRepo(config, force);
results.push({ name: config.name, status: statusTransform ? statusTransform(status) : status });
} catch (error) {
log?.(`${config.name}: Failed: ${error instanceof Error ? error.message : String(error)}`, "error");
results.push({
name: config.name,
status: `Error: ${error instanceof Error ? error.message : String(error)}`,
});
}
}

// Sort repos so aztec-packages is cloned first (needed to determine Noir version)
// Clone aztec-packages first (blocking - needed to determine Noir version)
const aztecPackages = reposToSync.find((r) => r.name === "aztec-packages");
const noirRepos = reposToSync.filter((r) => r.url.includes("noir-lang"));
const otherRepos = reposToSync.filter(
(r) => r.name !== "aztec-packages" && !r.url.includes("noir-lang")
);

// Clone aztec-packages first if present
let nextIndex = 1;
if (aztecPackages) {
await syncRepo(aztecPackages);
await syncRepo(aztecPackages, nextIndex++, totalRepos);
}

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

// Build list of all remaining repos to clone in parallel
const parallelBatch: { config: RepoConfig; index: number; statusTransform?: (s: string) => string }[] = [];

const noirRepos = reposToSync.filter((r) => r.url.includes("noir-lang"));
const otherRepos = reposToSync.filter(
(r) => r.name !== "aztec-packages" && !r.url.includes("noir-lang")
);

// Clone Noir repos with the commit from aztec-packages
for (const config of noirRepos) {
const useAztecCommit = config.name === "noir" && noirCommit;
const noirConfig: RepoConfig = useAztecCommit
? { ...config, commit: noirCommit, branch: undefined }
: config;

await syncRepo(
noirConfig,
useAztecCommit ? (s) => s.replace("(commit", "(commit from aztec-packages") : undefined
);
parallelBatch.push({
config: noirConfig,
index: nextIndex++,
statusTransform: useAztecCommit ? (s) => s.replace("(commit", "(commit from aztec-packages") : undefined,
});
}

// Clone other repos
for (const config of otherRepos) {
await syncRepo(config);
parallelBatch.push({ config, index: nextIndex++ });
}

for (const config of syntheticRepos) {
parallelBatch.push({ config, index: nextIndex++ });
}

// Clone all remaining repos in parallel
await Promise.all(
parallelBatch.map((item) => syncRepo(item.config, item.index, totalRepos, item.statusTransform))
);

const allSuccess = results.every(
(r) => !r.status.toLowerCase().includes("error")
);

log?.(`Sync complete: ${results.length} repos, ${allSuccess ? "all succeeded" : "some failed"}`, "info");

return {
success: allSuccess,
message: allSuccess
Expand Down
86 changes: 52 additions & 34 deletions src/utils/git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { join } from "path";
import { homedir } from "os";
import { RepoConfig } from "../repos/config.js";

export type Logger = (message: string, level?: "info" | "debug" | "warning" | "error") => void;

/** Base directory for cloned repos */
export const REPOS_DIR = join(
process.env.AZTEC_MCP_REPOS_DIR || join(homedir(), ".aztec-mcp"),
Expand Down Expand Up @@ -41,7 +43,8 @@ export function isRepoCloned(repoName: string): boolean {
*/
export async function cloneRepo(
config: RepoConfig,
force: boolean = false
force: boolean = false,
log?: Logger
): Promise<string> {
ensureReposDir();
const repoPath = getRepoPath(config.name);
Expand All @@ -51,21 +54,36 @@ export async function cloneRepo(

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

// If already cloned and version matches, just update
// If already cloned and version matches, skip or update
if (isRepoCloned(config.name)) {
return await updateRepo(config.name);
if (config.tag || config.commit) {
log?.(`${config.name}: Already cloned at correct ${config.tag ? "tag" : "commit"}, skipping`, "debug");
return `${config.name} already at ${config.commit || config.tag}`;
}
log?.(`${config.name}: Already cloned, updating`, "debug");
return await updateRepo(config.name, log);
}

const git: SimpleGit = simpleGit();

// Determine ref to checkout: commit > tag > branch
const ref = config.commit || config.tag || config.branch || "default";
const refType = config.commit ? "commit" : config.tag ? "tag" : "branch";
const isSparse = config.sparse && config.sparse.length > 0;

if (config.sparse && config.sparse.length > 0) {
log?.(`${config.name}: Cloning @ ${ref} (${refType}${isSparse ? ", sparse" : ""})`, "info");

const progressHandler = log
? (data: { method: string; stage: string; progress: number }) => {
log(`${config.name}: ${data.method} ${data.stage} ${data.progress}%`, "debug");
}
: undefined;

const git: SimpleGit = simpleGit({ progress: progressHandler });

if (isSparse) {
// Clone with sparse checkout for large repos
if (config.commit) {
// For commits, we need full history to fetch the commit
Expand All @@ -75,10 +93,13 @@ export async function cloneRepo(
"--no-checkout",
]);

const repoGit = simpleGit(repoPath);
const repoGit = simpleGit({ baseDir: repoPath, progress: progressHandler });
await repoGit.raw(["config", "gc.auto", "0"]);
await repoGit.raw(["sparse-checkout", "set", ...config.sparse]);
log?.(`${config.name}: Setting sparse checkout paths: ${config.sparse!.join(", ")}`, "debug");
await repoGit.raw(["sparse-checkout", "set", ...config.sparse!]);
log?.(`${config.name}: Fetching commit ${config.commit.substring(0, 7)}`, "info");
await repoGit.fetch(["origin", config.commit]);
log?.(`${config.name}: Checking out commit`, "debug");
await repoGit.checkout(config.commit);
} else if (config.tag) {
await git.clone(config.url, repoPath, [
Expand All @@ -87,29 +108,14 @@ export async function cloneRepo(
"--no-checkout",
]);

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

// Apply sparse path overrides from different branches
if (config.sparsePathOverrides) {
for (const override of config.sparsePathOverrides) {
await repoGit.fetch(["--depth=1", "origin", override.branch]);
try {
await repoGit.checkout([`origin/${override.branch}`, "--", ...override.paths]);
} catch (error) {
const repoBase = config.url.replace(/\.git$/, "");
const parentDirs = [...new Set(override.paths.map((p) => p.split("/").slice(0, -1).join("/")))];
const browseLinks = parentDirs.map((d) => `${repoBase}/tree/${override.branch}/${d}`);
throw new Error(
`sparsePathOverrides failed for branch "${override.branch}": could not checkout paths [${override.paths.join(", ")}]. ` +
`Check the actual folder names at: ${browseLinks.join(" , ")}`,
);
}
}
}
} else {
await git.clone(config.url, repoPath, [
"--filter=blob:none",
Expand All @@ -118,25 +124,31 @@ export async function cloneRepo(
...(config.branch ? ["-b", config.branch] : []),
]);

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

return `Cloned ${config.name} @ ${ref} (${refType}, sparse: ${config.sparse.join(", ")})`;
log?.(`${config.name}: Clone complete`, "info");
return `Cloned ${config.name} @ ${ref} (${refType}, sparse: ${config.sparse!.join(", ")})`;
} else {
// Clone for smaller repos
if (config.commit) {
// For commits, clone and checkout specific commit
await git.clone(config.url, repoPath, ["--no-checkout"]);
const repoGit = simpleGit(repoPath);
const repoGit = simpleGit({ baseDir: repoPath, progress: progressHandler });
log?.(`${config.name}: Fetching commit ${config.commit.substring(0, 7)}`, "info");
await repoGit.fetch(["origin", config.commit]);
log?.(`${config.name}: Checking out commit`, "debug");
await repoGit.checkout(config.commit);
} else if (config.tag) {
// Clone and checkout tag
await git.clone(config.url, repoPath, ["--no-checkout"]);
const repoGit = simpleGit(repoPath);
const repoGit = simpleGit({ baseDir: repoPath, progress: progressHandler });
log?.(`${config.name}: Fetching tag ${config.tag}`, "info");
await repoGit.fetch(["--depth=1", "origin", `refs/tags/${config.tag}:refs/tags/${config.tag}`]);
log?.(`${config.name}: Checking out tag`, "debug");
await repoGit.checkout(config.tag);
} else {
await git.clone(config.url, repoPath, [
Expand All @@ -145,32 +157,38 @@ export async function cloneRepo(
]);
}

log?.(`${config.name}: Clone complete`, "info");
return `Cloned ${config.name} @ ${ref} (${refType})`;
}
}

/**
* Update an existing repository
*/
export async function updateRepo(repoName: string): Promise<string> {
export async function updateRepo(repoName: string, log?: Logger): Promise<string> {
const repoPath = getRepoPath(repoName);

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

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

try {
await git.fetch(["--depth=1"]);
await git.reset(["--hard", "origin/HEAD"]);
log?.(`${repoName}: Update complete`, "info");
return `Updated ${repoName}`;
} catch (error) {
log?.(`${repoName}: Fetch failed, trying pull`, "warning");
// If fetch fails, try a simple pull
try {
await git.pull();
log?.(`${repoName}: Pull complete`, "info");
return `Updated ${repoName}`;
} catch (pullError) {
log?.(`${repoName}: Update failed: ${pullError}`, "error");
return `Failed to update ${repoName}: ${pullError}`;
}
}
Expand Down
Loading