@debugai/mcp 2.2.0 → 2.4.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/README.md CHANGED
@@ -150,9 +150,16 @@ You do not need this package. The [DebugAI extension](https://marketplace.visual
150
150
 
151
151
  Where it goes:
152
152
 
153
+ > **On Claude Code, `--scope user` is the part that matters.** `claude mcp add`
154
+ > defaults to `--scope local`, which writes the server under
155
+ > `projects."/your/dir".mcpServers` — it then works in that one directory and
156
+ > nowhere else, which reads exactly like DebugAI needing to be set up per repo.
157
+ > `debugai-mcp install` always writes the user scope. If you already ran the
158
+ > local-scope version, `debugai-mcp doctor` now tells you which one you have.
159
+
153
160
  | Client | File |
154
161
  |--------|------|
155
- | Claude Code | `~/.claude.json` (or `claude mcp add debugai -- npx -y @debugai/mcp`) |
162
+ | Claude Code | `~/.claude.json`, top-level `mcpServers` (or `claude mcp add --scope user debugai -- npx -y @debugai/mcp`) |
156
163
  | Claude Desktop | macOS `~/Library/Application Support/Claude/claude_desktop_config.json`, Windows `%APPDATA%\Claude\claude_desktop_config.json` |
157
164
  | Cursor | `~/.cursor/mcp.json` |
158
165
  | Windsurf | `~/.codeium/windsurf/mcp_config.json` |
package/dist/backend.d.ts CHANGED
@@ -44,12 +44,31 @@ export interface DebugResponse {
44
44
  memory_hit?: boolean;
45
45
  memory_times_seen?: number;
46
46
  memory_fix_confirmed?: boolean;
47
+ /**
48
+ * Four-valued: 'none' | 'confirmed' | 'unproven' | 'anti_pattern'. Sent by
49
+ * the engine since 2026-08-21. Absent from an older engine, where
50
+ * memory_fix_confirmed is the whole story — and that boolean now goes false
51
+ * on demotion, so the degraded reading is safe rather than wrong.
52
+ */
53
+ memory_fix_state?: string;
54
+ /** Times a live session watched the error stop after the fix. A click does not count. */
55
+ memory_verified_count?: number;
47
56
  }
48
57
  export interface OutcomeRequest {
49
58
  debug_log_id: string;
50
- result: 'worked' | 'failed';
59
+ /**
60
+ * 'unused' joined on 2026-08-26. An agent that read the fixes, used none of
61
+ * them and solved it another way previously had to report 'failed', which
62
+ * marks a fix as tried and beaten when nothing of ours was ever run.
63
+ */
64
+ result: 'worked' | 'failed' | 'unused';
51
65
  fix_rank?: number;
66
+ /** An error message observed after a failed fix. Not an explanation. */
52
67
  new_error?: string;
68
+ /** What actually resolved it, when our answer was not what worked. */
69
+ actual_fix?: string;
70
+ /** What would have made the tool more useful here. About the tool, not the bug. */
71
+ tool_feedback?: string;
53
72
  source: 'agent';
54
73
  }
55
74
  export interface OutcomeResponse {
@@ -17,7 +17,7 @@ import { clearStoredKey, configPath, loadFileConfig, maskKey, resolveSettings, w
17
17
  import { DeviceLinkError, startDeviceLink, waitForDeviceLink } from '../deviceLink.js';
18
18
  import { DEFAULT_API_BASE } from '../constants.js';
19
19
  import { detectedClients, findClient, isDetected, knownClients, } from './clients.js';
20
- import { applyToClient, isInstalled } from './install.js';
20
+ import { applyToClient, findInstall } from './install.js';
21
21
  import { FAIL, INFO, OK, WARN, bold, codeBox, dim, heading, openBrowser, say, yellow } from './ui.js';
22
22
  // ── shared helpers ───────────────────────────────────────────────────────────
23
23
  function flag(argv, name) {
@@ -215,7 +215,12 @@ function listClients(env) {
215
215
  heading('Known MCP clients');
216
216
  for (const c of knownClients(env)) {
217
217
  const mark = isDetected(c) ? OK() : INFO();
218
- const state = isDetected(c) ? (isInstalled(c) ? 'detected · debugai configured' : 'detected') : 'not found';
218
+ const site = isDetected(c) ? findInstall(c) : null;
219
+ const state = !isDetected(c)
220
+ ? 'not found'
221
+ : site?.scope === 'user' ? 'detected · debugai configured'
222
+ : site?.scope === 'project' ? 'detected · debugai configured (one directory only)'
223
+ : 'detected';
219
224
  say(` ${mark} ${bold(c.id.padEnd(15))} ${c.label.padEnd(22)} ${dim(state)}`);
220
225
  say(` ${dim(c.configPath ?? 'no config path on this OS')}`);
221
226
  if (c.note)
@@ -329,10 +334,23 @@ export async function cmdDoctor(_argv, env = process.env) {
329
334
  say(` ${INFO()} None detected. "debugai-mcp install --list" shows every supported client.`);
330
335
  }
331
336
  for (const c of detected) {
332
- if (isInstalled(c))
337
+ const site = findInstall(c);
338
+ if (site?.scope === 'user') {
333
339
  pass(`${c.label} — debugai configured`, c.configPath ?? undefined);
334
- else
340
+ }
341
+ else if (site?.scope === 'project') {
342
+ // Working, but only in one directory, and somebody who does not know that
343
+ // concludes DebugAI has to be set up per repo. That belief is what makes
344
+ // an MCP server not worth installing, so this is a warning with the exact
345
+ // command to widen it rather than a quiet pass.
346
+ say(` ${WARN()} ${yellow(`${c.label} — debugai configured for ONE directory only`)}`);
347
+ say(` ${dim(site.projectPath ?? '')}`);
348
+ say(` ${dim('`claude mcp add` defaults to --scope local. To use DebugAI in every repo:')}`);
349
+ say(` ${dim(`debugai-mcp install --client=${c.id}`)}`);
350
+ }
351
+ else {
335
352
  say(` ${WARN()} ${yellow(`${c.label} — installed but DebugAI is not in its config`)}\n ${dim(`fix: debugai-mcp install --client=${c.id}`)}`);
353
+ }
336
354
  }
337
355
  say();
338
356
  if (hardFailures) {
@@ -17,5 +17,42 @@ export interface InstallOptions {
17
17
  remove?: boolean;
18
18
  }
19
19
  export declare function applyToClient(client: McpClient, opts?: InstallOptions): InstallResult;
20
- /** True when the client's config already points at this server. */
20
+ /**
21
+ * Where a client's config points at this server, if anywhere.
22
+ *
23
+ * ## Why this is not a boolean any more
24
+ *
25
+ * Claude Code stores MCP servers in two places in one file, and the difference
26
+ * decides whether DebugAI works in every repo or exactly one:
27
+ *
28
+ * ~/.claude.json
29
+ * ├── mcpServers ← user scope. Every directory, every session.
30
+ * └── projects
31
+ * └── /home/me/thing
32
+ * └── mcpServers ← local scope. That one directory only.
33
+ *
34
+ * `claude mcp add` writes the SECOND one, because `--scope local` is its
35
+ * default. This function used to read only the first, so the officially
36
+ * documented install path produced a config that `doctor` then reported as
37
+ * missing — a diagnostic contradicting a working install, which is worse than
38
+ * no diagnostic, because it sends somebody to re-run an installer that then
39
+ * creates a duplicate entry in the other scope.
40
+ *
41
+ * Reported by an agent running a real build on 2026-08-24, who lost a whole
42
+ * session to it. Their words: "two documented paths disagree, and the
43
+ * diagnostic sides with the wrong one."
44
+ *
45
+ * The scope is returned rather than flattened away because the two are not
46
+ * equally good. A local-scope install is the thing that makes somebody think
47
+ * they have to reconfigure DebugAI per directory, and telling them where it
48
+ * actually is turns that into a one-line fix.
49
+ */
50
+ export interface InstallSite {
51
+ /** 'user' works everywhere. 'project' works in `projectPath` only. */
52
+ scope: 'user' | 'project';
53
+ /** Which directory it is scoped to, for 'project'. */
54
+ projectPath?: string;
55
+ }
56
+ export declare function findInstall(client: McpClient): InstallSite | null;
57
+ /** True when the client's config points at this server in ANY scope. */
21
58
  export declare function isInstalled(client: McpClient): boolean;
@@ -135,16 +135,32 @@ export function applyToClient(client, opts = {}) {
135
135
  warning,
136
136
  };
137
137
  }
138
- /** True when the client's config already points at this server. */
139
- export function isInstalled(client) {
138
+ export function findInstall(client) {
140
139
  if (!client.configPath || !existsSync(client.configPath))
141
- return false;
140
+ return null;
141
+ const has = (v) => Boolean(v && typeof v === 'object' && SERVER_NAME in v);
142
142
  try {
143
143
  const parsed = parseJsonc(readFileSync(client.configPath, 'utf8'));
144
- const servers = parsed.value?.[serversKey(client.shape)];
145
- return Boolean(servers && typeof servers === 'object' && SERVER_NAME in servers);
144
+ const root = parsed.value;
145
+ if (has(root?.[serversKey(client.shape)]))
146
+ return { scope: 'user' };
147
+ // Claude Code's per-directory scope. Other clients have no `projects` key,
148
+ // so this loop simply never runs for them rather than needing a special case.
149
+ const projects = root?.projects;
150
+ if (projects && typeof projects === 'object') {
151
+ for (const [dir, cfg] of Object.entries(projects)) {
152
+ if (cfg && typeof cfg === 'object' && has(cfg.mcpServers)) {
153
+ return { scope: 'project', projectPath: dir };
154
+ }
155
+ }
156
+ }
157
+ return null;
146
158
  }
147
159
  catch {
148
- return false;
160
+ return null;
149
161
  }
150
162
  }
163
+ /** True when the client's config points at this server in ANY scope. */
164
+ export function isInstalled(client) {
165
+ return findInstall(client) !== null;
166
+ }
package/dist/server.js CHANGED
@@ -26,6 +26,12 @@ yourself only when you already know the relevant code is somewhere the trace
26
26
  does not name.
27
27
 
28
28
  After you apply or abandon a fix, call report_outcome with the debug_log_id.
29
+ Report it even when you ignored the answer and fixed it your own way: pass
30
+ result "unused" and put what actually worked in actualFix. A case DebugAI got
31
+ wrong is worth more to it than one it got right, and "unused" is not the same
32
+ as "failed" — reporting a fix as failed when you never ran it buries it for the
33
+ next person. If the tool could have served you better here, say so in
34
+ toolFeedback; that reaches the people who build it.
29
35
  That is what turns a one-off answer into memory for the next person who hits
30
36
  the same error. Skipping it costs the user the feature they are paying for.
31
37
 
@@ -126,9 +126,53 @@ export function registerDebugError(server, config) {
126
126
  const seen = typeof result.memory_times_seen === 'number'
127
127
  ? `${result.memory_times_seen}x before in this project`
128
128
  : 'before in this project';
129
- sections.push(result.memory_fix_confirmed
130
- ? `\n## Seen before\nThis error has been seen ${seen}, and a fix was confirmed working. The confirmed fix led the analysis above.`
131
- : `\n## Seen before\nThis error has been seen ${seen}. No fix has been confirmed for it yet.`);
129
+ // Four states, not two. Until 2026-08-21 this said "a fix was
130
+ // confirmed working" on the strength of somebody clicking Apply,
131
+ // and a fix that had been tried and FAILED was indistinguishable
132
+ // from one nobody had ever tried — both fell into the else branch
133
+ // and read "no fix has been confirmed yet". An agent reading that
134
+ // will happily re-propose the fix that already did not work, which
135
+ // is the loop this whole change exists to break.
136
+ //
137
+ // memory_fix_state is what the engine sends now. When it is absent
138
+ // (older engine) the boolean still decides, exactly as before.
139
+ const state = typeof result.memory_fix_state === 'string'
140
+ ? result.memory_fix_state
141
+ : (result.memory_fix_confirmed ? 'confirmed' : 'none');
142
+ const verified = typeof result.memory_verified_count === 'number'
143
+ ? result.memory_verified_count
144
+ : 0;
145
+ let memoryBody;
146
+ if (state === 'confirmed' && verified > 0) {
147
+ memoryBody =
148
+ `This error has been seen ${seen}. A fix for it was VERIFIED BY ` +
149
+ `OBSERVATION ${verified}x: a live session watched the error stop ` +
150
+ `after that fix was applied. It led the analysis above.`;
151
+ }
152
+ else if (state === 'confirmed') {
153
+ memoryBody =
154
+ `This error has been seen ${seen}. A fix for it was applied and ` +
155
+ `accepted by a developer, though nothing has observed it working. ` +
156
+ `It led the analysis above. Treat it as a strong lead, not proof.`;
157
+ }
158
+ else if (state === 'unproven') {
159
+ memoryBody =
160
+ `This error has been seen ${seen}. A fix was applied for it and ` +
161
+ `THE ERROR HAS RECURRED ONCE SINCE, so that fix is unproven. The ` +
162
+ `analysis above was told not to lead with it.`;
163
+ }
164
+ else if (state === 'anti_pattern') {
165
+ memoryBody =
166
+ `This error has been seen ${seen}. A fix was applied for it and ` +
167
+ `THE ERROR KEPT HAPPENING. That fix did not work and the analysis ` +
168
+ `above was told not to propose it again. If you are about to ` +
169
+ `suggest something equivalent, the cause is somewhere it does not ` +
170
+ `touch.`;
171
+ }
172
+ else {
173
+ memoryBody = `This error has been seen ${seen}. No fix has been confirmed for it yet.`;
174
+ }
175
+ sections.push(`\n## Seen before\n${memoryBody}`);
132
176
  }
133
177
  const badges = [];
134
178
  // Named, not counted. "Read 2 files" is unverifiable; "read src/api.ts,
@@ -176,6 +220,7 @@ export function registerDebugError(server, config) {
176
220
  memory_hit: result.memory_hit ?? false,
177
221
  memory_times_seen: result.memory_times_seen ?? null,
178
222
  memory_fix_confirmed: result.memory_fix_confirmed ?? false,
223
+ memory_fix_state: result.memory_fix_state ?? null,
179
224
  // Which files this server read off disk, if any. Empty when the
180
225
  // agent supplied its own snippet or nothing resolved.
181
226
  local_files_read: resolved?.files.map((f) => f.label) ?? [],
@@ -1,3 +1,39 @@
1
1
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import type { BackendConfig } from '../backend.js';
3
+ /**
4
+ * The channel back.
5
+ *
6
+ * ## Why this tool grew on 2026-08-26
7
+ *
8
+ * An external user's agent reported an outcome and wrote this into `newError`,
9
+ * a field described to it as "the error observed AFTER applying the fix":
10
+ *
11
+ * "The first corrected node -e command was still rewritten by PowerShell
12
+ * quoting; a separate PowerShell HTTP verification was used successfully
13
+ * instead."
14
+ *
15
+ * That is not an error. It is a post-mortem: our answer was aimed at the wrong
16
+ * layer, and here is what actually worked. It is the most useful thing anybody
17
+ * has ever sent this system, and the agent had to force it through the wrong
18
+ * slot because nothing here asked for it.
19
+ *
20
+ * So the schema now asks. Three changes, each closing a case an honest agent
21
+ * could not previously report:
22
+ *
23
+ * 'unused' — you read the fixes, used none of them, and solved it your
24
+ * own way. Previously only 'worked' and 'failed' existed, so
25
+ * this had to be misreported as a failure or not reported.
26
+ * Reporting it as a failure is actively harmful: it marks a
27
+ * fix as tried and beaten when nothing of ours was run.
28
+ * actualFix — what actually resolved it. Ground truth on a case we got
29
+ * wrong, which is worth more than a case we got right.
30
+ * toolFeedback — what would have made this tool more useful here. About the
31
+ * tool, not the bug, and kept separate for exactly that
32
+ * reason: a complaint about DebugAI must never end up
33
+ * promoted into somebody's project memory as a fix.
34
+ *
35
+ * A field on a tool agents already call beats a new tool they would have to be
36
+ * told about. This one has a measured ~50% attach rate; a new `send_feedback`
37
+ * starts at zero and competes for the same attention.
38
+ */
3
39
  export declare function registerReportOutcome(server: McpServer, config: BackendConfig): void;
@@ -2,41 +2,98 @@ import { z } from 'zod';
2
2
  import { callOutcomeBackend } from '../backend.js';
3
3
  import { mapBackendErrorToToolResult } from '../errors.js';
4
4
  import { resolveAuth } from './authGate.js';
5
+ /**
6
+ * The channel back.
7
+ *
8
+ * ## Why this tool grew on 2026-08-26
9
+ *
10
+ * An external user's agent reported an outcome and wrote this into `newError`,
11
+ * a field described to it as "the error observed AFTER applying the fix":
12
+ *
13
+ * "The first corrected node -e command was still rewritten by PowerShell
14
+ * quoting; a separate PowerShell HTTP verification was used successfully
15
+ * instead."
16
+ *
17
+ * That is not an error. It is a post-mortem: our answer was aimed at the wrong
18
+ * layer, and here is what actually worked. It is the most useful thing anybody
19
+ * has ever sent this system, and the agent had to force it through the wrong
20
+ * slot because nothing here asked for it.
21
+ *
22
+ * So the schema now asks. Three changes, each closing a case an honest agent
23
+ * could not previously report:
24
+ *
25
+ * 'unused' — you read the fixes, used none of them, and solved it your
26
+ * own way. Previously only 'worked' and 'failed' existed, so
27
+ * this had to be misreported as a failure or not reported.
28
+ * Reporting it as a failure is actively harmful: it marks a
29
+ * fix as tried and beaten when nothing of ours was run.
30
+ * actualFix — what actually resolved it. Ground truth on a case we got
31
+ * wrong, which is worth more than a case we got right.
32
+ * toolFeedback — what would have made this tool more useful here. About the
33
+ * tool, not the bug, and kept separate for exactly that
34
+ * reason: a complaint about DebugAI must never end up
35
+ * promoted into somebody's project memory as a fix.
36
+ *
37
+ * A field on a tool agents already call beats a new tool they would have to be
38
+ * told about. This one has a measured ~50% attach rate; a new `send_feedback`
39
+ * starts at zero and competes for the same attention.
40
+ */
5
41
  export function registerReportOutcome(server, config) {
6
42
  server.registerTool('report_outcome', {
7
43
  title: 'Report Fix Outcome',
8
- description: 'Report whether a DebugAI fix actually worked after you applied it. ' +
9
- 'Call this ONCE after applying (or abandoning) a fix from debug_error, passing the ' +
10
- 'debug_log_id from that response. If the fix failed, include the new error text ' +
11
- 'failed-fix follow-ups directly improve future answers for this codebase, and ' +
12
- 'confirmed rank-1 fixes are remembered for the whole team.',
44
+ description: 'Report what happened after DebugAI answered including when you did not use its fix. ' +
45
+ 'Call this ONCE per debug_error response, passing the debug_log_id from it. ' +
46
+ 'If you solved the problem another way, say so with result "unused" and put the real ' +
47
+ 'fix in actualFix: a case DebugAI got wrong is worth more to it than one it got right. ' +
48
+ 'If the tool could have helped you more here, say that in toolFeedback. ' +
49
+ 'Confirmed rank-1 fixes are remembered for this project and returned to whoever hits ' +
50
+ 'the same error next.',
13
51
  inputSchema: {
14
52
  debugLogId: z
15
53
  .string()
16
54
  .min(1)
17
55
  .describe('The debug_log_id value from the debug_error response you are reporting on.'),
18
56
  result: z
19
- .enum(['worked', 'failed'])
20
- .describe('"worked" = the fix resolved the error; "failed" = it did not (or made things worse).'),
57
+ .enum(['worked', 'failed', 'unused'])
58
+ .describe('"worked" = you applied a DebugAI fix and the error stopped. ' +
59
+ '"failed" = you applied one and it did not help (or made things worse). ' +
60
+ '"unused" = you did not apply any of them and resolved it another way. ' +
61
+ 'Use "unused" rather than "failed" when nothing of ours was actually run — ' +
62
+ 'they are different facts and reporting the wrong one buries a fix nobody tried.'),
21
63
  fixRank: z
22
64
  .number()
23
65
  .int()
24
66
  .min(1)
25
67
  .max(3)
26
68
  .optional()
27
- .describe('Which ranked fix you applied (1-3). Rank 1 outcomes feed team error memory.'),
69
+ .describe('Which ranked fix you applied (1-3). Rank 1 outcomes feed team error memory. Omit for "unused".'),
28
70
  newError: z
29
71
  .string()
30
72
  .max(4000)
31
73
  .optional()
32
- .describe('If result is "failed": the error observed AFTER applying the fix.'),
74
+ .describe('If result is "failed": the error observed AFTER applying the fix. An error message, not an explanation.'),
75
+ actualFix: z
76
+ .string()
77
+ .max(4000)
78
+ .optional()
79
+ .describe('What actually resolved the error, when it was not the fix DebugAI suggested. ' +
80
+ 'Free text. Say what the real cause turned out to be and what you changed — ' +
81
+ 'especially if the cause was in a different layer than the answer addressed ' +
82
+ '(the shell, the runtime version, the environment, a missing package).'),
83
+ toolFeedback: z
84
+ .string()
85
+ .max(2000)
86
+ .optional()
87
+ .describe('What would have made DebugAI more useful on THIS call. About the tool, not the bug: ' +
88
+ 'context it lacked, a question it should have asked, an input shape it rejected, ' +
89
+ 'a wrong assumption in its reasoning. This reaches the people who build it.'),
33
90
  },
34
91
  annotations: {
35
92
  title: 'Report Fix Outcome',
36
93
  // Deliberately NO readOnlyHint — this records telemetry server-side.
37
94
  idempotentHint: true,
38
95
  },
39
- }, async ({ debugLogId, result, fixRank, newError }) => {
96
+ }, async ({ debugLogId, result, fixRank, newError, actualFix, toolFeedback }) => {
40
97
  const gate = await resolveAuth(config);
41
98
  if (!gate.ok)
42
99
  return gate.result;
@@ -46,11 +103,22 @@ export function registerReportOutcome(server, config) {
46
103
  result,
47
104
  fix_rank: fixRank,
48
105
  new_error: newError,
106
+ actual_fix: actualFix,
107
+ tool_feedback: toolFeedback,
49
108
  source: 'agent',
50
109
  }, gate.config);
110
+ // The acknowledgement is the only thing that tells an agent its report
111
+ // landed somewhere real. An 'unused' report especially: an agent that
112
+ // says "I ignored you and did it myself" and gets a generic thank-you
113
+ // learns that the field is decorative, and stops filling it.
51
114
  const ack = result === 'worked'
52
115
  ? 'Outcome recorded: fix worked. Rank-1 confirmations are remembered for this project, so the next hit on this error starts from the confirmed fix.'
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.';
116
+ : result === 'unused'
117
+ ? 'Outcome recorded: our fix was not used.' +
118
+ (actualFix
119
+ ? ' The fix that actually worked was logged — that is the most useful thing this tool receives, because it is a case we got wrong.'
120
+ : ' If you know what actually resolved it, send it in actualFix: a wrong answer we can see is worth more than a right one we cannot.')
121
+ : '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.';
54
122
  return {
55
123
  content: [{ type: 'text', text: ack }],
56
124
  structuredContent: { recorded: true, result },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@debugai/mcp",
3
- "version": "2.2.0",
3
+ "version": "2.4.0",
4
4
  "mcpName": "io.github.1shizaan/debugai-mcp",
5
5
  "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.",
6
6
  "license": "MIT",