ask_repo
Analyze Git repository codebases by asking questions to understand architecture, debug issues, review security, or evaluate code quality with AI-powered insights.
Instructions
Analyze any Git repository with AI. Point it at a repo and ask questions about the codebase.
Example prompts by use case:
Understanding Architecture:
repo: "nginx/nginx", prompt: "How does nginx handle concurrent connections? Walk through the event loop, worker process model, and connection state transitions."
Integration and API Usage:
repo: "hashicorp/terraform", prompt: "How do I implement a custom provider? What interfaces does the SDK expose, how are CRUD operations mapped to the resource lifecycle?"
Debugging and Troubleshooting:
repo: "docker/compose", prompt: "How does Compose resolve service dependencies and startup order? What happens with depends_on and health check conditions?"
Security Review:
repo: "redis/redis", prompt: "Review the ACL security model. How are per-user command permissions enforced, and how does AUTH prevent privilege escalation?"
Code Quality and Evaluation:
repo: "vitejs/vite", prompt: "How does Vite's plugin system compare to Rollup's? What are the Vite-specific hooks and tradeoffs?"
Deep Technical Analysis:
repo: "ggml-org/llama.cpp", prompt: "How does the KV cache work during autoregressive generation? How are past key-value pairs stored, reused, and evicted?"
Migration Planning (with ref — Pro/Max plans only):
repo: "mui/material-ui", ref: "v4.12.0", prompt: "Document the Button component's full API surface — every prop, its type, default value, and behavior."
repo: "mui/material-ui", ref: "v5.0.0", prompt: "Document the Button component's full API surface — every prop, its type, default value, and behavior." (Compare both results to build a migration guide between v4 and v5)
repo: "kubernetes/kubernetes", ref: "release-1.29", prompt: "How does the scheduler's scoring and filtering pipeline work for pod placement?"
Ask detailed, specific questions — the tool returns real function signatures, parameter types, return values, and source citations with exact file paths and line numbers.
If you hit a rate limit (429), the user's monthly token credits are exhausted — they can wait for the reset or upgrade their plan. Free-tier repos larger than 2 GB will be rejected with a 413 error; suggest upgrading to Pro or Max for unlimited repo size.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| repo | Yes | Repository to analyze. Accepts GitHub URLs (https://github.com/owner/repo), shorthand (owner/repo), GitLab/Bitbucket URLs, or any public Git URL | |
| prompt | Yes | What to analyze or ask about the codebase | |
| ref | No | Branch, commit SHA, or tag to analyze (default: repository's default branch) |
Implementation Reference
- src/index.ts:69-260 (handler)The main async handler function that executes the 'ask_repo' tool logic. It handles token authentication (anonymous or API key), calls the streaming API via analyzeRepoStreaming, sends progress notifications, handles errors (rate limits, auth failures, connection issues), and formats the final response with usage statistics.
async ({ repo, prompt, ref }, extra) => { const apiUrl = getApiUrl(); const tracker: ProgressTracker = createProgressTracker(); // Use client-provided progress token, or generate one so progress works regardless const progressToken = extra._meta?.progressToken ?? `ask_repo_${Date.now()}`; // Progress callback using MCP notifications const sendProgress = async (message: string) => { try { await extra.sendNotification({ method: "notifications/progress" as const, params: { progressToken, progress: tracker.outputTokens, message, }, }); } catch { // Ignore notification errors — client may not support progress } }; // Get or register token let token = getOrCreateToken(); if (!token) { await sendProgress(formatMessage(tracker, "Registering anonymous token...")); token = await registerAnonymousToken(apiUrl); if (!token) { return { content: [ { type: "text" as const, text: "Unable to register anonymous token. " + "This may be because you've reached the limit of 3 tokens per IP address.\n\n" + "To continue using Instagit:\n" + "1. Sign up for a free account at https://app.instagit.com/signup\n" + "2. Get an API key from your dashboard\n" + "3. Set INSTAGIT_API_KEY in your MCP configuration", }, ], }; } } await sendProgress(formatMessage(tracker, "Connecting...")); // Attempt the API call const callApi = async (authToken: string) => { return analyzeRepoStreaming({ repo, prompt, ref, token: authToken, progressCallback: sendProgress, tracker, }); }; let result; try { result = await callApi(token); } catch (err: unknown) { const error = err as Error & { status?: number; body?: string }; // Handle connection errors if (error.message?.includes("fetch failed") || error.message?.includes("ECONNREFUSED")) { return { content: [ { type: "text" as const, text: `Could not connect to Instagit API at ${apiUrl}. Make sure the API server is running.`, }, ], }; } // Handle rate limiting (429) if (error.status === 429) { let rateLimitUntil: string | undefined; try { const errorData = JSON.parse(error.body ?? "{}"); rateLimitUntil = errorData.rate_limit_until; } catch { // ignore } const resetInfo = rateLimitUntil ? `\nCredits will reset at: ${rateLimitUntil}\n` : ""; return { content: [ { type: "text" as const, text: `Rate limit exceeded. Your free credits have been exhausted.\n${resetInfo}\n` + "To continue using Instagit immediately:\n" + "- Upgrade to Pro ($20/mo) for 10x more credits and faster analysis\n" + "- Visit: https://app.instagit.com/pricing", }, ], }; } // Handle auth errors (401) — retry with fresh token only for anonymous tokens if (error.status === 401) { // If the user set an API key, don't discard it — report the failure directly if (process.env.INSTAGIT_API_KEY) { return { content: [ { type: "text" as const, text: "Authentication failed (401). Your INSTAGIT_API_KEY was rejected.\n\n" + "Please verify your API key is correct and active at https://app.instagit.com/dashboard", }, ], }; } clearStoredToken(); const newToken = await registerAnonymousToken(apiUrl); if (newToken) { try { result = await callApi(newToken); } catch (retryErr: unknown) { const retryError = retryErr as Error & { status?: number }; return { content: [ { type: "text" as const, text: `API error after re-auth: ${retryError.status ?? retryError.message}`, }, ], }; } } else { return { content: [ { type: "text" as const, text: "Authentication failed. Unable to register a new token.\n\n" + "Please set INSTAGIT_API_KEY in your MCP configuration, " + "or visit https://app.instagit.com/signup to create an account.", }, ], }; } } // Other errors if (!result) { return { content: [ { type: "text" as const, text: `API error: ${error.status ?? ""} ${error.message}`, }, ], }; } } // Format response with usage footer let responseText = result!.text; const footerParts: string[] = []; if (result!.totalTokens > 0) { footerParts.push( `Tokens: ${result!.inputTokens.toLocaleString()} input, ` + `${result!.outputTokens.toLocaleString()} output, ` + `${result!.totalTokens.toLocaleString()} total` ); } if (result!.tier) { footerParts.push(`Tier: ${result!.tier}`); } if (result!.tokensRemaining > 0) { footerParts.push(`Credits remaining: ${result!.tokensRemaining.toLocaleString()}`); } if (footerParts.length > 0) { responseText += "\n\n---\n" + footerParts.join(" | "); } if (result!.upgradeHint) { responseText += `\n\n${result!.upgradeHint}`; } return { content: [{ type: "text" as const, text: responseText }], }; } - src/index.ts:55-68 (schema)Zod schema definition for the tool inputs: 'repo' (string - GitHub URL, shorthand, or any Git URL), 'prompt' (string - what to analyze), and 'ref' (optional string - branch, commit SHA, or tag).
repo: z .string() .describe( "Repository to analyze. Accepts GitHub URLs (https://github.com/owner/repo), " + "shorthand (owner/repo), GitLab/Bitbucket URLs, or any public Git URL" ), prompt: z.string().describe("What to analyze or ask about the codebase"), ref: z .string() .nullable() .optional() .default(null) .describe("Branch, commit SHA, or tag to analyze (default: repository's default branch)"), }, - src/index.ts:51-261 (registration)Registration of the 'ask_repo' tool with the MCP server using server.tool(). Includes the tool name, description (with detailed usage examples), input schema, and the handler function.
server.tool( "ask_repo", TOOL_DESCRIPTION, { repo: z .string() .describe( "Repository to analyze. Accepts GitHub URLs (https://github.com/owner/repo), " + "shorthand (owner/repo), GitLab/Bitbucket URLs, or any public Git URL" ), prompt: z.string().describe("What to analyze or ask about the codebase"), ref: z .string() .nullable() .optional() .default(null) .describe("Branch, commit SHA, or tag to analyze (default: repository's default branch)"), }, async ({ repo, prompt, ref }, extra) => { const apiUrl = getApiUrl(); const tracker: ProgressTracker = createProgressTracker(); // Use client-provided progress token, or generate one so progress works regardless const progressToken = extra._meta?.progressToken ?? `ask_repo_${Date.now()}`; // Progress callback using MCP notifications const sendProgress = async (message: string) => { try { await extra.sendNotification({ method: "notifications/progress" as const, params: { progressToken, progress: tracker.outputTokens, message, }, }); } catch { // Ignore notification errors — client may not support progress } }; // Get or register token let token = getOrCreateToken(); if (!token) { await sendProgress(formatMessage(tracker, "Registering anonymous token...")); token = await registerAnonymousToken(apiUrl); if (!token) { return { content: [ { type: "text" as const, text: "Unable to register anonymous token. " + "This may be because you've reached the limit of 3 tokens per IP address.\n\n" + "To continue using Instagit:\n" + "1. Sign up for a free account at https://app.instagit.com/signup\n" + "2. Get an API key from your dashboard\n" + "3. Set INSTAGIT_API_KEY in your MCP configuration", }, ], }; } } await sendProgress(formatMessage(tracker, "Connecting...")); // Attempt the API call const callApi = async (authToken: string) => { return analyzeRepoStreaming({ repo, prompt, ref, token: authToken, progressCallback: sendProgress, tracker, }); }; let result; try { result = await callApi(token); } catch (err: unknown) { const error = err as Error & { status?: number; body?: string }; // Handle connection errors if (error.message?.includes("fetch failed") || error.message?.includes("ECONNREFUSED")) { return { content: [ { type: "text" as const, text: `Could not connect to Instagit API at ${apiUrl}. Make sure the API server is running.`, }, ], }; } // Handle rate limiting (429) if (error.status === 429) { let rateLimitUntil: string | undefined; try { const errorData = JSON.parse(error.body ?? "{}"); rateLimitUntil = errorData.rate_limit_until; } catch { // ignore } const resetInfo = rateLimitUntil ? `\nCredits will reset at: ${rateLimitUntil}\n` : ""; return { content: [ { type: "text" as const, text: `Rate limit exceeded. Your free credits have been exhausted.\n${resetInfo}\n` + "To continue using Instagit immediately:\n" + "- Upgrade to Pro ($20/mo) for 10x more credits and faster analysis\n" + "- Visit: https://app.instagit.com/pricing", }, ], }; } // Handle auth errors (401) — retry with fresh token only for anonymous tokens if (error.status === 401) { // If the user set an API key, don't discard it — report the failure directly if (process.env.INSTAGIT_API_KEY) { return { content: [ { type: "text" as const, text: "Authentication failed (401). Your INSTAGIT_API_KEY was rejected.\n\n" + "Please verify your API key is correct and active at https://app.instagit.com/dashboard", }, ], }; } clearStoredToken(); const newToken = await registerAnonymousToken(apiUrl); if (newToken) { try { result = await callApi(newToken); } catch (retryErr: unknown) { const retryError = retryErr as Error & { status?: number }; return { content: [ { type: "text" as const, text: `API error after re-auth: ${retryError.status ?? retryError.message}`, }, ], }; } } else { return { content: [ { type: "text" as const, text: "Authentication failed. Unable to register a new token.\n\n" + "Please set INSTAGIT_API_KEY in your MCP configuration, " + "or visit https://app.instagit.com/signup to create an account.", }, ], }; } } // Other errors if (!result) { return { content: [ { type: "text" as const, text: `API error: ${error.status ?? ""} ${error.message}`, }, ], }; } } // Format response with usage footer let responseText = result!.text; const footerParts: string[] = []; if (result!.totalTokens > 0) { footerParts.push( `Tokens: ${result!.inputTokens.toLocaleString()} input, ` + `${result!.outputTokens.toLocaleString()} output, ` + `${result!.totalTokens.toLocaleString()} total` ); } if (result!.tier) { footerParts.push(`Tier: ${result!.tier}`); } if (result!.tokensRemaining > 0) { footerParts.push(`Credits remaining: ${result!.tokensRemaining.toLocaleString()}`); } if (footerParts.length > 0) { responseText += "\n\n---\n" + footerParts.join(" | "); } if (result!.upgradeHint) { responseText += `\n\n${result!.upgradeHint}`; } return { content: [{ type: "text" as const, text: responseText }], }; } ); - src/api.ts:46-243 (helper)Core helper function analyzeRepoStreaming() that makes the actual API call to the Instagit backend. Handles SSE streaming, retry logic for transport errors, progress tracking via heartbeat intervals, and returns the analysis result with token usage info.
export async function analyzeRepoStreaming(opts: StreamOptions): Promise<AnalysisResult> { const { repo, prompt, ref = null, token = null, progressCallback, } = opts; const tracker = opts.tracker ?? createProgressTracker(); const apiUrl = getApiUrl(); const model = buildModelString(repo, ref); const headers: Record<string, string> = { "Content-Type": "application/json" }; if (token) { headers["Authorization"] = `Bearer ${token}`; } const payload = { model, input: prompt, stream: true, }; let collectedText = ""; let usage: Record<string, unknown> = {}; // Heartbeat: send progress updates at 250ms intervals let heartbeatInterval: ReturnType<typeof setInterval> | null = null; if (progressCallback) { heartbeatInterval = setInterval(async () => { if (!tracker.done) { try { await progressCallback(formatMessage(tracker)); } catch { // Ignore progress errors } } }, 250); } try { for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) { collectedText = ""; usage = {}; try { const response = await fetch(`${apiUrl}/v1/responses`, { method: "POST", headers, body: JSON.stringify(payload), signal: AbortSignal.timeout(FETCH_TIMEOUT), }); if (!response.ok) { const errorBody = await response.text().catch(() => ""); const error = new Error(`API error: ${response.status}`) as Error & { status: number; body: string; }; error.status = response.status; error.body = errorBody; throw error; } if (!response.body) { throw new Error("No response body"); } // Parse SSE stream await new Promise<void>((resolve, reject) => { const parser = createParser({ onEvent(event: EventSourceMessage) { if (event.data === "[DONE]") { resolve(); return; } let data: SSEEventData; try { data = JSON.parse(event.data); } catch { return; } const eventType = data.type ?? ""; if (eventType === "response.reasoning.delta") { const status = data.delta ?? ""; if (status) tracker.lastStatus = status; if (data.tokens) { if (data.tokens.input !== undefined) tracker.inputTokens = data.tokens.input; if (data.tokens.output !== undefined) tracker.outputTokens = data.tokens.output; } } else if (eventType === "response.output_text.delta") { const delta = data.delta ?? ""; collectedText += delta; if (tracker.outputTokens === 0) { tracker.outputTokens = Math.floor(collectedText.length / 4); } tracker.lastStatus = "Writing response..."; } else if (eventType === "response.completed") { usage = (data.response?.usage as Record<string, unknown>) ?? {}; } }, }); const reader = response.body!.getReader(); const decoder = new TextDecoder(); function read() { reader .read() .then(({ done, value }) => { if (done) { resolve(); return; } parser.feed(decoder.decode(value, { stream: true })); read(); }) .catch(reject); } read(); }); // Check for permanent security rejection before empty-body retry if (isSecurityRejection(collectedText)) { throw new Error(`Request blocked: ${collectedText.slice(0, 200)}`); } // Empty body on 200 → treat as retryable if (!collectedText) { if (attempt < MAX_RETRIES) { if (progressCallback) { tracker.lastStatus = `Retrying... (attempt ${attempt + 2}/${MAX_RETRIES + 1})`; } await sleep(getRetryDelay(attempt)); continue; } throw new Error("Empty response after retries"); } break; // success } catch (error: unknown) { // Security rejection — bail immediately if (error instanceof Error && error.message.startsWith("Request blocked:")) { throw error; } const statusError = error as Error & { status?: number }; // Retryable HTTP status codes if (statusError.status && RETRYABLE_STATUS_CODES.has(statusError.status)) { if (attempt < MAX_RETRIES) { if (progressCallback) { tracker.lastStatus = `Retrying... (attempt ${attempt + 2}/${MAX_RETRIES + 1})`; } await sleep(getRetryDelay(attempt)); continue; } throw error; } // Transport errors (connection reset, etc.) if (isTransportError(error)) { if (attempt < MAX_RETRIES) { if (progressCallback) { tracker.lastStatus = `Retrying... (attempt ${attempt + 2}/${MAX_RETRIES + 1})`; } await sleep(getRetryDelay(attempt)); continue; } throw error; } // Non-retryable — bail immediately throw error; } } } finally { tracker.done = true; if (heartbeatInterval) { clearInterval(heartbeatInterval); } } return { text: collectedText, inputTokens: (usage.input_tokens as number) ?? 0, outputTokens: (usage.output_tokens as number) ?? 0, totalTokens: (usage.total_tokens as number) ?? 0, tier: (usage.tier as string) ?? "free", tokensRemaining: (usage.tokens_remaining as number) ?? 0, upgradeHint: (usage.upgrade_hint as string) ?? null, }; } - src/types.ts:5-23 (schema)TypeScript type definitions for AnalysisResult (text, token counts, tier, remaining credits) and ProgressTracker (timing, token display state, status) used by the tool handler and streaming API.
export interface AnalysisResult { text: string; inputTokens: number; outputTokens: number; totalTokens: number; tier: string; tokensRemaining: number; upgradeHint: string | null; } export interface ProgressTracker { startTime: number; frameIndex: number; inputTokens: number; outputTokens: number; displayedTokens: number; lastStatus: string; done: boolean; }