@side-quest/bun-runner 1.0.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/CHANGELOG.md +15 -0
- package/LICENSE +21 -0
- package/README.md +32 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +394 -0
- package/package.json +57 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# @side-quest/bun-runner
|
|
2
|
+
|
|
3
|
+
## 1.0.0
|
|
4
|
+
|
|
5
|
+
### Major Changes
|
|
6
|
+
|
|
7
|
+
- Initial release of MCP server runner packages extracted from side-quest-marketplace.
|
|
8
|
+
|
|
9
|
+
- @side-quest/bun-runner: Test execution with bun_runTests, bun_testFile, bun_testCoverage
|
|
10
|
+
- @side-quest/biome-runner: Lint & format with biome_lintCheck, biome_lintFix, biome_formatCheck
|
|
11
|
+
- @side-quest/tsc-runner: Type checking with tsc_check
|
|
12
|
+
|
|
13
|
+
## 0.0.0
|
|
14
|
+
|
|
15
|
+
Initial development version. See [README](./README.md) for details.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Nathan Vale
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# @side-quest/bun-runner
|
|
2
|
+
|
|
3
|
+
Bun test runner MCP server for Claude Code. Runs tests with structured, token-efficient output.
|
|
4
|
+
|
|
5
|
+
## Tools
|
|
6
|
+
|
|
7
|
+
- `bun_runTests` — Run tests with optional pattern filter
|
|
8
|
+
- `bun_testFile` — Run specific test file
|
|
9
|
+
- `bun_testCoverage` — Run tests with coverage report
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
bunx --bun @side-quest/bun-runner
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Or in `.mcp.json`:
|
|
18
|
+
|
|
19
|
+
```json
|
|
20
|
+
{
|
|
21
|
+
"mcpServers": {
|
|
22
|
+
"bun-runner": {
|
|
23
|
+
"command": "bunx",
|
|
24
|
+
"args": ["--bun", "@side-quest/bun-runner"]
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## License
|
|
31
|
+
|
|
32
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bun test output parsing utilities
|
|
3
|
+
*
|
|
4
|
+
* Extracted to a separate file to allow testing without importing mcpez
|
|
5
|
+
*/
|
|
6
|
+
interface TestFailure {
|
|
7
|
+
file: string;
|
|
8
|
+
message: string;
|
|
9
|
+
line?: number;
|
|
10
|
+
stack?: string;
|
|
11
|
+
}
|
|
12
|
+
interface TestSummary {
|
|
13
|
+
passed: number;
|
|
14
|
+
failed: number;
|
|
15
|
+
total: number;
|
|
16
|
+
failures: TestFailure[];
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Parse bun test output to extract test results
|
|
20
|
+
*/
|
|
21
|
+
declare function parseBunTestOutput(output: string): TestSummary;
|
|
22
|
+
export { parseBunTestOutput, TestSummary, TestFailure };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// @bun
|
|
3
|
+
|
|
4
|
+
// mcp/index.ts
|
|
5
|
+
import {
|
|
6
|
+
createCorrelationId,
|
|
7
|
+
createPluginLogger
|
|
8
|
+
} from "@side-quest/core/logging";
|
|
9
|
+
import { startServer, tool, z } from "@side-quest/core/mcp";
|
|
10
|
+
import {
|
|
11
|
+
createLoggerAdapter,
|
|
12
|
+
ResponseFormat,
|
|
13
|
+
wrapToolHandler
|
|
14
|
+
} from "@side-quest/core/mcp-response";
|
|
15
|
+
import { spawnWithTimeout } from "@side-quest/core/spawn";
|
|
16
|
+
import {
|
|
17
|
+
validatePath,
|
|
18
|
+
validateShellSafePattern
|
|
19
|
+
} from "@side-quest/core/validation";
|
|
20
|
+
|
|
21
|
+
// mcp/parse-utils.ts
|
|
22
|
+
function parseBunTestOutput(output) {
|
|
23
|
+
const failures = [];
|
|
24
|
+
const lines = output.split(`
|
|
25
|
+
`);
|
|
26
|
+
let currentFailure = null;
|
|
27
|
+
let currentTestName;
|
|
28
|
+
for (const line of lines) {
|
|
29
|
+
if (!line)
|
|
30
|
+
continue;
|
|
31
|
+
const failMatch = line.match(/\(fail\)\s+(.+?)\s+\[/);
|
|
32
|
+
if (failMatch) {
|
|
33
|
+
if (currentFailure) {
|
|
34
|
+
currentTestName = failMatch[1];
|
|
35
|
+
currentFailure.message = `${currentTestName}: ${currentFailure.message}`;
|
|
36
|
+
failures.push(currentFailure);
|
|
37
|
+
currentFailure = null;
|
|
38
|
+
}
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (line.includes("✗") || line.startsWith("FAIL ")) {
|
|
42
|
+
if (currentFailure)
|
|
43
|
+
failures.push(currentFailure);
|
|
44
|
+
currentFailure = {
|
|
45
|
+
file: "unknown",
|
|
46
|
+
message: line.trim()
|
|
47
|
+
};
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (line.trim().startsWith("error:")) {
|
|
51
|
+
if (currentFailure) {
|
|
52
|
+
currentFailure.message += `
|
|
53
|
+
${line.trim()}`;
|
|
54
|
+
} else {
|
|
55
|
+
currentFailure = {
|
|
56
|
+
file: "unknown",
|
|
57
|
+
message: line.trim()
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (currentFailure) {
|
|
63
|
+
if (line.trim().startsWith("at ")) {
|
|
64
|
+
const match = line.match(/\((.+):(\d+):(\d+)\)/) || line.match(/at (.+):(\d+):(\d+)/);
|
|
65
|
+
if (match?.[1] && match[2]) {
|
|
66
|
+
currentFailure.file = match[1];
|
|
67
|
+
currentFailure.line = Number.parseInt(match[2], 10);
|
|
68
|
+
}
|
|
69
|
+
currentFailure.stack = `${currentFailure.stack || ""}${line}
|
|
70
|
+
`;
|
|
71
|
+
} else if (line.trim() && !line.match(/^\d+ \| /)) {
|
|
72
|
+
currentFailure.message += `
|
|
73
|
+
${line.trim()}`;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (currentFailure)
|
|
78
|
+
failures.push(currentFailure);
|
|
79
|
+
const passMatch = output.match(/(\d+) pass/);
|
|
80
|
+
const failMatchNum = output.match(/(\d+) fail/);
|
|
81
|
+
const passed = passMatch?.[1] ? Number.parseInt(passMatch[1], 10) : 0;
|
|
82
|
+
const failed = failMatchNum?.[1] ? Number.parseInt(failMatchNum[1], 10) : failures.length;
|
|
83
|
+
return {
|
|
84
|
+
passed,
|
|
85
|
+
failed,
|
|
86
|
+
total: passed + failed,
|
|
87
|
+
failures
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// mcp/index.ts
|
|
92
|
+
var { initLogger, getSubsystemLogger } = createPluginLogger({
|
|
93
|
+
name: "bun-runner",
|
|
94
|
+
subsystems: ["mcp"]
|
|
95
|
+
});
|
|
96
|
+
initLogger().catch(console.error);
|
|
97
|
+
var mcpLogger = getSubsystemLogger("mcp");
|
|
98
|
+
function parseBunTestOutputImpl(output) {
|
|
99
|
+
const failures = [];
|
|
100
|
+
const lines = output.split(`
|
|
101
|
+
`);
|
|
102
|
+
let currentFailure = null;
|
|
103
|
+
let currentTestName;
|
|
104
|
+
for (const line of lines) {
|
|
105
|
+
if (!line)
|
|
106
|
+
continue;
|
|
107
|
+
const failMatch = line.match(/\(fail\)\s+(.+?)\s+\[/);
|
|
108
|
+
if (failMatch) {
|
|
109
|
+
if (currentFailure) {
|
|
110
|
+
currentTestName = failMatch[1];
|
|
111
|
+
currentFailure.message = `${currentTestName}: ${currentFailure.message}`;
|
|
112
|
+
failures.push(currentFailure);
|
|
113
|
+
currentFailure = null;
|
|
114
|
+
}
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (line.includes("\u2717") || line.startsWith("FAIL ")) {
|
|
118
|
+
if (currentFailure)
|
|
119
|
+
failures.push(currentFailure);
|
|
120
|
+
currentFailure = {
|
|
121
|
+
file: "unknown",
|
|
122
|
+
message: line.trim()
|
|
123
|
+
};
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (line.trim().startsWith("error:")) {
|
|
127
|
+
if (currentFailure) {
|
|
128
|
+
currentFailure.message += `
|
|
129
|
+
${line.trim()}`;
|
|
130
|
+
} else {
|
|
131
|
+
currentFailure = {
|
|
132
|
+
file: "unknown",
|
|
133
|
+
message: line.trim()
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (currentFailure) {
|
|
139
|
+
if (line.trim().startsWith("at ")) {
|
|
140
|
+
const match = line.match(/\((.+):(\d+):(\d+)\)/) || line.match(/at (.+):(\d+):(\d+)/);
|
|
141
|
+
if (match?.[1] && match[2]) {
|
|
142
|
+
currentFailure.file = match[1];
|
|
143
|
+
currentFailure.line = Number.parseInt(match[2], 10);
|
|
144
|
+
}
|
|
145
|
+
currentFailure.stack = `${currentFailure.stack || ""}${line}
|
|
146
|
+
`;
|
|
147
|
+
} else if (line.trim() && !line.match(/^\d+ \| /)) {
|
|
148
|
+
currentFailure.message += `
|
|
149
|
+
${line.trim()}`;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (currentFailure)
|
|
154
|
+
failures.push(currentFailure);
|
|
155
|
+
const passMatch = output.match(/(\d+) pass/);
|
|
156
|
+
const failMatchNum = output.match(/(\d+) fail/);
|
|
157
|
+
const passed = passMatch?.[1] ? Number.parseInt(passMatch[1], 10) : 0;
|
|
158
|
+
const failed = failMatchNum?.[1] ? Number.parseInt(failMatchNum[1], 10) : failures.length;
|
|
159
|
+
return {
|
|
160
|
+
passed,
|
|
161
|
+
failed,
|
|
162
|
+
total: passed + failed,
|
|
163
|
+
failures
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
async function runBunTests(pattern) {
|
|
167
|
+
const cmd = pattern ? ["bun", "test", pattern] : ["bun", "test"];
|
|
168
|
+
const TIMEOUT_MS = 30000;
|
|
169
|
+
const { stdout, stderr, exitCode, timedOut } = await spawnWithTimeout(cmd, TIMEOUT_MS, { env: { CI: "true" } });
|
|
170
|
+
if (timedOut) {
|
|
171
|
+
return {
|
|
172
|
+
passed: 0,
|
|
173
|
+
failed: 1,
|
|
174
|
+
total: 1,
|
|
175
|
+
failures: [
|
|
176
|
+
{
|
|
177
|
+
file: "timeout",
|
|
178
|
+
message: "Tests timed out after 30 seconds. Possible causes: open handles, infinite loops, or watch mode accidentally enabled."
|
|
179
|
+
}
|
|
180
|
+
]
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
const output = `${stdout}
|
|
184
|
+
${stderr}`;
|
|
185
|
+
if (exitCode === 0) {
|
|
186
|
+
const passMatch = output.match(/(\d+) pass/);
|
|
187
|
+
const passed = passMatch?.[1] ? Number.parseInt(passMatch[1], 10) : 0;
|
|
188
|
+
return {
|
|
189
|
+
passed,
|
|
190
|
+
failed: 0,
|
|
191
|
+
total: passed,
|
|
192
|
+
failures: []
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
return parseBunTestOutputImpl(output);
|
|
196
|
+
}
|
|
197
|
+
async function runBunTestCoverage() {
|
|
198
|
+
const TIMEOUT_MS = 60000;
|
|
199
|
+
const cmd = ["bun", "test", "--coverage"];
|
|
200
|
+
const { stdout, stderr, exitCode, timedOut } = await spawnWithTimeout(cmd, TIMEOUT_MS, { env: { CI: "true" } });
|
|
201
|
+
const output = `${stdout}
|
|
202
|
+
${stderr}`;
|
|
203
|
+
if (timedOut) {
|
|
204
|
+
return {
|
|
205
|
+
summary: {
|
|
206
|
+
passed: 0,
|
|
207
|
+
failed: 1,
|
|
208
|
+
total: 1,
|
|
209
|
+
failures: [
|
|
210
|
+
{
|
|
211
|
+
file: "timeout",
|
|
212
|
+
message: "Tests timed out after 60 seconds."
|
|
213
|
+
}
|
|
214
|
+
]
|
|
215
|
+
},
|
|
216
|
+
coverage: { percent: 0, uncovered: [] }
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
const summary = exitCode === 0 ? parseBunTestOutputImpl(stdout) : parseBunTestOutputImpl(output);
|
|
220
|
+
const coverageMatch = output.match(/(\d+(?:\.\d+)?)\s*%/);
|
|
221
|
+
const percent = coverageMatch?.[1] ? Number.parseFloat(coverageMatch[1]) : 0;
|
|
222
|
+
const uncovered = [];
|
|
223
|
+
const lines = output.split(`
|
|
224
|
+
`);
|
|
225
|
+
for (const line of lines) {
|
|
226
|
+
const match = line.match(/^([^\s|]+)\s*\|\s*(\d+(?:\.\d+)?)\s*%/);
|
|
227
|
+
if (match?.[1] && match[2]) {
|
|
228
|
+
const file = match[1].trim();
|
|
229
|
+
const fileCoverage = Number.parseFloat(match[2]);
|
|
230
|
+
if (fileCoverage < 50 && file.endsWith(".ts")) {
|
|
231
|
+
uncovered.push(`${file} (${fileCoverage}%)`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return {
|
|
236
|
+
summary,
|
|
237
|
+
coverage: { percent, uncovered }
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
function formatTestSummary(summary, format = ResponseFormat.MARKDOWN, context) {
|
|
241
|
+
if (format === ResponseFormat.JSON) {
|
|
242
|
+
return JSON.stringify({ ...summary, context }, null, 2);
|
|
243
|
+
}
|
|
244
|
+
if (summary.failed === 0) {
|
|
245
|
+
const ctx = context ? ` in ${context}` : "";
|
|
246
|
+
return `All ${summary.passed} tests passed${ctx}.`;
|
|
247
|
+
}
|
|
248
|
+
let output = `${summary.failed} tests failed${context ? ` in ${context}` : ""} (${summary.passed} passed)
|
|
249
|
+
|
|
250
|
+
`;
|
|
251
|
+
summary.failures.forEach((f, i) => {
|
|
252
|
+
output += `${i + 1}. ${f.file}:${f.line || "?"}
|
|
253
|
+
`;
|
|
254
|
+
output += ` ${f.message.split(`
|
|
255
|
+
`)[0]}
|
|
256
|
+
`;
|
|
257
|
+
if (f.stack) {
|
|
258
|
+
output += `${f.stack.split(`
|
|
259
|
+
`).map((l) => ` ${l}`).join(`
|
|
260
|
+
`)}
|
|
261
|
+
`;
|
|
262
|
+
}
|
|
263
|
+
output += `
|
|
264
|
+
`;
|
|
265
|
+
});
|
|
266
|
+
return output.trim();
|
|
267
|
+
}
|
|
268
|
+
function formatCoverageResult(summary, coverage, format = ResponseFormat.MARKDOWN) {
|
|
269
|
+
if (format === ResponseFormat.JSON) {
|
|
270
|
+
return JSON.stringify({ summary, coverage }, null, 2);
|
|
271
|
+
}
|
|
272
|
+
let output = "";
|
|
273
|
+
if (summary.failed === 0) {
|
|
274
|
+
output += `All ${summary.passed} tests passed.
|
|
275
|
+
|
|
276
|
+
`;
|
|
277
|
+
} else {
|
|
278
|
+
output += `${summary.failed} tests failed (${summary.passed} passed)
|
|
279
|
+
|
|
280
|
+
`;
|
|
281
|
+
}
|
|
282
|
+
output += `Coverage: ${coverage.percent}%
|
|
283
|
+
`;
|
|
284
|
+
if (coverage.uncovered.length > 0) {
|
|
285
|
+
output += `
|
|
286
|
+
Files with low coverage (<50%):
|
|
287
|
+
`;
|
|
288
|
+
coverage.uncovered.forEach((f) => {
|
|
289
|
+
output += ` - ${f}
|
|
290
|
+
`;
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
return output.trim();
|
|
294
|
+
}
|
|
295
|
+
tool("bun_runTests", {
|
|
296
|
+
description: "Run tests using Bun and return a concise summary of failures. Use this instead of 'bun test' to save tokens and get structured error reports.",
|
|
297
|
+
inputSchema: {
|
|
298
|
+
pattern: z.string().optional().describe("File pattern or test name to filter tests (e.g., 'auth' or 'login.test.ts')"),
|
|
299
|
+
response_format: z.enum(["markdown", "json"]).optional().default("json").describe("Output format: 'markdown' or 'json' (default)")
|
|
300
|
+
},
|
|
301
|
+
annotations: {
|
|
302
|
+
readOnlyHint: true,
|
|
303
|
+
destructiveHint: false,
|
|
304
|
+
idempotentHint: true,
|
|
305
|
+
openWorldHint: false
|
|
306
|
+
}
|
|
307
|
+
}, wrapToolHandler(async (args, format) => {
|
|
308
|
+
const { pattern } = args;
|
|
309
|
+
if (pattern) {
|
|
310
|
+
validateShellSafePattern(pattern);
|
|
311
|
+
if (pattern.includes("/") || pattern.includes("..")) {
|
|
312
|
+
await validatePath(pattern);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
const summary = await runBunTests(pattern);
|
|
316
|
+
const text = formatTestSummary(summary, format);
|
|
317
|
+
if (summary.failed > 0) {
|
|
318
|
+
const error = new Error(text);
|
|
319
|
+
error.summary = summary;
|
|
320
|
+
throw error;
|
|
321
|
+
}
|
|
322
|
+
return text;
|
|
323
|
+
}, {
|
|
324
|
+
toolName: "bun_runTests",
|
|
325
|
+
logger: createLoggerAdapter(mcpLogger),
|
|
326
|
+
createCid: createCorrelationId
|
|
327
|
+
}));
|
|
328
|
+
tool("bun_testFile", {
|
|
329
|
+
description: "Run tests for a specific file only. More targeted than bun_runTests with a pattern.",
|
|
330
|
+
inputSchema: {
|
|
331
|
+
file: z.string().describe("Path to the test file to run (e.g., 'src/utils.test.ts')"),
|
|
332
|
+
response_format: z.enum(["markdown", "json"]).optional().default("json").describe("Output format: 'markdown' or 'json' (default)")
|
|
333
|
+
},
|
|
334
|
+
annotations: {
|
|
335
|
+
readOnlyHint: true,
|
|
336
|
+
destructiveHint: false,
|
|
337
|
+
idempotentHint: true,
|
|
338
|
+
openWorldHint: false
|
|
339
|
+
}
|
|
340
|
+
}, wrapToolHandler(async (args, format) => {
|
|
341
|
+
const { file } = args;
|
|
342
|
+
const validatedFile = await validatePath(file);
|
|
343
|
+
const summary = await runBunTests(validatedFile);
|
|
344
|
+
const text = formatTestSummary(summary, format, file);
|
|
345
|
+
if (summary.failed > 0) {
|
|
346
|
+
const error = new Error(text);
|
|
347
|
+
error.summary = summary;
|
|
348
|
+
throw error;
|
|
349
|
+
}
|
|
350
|
+
return text;
|
|
351
|
+
}, {
|
|
352
|
+
toolName: "bun_testFile",
|
|
353
|
+
logger: createLoggerAdapter(mcpLogger),
|
|
354
|
+
createCid: createCorrelationId
|
|
355
|
+
}));
|
|
356
|
+
tool("bun_testCoverage", {
|
|
357
|
+
description: "Run tests with code coverage and return a summary. Shows overall coverage percentage and files with low coverage.",
|
|
358
|
+
inputSchema: {
|
|
359
|
+
response_format: z.enum(["markdown", "json"]).optional().default("json").describe("Output format: 'markdown' or 'json' (default)")
|
|
360
|
+
},
|
|
361
|
+
annotations: {
|
|
362
|
+
readOnlyHint: true,
|
|
363
|
+
destructiveHint: false,
|
|
364
|
+
idempotentHint: true,
|
|
365
|
+
openWorldHint: false
|
|
366
|
+
}
|
|
367
|
+
}, wrapToolHandler(async (_args, format) => {
|
|
368
|
+
const { summary, coverage } = await runBunTestCoverage();
|
|
369
|
+
const text = formatCoverageResult(summary, coverage, format);
|
|
370
|
+
if (summary.failed > 0) {
|
|
371
|
+
const error = new Error(text);
|
|
372
|
+
error.summary = summary;
|
|
373
|
+
error.coverage = coverage;
|
|
374
|
+
throw error;
|
|
375
|
+
}
|
|
376
|
+
return text;
|
|
377
|
+
}, {
|
|
378
|
+
toolName: "bun_testCoverage",
|
|
379
|
+
logger: createLoggerAdapter(mcpLogger),
|
|
380
|
+
createCid: createCorrelationId
|
|
381
|
+
}));
|
|
382
|
+
if (__require.main == __require.module) {
|
|
383
|
+
startServer("bun-runner", {
|
|
384
|
+
version: "1.0.0",
|
|
385
|
+
fileLogging: {
|
|
386
|
+
enabled: true,
|
|
387
|
+
subsystems: ["mcp"],
|
|
388
|
+
level: "info"
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
export {
|
|
393
|
+
parseBunTestOutput
|
|
394
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@side-quest/bun-runner",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Bun test runner MCP server — structured, token-efficient test output for Claude Code",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "Nathan Vale",
|
|
7
|
+
"url": "https://github.com/nathanvale"
|
|
8
|
+
},
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"type": "module",
|
|
11
|
+
"bin": "./dist/index.js",
|
|
12
|
+
"main": "./dist/index.js",
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"import": "./dist/index.js"
|
|
18
|
+
},
|
|
19
|
+
"./package.json": "./package.json"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist/**",
|
|
23
|
+
"README.md",
|
|
24
|
+
"LICENSE",
|
|
25
|
+
"CHANGELOG.md"
|
|
26
|
+
],
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/nathanvale/side-quest-runners.git",
|
|
30
|
+
"directory": "packages/bun-runner"
|
|
31
|
+
},
|
|
32
|
+
"bugs": {
|
|
33
|
+
"url": "https://github.com/nathanvale/side-quest-runners/issues"
|
|
34
|
+
},
|
|
35
|
+
"homepage": "https://github.com/nathanvale/side-quest-runners/tree/main/packages/bun-runner#readme",
|
|
36
|
+
"keywords": [
|
|
37
|
+
"mcp",
|
|
38
|
+
"claude-code",
|
|
39
|
+
"bun",
|
|
40
|
+
"testing",
|
|
41
|
+
"test-runner"
|
|
42
|
+
],
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"access": "public",
|
|
45
|
+
"provenance": true
|
|
46
|
+
},
|
|
47
|
+
"scripts": {
|
|
48
|
+
"build": "bunx bunup",
|
|
49
|
+
"clean": "rimraf dist 2>/dev/null || true",
|
|
50
|
+
"test": "bun test",
|
|
51
|
+
"test:ci": "TF_BUILD=true bun test",
|
|
52
|
+
"typecheck": "tsc --noEmit"
|
|
53
|
+
},
|
|
54
|
+
"dependencies": {
|
|
55
|
+
"@side-quest/core": "^0.1.1"
|
|
56
|
+
}
|
|
57
|
+
}
|