zopkit-bug-resolution-mcp 1.5.0 → 1.6.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.
Files changed (3) hide show
  1. package/README.md +27 -4
  2. package/index.js +282 -10
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # bug-resolution MCP server
2
2
 
3
- A five-tool MCP server so Claude Code (or Cursor, or any other MCP client)
3
+ A six-tool MCP server so Claude Code (or Cursor, or any other MCP client)
4
4
  can work a Zopkit bug report end to end as native tool calls — plus report
5
5
  usage status — instead of hand-rolling curl commands:
6
6
 
@@ -25,8 +25,22 @@ usage status — instead of hand-rolling curl commands:
25
25
  Code usage percentage (there's no API to fetch it on your behalf — see
26
26
  `backend/src/features/claude-usage/README.md`), shown on the
27
27
  company-admin panel's "Claude Usage" tab, via `POST /api/admin/claude-usage`.
28
-
29
- All five are thin wrappers — see `backend/src/features/bug-reports/README.md`
28
+ - **`report_claude_token_usage`** — unlike the one above, this one is NOT
29
+ self-attested: it scans this developer's own local Claude Code transcripts
30
+ (`~/.claude/projects/**/*.jsonl`, same machine the MCP server runs on — no
31
+ `/usage`, no status line, no asking a human) for the last 7 days, dedupes
32
+ streamed duplicates by `message.id`, and reports real input/output/
33
+ cache-creation/cache-read token totals, session count, and whether any
34
+ sessions used web search or spawned subagents, via
35
+ `POST /api/admin/claude-usage/token-breakdown`. Also runs automatically,
36
+ fire-and-forget, as a side effect of the four bug-report tools above
37
+ (`submit_bug_resolution`, `fetch_bug_context`, `fetch_assigned_bugs`,
38
+ `create_bug_report`) on success — so a developer who never touches this
39
+ tool directly still gets a token breakdown reported just by doing normal
40
+ bug-report work. Failures from the auto-report path are swallowed
41
+ entirely; they never surface as a tool error.
42
+
43
+ All six are thin wrappers — see `backend/src/features/bug-reports/README.md`
30
44
  and `backend/src/features/claude-usage/README.md` for what each endpoint
31
45
  actually does server-side.
32
46
 
@@ -120,7 +134,7 @@ whoever asked you to fix it).
120
134
  writeup and moves its status to `needs_testing` — a human still needs to
121
135
  verify it before it's `resolved`.
122
136
 
123
- Three more, independent of the fetch/resolve flow above:
137
+ Four more, independent of the fetch/resolve flow above:
124
138
 
125
139
  - Ask it to call `fetch_assigned_bugs` to see what's currently assigned to
126
140
  you — no id needed. By default it shows everything except `resolved`,
@@ -137,6 +151,15 @@ Three more, independent of the fetch/resolve flow above:
137
151
  for the tool to look this up itself) to update the "Claude Usage" tab in
138
152
  the company-admin panel. Pass an optional `note` for context (e.g. "Team
139
153
  plan, resets Monday").
154
+ - Ask it to call `report_claude_token_usage` (no input needed) to push a
155
+ real token/session breakdown for the last 7 days up to the same "Claude
156
+ Usage" tab — distinct from and independent of `report_claude_usage`
157
+ above. You usually don't need to call this directly: it already fires
158
+ automatically after every successful `fetch_bug_context`,
159
+ `submit_bug_resolution`, `fetch_assigned_bugs`, or `create_bug_report`
160
+ call, and a short note gets appended to that tool's own response telling
161
+ you it happened (e.g. "Also updated your Claude token usage: 2.1M new
162
+ tokens across 3 session(s) this week.").
140
163
 
141
164
  ## Notes
142
165
 
package/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // bug-resolution MCP server — five tools wrapping the bug-reports resolution
2
+ // bug-resolution MCP server — six tools wrapping the bug-reports resolution
3
3
  // API (see backend/src/features/bug-reports/README.md) so a developer's
4
4
  // Claude Code / Cursor session can work a bug report end to end as native
5
5
  // tool calls instead of hand-rolling curl commands:
@@ -14,11 +14,18 @@
14
14
  // my plate", always the API key owner's own assigned reports.
15
15
  // - create_bug_report: POST /api/admin/bug-reports/report — file a new bug
16
16
  // the tool found, optionally self-assigning it.
17
- // - report_claude_usage: POST /api/admin/claude-usage — self-reports this
18
- // developer's current Claude Code usage percentage (see
19
- // backend/src/features/claude-usage/README.md for why this is
17
+ // - report_claude_usage: POST /api/admin/claude-usage — SELF-ATTESTED:
18
+ // self-reports this developer's current Claude Code usage percentage
19
+ // (see backend/src/features/claude-usage/README.md for why this is
20
20
  // self-reported rather than fetched from an Anthropic API), shown on
21
21
  // the company-admin panel's "Claude Usage" tab.
22
+ // - report_claude_token_usage: POST /api/admin/claude-usage/token-breakdown
23
+ // — REAL, not self-attested: scans this developer's own local Claude
24
+ // Code transcripts (~/.claude/projects/**/*.jsonl — this server runs on
25
+ // the same machine) for a 7-day token/web-search/subagent breakdown. The
26
+ // other four bug-resolution tools above also trigger this automatically
27
+ // as a fire-and-forget side effect (see tryAutoReportTokenUsage) — "every
28
+ // task done in the bug reporter" keeps this fresh with zero extra asks.
22
29
  // Thin wrapper: all the actual logic (auth, scoping, the DB writes) lives
23
30
  // server-side; this just forwards the HTTP calls the README documents.
24
31
  //
@@ -27,6 +34,9 @@
27
34
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
28
35
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
29
36
  import { z } from 'zod';
37
+ import { readdirSync, readFileSync, statSync } from 'node:fs';
38
+ import { join } from 'node:path';
39
+ import { homedir } from 'node:os';
30
40
 
31
41
  const API_KEY = process.env.BUG_REPORT_API_KEY;
32
42
  const BASE_URL = (process.env.BUG_REPORT_API_BASE_URL || 'https://api.zopkit.com').replace(/\/$/, '');
@@ -36,7 +46,7 @@ if (!API_KEY) {
36
46
  'Generate one from the company-admin panel (Bug Reports → API Keys) and set it when registering this server.');
37
47
  }
38
48
 
39
- const server = new McpServer({ name: 'zopkit-bug-resolution', version: '1.5.0' });
49
+ const server = new McpServer({ name: 'zopkit-bug-resolution', version: '1.6.0' });
40
50
 
41
51
  const BUG_SEVERITIES = ['low', 'medium', 'high', 'critical'];
42
52
  // Keep in sync with BUG_STATUSES in backend/src/db/schema/platform/bug-reports.ts.
@@ -66,6 +76,194 @@ async function fetchImageContentBlock(url) {
66
76
  return { type: 'image', data: buffer.toString('base64'), mimeType: contentType };
67
77
  }
68
78
 
79
+ // ── Token usage breakdown (report_claude_token_usage) ──────────────────────
80
+ // Unlike usagePercent (self-attested, see submit_bug_resolution's comment on
81
+ // why), this is read straight off Claude Code's own local transcript files —
82
+ // this MCP server is a local process on the SAME machine as the Claude Code
83
+ // session using it, so it can read ~/.claude/projects/**/*.jsonl directly.
84
+ // No statusLine setup, no asking a human, no per-seat API that doesn't exist.
85
+ //
86
+ // Each API response gets logged 2-3x in these files (streaming writes the
87
+ // same final usage numbers once per content-block flush) — every count below
88
+ // dedupes by message.id, or totals would be inflated 2-3x.
89
+ const PROJECTS_DIR = join(homedir(), '.claude', 'projects');
90
+ const SUBAGENT_TOOL_NAMES = new Set(['Agent', 'Task']);
91
+
92
+ function listRecentTranscripts(sinceMs) {
93
+ const files = [];
94
+ let projectDirs;
95
+ try {
96
+ projectDirs = readdirSync(PROJECTS_DIR, { withFileTypes: true });
97
+ } catch {
98
+ return files; // no ~/.claude/projects at all — nothing to scan
99
+ }
100
+ for (const projectDir of projectDirs) {
101
+ if (!projectDir.isDirectory()) continue;
102
+ const dirPath = join(PROJECTS_DIR, projectDir.name);
103
+ let entries;
104
+ try {
105
+ entries = readdirSync(dirPath, { withFileTypes: true });
106
+ } catch {
107
+ continue;
108
+ }
109
+ for (const entry of entries) {
110
+ if (!entry.isFile() || !entry.name.endsWith('.jsonl')) continue;
111
+ const filePath = join(dirPath, entry.name);
112
+ try {
113
+ if (statSync(filePath).mtimeMs >= sinceMs) files.push(filePath);
114
+ } catch {
115
+ // file vanished between readdir and stat — skip
116
+ }
117
+ }
118
+ }
119
+ return files;
120
+ }
121
+
122
+ /** One session's token totals + what it used — null if the file had nothing
123
+ * in range (e.g. mtime was recent but every logged message predates sinceMs). */
124
+ function scanTranscript(filePath, sinceMs) {
125
+ let raw;
126
+ try {
127
+ raw = readFileSync(filePath, 'utf8');
128
+ } catch {
129
+ return null;
130
+ }
131
+
132
+ const seenMessageIds = new Set();
133
+ let inputTokens = 0, outputTokens = 0, cacheCreationTokens = 0, cacheReadTokens = 0;
134
+ let usedWebSearch = false, usedSubagents = false;
135
+ let startedAt = null, cwd = null;
136
+
137
+ for (const line of raw.split('\n')) {
138
+ if (!line.trim()) continue;
139
+ let entry;
140
+ try {
141
+ entry = JSON.parse(line);
142
+ } catch {
143
+ continue; // truncated/partial last line — skip it, not the whole file
144
+ }
145
+ if (entry.type !== 'assistant' || !entry.message) continue;
146
+ if (entry.timestamp && Date.parse(entry.timestamp) < sinceMs) continue;
147
+
148
+ if (cwd === null && entry.cwd) cwd = entry.cwd;
149
+ if (entry.timestamp && (!startedAt || entry.timestamp < startedAt)) startedAt = entry.timestamp;
150
+
151
+ // Tool-use detection runs on every line, duplicates included — setting a
152
+ // boolean true twice is harmless, unlike double-counting tokens below.
153
+ if (Array.isArray(entry.message.content)) {
154
+ for (const block of entry.message.content) {
155
+ if (block?.type === 'tool_use' && SUBAGENT_TOOL_NAMES.has(block.name)) usedSubagents = true;
156
+ }
157
+ }
158
+
159
+ const messageId = entry.message.id;
160
+ if (messageId) {
161
+ if (seenMessageIds.has(messageId)) continue;
162
+ seenMessageIds.add(messageId);
163
+ }
164
+
165
+ const usage = entry.message.usage;
166
+ if (!usage) continue;
167
+ inputTokens += usage.input_tokens || 0;
168
+ outputTokens += usage.output_tokens || 0;
169
+ cacheCreationTokens += usage.cache_creation_input_tokens || 0;
170
+ cacheReadTokens += usage.cache_read_input_tokens || 0;
171
+ const serverTools = usage.server_tool_use;
172
+ if (serverTools && ((serverTools.web_search_requests || 0) > 0 || (serverTools.web_fetch_requests || 0) > 0)) {
173
+ usedWebSearch = true;
174
+ }
175
+ }
176
+
177
+ const totalTokens = inputTokens + outputTokens + cacheCreationTokens + cacheReadTokens;
178
+ if (totalTokens === 0) return null;
179
+ return { totalTokens, inputTokens, outputTokens, cacheCreationTokens, cacheReadTokens, usedWebSearch, usedSubagents, startedAt, cwd };
180
+ }
181
+
182
+ /** Scans every local Claude Code transcript touched in the last `days` days
183
+ * and aggregates it into the shape reportClaudeTokenUsageSchema expects.
184
+ * Deliberately NOT scoped to this repo/project — the whole point is "how
185
+ * much Claude Code usage this week", same scope /usage itself reports. */
186
+ function computeWeeklyTokenBreakdown(days = 7) {
187
+ const sinceMs = Date.now() - days * 24 * 60 * 60 * 1000;
188
+ const sessions = listRecentTranscripts(sinceMs)
189
+ .map((f) => scanTranscript(f, sinceMs))
190
+ .filter((s) => s !== null);
191
+
192
+ const totals = sessions.reduce(
193
+ (acc, s) => ({
194
+ inputTokens: acc.inputTokens + s.inputTokens,
195
+ outputTokens: acc.outputTokens + s.outputTokens,
196
+ cacheCreationTokens: acc.cacheCreationTokens + s.cacheCreationTokens,
197
+ cacheReadTokens: acc.cacheReadTokens + s.cacheReadTokens,
198
+ }),
199
+ { inputTokens: 0, outputTokens: 0, cacheCreationTokens: 0, cacheReadTokens: 0 }
200
+ );
201
+
202
+ const topSessions = [...sessions]
203
+ .sort((a, b) => b.totalTokens - a.totalTokens)
204
+ .slice(0, 5)
205
+ .map((s) => ({
206
+ totalTokens: s.totalTokens,
207
+ startedAt: s.startedAt || new Date(sinceMs).toISOString(),
208
+ cwd: s.cwd,
209
+ usedWebSearch: s.usedWebSearch,
210
+ usedSubagents: s.usedSubagents,
211
+ }));
212
+
213
+ return {
214
+ periodStart: new Date(sinceMs).toISOString(),
215
+ periodEnd: new Date().toISOString(),
216
+ ...totals,
217
+ sessionCount: sessions.length,
218
+ sessionsWithWebSearch: sessions.filter((s) => s.usedWebSearch).length,
219
+ sessionsWithSubagents: sessions.filter((s) => s.usedSubagents).length,
220
+ topSessions,
221
+ };
222
+ }
223
+
224
+ /** "585k" / "2.3M" / "1.1B" — raw token counts in a long session routinely
225
+ * reach the hundreds of millions (every turn re-pays for its whole cached
226
+ * context, so cache-read alone compounds fast), so a plain toLocaleString()
227
+ * reads as an alarming wall of digits. Still real numbers — just formatted
228
+ * for a human skimming a report, not a ledger. */
229
+ function formatTokenCount(n) {
230
+ if (n >= 1e9) return `${(n / 1e9).toFixed(1)}B`;
231
+ if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
232
+ if (n >= 1e3) return `${(n / 1e3).toFixed(0)}k`;
233
+ return String(n);
234
+ }
235
+
236
+ /** input + output + cache-WRITE tokens — genuinely new content processed
237
+ * this period. Deliberately excludes cache-READ: that's a heavily-discounted
238
+ * re-read of context already paid for once, and in a long session it
239
+ * dwarfs everything else by 10-100x, so folding it into one headline
240
+ * number would make "new work this week" unreadable. Still reported
241
+ * separately in full (see the schema) — just not what ranks/headlines. */
242
+ function newTokenCount(breakdown) {
243
+ return breakdown.inputTokens + breakdown.outputTokens + breakdown.cacheCreationTokens;
244
+ }
245
+
246
+ /** Fire-and-forget: scan + POST the breakdown, swallowing any failure (a
247
+ * missing ~/.claude/projects dir, a network blip, whatever) — this must
248
+ * never fail or slow down the tool call it's piggybacking on. Returns a
249
+ * short status string for an optional footer, or null if it didn't run. */
250
+ async function tryAutoReportTokenUsage() {
251
+ if (!API_KEY) return null;
252
+ try {
253
+ const breakdown = computeWeeklyTokenBreakdown();
254
+ if (breakdown.sessionCount === 0) return null;
255
+ const res = await fetch(`${BASE_URL}/api/admin/claude-usage/token-breakdown`, {
256
+ method: 'POST',
257
+ headers: { 'Content-Type': 'application/json', 'X-API-Key': API_KEY },
258
+ body: JSON.stringify(breakdown),
259
+ });
260
+ if (!res.ok) return null;
261
+ return `${formatTokenCount(newTokenCount(breakdown))} new tokens across ${breakdown.sessionCount} session(s) this week`;
262
+ } catch {
263
+ return null; // never let this break the real tool call
264
+ }
265
+ }
266
+
69
267
  server.registerTool(
70
268
  'submit_bug_resolution',
71
269
  {
@@ -116,12 +314,13 @@ server.registerTool(
116
314
  }
117
315
 
118
316
  const report = body?.data;
317
+ const usageNote = await tryAutoReportTokenUsage();
119
318
  return {
120
319
  content: [{
121
320
  type: 'text',
122
- text: report
321
+ text: (report
123
322
  ? `Resolution submitted. "${report.title}" is now ${report.status}.`
124
- : 'Resolution submitted.',
323
+ : 'Resolution submitted.') + (usageNote ? `\n\n(Also updated your Claude token usage: ${usageNote}.)` : ''),
125
324
  }],
126
325
  };
127
326
  }
@@ -209,6 +408,9 @@ server.registerTool(
209
408
  }
210
409
  }
211
410
 
411
+ const usageNote = await tryAutoReportTokenUsage();
412
+ if (usageNote) content.push({ type: 'text', text: `(Also updated your Claude token usage: ${usageNote}.)` });
413
+
212
414
  return { content };
213
415
  }
214
416
  );
@@ -282,7 +484,13 @@ server.registerTool(
282
484
  ? `${reports.length} of ${meta.total} assigned report(s) (showing offset ${meta.offset}–${meta.offset + reports.length - 1}; pass a higher offset for more):`
283
485
  : `${reports.length} assigned report(s):`;
284
486
 
285
- return { content: [{ type: 'text', text: `${header}\n${summary}` }] };
487
+ const usageNote = await tryAutoReportTokenUsage();
488
+ return {
489
+ content: [{
490
+ type: 'text',
491
+ text: `${header}\n${summary}` + (usageNote ? `\n\n(Also updated your Claude token usage: ${usageNote}.)` : ''),
492
+ }],
493
+ };
286
494
  }
287
495
  );
288
496
 
@@ -332,12 +540,13 @@ server.registerTool(
332
540
  }
333
541
 
334
542
  const report = body?.data;
543
+ const usageNote = await tryAutoReportTokenUsage();
335
544
  return {
336
545
  content: [{
337
546
  type: 'text',
338
- text: report
547
+ text: (report
339
548
  ? `Bug report created: "${report.title}" (id: ${report.id}).`
340
- : 'Bug report created.',
549
+ : 'Bug report created.') + (usageNote ? `\n\n(Also updated your Claude token usage: ${usageNote}.)` : ''),
341
550
  }],
342
551
  };
343
552
  }
@@ -401,5 +610,68 @@ server.registerTool(
401
610
  }
402
611
  );
403
612
 
613
+ server.registerTool(
614
+ 'report_claude_token_usage',
615
+ {
616
+ title: 'Report Claude token usage breakdown',
617
+ description:
618
+ "Scans this developer's own local Claude Code transcripts " +
619
+ '(~/.claude/projects/**/*.jsonl — this MCP server runs on the same machine, so it can read them ' +
620
+ 'directly) and reports a real token-usage breakdown for the last 7 days: total tokens, how many ' +
621
+ 'sessions, and how many of those sessions used web search or spawned a subagent. Unlike ' +
622
+ "report_claude_usage's percentage, nothing here is self-attested — every number is read off real " +
623
+ 'local files, so call this freely with no arguments; it never needs to ask a human for anything. ' +
624
+ "The other bug-resolution tools already call this automatically as a side effect, so you usually " +
625
+ "don't need to call it directly — this exists for an explicit, on-demand check.",
626
+ inputSchema: {},
627
+ },
628
+ async () => {
629
+ if (!API_KEY) {
630
+ return {
631
+ isError: true,
632
+ content: [{ type: 'text', text: 'BUG_REPORT_API_KEY is not set on this MCP server — see README.md.' }],
633
+ };
634
+ }
635
+
636
+ const breakdown = computeWeeklyTokenBreakdown();
637
+ if (breakdown.sessionCount === 0) {
638
+ return {
639
+ content: [{ type: 'text', text: 'No Claude Code transcripts found in the last 7 days under ~/.claude/projects — nothing to report.' }],
640
+ };
641
+ }
642
+
643
+ let res;
644
+ try {
645
+ res = await fetch(`${BASE_URL}/api/admin/claude-usage/token-breakdown`, {
646
+ method: 'POST',
647
+ headers: { 'Content-Type': 'application/json', 'X-API-Key': API_KEY },
648
+ body: JSON.stringify(breakdown),
649
+ });
650
+ } catch (err) {
651
+ return {
652
+ isError: true,
653
+ content: [{ type: 'text', text: `Could not reach ${BASE_URL}: ${err instanceof Error ? err.message : String(err)}` }],
654
+ };
655
+ }
656
+
657
+ const body = await res.json().catch(() => null);
658
+
659
+ if (!res.ok) {
660
+ const message = body?.message || body?.error || `HTTP ${res.status}`;
661
+ return { isError: true, content: [{ type: 'text', text: `Failed to report Claude token usage: ${message}` }] };
662
+ }
663
+
664
+ return {
665
+ content: [{
666
+ type: 'text',
667
+ text: `Reported ${formatTokenCount(newTokenCount(breakdown))} new tokens ` +
668
+ `(plus ${formatTokenCount(breakdown.cacheReadTokens)} discounted cache-read re-reads) ` +
669
+ `across ${breakdown.sessionCount} session(s) this week ` +
670
+ `(${breakdown.sessionsWithWebSearch} used web search, ${breakdown.sessionsWithSubagents} used subagents).`,
671
+ }],
672
+ };
673
+ }
674
+ );
675
+
404
676
  const transport = new StdioServerTransport();
405
677
  await server.connect(transport);
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "zopkit-bug-resolution-mcp",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "type": "module",
5
- "description": "MCP server for Zopkit wrapper bug reports: fetch full bug context (with screenshots), submit how a bug was fixed, list bugs assigned to you, file new bug reports, and report your Claude Code usage status. Requires a BUG_REPORT_API_KEY (generated in the company-admin panel) — see README.md.",
5
+ "description": "MCP server for Zopkit wrapper bug reports: fetch full bug context (with screenshots), submit how a bug was fixed, list bugs assigned to you, file new bug reports, report your Claude Code usage status, and report a real weekly token-usage breakdown from your local transcripts. Requires a BUG_REPORT_API_KEY (generated in the company-admin panel) — see README.md.",
6
6
  "license": "UNLICENSED",
7
7
  "engines": {
8
8
  "node": ">=18"