zopkit-bug-resolution-mcp 1.2.1 → 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 +62 -16
  2. package/index.js +385 -19
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -1,24 +1,48 @@
1
1
  # bug-resolution MCP server
2
2
 
3
- A four-tool MCP server so Claude Code (or Cursor, or any other MCP client)
4
- can work a Zopkit bug report end to end as native tool calls, instead of
5
- hand-rolling curl commands:
3
+ A six-tool MCP server so Claude Code (or Cursor, or any other MCP client)
4
+ can work a Zopkit bug report end to end as native tool calls — plus report
5
+ usage status — instead of hand-rolling curl commands:
6
6
 
7
7
  - **`fetch_bug_context`** — pulls the full report (title, description,
8
8
  severity, status, page URL, browser info, discussion comments) plus every
9
9
  inline screenshot as real image content, via
10
- `GET /api/admin/bug-reports/context/:id`.
10
+ `GET /api/admin/bug-reports/context/:id`. Calling it IS starting work on
11
+ the ticket: `open`/`failed_testing` auto-moves to `in_progress` as a side
12
+ effect (anything further along is left untouched).
11
13
  - **`submit_bug_resolution`** — writes back how it was fixed as a Markdown
12
- writeup and moves the report to `resolved`, via
14
+ writeup and moves the report to `needs_testing` (not `resolved` — a human
15
+ still verifies the fix first), via
13
16
  `POST /api/admin/bug-reports/resolution/:id`.
14
17
  - **`fetch_assigned_bugs`** — lists reports currently assigned to the API
15
- key's owner ("what's on my plate"), optionally filtered by status, via
18
+ key's owner ("what's on my plate"), defaulting to everything but
19
+ `resolved` and sorted by what needs attention soonest, optionally filtered
20
+ to specific statuses and paginated, via
16
21
  `GET /api/admin/bug-reports/assigned`.
17
22
  - **`create_bug_report`** — files a new bug report (plain-text description),
18
23
  optionally self-assigning it, via `POST /api/admin/bug-reports/report`.
19
-
20
- All four are thin wrappers — see `backend/src/features/bug-reports/README.md`
21
- for what each endpoint actually does server-side.
24
+ - **`report_claude_usage`** — self-reports this developer's current Claude
25
+ Code usage percentage (there's no API to fetch it on your behalf — see
26
+ `backend/src/features/claude-usage/README.md`), shown on the
27
+ company-admin panel's "Claude Usage" tab, via `POST /api/admin/claude-usage`.
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`
44
+ and `backend/src/features/claude-usage/README.md` for what each endpoint
45
+ actually does server-side.
22
46
 
23
47
  Published to npm as
24
48
  [`zopkit-bug-resolution-mcp`](https://www.npmjs.com/package/zopkit-bug-resolution-mcp)
@@ -102,18 +126,40 @@ whoever asked you to fix it).
102
126
  1. Ask it to call `fetch_bug_context` with that id first — you'll get the
103
127
  title, description, severity/status, discussion, and every screenshot
104
128
  actually rendered as images, so the tool can see the bug instead of just
105
- reading a URL to it.
129
+ reading a URL to it. This call also moves the board card to `In
130
+ Progress` if it was sitting at `Open`/`Failed Testing` — the board
131
+ reflects that work has started without anyone dragging the card by hand.
106
132
  2. After fixing it, ask it to call `submit_bug_resolution` with the same id
107
133
  and a Markdown writeup. It sets the report's resolution notes to that
108
- writeup and moves its status straight to `resolved`.
109
-
110
- Two more, independent of the fetch/resolve flow above:
111
-
112
- - Ask it to call `fetch_assigned_bugs` (optionally with a `status` filter) to
113
- see what's currently assigned to you — no id needed.
134
+ writeup and moves its status to `needs_testing` — a human still needs to
135
+ verify it before it's `resolved`.
136
+
137
+ Four more, independent of the fetch/resolve flow above:
138
+
139
+ - Ask it to call `fetch_assigned_bugs` to see what's currently assigned to
140
+ you — no id needed. By default it shows everything except `resolved`,
141
+ ranked by what needs your attention soonest (a fix QA bounced back first,
142
+ then active work, then fixes just waiting on QA). Pass
143
+ `status: ['failed_testing']` etc. to narrow it down, or `status: ['all']`
144
+ to include resolved history; `limit`/`offset` page past the first batch
145
+ if it says there's more.
114
146
  - Ask it to call `create_bug_report` with a `title` and plain-text
115
147
  `description` to file something it noticed — pass `assignToSelf: true` to
116
148
  claim it immediately.
149
+ - Ask it to call `report_claude_usage` with your current usage `%` (from
150
+ `/usage`, the status line, or the account's billing page — there's no way
151
+ for the tool to look this up itself) to update the "Claude Usage" tab in
152
+ the company-admin panel. Pass an optional `note` for context (e.g. "Team
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.").
117
163
 
118
164
  ## Notes
119
165
 
package/index.js CHANGED
@@ -1,17 +1,31 @@
1
1
  #!/usr/bin/env node
2
- // bug-resolution MCP server — four 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:
6
6
  // - fetch_bug_context: GET /api/admin/bug-reports/context/:id — pulls the
7
7
  // full report (title/description/severity/status/comments) plus every
8
- // inline screenshot as real image content.
8
+ // inline screenshot as real image content. Also IS "starting work on
9
+ // it": open/failed_testing auto-moves to in_progress as a side effect.
9
10
  // - submit_bug_resolution: POST /api/admin/bug-reports/resolution/:id —
10
- // writes back the fix as a Markdown resolution and marks it resolved.
11
+ // writes back the fix as a Markdown resolution and moves it to
12
+ // needs_testing (not resolved — a human verifies it first).
11
13
  // - fetch_assigned_bugs: GET /api/admin/bug-reports/assigned — "what's on
12
14
  // my plate", always the API key owner's own assigned reports.
13
15
  // - create_bug_report: POST /api/admin/bug-reports/report — file a new bug
14
16
  // the tool found, optionally self-assigning it.
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
+ // self-reported rather than fetched from an Anthropic API), shown on
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.
15
29
  // Thin wrapper: all the actual logic (auth, scoping, the DB writes) lives
16
30
  // server-side; this just forwards the HTTP calls the README documents.
17
31
  //
@@ -20,6 +34,9 @@
20
34
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
21
35
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
22
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';
23
40
 
24
41
  const API_KEY = process.env.BUG_REPORT_API_KEY;
25
42
  const BASE_URL = (process.env.BUG_REPORT_API_BASE_URL || 'https://api.zopkit.com').replace(/\/$/, '');
@@ -29,11 +46,16 @@ if (!API_KEY) {
29
46
  'Generate one from the company-admin panel (Bug Reports → API Keys) and set it when registering this server.');
30
47
  }
31
48
 
32
- const server = new McpServer({ name: 'zopkit-bug-resolution', version: '1.2.0' });
49
+ const server = new McpServer({ name: 'zopkit-bug-resolution', version: '1.6.0' });
33
50
 
34
51
  const BUG_SEVERITIES = ['low', 'medium', 'high', 'critical'];
35
- const BUG_STATUSES = ['open', 'in_progress', 'blocked', 'further_discussion', 'resolved'];
36
- const BUG_PROJECTS = ['wrapper', 'crm', 'fa', 'accounting', 'academy', 'itsm', 'studio'];
52
+ // Keep in sync with BUG_STATUSES in backend/src/db/schema/platform/bug-reports.ts.
53
+ // This list was missing needs_testing/failed_testing for a while, which meant
54
+ // fetch_assigned_bugs couldn't even be asked to filter to exactly the two
55
+ // statuses a developer cares about most (a fresh fix bounced back by QA, or
56
+ // one still waiting on QA) — the backend has allowed both for a while.
57
+ const BUG_STATUSES = ['open', 'in_progress', 'needs_testing', 'failed_testing', 'blocked', 'further_discussion', 'resolved'];
58
+ const BUG_PROJECTS = ['wrapper', 'crm', 'fa', 'accounting', 'academy', 'itsm', 'studio', 'retail_erp', 'entertainment_erp'];
37
59
 
38
60
  const IMG_SRC = /<img\b[^>]*\bsrc="([^"]+)"[^>]*>/gi;
39
61
 
@@ -54,13 +76,202 @@ async function fetchImageContentBlock(url) {
54
76
  return { type: 'image', data: buffer.toString('base64'), mimeType: contentType };
55
77
  }
56
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
+
57
267
  server.registerTool(
58
268
  'submit_bug_resolution',
59
269
  {
60
270
  title: 'Submit bug resolution',
61
271
  description:
62
272
  "Report how a Zopkit bug report was fixed. Sets the report's resolution notes to the given " +
63
- "Markdown writeup and moves its status straight to 'resolved'. Use the bug report's id " +
273
+ "Markdown writeup and moves its status to 'needs_testing' — NOT 'resolved' directly; a human " +
274
+ "still needs to verify the fix before it's truly done. Use the bug report's id " +
64
275
  '(a UUID, given to you by whoever asked you to fix it, or visible in its admin-panel URL).',
65
276
  inputSchema: {
66
277
  bugReportId: z.string().min(1).describe("The bug report's id (UUID)."),
@@ -103,12 +314,13 @@ server.registerTool(
103
314
  }
104
315
 
105
316
  const report = body?.data;
317
+ const usageNote = await tryAutoReportTokenUsage();
106
318
  return {
107
319
  content: [{
108
320
  type: 'text',
109
- text: report
321
+ text: (report
110
322
  ? `Resolution submitted. "${report.title}" is now ${report.status}.`
111
- : 'Resolution submitted.',
323
+ : 'Resolution submitted.') + (usageNote ? `\n\n(Also updated your Claude token usage: ${usageNote}.)` : ''),
112
324
  }],
113
325
  };
114
326
  }
@@ -122,7 +334,10 @@ server.registerTool(
122
334
  'Pull the full context for a Zopkit bug report — title, description, severity, status, ' +
123
335
  'page URL, browser info, discussion comments, and every inline screenshot as actual images — ' +
124
336
  'so an AI coding tool has everything it needs to diagnose and fix the bug without ' +
125
- 'opening the admin panel. Screenshots come back as image content alongside the text summary.',
337
+ "opening the admin panel. Screenshots come back as image content alongside the text summary. " +
338
+ "Calling this IS starting work on it: a report sitting at 'open' or 'failed_testing' moves " +
339
+ "straight to 'in_progress' as a side effect (anything further along — in_progress itself, " +
340
+ "needs_testing, blocked, further_discussion, resolved — is left untouched).",
126
341
  inputSchema: {
127
342
  bugReportId: z.string().min(1).describe("The bug report's id (UUID)."),
128
343
  },
@@ -193,6 +408,9 @@ server.registerTool(
193
408
  }
194
409
  }
195
410
 
411
+ const usageNote = await tryAutoReportTokenUsage();
412
+ if (usageNote) content.push({ type: 'text', text: `(Also updated your Claude token usage: ${usageNote}.)` });
413
+
196
414
  return { content };
197
415
  }
198
416
  );
@@ -203,13 +421,24 @@ server.registerTool(
203
421
  title: 'Fetch bugs assigned to me',
204
422
  description:
205
423
  "List Zopkit bug reports currently assigned to this API key's owner — \"what's on my plate\". " +
206
- 'Always the key owner\'s own assigned reports, never anyone else\'s, regardless of what the key ' +
207
- 'can otherwise see. Optionally filter by status.',
424
+ "Always the key owner's own assigned reports, never anyone else's, regardless of what the key " +
425
+ "can otherwise see. Defaults to every status EXCEPT 'resolved' (pass status: ['all'] to include " +
426
+ "resolved history too), sorted with whatever needs attention soonest first (a fix QA bounced back " +
427
+ "via 'failed_testing', then active work, then fixes merely awaiting QA). Paginated — use limit/offset " +
428
+ 'if the result says there are more than fit on one page.',
208
429
  inputSchema: {
209
- status: z.enum(BUG_STATUSES).optional().describe('Only return reports in this status.'),
430
+ status: z
431
+ .array(z.enum([...BUG_STATUSES, 'all']))
432
+ .optional()
433
+ .describe(
434
+ "One or more statuses to filter to. Omit for the default (everything but 'resolved'); " +
435
+ "pass ['all'] to include resolved history too."
436
+ ),
437
+ limit: z.number().int().min(1).max(100).optional().describe('Max reports to return (default 50, max 100).'),
438
+ offset: z.number().int().min(0).optional().describe('Skip this many reports, for paging past the first batch.'),
210
439
  },
211
440
  },
212
- async ({ status }) => {
441
+ async ({ status, limit, offset }) => {
213
442
  if (!API_KEY) {
214
443
  return {
215
444
  isError: true,
@@ -218,7 +447,9 @@ server.registerTool(
218
447
  }
219
448
 
220
449
  const url = new URL(`${BASE_URL}/api/admin/bug-reports/assigned`);
221
- if (status) url.searchParams.set('status', status);
450
+ if (status?.length) url.searchParams.set('status', status.join(','));
451
+ if (limit != null) url.searchParams.set('limit', String(limit));
452
+ if (offset != null) url.searchParams.set('offset', String(offset));
222
453
 
223
454
  let res;
224
455
  try {
@@ -238,15 +469,28 @@ server.registerTool(
238
469
  }
239
470
 
240
471
  const reports = body?.data || [];
472
+ const meta = body?.meta;
241
473
  if (reports.length === 0) {
242
- return { content: [{ type: 'text', text: 'No bug reports currently assigned to you.' }] };
474
+ return { content: [{ type: 'text', text: 'No bug reports currently assigned to you (in the requested status(es)).' }] };
243
475
  }
244
476
 
245
477
  const summary = reports
246
478
  .map((r) => `- [${r.severity}/${r.status}] ${r.title} (id: ${r.id})${r.project ? ` — project: ${r.project}` : ''}`)
247
479
  .join('\n');
248
480
 
249
- return { content: [{ type: 'text', text: `${reports.length} assigned report(s):\n${summary}` }] };
481
+ // meta.total counts everything matching the filter, not just this page —
482
+ // say so explicitly instead of letting a truncated page read as "that's all of it".
483
+ const header = meta && meta.total > reports.length
484
+ ? `${reports.length} of ${meta.total} assigned report(s) (showing offset ${meta.offset}–${meta.offset + reports.length - 1}; pass a higher offset for more):`
485
+ : `${reports.length} assigned report(s):`;
486
+
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
+ };
250
494
  }
251
495
  );
252
496
 
@@ -296,12 +540,134 @@ server.registerTool(
296
540
  }
297
541
 
298
542
  const report = body?.data;
543
+ const usageNote = await tryAutoReportTokenUsage();
299
544
  return {
300
545
  content: [{
301
546
  type: 'text',
302
- text: report
547
+ text: (report
303
548
  ? `Bug report created: "${report.title}" (id: ${report.id}).`
304
- : 'Bug report created.',
549
+ : 'Bug report created.') + (usageNote ? `\n\n(Also updated your Claude token usage: ${usageNote}.)` : ''),
550
+ }],
551
+ };
552
+ }
553
+ );
554
+
555
+ server.registerTool(
556
+ 'report_claude_usage',
557
+ {
558
+ title: 'Report Claude usage status',
559
+ description:
560
+ "Report this developer's current Claude Code account/session usage percentage, shown on the " +
561
+ 'company-admin panel\'s "Claude Usage" tab. Self-reported — there is no API to fetch this on your ' +
562
+ 'behalf (Anthropic has no per-seat "current usage %" endpoint, and the percentage Claude Code\'s own ' +
563
+ '/usage shows is computed client-side from this session\'s own rate-limit headers, not retrievable by ' +
564
+ 'a third party). Pass whatever percentage you or the user currently see — e.g. from the /usage ' +
565
+ "command, the status line, or the account's billing page. Identity (who this is) is resolved from " +
566
+ 'your API key, not from anything passed here.',
567
+ inputSchema: {
568
+ usagePercent: z.number().min(0).max(100).describe('Current usage, 0-100 (fractional values like 72.5 are fine).'),
569
+ note: z.string().max(500).optional().describe('Optional context, e.g. "Team plan, resets Monday".'),
570
+ },
571
+ },
572
+ async ({ usagePercent, note }) => {
573
+ if (!API_KEY) {
574
+ return {
575
+ isError: true,
576
+ content: [{ type: 'text', text: 'BUG_REPORT_API_KEY is not set on this MCP server — see README.md.' }],
577
+ };
578
+ }
579
+
580
+ let res;
581
+ try {
582
+ res = await fetch(`${BASE_URL}/api/admin/claude-usage`, {
583
+ method: 'POST',
584
+ headers: { 'Content-Type': 'application/json', 'X-API-Key': API_KEY },
585
+ body: JSON.stringify({ usagePercent, note }),
586
+ });
587
+ } catch (err) {
588
+ return {
589
+ isError: true,
590
+ content: [{ type: 'text', text: `Could not reach ${BASE_URL}: ${err instanceof Error ? err.message : String(err)}` }],
591
+ };
592
+ }
593
+
594
+ const body = await res.json().catch(() => null);
595
+
596
+ if (!res.ok) {
597
+ const message = body?.message || body?.error || `HTTP ${res.status}`;
598
+ return { isError: true, content: [{ type: 'text', text: `Failed to report Claude usage: ${message}` }] };
599
+ }
600
+
601
+ const status = body?.data;
602
+ return {
603
+ content: [{
604
+ type: 'text',
605
+ text: status
606
+ ? `Reported ${status.usagePercent}% usage for ${status.email}.`
607
+ : 'Usage reported.',
608
+ }],
609
+ };
610
+ }
611
+ );
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).`,
305
671
  }],
306
672
  };
307
673
  }
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "zopkit-bug-resolution-mcp",
3
- "version": "1.2.1",
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, and file new bug reports. 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"