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.
- package/README.md +62 -16
- package/index.js +385 -19
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,24 +1,48 @@
|
|
|
1
1
|
# bug-resolution MCP server
|
|
2
2
|
|
|
3
|
-
A
|
|
4
|
-
can work a Zopkit bug report end to end as native tool calls
|
|
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
|
|
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"),
|
|
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
|
-
|
|
21
|
-
|
|
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
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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 —
|
|
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
|
|
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.
|
|
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
|
-
|
|
36
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
207
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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,
|
|
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"
|