@debugai/mcp
Advanced tools
| /** Repo root per git, or null when this is not a git worktree. */ | ||
| export declare function gitToplevel(cwd?: string): string | null; | ||
| export declare function hashPath(p: string): string; | ||
| /** | ||
| * Resolve the project id for this process. Never throws: a debug request | ||
| * without memory is far better than a debug request that failed. | ||
| */ | ||
| export declare function resolveProjectId(cwd?: string): string | null; | ||
| /** | ||
| * Cached across the process. The MCP server is long-lived and its cwd does not | ||
| * move, so shelling out to git once per debug call would be wasted work. | ||
| */ | ||
| export declare function getProjectId(): string | null; | ||
| /** | ||
| * The directory local source resolution is allowed to read inside. | ||
| * | ||
| * Same value the project id is derived from — git toplevel, falling back to | ||
| * cwd — so "the project we remember errors for" and "the project we may read | ||
| * files from" can never be two different directories. sourceContext.ts treats | ||
| * this as a hard boundary: a path that resolves outside it is not read, which | ||
| * is what keeps a crafted stack trace from turning into an arbitrary file read. | ||
| */ | ||
| export declare function getProjectRoot(cwd?: string): string; | ||
| /** Test seam. */ | ||
| export declare function __resetProjectIdCache(): void; |
| /** | ||
| * Project identity for the MCP path. | ||
| * | ||
| * Team Error Memory (recurrence counts, confirmed fixes) is project-scoped in | ||
| * the engine: a request with no project_id is never recalled and never stored. | ||
| * Until this module existed, nothing in this package ever set project_id, so | ||
| * every agent request went unscoped and the memory features were silently | ||
| * inert on the MCP path while the server instructions advertised them. | ||
| * | ||
| * What identifies a "project" here: | ||
| * | ||
| * md5(git toplevel path) → falling back to md5(cwd) | ||
| * | ||
| * Two constraints drove that choice. | ||
| * | ||
| * 1. It has to MATCH the VS Code extension, which uses | ||
| * md5(workspaceFolder.fsPath). Someone running both against one repo should | ||
| * get one memory, not two. Git toplevel equals the workspace folder in the | ||
| * normal case, and resolving to the repo root also means an agent invoked | ||
| * from a subdirectory lands on the same id as one invoked from the root. | ||
| * | ||
| * 2. It does NOT need to be unguessable. The API gateway namespaces every | ||
| * incoming project_id as `u_<user.id>__<project_id>` before the engine sees | ||
| * it (apps/api/src/routes/debug.ts), so the authenticated user is the real | ||
| * tenancy boundary. A path string that collides across two users (`/app` in | ||
| * a devcontainer is the obvious case) still resolves to two different | ||
| * scoped ids server-side. | ||
| * | ||
| * Format note: the gateway rejects anything not matching | ||
| * /^[A-Za-z0-9_\-]{1,64}$/, and md5 hex satisfies that. | ||
| */ | ||
| import { createHash } from 'node:crypto'; | ||
| import { execFileSync } from 'node:child_process'; | ||
| let cached; | ||
| /** Repo root per git, or null when this is not a git worktree. */ | ||
| export function gitToplevel(cwd = process.cwd()) { | ||
| try { | ||
| const out = execFileSync('git', ['rev-parse', '--show-toplevel'], { | ||
| cwd, | ||
| encoding: 'utf8', | ||
| stdio: ['ignore', 'pipe', 'ignore'], | ||
| timeout: 2000, | ||
| }); | ||
| const trimmed = out.trim(); | ||
| return trimmed.length > 0 ? trimmed : null; | ||
| } | ||
| catch { | ||
| // Not a repo, git missing, or timeout. All three mean "fall back", not "fail". | ||
| return null; | ||
| } | ||
| } | ||
| export function hashPath(p) { | ||
| return createHash('md5').update(p).digest('hex'); | ||
| } | ||
| /** | ||
| * Resolve the project id for this process. Never throws: a debug request | ||
| * without memory is far better than a debug request that failed. | ||
| */ | ||
| export function resolveProjectId(cwd = process.cwd()) { | ||
| try { | ||
| const root = gitToplevel(cwd) ?? cwd; | ||
| if (!root) | ||
| return null; | ||
| return hashPath(root); | ||
| } | ||
| catch { | ||
| return null; | ||
| } | ||
| } | ||
| /** | ||
| * Cached across the process. The MCP server is long-lived and its cwd does not | ||
| * move, so shelling out to git once per debug call would be wasted work. | ||
| */ | ||
| export function getProjectId() { | ||
| if (cached === undefined) | ||
| cached = resolveProjectId(); | ||
| return cached; | ||
| } | ||
| /** | ||
| * The directory local source resolution is allowed to read inside. | ||
| * | ||
| * Same value the project id is derived from — git toplevel, falling back to | ||
| * cwd — so "the project we remember errors for" and "the project we may read | ||
| * files from" can never be two different directories. sourceContext.ts treats | ||
| * this as a hard boundary: a path that resolves outside it is not read, which | ||
| * is what keeps a crafted stack trace from turning into an arbitrary file read. | ||
| */ | ||
| export function getProjectRoot(cwd = process.cwd()) { | ||
| return gitToplevel(cwd) ?? cwd; | ||
| } | ||
| /** Test seam. */ | ||
| export function __resetProjectIdCache() { | ||
| cached = undefined; | ||
| } |
| /** Files read per request. Three frames is a cause, a caller, and one spare. */ | ||
| export declare const MAX_FILES = 3; | ||
| /** Per file. Larger than this and the window, not the file, is what we send. */ | ||
| export declare const MAX_BYTES = 256000; | ||
| /** Lines of context either side of the failing line. */ | ||
| export declare const WINDOW = 40; | ||
| export interface Frame { | ||
| path: string; | ||
| line: number | null; | ||
| } | ||
| export interface ResolvedFile { | ||
| /** Path as it will be shown to the model — relative to the root when inside it. */ | ||
| label: string; | ||
| line: number | null; | ||
| text: string; | ||
| /** True when only a window around `line` was sent, not the whole file. */ | ||
| windowed: boolean; | ||
| } | ||
| /** | ||
| * Frames in the order worth reading: innermost user frame first. | ||
| * | ||
| * Python tracebacks print outermost-first, so the interesting frame is LAST and | ||
| * the list is reversed. JS stacks print innermost-first and are read forward. | ||
| * Getting this backwards names the caller instead of the bug — the exact | ||
| * regression the extension shipped and had to fix. | ||
| */ | ||
| export declare function extractFrames(errorText: string): Frame[]; | ||
| /** Inside `root` after symlinks resolve? The check that stops `../../.ssh/…`. */ | ||
| export declare function isInsideRoot(candidate: string, root: string): boolean; | ||
| /** | ||
| * The extension ALLOWLIST is the mechanism — there is deliberately no secret | ||
| * denylist beside it. | ||
| * | ||
| * A denylist was written here first and deleted. Everything it caught was | ||
| * already unreachable: `.env`, `id_rsa` and `credentials` have no extension, | ||
| * `.env.local` extends to `.local`, `server.pem` to `.pem` — none are in | ||
| * SOURCE_EXT, so all four were refused before the denylist was consulted. It | ||
| * bought nothing. | ||
| * | ||
| * What it did buy was a bug. `^\.?env(\..+)?$` also matches `env.js` and | ||
| * `env.ts`, and `src/env.ts` is a real, common source file (it is the standard | ||
| * name for typed environment schemas). A config error names that file more | ||
| * often than any other, and the denylist would have silently dropped it while | ||
| * looking like a security control. | ||
| * | ||
| * If a denylist is ever wanted back, it needs a case the allowlist does not | ||
| * already cover, and a test proving it. | ||
| */ | ||
| export declare function isReadableSource(p: string): boolean; | ||
| /** | ||
| * Read the source behind an error. Returns null when there is nothing honest to | ||
| * send, which is a real answer: a wrong file is worse than no file, and the | ||
| * engine's own advice path handles the empty case. | ||
| * | ||
| * Never throws. A debug request that loses local context is worth far more than | ||
| * one that fails because a path was odd. | ||
| */ | ||
| export declare function resolveSourceContext(opts: { | ||
| errorText: string; | ||
| filePath?: string; | ||
| root: string; | ||
| cwd?: string; | ||
| maxFiles?: number; | ||
| }): { | ||
| snippet: string; | ||
| files: ResolvedFile[]; | ||
| } | null; | ||
| /** | ||
| * One block per file, labelled with the path and the failing line. | ||
| * | ||
| * The label matters as much as the code: the engine's v2 contract derives | ||
| * machine-applicable edits from what it was sent, and an edit is only | ||
| * applicable if the file it names is the file the agent can open. | ||
| */ | ||
| export declare function formatSnippet(files: ResolvedFile[]): string; |
| /** | ||
| * Local source resolution — read the files a stack trace names. | ||
| * | ||
| * ## Why this is here and not in the engine | ||
| * | ||
| * The engine cannot read your disk. It resolves a trace to `bad.js`, asks | ||
| * pgvector for that file, and gets nothing back unless the project was indexed | ||
| * first — and indexing only ever happened from the VS Code extension. So an | ||
| * agent on the MCP path hit this, every time, on every project: | ||
| * | ||
| * Fix 1 — Insufficient context for specific fix 20% confidence | ||
| * | ||
| * and the advice attached to it (services/context_advice.py) names | ||
| * "DebugAI: Index Entire Workspace", a VS Code palette command. A Claude Code | ||
| * or Cursor agent has no palette and no such command. That is the same defect | ||
| * we fixed in the extension on 2026-08-08 — advice naming a step the reader | ||
| * cannot take — aimed at a different audience. | ||
| * | ||
| * Rewording it would be the small fix. This is the real one: the MCP server is | ||
| * a local process. It has the filesystem the engine lacks, so it can send the | ||
| * actual source and skip the whole missing-context branch rather than write | ||
| * better copy about it. | ||
| * | ||
| * ## What it does | ||
| * | ||
| * Reads the error text, ranks the frames it names, resolves them against the | ||
| * project root, and returns a labelled snippet windowed around the failing | ||
| * lines. The agent's own `codeSnippet` always wins — this only fills a gap. | ||
| * | ||
| * ## What it refuses to do | ||
| * | ||
| * This module turns text the model produced into filesystem reads, so its | ||
| * limits are a security boundary, not tidiness: | ||
| * | ||
| * - every candidate must resolve INSIDE the project root, checked after | ||
| * symlink resolution, so `../../.ssh/id_rsa` in a crafted error cannot | ||
| * walk out | ||
| * - secret-shaped names (.env, *.pem, id_rsa, *.key) are never read even | ||
| * when they sit inside the root | ||
| * - source extensions only, MAX_FILES files, MAX_BYTES each | ||
| * | ||
| * Frame ORDERING is duplicated from apps/extension/src/errorPaths.ts on | ||
| * purpose: this package publishes to npm standalone and cannot import from the | ||
| * extension. The rule both must keep — Python innermost LAST, JS innermost | ||
| * FIRST — is stated in both files and tested in both. | ||
| */ | ||
| import { readFileSync, realpathSync, statSync } from 'node:fs'; | ||
| import { isAbsolute, join, relative, basename, extname } from 'node:path'; | ||
| /** Files read per request. Three frames is a cause, a caller, and one spare. */ | ||
| export const MAX_FILES = 3; | ||
| /** Per file. Larger than this and the window, not the file, is what we send. */ | ||
| export const MAX_BYTES = 256_000; | ||
| /** Lines of context either side of the failing line. */ | ||
| export const WINDOW = 40; | ||
| const SOURCE_EXT = new Set([ | ||
| '.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs', | ||
| '.py', '.go', '.rs', '.rb', '.java', '.kt', '.php', '.cs', '.swift', | ||
| '.vue', '.svelte', | ||
| ]); | ||
| const VENDOR = /(^|\/)(node_modules|site-packages|\.venv|venv|vendor|dist|build|\.next|target)(\/|$)/; | ||
| // ── Frame extraction ───────────────────────────────────────────────────────── | ||
| // | ||
| // Same shapes as the extension's parser, but this one keeps the LINE NUMBER and | ||
| // returns every frame rather than one best guess: the window we send is built | ||
| // around the line, and a cause plus its caller is usually worth two reads. | ||
| const PY_FRAME = /File ["']([^"'\r\n]+\.py)["'],\s*line\s*(\d+)/g; | ||
| const PY_TEST_ID = /([\w./\\-]+\.py)::/g; | ||
| const PY_LINE_REF = /(?:^|\s)([\w./\\-]+\.py):(\d+):/gm; | ||
| const JS_FRAME = /(?:at\s+\S+\s+\()?((?:[A-Za-z]:)?[^\s()]+\.(?:js|ts|jsx|tsx|mjs|cjs)):(\d+)/g; | ||
| /** Go, Rust, Ruby, Java and friends: `path/file.ext:123` on its own. */ | ||
| const GENERIC = /(?:^|[\s(])((?:[A-Za-z]:)?[^\s():]+\.(?:go|rs|rb|java|kt|php|cs|swift|vue|svelte)):(\d+)/g; | ||
| function collect(text, re, withLine) { | ||
| return [...text.matchAll(re)].map((m) => ({ | ||
| path: m[1], | ||
| line: withLine && m[2] ? Number(m[2]) : null, | ||
| })); | ||
| } | ||
| function isVendor(p) { | ||
| return VENDOR.test(p.replace(/\\/g, '/')); | ||
| } | ||
| /** | ||
| * Frames in the order worth reading: innermost user frame first. | ||
| * | ||
| * Python tracebacks print outermost-first, so the interesting frame is LAST and | ||
| * the list is reversed. JS stacks print innermost-first and are read forward. | ||
| * Getting this backwards names the caller instead of the bug — the exact | ||
| * regression the extension shipped and had to fix. | ||
| */ | ||
| export function extractFrames(errorText) { | ||
| if (!errorText) | ||
| return []; | ||
| const out = []; | ||
| const seen = new Set(); | ||
| const push = (f) => { | ||
| const key = `${f.path}:${f.line ?? ''}`; | ||
| if (!isVendor(f.path) && !seen.has(key)) { | ||
| seen.add(key); | ||
| out.push(f); | ||
| } | ||
| }; | ||
| collect(errorText, PY_FRAME, true).reverse().forEach(push); | ||
| collect(errorText, JS_FRAME, true).forEach(push); | ||
| collect(errorText, GENERIC, true).forEach(push); | ||
| // Test-runner shapes carry no frame at all. Last, because a pytest node id | ||
| // names the TEST file, which is a weaker guess than any real frame. | ||
| collect(errorText, PY_LINE_REF, true).forEach(push); | ||
| collect(errorText, PY_TEST_ID, false).forEach(push); | ||
| return out; | ||
| } | ||
| // ── Safety ─────────────────────────────────────────────────────────────────── | ||
| /** Inside `root` after symlinks resolve? The check that stops `../../.ssh/…`. */ | ||
| export function isInsideRoot(candidate, root) { | ||
| let realRoot; | ||
| let realCandidate; | ||
| try { | ||
| realRoot = realpathSync(root); | ||
| } | ||
| catch { | ||
| return false; | ||
| } | ||
| try { | ||
| realCandidate = realpathSync(candidate); | ||
| } | ||
| catch { | ||
| // Not a real file. Nothing to read, so nothing to allow. | ||
| return false; | ||
| } | ||
| const rel = relative(realRoot, realCandidate); | ||
| return rel !== '' && !rel.startsWith('..') && !isAbsolute(rel); | ||
| } | ||
| /** | ||
| * The extension ALLOWLIST is the mechanism — there is deliberately no secret | ||
| * denylist beside it. | ||
| * | ||
| * A denylist was written here first and deleted. Everything it caught was | ||
| * already unreachable: `.env`, `id_rsa` and `credentials` have no extension, | ||
| * `.env.local` extends to `.local`, `server.pem` to `.pem` — none are in | ||
| * SOURCE_EXT, so all four were refused before the denylist was consulted. It | ||
| * bought nothing. | ||
| * | ||
| * What it did buy was a bug. `^\.?env(\..+)?$` also matches `env.js` and | ||
| * `env.ts`, and `src/env.ts` is a real, common source file (it is the standard | ||
| * name for typed environment schemas). A config error names that file more | ||
| * often than any other, and the denylist would have silently dropped it while | ||
| * looking like a security control. | ||
| * | ||
| * If a denylist is ever wanted back, it needs a case the allowlist does not | ||
| * already cover, and a test proving it. | ||
| */ | ||
| export function isReadableSource(p) { | ||
| return SOURCE_EXT.has(extname(p).toLowerCase()); | ||
| } | ||
| // ── Reading ────────────────────────────────────────────────────────────────── | ||
| /** | ||
| * Candidate absolute paths for one frame, best first. | ||
| * | ||
| * A trace may carry an absolute path (already correct), a path relative to the | ||
| * root, or a path relative to a subdirectory the process ran from. All three | ||
| * are tried; the first that exists and passes the root check wins. | ||
| */ | ||
| function candidatesFor(frame, root, cwd) { | ||
| const p = frame.path.replace(/^file:\/\//, ''); | ||
| if (isAbsolute(p)) | ||
| return [p]; | ||
| const out = [join(root, p), join(cwd, p)]; | ||
| // `src/app/x.ts` printed from inside `src/` — strip one leading segment. | ||
| const stripped = p.split('/').slice(1).join('/'); | ||
| if (stripped) | ||
| out.push(join(root, stripped)); | ||
| return out; | ||
| } | ||
| function windowAround(text, line) { | ||
| if (line === null) { | ||
| const lines = text.split('\n'); | ||
| if (lines.length <= WINDOW * 2 + 1) | ||
| return { text, windowed: false }; | ||
| return { text: lines.slice(0, WINDOW * 2 + 1).join('\n'), windowed: true }; | ||
| } | ||
| const lines = text.split('\n'); | ||
| if (lines.length <= WINDOW * 2 + 1) | ||
| return { text, windowed: false }; | ||
| const start = Math.max(0, line - 1 - WINDOW); | ||
| const end = Math.min(lines.length, line + WINDOW); | ||
| return { text: lines.slice(start, end).join('\n'), windowed: true }; | ||
| } | ||
| function readOne(frame, root, cwd) { | ||
| for (const candidate of candidatesFor(frame, root, cwd)) { | ||
| if (!isReadableSource(candidate)) | ||
| continue; | ||
| if (!isInsideRoot(candidate, root)) | ||
| continue; | ||
| try { | ||
| const st = statSync(candidate); | ||
| if (!st.isFile()) | ||
| continue; | ||
| const raw = st.size > MAX_BYTES | ||
| ? readFileSync(candidate, 'utf8').slice(0, MAX_BYTES) | ||
| : readFileSync(candidate, 'utf8'); | ||
| const win = windowAround(raw, frame.line); | ||
| const real = realpathSync(candidate); | ||
| const rel = relative(realpathSync(root), real); | ||
| return { label: rel || basename(real), line: frame.line, text: win.text, windowed: win.windowed }; | ||
| } | ||
| catch { | ||
| continue; // unreadable, binary, permissions — try the next candidate | ||
| } | ||
| } | ||
| return null; | ||
| } | ||
| /** | ||
| * Read the source behind an error. Returns null when there is nothing honest to | ||
| * send, which is a real answer: a wrong file is worse than no file, and the | ||
| * engine's own advice path handles the empty case. | ||
| * | ||
| * Never throws. A debug request that loses local context is worth far more than | ||
| * one that fails because a path was odd. | ||
| */ | ||
| export function resolveSourceContext(opts) { | ||
| const cwd = opts.cwd ?? opts.root; | ||
| const maxFiles = opts.maxFiles ?? MAX_FILES; | ||
| try { | ||
| const frames = []; | ||
| // An explicit filePath from the agent outranks anything parsed out of text. | ||
| // Its line, if the trace names one for the same file, comes along. | ||
| if (opts.filePath) { | ||
| const fromText = extractFrames(opts.errorText) | ||
| .find((f) => f.path.endsWith(basename(opts.filePath))); | ||
| frames.push({ path: opts.filePath, line: fromText?.line ?? null }); | ||
| } | ||
| frames.push(...extractFrames(opts.errorText)); | ||
| const files = []; | ||
| const takenLabels = new Set(); | ||
| for (const frame of frames) { | ||
| if (files.length >= maxFiles) | ||
| break; | ||
| const got = readOne(frame, opts.root, cwd); | ||
| if (got && !takenLabels.has(got.label)) { | ||
| takenLabels.add(got.label); | ||
| files.push(got); | ||
| } | ||
| } | ||
| if (files.length === 0) | ||
| return null; | ||
| return { snippet: formatSnippet(files), files }; | ||
| } | ||
| catch { | ||
| return null; | ||
| } | ||
| } | ||
| /** | ||
| * One block per file, labelled with the path and the failing line. | ||
| * | ||
| * The label matters as much as the code: the engine's v2 contract derives | ||
| * machine-applicable edits from what it was sent, and an edit is only | ||
| * applicable if the file it names is the file the agent can open. | ||
| */ | ||
| export function formatSnippet(files) { | ||
| return files | ||
| .map((f) => { | ||
| const where = f.line !== null ? `${f.label}:${f.line}` : f.label; | ||
| const note = f.windowed | ||
| ? ` (lines around ${f.line ?? 1}; file truncated for context budget)` | ||
| : ''; | ||
| return `--- ${where}${note} ---\n${f.text}`; | ||
| }) | ||
| .join('\n\n'); | ||
| } |
@@ -45,2 +45,3 @@ import type { AuthProvider } from './auth.js'; | ||
| memory_hit?: boolean; | ||
| memory_times_seen?: number; | ||
| memory_fix_confirmed?: boolean; | ||
@@ -47,0 +48,0 @@ } |
+3
-1
@@ -44,3 +44,5 @@ #!/usr/bin/env node | ||
| debug_error hand it an error or stack trace, get root cause + ranked | ||
| fixes with machine-applicable edits (v2 contract) | ||
| fixes with machine-applicable edits (v2 contract). Reads the | ||
| source files the trace names, from inside this project only — | ||
| no need to locate and paste code first. | ||
| report_outcome tell DebugAI whether an applied fix worked — failed-fix | ||
@@ -47,0 +49,0 @@ follow-ups improve future answers for your codebase |
+7
-0
@@ -21,2 +21,9 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; | ||
| Pass the raw error or traceback. You do not need to find and paste the source | ||
| first: this server runs on the same machine as the code, so it resolves the | ||
| files the trace names and sends them along. It reads source files inside the | ||
| current project only, and the response says which ones. Pass codeSnippet | ||
| yourself only when you already know the relevant code is somewhere the trace | ||
| does not name. | ||
| After you apply or abandon a fix, call report_outcome with the debug_log_id. | ||
@@ -23,0 +30,0 @@ That is what turns a one-off answer into memory for the next person who hits |
@@ -5,2 +5,4 @@ import { z } from 'zod'; | ||
| import { resolveAuth } from './authGate.js'; | ||
| import { getProjectId, getProjectRoot } from '../project.js'; | ||
| import { resolveSourceContext } from '../sourceContext.js'; | ||
| // Tri-state verification labeling (docs/plan-v2-contract-phase1.md §1). | ||
@@ -49,2 +51,4 @@ // The null case is rendered ON PURPOSE: a confidence number nothing checked | ||
| 'Works for Python, JavaScript, TypeScript, Go, Rust. ' + | ||
| 'Pass the raw error text — this server runs locally and reads the source files the ' + | ||
| 'stack trace names, so you do not need to locate and paste the code first. ' + | ||
| 'Returns root cause explanation plus up to 3 ranked fixes with machine-applicable code edits. ' + | ||
@@ -64,3 +68,5 @@ 'After applying a fix, report whether it worked via the report_outcome tool.', | ||
| .optional() | ||
| .describe('Surrounding code lines near where the error was thrown, if available.'), | ||
| .describe('Optional. Surrounding code near where the error was thrown. Usually unnecessary — ' + | ||
| 'the server reads the files the trace names off local disk. Supply it only when the ' + | ||
| 'relevant code is somewhere the trace does not name; it overrides the local read.'), | ||
| filePath: z | ||
@@ -79,2 +85,18 @@ .string() | ||
| return gate.result; | ||
| // Local source resolution. The agent's own snippet always wins — it knows | ||
| // what it was looking at. This only fills the gap, and the gap was the | ||
| // normal case: an agent that pastes a traceback and nothing else used to | ||
| // get "Insufficient context for specific fix" at 20% confidence, because | ||
| // the engine's only other source of code is a pgvector index that nothing | ||
| // outside the VS Code extension ever populates. See ../sourceContext.ts. | ||
| let resolved = null; | ||
| if (!codeSnippet) { | ||
| resolved = resolveSourceContext({ | ||
| errorText, | ||
| filePath, | ||
| root: getProjectRoot(), | ||
| }); | ||
| } | ||
| const snippet = codeSnippet ?? resolved?.snippet; | ||
| const effectiveFilePath = filePath ?? resolved?.files[0]?.label; | ||
| try { | ||
@@ -84,4 +106,10 @@ const result = await callDebugBackend({ | ||
| language: language !== 'auto' ? language : undefined, | ||
| code_snippet: codeSnippet, | ||
| file_path: filePath, | ||
| code_snippet: snippet, | ||
| file_path: effectiveFilePath, | ||
| // Project scope. Team Error Memory is project-scoped in the engine: | ||
| // without this, recurrence is never recalled and never recorded, and | ||
| // the confirmed-fix promotion in feedback.ts cannot match. Derived | ||
| // from the git repo root so it agrees with the VS Code extension's | ||
| // md5(workspaceFolder) for the same checkout. See ../project.ts. | ||
| project_id: getProjectId() ?? undefined, | ||
| // framework_hint deliberately omitted: a language ('python') is not a | ||
@@ -98,3 +126,22 @@ // framework ('fastapi'), and sending it bypasses the engine's | ||
| } | ||
| // Memory first, above the badges: "this project has hit this exact | ||
| // error before, and a fix was confirmed" is the one thing here the | ||
| // agent cannot derive from the code in front of it, so it should not | ||
| // be buried in a metadata footer. | ||
| if (result.memory_hit) { | ||
| const seen = typeof result.memory_times_seen === 'number' | ||
| ? `${result.memory_times_seen}x before in this project` | ||
| : 'before in this project'; | ||
| sections.push(result.memory_fix_confirmed | ||
| ? `\n## Seen before\nThis error has been seen ${seen}, and a fix was confirmed working. The confirmed fix led the analysis above.` | ||
| : `\n## Seen before\nThis error has been seen ${seen}. No fix has been confirmed for it yet.`); | ||
| } | ||
| const badges = []; | ||
| // Named, not counted. "Read 2 files" is unverifiable; "read src/api.ts, | ||
| // src/db.ts" lets the agent notice DebugAI looked at the wrong thing — | ||
| // which is the failure mode local resolution introduces and the one | ||
| // worth making visible. | ||
| if (resolved) { | ||
| badges.push(`Read locally: ${resolved.files.map((f) => f.label).join(', ')}`); | ||
| } | ||
| if (result.model_used) { | ||
@@ -132,2 +179,11 @@ badges.push(`Model: ${result.model_used}`); | ||
| error_signature: result.error_signature ?? null, | ||
| // Advertised in the server instructions ("whether this exact error | ||
| // has been seen before in this project, and how often"), so they | ||
| // belong in the structured payload, not only in the prose. | ||
| memory_hit: result.memory_hit ?? false, | ||
| memory_times_seen: result.memory_times_seen ?? null, | ||
| memory_fix_confirmed: result.memory_fix_confirmed ?? false, | ||
| // Which files this server read off disk, if any. Empty when the | ||
| // agent supplied its own snippet or nothing resolved. | ||
| local_files_read: resolved?.files.map((f) => f.label) ?? [], | ||
| }, | ||
@@ -134,0 +190,0 @@ }; |
+1
-1
| { | ||
| "name": "@debugai/mcp", | ||
| "version": "2.1.1", | ||
| "version": "2.2.0", | ||
| "mcpName": "io.github.1shizaan/debugai-mcp", | ||
@@ -5,0 +5,0 @@ "description": "DebugAI MCP server. One command sets it up in Claude Desktop, Claude Code, Cursor, Zed, Windsurf, Cline or any MCP client: browser sign-in, no key pasting, no config editing.", |
+31
-3
@@ -78,9 +78,25 @@ # @debugai/mcp | ||
| | `language` | no | `javascript`, `typescript`, `python`, `go`, `rust`, or `auto` (default). | | ||
| | `codeSnippet` | no | Code around the failing line, if the agent has it. | | ||
| | `codeSnippet` | no | Usually unnecessary — see below. Overrides the local read when given. | | ||
| | `filePath` | no | Path to the file that threw. | | ||
| Returns the root cause, up to 3 fixes ranked by confidence, the detected framework, and whether the answer came from cache. Since 2.0 each fix also carries, where derivable: `edits` (exact old/new strings your agent's edit tool can apply directly), `unified_diff`, and `verify_with` (a syntax-level check command to run after applying). Read-only: it never touches your files. Applying a fix is your agent's call, and yours. | ||
| Returns the root cause, up to 3 fixes ranked by confidence, the detected framework, and whether the answer came from cache. Since 2.0 each fix also carries, where derivable: `edits` (exact old/new strings your agent's edit tool can apply directly), `unified_diff`, and `verify_with` (a syntax-level check command to run after applying). It never writes to your files. Applying a fix is your agent's call, and yours. | ||
| Every fix is labeled with its verification state, and there are three of them, not two: **verified** (a mechanical check passed, currently parse and import classes), **failed check** (confidence capped hard), or **not verified** (the confidence number is the model's own estimate, nothing checked it). We label the third case instead of hiding it. | ||
| #### It reads the source the trace names (2.2) | ||
| Paste a raw traceback and nothing else. This server runs on your machine, so it resolves the files in the stack trace and sends the relevant code along. Before 2.2 it could not, and an error pasted without code came back as *"Insufficient context for specific fix"* at 20% confidence — the common case, not the edge one. | ||
| The response names every file it read (`Read locally: src/api.ts, src/db.ts`, and `local_files_read` in the structured payload), so a wrong guess is visible rather than silent. | ||
| Exactly what it will and will not read: | ||
| - **Inside your project only.** The boundary is your git repo root, or the working directory when that is not a repo. It is checked *after* symlinks resolve, so a path in an error message cannot walk out of the project — `../../.ssh/id_rsa` and a symlink pointing outside both fail the same check. | ||
| - **Source files only**, by extension allowlist (`.ts .tsx .js .jsx .mjs .cjs .py .go .rs .rb .java .kt .php .cs .swift .vue .svelte`). `.env`, `.pem`, key files, dumps and images are not on it, so they are never candidates. | ||
| - **Bounded**: at most 3 files, 256 KB each, and a large file is sent as a ±40-line window around the failing line rather than whole. Windowed blocks say so. | ||
| - **Vendored code is skipped** — `node_modules`, `site-packages`, `dist`, `.venv` and friends. Your code is what needs explaining. | ||
| - **Your snippet always wins.** Pass `codeSnippet` and nothing is read from disk. | ||
| Whatever it reads is sent to DebugAI's API for analysis, same as the error text. If that is not acceptable for a given repo, pass `codeSnippet` explicitly and the local read never happens. | ||
| ### `report_outcome` | ||
@@ -176,3 +192,4 @@ | ||
| - Simple errors route to a fast model. Ugly cross-file ones route to a stronger one on paid tiers. The `Model:` badge in each response tells you which one answered. | ||
| - Analyses run on DebugAI's servers. The error text and any snippet you pass are sent there, and Claude (Anthropic) does the analysis. Privacy policy: [debugai.io/privacy](https://debugai.io/privacy?src=npm). | ||
| - Analyses run on DebugAI's servers. The error text, any snippet you pass, and — since 2.2 — the project source files the stack trace names are sent there, and Claude (Anthropic) does the analysis. The response lists every file that was read. Scope and limits are spelled out under [`debug_error`](#it-reads-the-source-the-trace-names-22). Privacy policy: [debugai.io/privacy](https://debugai.io/privacy?src=npm). | ||
| - Errors are remembered per project so a repeat hit starts from a fix that was already confirmed. The project identity is a hash of your repo root path; the path itself is never sent. | ||
@@ -190,2 +207,13 @@ ## Troubleshooting | ||
| **Note on updates**: the analysis runs on DebugAI's servers, so improvements to | ||
| error parsing, retrieval and root-cause quality reach you without upgrading this | ||
| package. Recent server-side work: pytest, unittest, jest, vitest and mocha output | ||
| now yields real file paths, so a failing test gets cross-file context instead of | ||
| none. Client releases are only for things that change on your machine. | ||
| **2.2.0**: two things that were advertised but not delivered. | ||
| - **Local source resolution.** The server now reads the files a stack trace names and sends them with the request. Before this, an agent that pasted a traceback got "Insufficient context for specific fix" at 20% confidence, because the only other source of code was an index that nothing outside the VS Code extension populates. Bounded to your repo root, source extensions, 3 files. The response names what it read. | ||
| - **Project-scoped error memory.** Nothing in this package ever sent a `project_id`, and the engine gates recurrence counts and confirmed fixes on it — so every agent request got no recall and recorded none, while the server instructions told agents it did. Now derived from your git repo root, matching the VS Code extension's id for the same checkout, so both surfaces share one memory. `debug_error` surfaces "seen 3x before, fix confirmed" when it applies. | ||
| **2.1.1**: metadata only. Adds `mcpName` for official MCP registry ownership verification, fixes the npm package page's repository link. | ||
@@ -192,0 +220,0 @@ |
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
127093
27.49%39
11.43%2557
25.9%238
13.33%32
3.23%8
14.29%