@debugai/mcp 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/ui.js ADDED
@@ -0,0 +1,56 @@
1
+ // Terminal output helpers.
2
+ //
3
+ // Everything the CLI prints goes to STDOUT here — but note the MCP server
4
+ // itself prints only to stderr (stdout carries the protocol). The two never
5
+ // run at the same time: a subcommand exits before any transport is created.
6
+ import { spawn } from 'node:child_process';
7
+ import { platform } from 'node:os';
8
+ const useColor = process.stdout.isTTY === true &&
9
+ !process.env.NO_COLOR &&
10
+ process.env.TERM !== 'dumb';
11
+ const wrap = (code) => (s) => (useColor ? `[${code}m${s}` : s);
12
+ export const bold = wrap('1');
13
+ export const dim = wrap('2');
14
+ export const red = wrap('31');
15
+ export const green = wrap('32');
16
+ export const yellow = wrap('33');
17
+ export const OK = () => green('✓');
18
+ export const FAIL = () => red('✗');
19
+ export const WARN = () => yellow('!');
20
+ export const INFO = () => dim('·');
21
+ export function say(line = '') {
22
+ process.stdout.write(`${line}\n`);
23
+ }
24
+ export function heading(text) {
25
+ say();
26
+ say(bold(text));
27
+ }
28
+ /**
29
+ * Opens a URL in the user's browser, best effort. Returns false when there is
30
+ * clearly no browser to open (headless Linux, CI) so the caller prints the URL
31
+ * instead of pretending something happened.
32
+ */
33
+ export function openBrowser(url) {
34
+ const os = platform();
35
+ const headlessLinux = os === 'linux' && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY && !process.env.WSL_DISTRO_NAME;
36
+ if (process.env.CI || headlessLinux)
37
+ return false;
38
+ const [cmd, args] = os === 'darwin' ? ['open', [url]] :
39
+ os === 'win32' ? ['cmd', ['/c', 'start', '', url]] :
40
+ ['xdg-open', [url]];
41
+ try {
42
+ const child = spawn(cmd, args, { stdio: 'ignore', detached: true });
43
+ child.on('error', () => { });
44
+ child.unref();
45
+ return true;
46
+ }
47
+ catch {
48
+ return false;
49
+ }
50
+ }
51
+ /** Big enough to read across a room, small enough to fit a narrow terminal. */
52
+ export function codeBox(code) {
53
+ const inner = ` ${code} `;
54
+ const rule = '─'.repeat(inner.length);
55
+ return [`┌${rule}┐`, `│${inner}│`, `└${rule}┘`].join('\n');
56
+ }
package/dist/config.d.ts CHANGED
@@ -11,3 +11,13 @@ export interface ResolvedSettings {
11
11
  export declare function configPath(env?: NodeJS.ProcessEnv): string;
12
12
  export declare function loadFileConfig(env?: NodeJS.ProcessEnv, warn?: (msg: string) => void): FileConfig;
13
13
  export declare function resolveSettings(defaultApiBase: string, env?: NodeJS.ProcessEnv, warn?: (msg: string) => void): ResolvedSettings;
14
+ /** Fields a write may touch. `undefined` leaves the existing value alone. */
15
+ export interface ConfigPatch {
16
+ apiKey?: string;
17
+ apiBase?: string;
18
+ }
19
+ export declare function writeFileConfig(patch: ConfigPatch, env?: NodeJS.ProcessEnv): string;
20
+ /** Removes the stored key, keeping any api_base override. True if a key was there. */
21
+ export declare function clearStoredKey(env?: NodeJS.ProcessEnv): boolean;
22
+ /** Keys are long secrets — never print more than their shape. */
23
+ export declare function maskKey(key: string): string;
package/dist/config.js CHANGED
@@ -6,9 +6,14 @@
6
6
  // Environment variables always win over the file. DEBUGAI_CONFIG_PATH
7
7
  // overrides the file location (tests point it at a temp dir; users normally
8
8
  // never set it).
9
- import { readFileSync } from 'node:fs';
9
+ //
10
+ // This file is the ONLY place the API key is ever written. `debugai-mcp
11
+ // install` deliberately does not put the key into any MCP client config:
12
+ // one secret, one file, 0600 — rotating or revoking is a single edit, and a
13
+ // shared or committed client config never carries a live key.
14
+ import { chmodSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
10
15
  import { homedir } from 'node:os';
11
- import { join } from 'node:path';
16
+ import { dirname, join } from 'node:path';
12
17
  export function configPath(env = process.env) {
13
18
  const override = (env.DEBUGAI_CONFIG_PATH ?? '').trim();
14
19
  return override || join(homedir(), '.debugai', 'config.json');
@@ -46,3 +51,50 @@ export function resolveSettings(defaultApiBase, env = process.env, warn = (msg)
46
51
  const apiBase = (envBase || file.apiBase || defaultApiBase).replace(/\/+$/, '');
47
52
  return { apiKey, apiBase, keySource };
48
53
  }
54
+ export function writeFileConfig(patch, env = process.env) {
55
+ const path = configPath(env);
56
+ const existing = loadFileConfig(env, () => { }); // corrupt file → start clean
57
+ const next = {};
58
+ const apiKey = patch.apiKey !== undefined ? patch.apiKey : existing.apiKey;
59
+ const apiBase = patch.apiBase !== undefined ? patch.apiBase : existing.apiBase;
60
+ if (apiKey)
61
+ next.api_key = apiKey;
62
+ if (apiBase)
63
+ next.api_base = apiBase;
64
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
65
+ const tmp = `${path}.tmp-${process.pid}`;
66
+ writeFileSync(tmp, `${JSON.stringify(next, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
67
+ try {
68
+ chmodSync(tmp, 0o600); // umask can defeat the mode passed to writeFileSync
69
+ }
70
+ catch {
71
+ /* non-POSIX filesystem — best effort */
72
+ }
73
+ try {
74
+ renameSync(tmp, path);
75
+ }
76
+ catch (err) {
77
+ try {
78
+ unlinkSync(tmp);
79
+ }
80
+ catch { /* ignore */ }
81
+ throw err;
82
+ }
83
+ return path;
84
+ }
85
+ /** Removes the stored key, keeping any api_base override. True if a key was there. */
86
+ export function clearStoredKey(env = process.env) {
87
+ const existing = loadFileConfig(env, () => { });
88
+ if (!existing.apiKey)
89
+ return false;
90
+ writeFileConfig({ apiKey: '' }, env);
91
+ return true;
92
+ }
93
+ /** Keys are long secrets — never print more than their shape. */
94
+ export function maskKey(key) {
95
+ if (!key)
96
+ return '(none)';
97
+ if (key.length <= 12)
98
+ return `${key.slice(0, 4)}…`;
99
+ return `${key.slice(0, 8)}…${key.slice(-4)}`;
100
+ }
@@ -0,0 +1,2 @@
1
+ export declare const DEFAULT_API_BASE = "https://debugai-mvp-production.up.railway.app/api";
2
+ export declare const WEB_BASE = "https://debugai.io";
@@ -0,0 +1,5 @@
1
+ // Shared endpoints. The API base is overridable per-user (DEBUGAI_API_BASE or
2
+ // api_base in the config file) for self-hosted and staging setups; these are
3
+ // only the defaults.
4
+ export const DEFAULT_API_BASE = 'https://debugai-mvp-production.up.railway.app/api';
5
+ export const WEB_BASE = 'https://debugai.io';
@@ -0,0 +1,51 @@
1
+ export interface DeviceLinkStart {
2
+ deviceCode: string;
3
+ userCode: string;
4
+ verificationUri: string;
5
+ /** URL with the code pre-filled — what we actually open / print first. */
6
+ verificationUriComplete: string;
7
+ expiresIn: number;
8
+ /** Minimum seconds between polls, per the server. */
9
+ interval: number;
10
+ }
11
+ export type DeviceLinkPoll = {
12
+ status: 'pending';
13
+ } | {
14
+ status: 'slow_down';
15
+ interval: number;
16
+ } | {
17
+ status: 'expired';
18
+ } | {
19
+ status: 'denied';
20
+ } | {
21
+ status: 'linked';
22
+ apiKey: string;
23
+ email?: string;
24
+ tier?: string;
25
+ };
26
+ export interface DeviceLinkOptions {
27
+ apiBase: string;
28
+ /** Shown on the approval page so the human knows what they're authorizing. */
29
+ clientLabel?: string;
30
+ timeoutMs?: number;
31
+ fetchImpl?: typeof fetch;
32
+ }
33
+ export declare class DeviceLinkError extends Error {
34
+ readonly status: number;
35
+ constructor(message: string, status?: number);
36
+ }
37
+ export declare function startDeviceLink(opts: DeviceLinkOptions): Promise<DeviceLinkStart>;
38
+ /** One poll. Never throws on a normal pending/expired answer — those are statuses. */
39
+ export declare function pollDeviceLink(deviceCode: string, opts: DeviceLinkOptions): Promise<DeviceLinkPoll>;
40
+ export interface WaitOptions extends DeviceLinkOptions {
41
+ /** Called once per state change so a CLI can show progress. */
42
+ onTick?: (secondsLeft: number) => void;
43
+ sleep?: (ms: number) => Promise<void>;
44
+ now?: () => number;
45
+ }
46
+ /**
47
+ * Blocks until the human approves, denies, or the code expires. Honors the
48
+ * server's interval and backs off when told to (`slow_down`) — a client that
49
+ * ignores that is how a device flow turns into a self-inflicted DoS.
50
+ */
51
+ export declare function waitForDeviceLink(start: DeviceLinkStart, opts: WaitOptions): Promise<DeviceLinkPoll>;
@@ -0,0 +1,125 @@
1
+ // Device-link client — the API-key-paste killer.
2
+ //
3
+ // Modeled on the OAuth 2.0 Device Authorization Grant (RFC 8628), which
4
+ // exists for exactly this shape of problem: a program that can't own a
5
+ // browser redirect needs a credential a human holds. Same three moves:
6
+ //
7
+ // 1. start → server mints a long secret (device_code) + a short
8
+ // human-typeable code (user_code) and a verification URL
9
+ // 2. human → opens the URL in a real browser, signs in, confirms the code
10
+ // 3. poll → the program exchanges device_code for the credential
11
+ //
12
+ // Deviations from RFC 8628, on purpose: this returns a DebugAI API key
13
+ // rather than an OAuth access token (no token endpoint, no refresh cycle,
14
+ // and the key is the same one the extension and dashboard already use), and
15
+ // there is no client_id — the npm package is the only client.
16
+ //
17
+ // Poll statuses mirror the RFC's error codes so the state machine is
18
+ // familiar: authorization_pending, slow_down, expired_token, access_denied.
19
+ const START_TIMEOUT_MS = 15_000;
20
+ const POLL_TIMEOUT_MS = 15_000;
21
+ export class DeviceLinkError extends Error {
22
+ status;
23
+ constructor(message, status = 0) {
24
+ super(message);
25
+ this.status = status;
26
+ this.name = 'DeviceLinkError';
27
+ }
28
+ }
29
+ async function postJson(url, body, timeoutMs, fetchImpl) {
30
+ const controller = new AbortController();
31
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
32
+ try {
33
+ const res = await fetchImpl(url, {
34
+ method: 'POST',
35
+ headers: { 'content-type': 'application/json' },
36
+ body: JSON.stringify(body),
37
+ signal: controller.signal,
38
+ });
39
+ const text = await res.text().catch(() => '');
40
+ let json = {};
41
+ try {
42
+ json = text ? JSON.parse(text) : {};
43
+ }
44
+ catch {
45
+ json = { raw: text };
46
+ }
47
+ return { status: res.status, json };
48
+ }
49
+ catch (err) {
50
+ if (controller.signal.aborted) {
51
+ throw new DeviceLinkError(`DebugAI did not answer within ${Math.round(timeoutMs / 1000)}s`, 504);
52
+ }
53
+ throw new DeviceLinkError(`Could not reach DebugAI at ${url}: ${err?.message ?? String(err)}`, 0);
54
+ }
55
+ finally {
56
+ clearTimeout(timer);
57
+ }
58
+ }
59
+ export async function startDeviceLink(opts) {
60
+ const fetchImpl = opts.fetchImpl ?? fetch;
61
+ const { status, json } = await postJson(`${opts.apiBase}/device-link/start`, { client_label: opts.clientLabel ?? 'DebugAI MCP server' }, opts.timeoutMs ?? START_TIMEOUT_MS, fetchImpl);
62
+ if (status !== 200 || !json?.device_code || !json?.user_code) {
63
+ throw new DeviceLinkError(json?.error
64
+ ? `DebugAI refused to start the link: ${json.error}`
65
+ : `DebugAI returned HTTP ${status} when starting the link`, status);
66
+ }
67
+ return {
68
+ deviceCode: String(json.device_code),
69
+ userCode: String(json.user_code),
70
+ verificationUri: String(json.verification_uri),
71
+ verificationUriComplete: String(json.verification_uri_complete ?? json.verification_uri),
72
+ expiresIn: Number(json.expires_in) || 600,
73
+ interval: Number(json.interval) || 5,
74
+ };
75
+ }
76
+ /** One poll. Never throws on a normal pending/expired answer — those are statuses. */
77
+ export async function pollDeviceLink(deviceCode, opts) {
78
+ const fetchImpl = opts.fetchImpl ?? fetch;
79
+ const { status, json } = await postJson(`${opts.apiBase}/device-link/poll`, { device_code: deviceCode }, opts.timeoutMs ?? POLL_TIMEOUT_MS, fetchImpl);
80
+ if (status === 200 && json?.api_key) {
81
+ return { status: 'linked', apiKey: String(json.api_key), email: json.email, tier: json.tier };
82
+ }
83
+ // A proxy or rate limiter answering 429 means "you are early", never "this
84
+ // code is dead" — treating it as expired would kill a perfectly good login.
85
+ if (status === 429) {
86
+ return { status: 'slow_down', interval: Number(json?.interval) || 15 };
87
+ }
88
+ switch (json?.error) {
89
+ case 'authorization_pending': return { status: 'pending' };
90
+ case 'slow_down': return { status: 'slow_down', interval: Number(json.interval) || 10 };
91
+ case 'expired_token': return { status: 'expired' };
92
+ case 'access_denied': return { status: 'denied' };
93
+ default:
94
+ // An unknown 4xx means this device_code will never succeed — treat it as
95
+ // expired rather than spinning forever against a dead code.
96
+ if (status >= 400 && status < 500)
97
+ return { status: 'expired' };
98
+ throw new DeviceLinkError(`Unexpected response while polling (HTTP ${status})`, status);
99
+ }
100
+ }
101
+ const defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
102
+ /**
103
+ * Blocks until the human approves, denies, or the code expires. Honors the
104
+ * server's interval and backs off when told to (`slow_down`) — a client that
105
+ * ignores that is how a device flow turns into a self-inflicted DoS.
106
+ */
107
+ export async function waitForDeviceLink(start, opts) {
108
+ const sleep = opts.sleep ?? defaultSleep;
109
+ const now = opts.now ?? Date.now;
110
+ const deadline = now() + start.expiresIn * 1000;
111
+ let interval = Math.max(1, start.interval);
112
+ for (;;) {
113
+ if (now() >= deadline)
114
+ return { status: 'expired' };
115
+ await sleep(interval * 1000);
116
+ const result = await pollDeviceLink(start.deviceCode, opts);
117
+ if (result.status === 'slow_down') {
118
+ interval = Math.max(interval + 5, result.interval);
119
+ continue;
120
+ }
121
+ if (result.status !== 'pending')
122
+ return result;
123
+ opts.onTick?.(Math.max(0, Math.round((deadline - now()) / 1000)));
124
+ }
125
+ }
package/dist/index.js CHANGED
@@ -5,8 +5,10 @@ import { dirname, join } from 'node:path';
5
5
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
6
6
  import { createServer } from './server.js';
7
7
  import { DEFAULT_TIMEOUT_MS } from './backend.js';
8
+ import { AuthProvider } from './auth.js';
9
+ import { DEFAULT_API_BASE } from './constants.js';
8
10
  import { configPath, resolveSettings } from './config.js';
9
- const DEFAULT_API_BASE = 'https://debugai-mvp-production.up.railway.app/api';
11
+ import { cmdDoctor, cmdInstall, cmdLogin, cmdLogout, cmdSetup, cmdStatus, cmdUninstall, } from './cli/commands.js';
10
12
  function packageVersion() {
11
13
  try {
12
14
  const pkgPath = join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json');
@@ -17,60 +19,102 @@ function packageVersion() {
17
19
  }
18
20
  }
19
21
  const VERSION = packageVersion();
20
- const HELP = `debugai-mcp v${VERSION} — DebugAI MCP server (stdio)
22
+ const HELP = `debugai-mcp v${VERSION} — DebugAI MCP server + setup CLI
21
23
 
22
- Exposes two tools to any MCP client:
24
+ Quick start (one command, no config files to edit):
25
+ npx -y @debugai/mcp setup
26
+
27
+ Commands:
28
+ setup sign in, then wire up every MCP client found on this machine
29
+ login sign in via browser and store the key (--key dbg_… to paste one)
30
+ logout remove the stored key
31
+ status show the active key and account
32
+ install add DebugAI to MCP client configs
33
+ --list show every supported client and where it lives
34
+ --client=cursor target one client (comma-separate for several)
35
+ --all every supported client, detected or not
36
+ --dry-run print what would change, write nothing
37
+ --remove take the entry back out
38
+ uninstall remove DebugAI from every client config
39
+ doctor diagnose setup: key, API reachability, client wiring
40
+ (no command) run the MCP server on stdio — this is what clients launch
41
+
42
+ Tools exposed to your agent:
23
43
  debug_error hand it an error or stack trace, get root cause + ranked
24
44
  fixes with machine-applicable edits (v2 contract)
25
45
  report_outcome tell DebugAI whether an applied fix worked — failed-fix
26
46
  follow-ups improve future answers for your codebase
27
47
 
28
- Usage:
29
- npx @debugai/mcp # start the server (stdio transport)
30
- npx @debugai/mcp --version
31
- npx @debugai/mcp --help
32
-
33
48
  Environment:
34
- DEBUGAI_API_KEY your API key (dbg_...) from https://debugai.io/dashboard
49
+ DEBUGAI_API_KEY your API key (dbg_). Overrides the stored key.
35
50
  DEBUGAI_API_BASE optional — API base URL (default: DebugAI production)
36
51
  DEBUGAI_TIMEOUT_MS optional — per-request deadline in ms (default: ${DEFAULT_TIMEOUT_MS})
52
+ DEBUGAI_CONFIG_PATH optional — alternate config file location
37
53
 
38
- Config file (set the key once, every MCP client picks it up):
39
- ~/.debugai/config.json {"api_key": "dbg_..."}
40
- Env vars win over the file. api_base is also accepted.
54
+ Config file (written by login, read by every MCP client on this machine):
55
+ ${configPath()} {"api_key": "dbg_"}
41
56
 
42
- This is a stdio MCP server: it is meant to be launched BY an MCP client
43
- (Claude Desktop, Claude Code, Cursor, Zed, ...), not run interactively.
44
- Config snippets: https://www.npmjs.com/package/@debugai/mcp
57
+ Docs: https://debugai.io/start?src=mcp
45
58
  `;
46
- // stdout carries the MCP protocol — every human-facing line goes to stderr.
47
- function main() {
59
+ const SUBCOMMANDS = new Set([
60
+ 'setup', 'login', 'logout', 'status', 'install', 'uninstall', 'doctor',
61
+ ]);
62
+ async function runSubcommand(name, argv) {
63
+ switch (name) {
64
+ case 'setup': return cmdSetup(argv);
65
+ case 'login': return cmdLogin(argv);
66
+ case 'logout': return cmdLogout(argv);
67
+ case 'status': return cmdStatus(argv);
68
+ case 'install': return cmdInstall(argv);
69
+ case 'uninstall': return cmdUninstall(argv);
70
+ case 'doctor': return cmdDoctor(argv);
71
+ default: return 1;
72
+ }
73
+ }
74
+ // stdout carries the MCP protocol — in server mode every human-facing line goes
75
+ // to stderr. Subcommands print to stdout and exit before any transport exists,
76
+ // so the two can never interleave.
77
+ async function main() {
48
78
  const args = process.argv.slice(2);
49
79
  if (args.includes('--version') || args.includes('-v')) {
50
80
  process.stdout.write(`${VERSION}\n`);
51
81
  return;
52
82
  }
53
- if (args.includes('--help') || args.includes('-h')) {
83
+ if (args.includes('--help') || args.includes('-h') || args[0] === 'help') {
54
84
  process.stdout.write(HELP);
55
85
  return;
56
86
  }
87
+ const [first, ...rest] = args;
88
+ if (first && SUBCOMMANDS.has(first)) {
89
+ process.exitCode = await runSubcommand(first, rest);
90
+ return;
91
+ }
92
+ if (first && !first.startsWith('-')) {
93
+ console.error(`[debugai-mcp] unknown command: ${first} (see --help)`);
94
+ process.exitCode = 1;
95
+ return;
96
+ }
57
97
  if (args.length > 0) {
58
98
  console.error(`[debugai-mcp] unknown argument(s): ${args.join(' ')} (see --help)`);
59
99
  process.exitCode = 1;
60
100
  return;
61
101
  }
102
+ // ── server mode ────────────────────────────────────────────────────────────
62
103
  const { apiKey, apiBase, keySource } = resolveSettings(DEFAULT_API_BASE);
63
104
  const rawTimeout = Number(process.env.DEBUGAI_TIMEOUT_MS);
64
105
  const timeoutMs = Number.isFinite(rawTimeout) && rawTimeout > 0 ? rawTimeout : DEFAULT_TIMEOUT_MS;
106
+ // The auth provider re-reads the key on every tool call and can start a
107
+ // browser sign-in from inside a conversation, so a missing key is a
108
+ // 20-second detour instead of a dead end. See auth.ts.
109
+ const auth = new AuthProvider({ apiBase, clientLabel: 'DebugAI MCP server' });
65
110
  if (!apiKey) {
66
- console.error('[debugai-mcp] no API key foundtools will return auth errors. ' +
67
- 'Get a key at https://debugai.io/dashboard, then either set DEBUGAI_API_KEY in your ' +
68
- `MCP client config or write it once to ${configPath()} as {"api_key": "dbg_..."}.`);
111
+ console.error('[debugai-mcp] no API key yetthe first tool call will hand your agent a sign-in link. ' +
112
+ 'To do it now instead, run: npx -y @debugai/mcp login');
69
113
  }
70
114
  else if (!apiKey.startsWith('dbg_')) {
71
115
  console.error('[debugai-mcp] warning: DEBUGAI_API_KEY does not look like a DebugAI key (expected dbg_ prefix).');
72
116
  }
73
- const server = createServer({ apiKey, apiBase, version: VERSION, timeoutMs });
117
+ const server = createServer({ apiKey, apiBase, version: VERSION, timeoutMs, auth });
74
118
  const shutdown = (signal) => {
75
119
  console.error(`[debugai-mcp] received ${signal}, shutting down`);
76
120
  void server.close().finally(() => process.exit(0));
@@ -83,4 +127,4 @@ function main() {
83
127
  process.exit(1);
84
128
  });
85
129
  }
86
- main();
130
+ void main();
package/dist/server.js CHANGED
@@ -1,8 +1,36 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { registerDebugError } from './tools/debugError.js';
3
3
  import { registerReportOutcome } from './tools/reportOutcome.js';
4
+ // `instructions` is handed to the agent's context at connect time — the one
5
+ // channel the protocol gives a server to say what it is FOR. Without it,
6
+ // agents skip debug_error, because "analyze this error" looks like something
7
+ // they already do themselves. The framing here is deliberate: lead with what
8
+ // the model cannot derive on its own (this project's error history, a fix a
9
+ // human confirmed, a mechanical check), not with "AI-powered analysis".
10
+ const INSTRUCTIONS = `DebugAI is this project's debugging memory and fix verifier.
11
+
12
+ When a runtime error, exception, stack trace, or failing test output appears,
13
+ call debug_error before reasoning your way to a fix alone. It returns things
14
+ you cannot derive from the code in front of you:
15
+ - whether this exact error has been seen before in this project, and how often
16
+ - the fix a human already confirmed worked for it, if there is one
17
+ - a verification state per fix: mechanically checked and passed, checked and
18
+ FAILED, or not checked at all (never treat "not checked" as verified)
19
+ - machine-applicable edits (exact old/new strings) you can apply directly
20
+
21
+ After you apply or abandon a fix, call report_outcome with the debug_log_id.
22
+ That is what turns a one-off answer into memory for the next person who hits
23
+ the same error. Skipping it costs the user the feature they are paying for.
24
+
25
+ When you present findings that came from DebugAI's memory or verification,
26
+ say so — "DebugAI has seen this 3 times; the confirmed fix is …" — so the user
27
+ can tell which claims were checked and which are your own reasoning.
28
+
29
+ Not signed in yet? The first tool call returns a short code and a URL. Read
30
+ both out to the user; once they confirm in the browser, retry the same call.
31
+ No config editing and no client restart needed.`;
4
32
  export function createServer(config) {
5
- const server = new McpServer({ name: 'debugai', version: config.version }, { capabilities: { tools: { listChanged: true } } });
33
+ const server = new McpServer({ name: 'debugai', version: config.version }, { capabilities: { tools: { listChanged: true } }, instructions: INSTRUCTIONS });
6
34
  registerDebugError(server, config);
7
35
  registerReportOutcome(server, config);
8
36
  return server;
@@ -0,0 +1,10 @@
1
+ import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
2
+ import type { BackendConfig } from '../backend.js';
3
+ export type AuthGate = {
4
+ ok: true;
5
+ config: BackendConfig;
6
+ } | {
7
+ ok: false;
8
+ result: CallToolResult;
9
+ };
10
+ export declare function resolveAuth(config: BackendConfig): Promise<AuthGate>;
@@ -0,0 +1,29 @@
1
+ export async function resolveAuth(config) {
2
+ if (!config.auth) {
3
+ return { ok: true, config }; // tests and direct embedders pass a fixed key
4
+ }
5
+ const state = await config.auth.ensure();
6
+ if (state.ok) {
7
+ return {
8
+ ok: true,
9
+ config: state.apiKey === config.apiKey ? config : { ...config, apiKey: state.apiKey },
10
+ };
11
+ }
12
+ return {
13
+ ok: false,
14
+ result: {
15
+ isError: true,
16
+ content: [{ type: 'text', text: state.text }],
17
+ structuredContent: state.reason === 'link_pending'
18
+ ? {
19
+ error_type: 'not_linked',
20
+ user_code: state.userCode,
21
+ verification_uri: state.verificationUri,
22
+ // Retryable on purpose: the SAME call succeeds once the human
23
+ // confirms. Agents should retry after telling the user, not give up.
24
+ retryable: true,
25
+ }
26
+ : { error_type: 'not_linked', retryable: true },
27
+ },
28
+ };
29
+ }
@@ -1,6 +1,7 @@
1
1
  import { z } from 'zod';
2
2
  import { callDebugBackend } from '../backend.js';
3
3
  import { mapBackendErrorToToolResult } from '../errors.js';
4
+ import { resolveAuth } from './authGate.js';
4
5
  // Tri-state verification labeling (docs/plan-v2-contract-phase1.md §1).
5
6
  // The null case is rendered ON PURPOSE: a confidence number nothing checked
6
7
  // must never look the same as one that was mechanically verified.
@@ -69,6 +70,9 @@ export function registerDebugError(server, config) {
69
70
  title: 'Debug Error',
70
71
  },
71
72
  }, async ({ errorText, language, codeSnippet, filePath }) => {
73
+ const gate = await resolveAuth(config);
74
+ if (!gate.ok)
75
+ return gate.result;
72
76
  try {
73
77
  const result = await callDebugBackend({
74
78
  error_message: errorText,
@@ -78,7 +82,7 @@ export function registerDebugError(server, config) {
78
82
  // framework_hint deliberately omitted: a language ('python') is not a
79
83
  // framework ('fastapi'), and sending it bypasses the engine's
80
84
  // framework detection — FastAPI/React errors lose their expert hints.
81
- }, config);
85
+ }, gate.config);
82
86
  const sections = ['## Root Cause', result.root_cause ?? '(no root cause returned)'];
83
87
  if (result.fixes?.length) {
84
88
  sections.push('\n## Fixes');
@@ -1,6 +1,7 @@
1
1
  import { z } from 'zod';
2
2
  import { callOutcomeBackend } from '../backend.js';
3
3
  import { mapBackendErrorToToolResult } from '../errors.js';
4
+ import { resolveAuth } from './authGate.js';
4
5
  export function registerReportOutcome(server, config) {
5
6
  server.registerTool('report_outcome', {
6
7
  title: 'Report Fix Outcome',
@@ -36,6 +37,9 @@ export function registerReportOutcome(server, config) {
36
37
  idempotentHint: true,
37
38
  },
38
39
  }, async ({ debugLogId, result, fixRank, newError }) => {
40
+ const gate = await resolveAuth(config);
41
+ if (!gate.ok)
42
+ return gate.result;
39
43
  try {
40
44
  await callOutcomeBackend({
41
45
  debug_log_id: debugLogId,
@@ -43,7 +47,7 @@ export function registerReportOutcome(server, config) {
43
47
  fix_rank: fixRank,
44
48
  new_error: newError,
45
49
  source: 'agent',
46
- }, config);
50
+ }, gate.config);
47
51
  const ack = result === 'worked'
48
52
  ? 'Outcome recorded: fix worked. Rank-1 confirmations are remembered for this project, so the next hit on this error starts from the confirmed fix.'
49
53
  : 'Outcome recorded: fix failed. The follow-up error was logged and feeds directly into improving future answers. If you are still stuck, call debug_error again with the NEW error text.';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@debugai/mcp",
3
- "version": "2.0.0",
4
- "description": "DebugAI MCP server hand any error to DebugAI from Claude Desktop, Claude Code, Cursor, Zed, or any MCP client and get root cause + ranked fixes.",
3
+ "version": "2.1.0",
4
+ "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.",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "bin": {