mcpmon 0.1.0 → 0.3.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/.github/workflows/release.yml +74 -7
- package/.nojekyll +0 -0
- package/.pre-commit-config.yaml +44 -0
- package/README.md +59 -4
- package/__main__.py +6 -0
- package/index.html +414 -0
- package/mcpmon.py +245 -38
- package/mcpmon.test.ts +441 -0
- package/mcpmon.ts +202 -35
- package/package.json +9 -3
- package/pyproject.toml +11 -1
- package/tests/__init__.py +1 -0
- package/tests/test_mcpmon.py +493 -0
- package/.github/workflows/publish-npm.yml +0 -50
- package/.github/workflows/publish.yml +0 -26
package/mcpmon.test.ts
ADDED
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit and integration tests for mcpmon (Bun/TypeScript version).
|
|
3
|
+
*
|
|
4
|
+
* Run with: bun test
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, test, expect, beforeEach, afterEach } from "bun:test";
|
|
8
|
+
import { spawn, type Subprocess } from "bun";
|
|
9
|
+
import { mkdtempSync, writeFileSync, rmSync, mkdirSync } from "fs";
|
|
10
|
+
import { join } from "path";
|
|
11
|
+
import { tmpdir } from "os";
|
|
12
|
+
|
|
13
|
+
// =============================================================================
|
|
14
|
+
// Test Utilities
|
|
15
|
+
// =============================================================================
|
|
16
|
+
|
|
17
|
+
function sleep(ms: number): Promise<void> {
|
|
18
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function createTempDir(): string {
|
|
22
|
+
return mkdtempSync(join(tmpdir(), "mcpmon-test-"));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function createDummyServer(dir: string): string {
|
|
26
|
+
const serverPath = join(dir, "server.py");
|
|
27
|
+
writeFileSync(
|
|
28
|
+
serverPath,
|
|
29
|
+
`
|
|
30
|
+
import sys
|
|
31
|
+
import time
|
|
32
|
+
import signal
|
|
33
|
+
|
|
34
|
+
print(f"[test-server] Started (pid={__import__('os').getpid()})", file=sys.stderr)
|
|
35
|
+
sys.stderr.flush()
|
|
36
|
+
|
|
37
|
+
def handle_term(sig, frame):
|
|
38
|
+
print("[test-server] Received SIGTERM", file=sys.stderr)
|
|
39
|
+
sys.exit(0)
|
|
40
|
+
|
|
41
|
+
signal.signal(signal.SIGTERM, handle_term)
|
|
42
|
+
|
|
43
|
+
while True:
|
|
44
|
+
time.sleep(0.1)
|
|
45
|
+
`
|
|
46
|
+
);
|
|
47
|
+
return serverPath;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// =============================================================================
|
|
51
|
+
// Unit Tests - Logging
|
|
52
|
+
// =============================================================================
|
|
53
|
+
|
|
54
|
+
describe("Logging", () => {
|
|
55
|
+
test("help shows all options", async () => {
|
|
56
|
+
const proc = spawn({
|
|
57
|
+
cmd: ["bun", "mcpmon.ts", "--help"],
|
|
58
|
+
stdout: "pipe",
|
|
59
|
+
stderr: "pipe",
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const output = await new Response(proc.stdout).text();
|
|
63
|
+
await proc.exited;
|
|
64
|
+
|
|
65
|
+
expect(output).toContain("--watch");
|
|
66
|
+
expect(output).toContain("--ext");
|
|
67
|
+
expect(output).toContain("--quiet");
|
|
68
|
+
expect(output).toContain("--verbose");
|
|
69
|
+
expect(output).toContain("--debug");
|
|
70
|
+
expect(output).toContain("--timestamps");
|
|
71
|
+
expect(output).toContain("--log-file");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("help shows logging levels", async () => {
|
|
75
|
+
const proc = spawn({
|
|
76
|
+
cmd: ["bun", "mcpmon.ts", "--help"],
|
|
77
|
+
stdout: "pipe",
|
|
78
|
+
stderr: "pipe",
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const output = await new Response(proc.stdout).text();
|
|
82
|
+
await proc.exited;
|
|
83
|
+
|
|
84
|
+
expect(output).toContain("Logging levels:");
|
|
85
|
+
expect(output).toContain("--quiet");
|
|
86
|
+
expect(output).toContain("--verbose");
|
|
87
|
+
expect(output).toContain("--debug");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("help shows examples", async () => {
|
|
91
|
+
const proc = spawn({
|
|
92
|
+
cmd: ["bun", "mcpmon.ts", "--help"],
|
|
93
|
+
stdout: "pipe",
|
|
94
|
+
stderr: "pipe",
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const output = await new Response(proc.stdout).text();
|
|
98
|
+
await proc.exited;
|
|
99
|
+
|
|
100
|
+
expect(output).toContain("Examples:");
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// =============================================================================
|
|
105
|
+
// Unit Tests - CLI Arguments
|
|
106
|
+
// =============================================================================
|
|
107
|
+
|
|
108
|
+
describe("CLI Arguments", () => {
|
|
109
|
+
test("errors without command", async () => {
|
|
110
|
+
const proc = spawn({
|
|
111
|
+
cmd: ["bun", "mcpmon.ts", "--watch", ".", "--"],
|
|
112
|
+
stdout: "pipe",
|
|
113
|
+
stderr: "pipe",
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const stderr = await new Response(proc.stderr).text();
|
|
117
|
+
const code = await proc.exited;
|
|
118
|
+
|
|
119
|
+
// Should exit with error when no command after --
|
|
120
|
+
expect(code).not.toBe(0);
|
|
121
|
+
expect(stderr.toLowerCase()).toContain("error");
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("accepts -- separator", async () => {
|
|
125
|
+
const proc = spawn({
|
|
126
|
+
cmd: ["bun", "mcpmon.ts", "--help", "--"],
|
|
127
|
+
stdout: "pipe",
|
|
128
|
+
stderr: "pipe",
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const output = await new Response(proc.stdout).text();
|
|
132
|
+
const code = await proc.exited;
|
|
133
|
+
|
|
134
|
+
expect(code).toBe(0);
|
|
135
|
+
expect(output).toContain("Usage:");
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// =============================================================================
|
|
140
|
+
// Integration Tests
|
|
141
|
+
// =============================================================================
|
|
142
|
+
|
|
143
|
+
describe("Integration", () => {
|
|
144
|
+
let tempDir: string;
|
|
145
|
+
let proc: Subprocess | null = null;
|
|
146
|
+
|
|
147
|
+
beforeEach(() => {
|
|
148
|
+
tempDir = createTempDir();
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
afterEach(async () => {
|
|
152
|
+
if (proc && proc.exitCode === null) {
|
|
153
|
+
proc.kill("SIGTERM");
|
|
154
|
+
await proc.exited;
|
|
155
|
+
}
|
|
156
|
+
try {
|
|
157
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
158
|
+
} catch {
|
|
159
|
+
// Ignore cleanup errors
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("starts server and shows startup message", async () => {
|
|
164
|
+
const serverPath = createDummyServer(tempDir);
|
|
165
|
+
const logFile = join(tempDir, "mcpmon.log");
|
|
166
|
+
|
|
167
|
+
proc = spawn({
|
|
168
|
+
cmd: [
|
|
169
|
+
"bun",
|
|
170
|
+
"mcpmon.ts",
|
|
171
|
+
"--watch",
|
|
172
|
+
tempDir,
|
|
173
|
+
"--log-file",
|
|
174
|
+
logFile,
|
|
175
|
+
"--",
|
|
176
|
+
"python",
|
|
177
|
+
serverPath,
|
|
178
|
+
],
|
|
179
|
+
stdout: "pipe",
|
|
180
|
+
stderr: "pipe",
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// Wait for startup
|
|
184
|
+
await sleep(2000);
|
|
185
|
+
|
|
186
|
+
// Should still be running
|
|
187
|
+
expect(proc.exitCode).toBeNull();
|
|
188
|
+
|
|
189
|
+
// Check log file
|
|
190
|
+
const logContent = await Bun.file(logFile).text();
|
|
191
|
+
expect(logContent).toContain("Watching");
|
|
192
|
+
expect(logContent).toContain("Started:");
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("restarts on file change", async () => {
|
|
196
|
+
const serverPath = createDummyServer(tempDir);
|
|
197
|
+
const watchedFile = join(tempDir, "watched.py");
|
|
198
|
+
writeFileSync(watchedFile, "# initial");
|
|
199
|
+
|
|
200
|
+
const logFile = join(tempDir, "mcpmon.log");
|
|
201
|
+
|
|
202
|
+
proc = spawn({
|
|
203
|
+
cmd: [
|
|
204
|
+
"bun",
|
|
205
|
+
"mcpmon.ts",
|
|
206
|
+
"--watch",
|
|
207
|
+
tempDir,
|
|
208
|
+
"--ext",
|
|
209
|
+
"py",
|
|
210
|
+
"--log-file",
|
|
211
|
+
logFile,
|
|
212
|
+
"--",
|
|
213
|
+
"python",
|
|
214
|
+
serverPath,
|
|
215
|
+
],
|
|
216
|
+
stdout: "pipe",
|
|
217
|
+
stderr: "pipe",
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// Wait for startup
|
|
221
|
+
await sleep(2000);
|
|
222
|
+
|
|
223
|
+
// Trigger reload
|
|
224
|
+
writeFileSync(watchedFile, "# modified");
|
|
225
|
+
|
|
226
|
+
// Wait for restart
|
|
227
|
+
await sleep(2000);
|
|
228
|
+
|
|
229
|
+
const logContent = await Bun.file(logFile).text();
|
|
230
|
+
expect(logContent).toContain("Restart #1");
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test("ignores non-matching extensions", async () => {
|
|
234
|
+
const serverPath = createDummyServer(tempDir);
|
|
235
|
+
const otherFile = join(tempDir, "readme.txt");
|
|
236
|
+
writeFileSync(otherFile, "initial");
|
|
237
|
+
|
|
238
|
+
const logFile = join(tempDir, "mcpmon.log");
|
|
239
|
+
|
|
240
|
+
proc = spawn({
|
|
241
|
+
cmd: [
|
|
242
|
+
"bun",
|
|
243
|
+
"mcpmon.ts",
|
|
244
|
+
"--watch",
|
|
245
|
+
tempDir,
|
|
246
|
+
"--ext",
|
|
247
|
+
"py",
|
|
248
|
+
"--debug",
|
|
249
|
+
"--log-file",
|
|
250
|
+
logFile,
|
|
251
|
+
"--",
|
|
252
|
+
"python",
|
|
253
|
+
serverPath,
|
|
254
|
+
],
|
|
255
|
+
stdout: "pipe",
|
|
256
|
+
stderr: "pipe",
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
// Wait for startup
|
|
260
|
+
await sleep(2000);
|
|
261
|
+
|
|
262
|
+
// Modify non-matching file
|
|
263
|
+
writeFileSync(otherFile, "modified");
|
|
264
|
+
|
|
265
|
+
// Wait
|
|
266
|
+
await sleep(2000);
|
|
267
|
+
|
|
268
|
+
const logContent = await Bun.file(logFile).text();
|
|
269
|
+
expect(logContent).not.toContain("Restart #1");
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
test("handles graceful shutdown", async () => {
|
|
273
|
+
const serverPath = createDummyServer(tempDir);
|
|
274
|
+
const logFile = join(tempDir, "mcpmon.log");
|
|
275
|
+
|
|
276
|
+
proc = spawn({
|
|
277
|
+
cmd: [
|
|
278
|
+
"bun",
|
|
279
|
+
"mcpmon.ts",
|
|
280
|
+
"--watch",
|
|
281
|
+
tempDir,
|
|
282
|
+
"--log-file",
|
|
283
|
+
logFile,
|
|
284
|
+
"--",
|
|
285
|
+
"python",
|
|
286
|
+
serverPath,
|
|
287
|
+
],
|
|
288
|
+
stdout: "pipe",
|
|
289
|
+
stderr: "pipe",
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
// Wait for startup
|
|
293
|
+
await sleep(2000);
|
|
294
|
+
|
|
295
|
+
// Send SIGTERM
|
|
296
|
+
proc.kill("SIGTERM");
|
|
297
|
+
|
|
298
|
+
// Wait for shutdown
|
|
299
|
+
const code = await proc.exited;
|
|
300
|
+
|
|
301
|
+
const logContent = await Bun.file(logFile).text();
|
|
302
|
+
expect(logContent).toContain("Shutdown complete");
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
test("tracks restart count", async () => {
|
|
306
|
+
const serverPath = createDummyServer(tempDir);
|
|
307
|
+
const watchedFile = join(tempDir, "watched.py");
|
|
308
|
+
writeFileSync(watchedFile, "# v1");
|
|
309
|
+
|
|
310
|
+
const logFile = join(tempDir, "mcpmon.log");
|
|
311
|
+
|
|
312
|
+
proc = spawn({
|
|
313
|
+
cmd: [
|
|
314
|
+
"bun",
|
|
315
|
+
"mcpmon.ts",
|
|
316
|
+
"--watch",
|
|
317
|
+
tempDir,
|
|
318
|
+
"--ext",
|
|
319
|
+
"py",
|
|
320
|
+
"--log-file",
|
|
321
|
+
logFile,
|
|
322
|
+
"--",
|
|
323
|
+
"python",
|
|
324
|
+
serverPath,
|
|
325
|
+
],
|
|
326
|
+
stdout: "pipe",
|
|
327
|
+
stderr: "pipe",
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
// Wait for startup
|
|
331
|
+
await sleep(2500);
|
|
332
|
+
|
|
333
|
+
// Trigger first restart
|
|
334
|
+
writeFileSync(watchedFile, "# v2");
|
|
335
|
+
await sleep(2500);
|
|
336
|
+
|
|
337
|
+
// Verify first restart happened before triggering second
|
|
338
|
+
let logContent = await Bun.file(logFile).text();
|
|
339
|
+
expect(logContent).toContain("Restart #1");
|
|
340
|
+
|
|
341
|
+
// Trigger second restart
|
|
342
|
+
writeFileSync(watchedFile, "# v3");
|
|
343
|
+
await sleep(2500);
|
|
344
|
+
|
|
345
|
+
logContent = await Bun.file(logFile).text();
|
|
346
|
+
expect(logContent).toContain("Restart #2");
|
|
347
|
+
}, 15000); // Increase timeout to 15s
|
|
348
|
+
|
|
349
|
+
test("verbose mode shows file details", async () => {
|
|
350
|
+
const serverPath = createDummyServer(tempDir);
|
|
351
|
+
const watchedFile = join(tempDir, "mymodule.py");
|
|
352
|
+
writeFileSync(watchedFile, "# initial");
|
|
353
|
+
|
|
354
|
+
const logFile = join(tempDir, "mcpmon.log");
|
|
355
|
+
|
|
356
|
+
proc = spawn({
|
|
357
|
+
cmd: [
|
|
358
|
+
"bun",
|
|
359
|
+
"mcpmon.ts",
|
|
360
|
+
"--watch",
|
|
361
|
+
tempDir,
|
|
362
|
+
"--ext",
|
|
363
|
+
"py",
|
|
364
|
+
"--verbose",
|
|
365
|
+
"--log-file",
|
|
366
|
+
logFile,
|
|
367
|
+
"--",
|
|
368
|
+
"python",
|
|
369
|
+
serverPath,
|
|
370
|
+
],
|
|
371
|
+
stdout: "pipe",
|
|
372
|
+
stderr: "pipe",
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
// Wait for startup
|
|
376
|
+
await sleep(2000);
|
|
377
|
+
|
|
378
|
+
// Trigger reload
|
|
379
|
+
writeFileSync(watchedFile, "# modified");
|
|
380
|
+
await sleep(2000);
|
|
381
|
+
|
|
382
|
+
const logContent = await Bun.file(logFile).text();
|
|
383
|
+
expect(logContent).toContain("File");
|
|
384
|
+
expect(logContent).toContain("mymodule.py");
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
// =============================================================================
|
|
389
|
+
// Timestamp Tests
|
|
390
|
+
// =============================================================================
|
|
391
|
+
|
|
392
|
+
describe("Timestamps", () => {
|
|
393
|
+
let tempDir: string;
|
|
394
|
+
let proc: Subprocess | null = null;
|
|
395
|
+
|
|
396
|
+
beforeEach(() => {
|
|
397
|
+
tempDir = createTempDir();
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
afterEach(async () => {
|
|
401
|
+
if (proc && proc.exitCode === null) {
|
|
402
|
+
proc.kill("SIGTERM");
|
|
403
|
+
await proc.exited;
|
|
404
|
+
}
|
|
405
|
+
try {
|
|
406
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
407
|
+
} catch {
|
|
408
|
+
// Ignore cleanup errors
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
test("log file always has full timestamps", async () => {
|
|
413
|
+
const serverPath = createDummyServer(tempDir);
|
|
414
|
+
const logFile = join(tempDir, "mcpmon.log");
|
|
415
|
+
|
|
416
|
+
proc = spawn({
|
|
417
|
+
cmd: [
|
|
418
|
+
"bun",
|
|
419
|
+
"mcpmon.ts",
|
|
420
|
+
"--watch",
|
|
421
|
+
tempDir,
|
|
422
|
+
"--log-file",
|
|
423
|
+
logFile,
|
|
424
|
+
"--",
|
|
425
|
+
"python",
|
|
426
|
+
serverPath,
|
|
427
|
+
],
|
|
428
|
+
stdout: "pipe",
|
|
429
|
+
stderr: "pipe",
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
await sleep(2000);
|
|
433
|
+
|
|
434
|
+
proc.kill("SIGTERM");
|
|
435
|
+
await proc.exited;
|
|
436
|
+
|
|
437
|
+
const logContent = await Bun.file(logFile).text();
|
|
438
|
+
// Full timestamp format: YYYY-MM-DD HH:MM:SS
|
|
439
|
+
expect(logContent).toMatch(/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/);
|
|
440
|
+
});
|
|
441
|
+
});
|