magus-cli 7.11.4 → 7.11.6

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.
@@ -0,0 +1,551 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import {
3
+ type RegistryInstallPlan,
4
+ registryInstallPlan,
5
+ } from "../services/mcp-registry.js";
6
+
7
+ // Contract-only expectations. Every alternative in the behaviour table has its
8
+ // own row; strict equality also rejects invented type/env/config properties.
9
+ type PlanInput = Parameters<typeof registryInstallPlan>[0] & {
10
+ package_registry?: string;
11
+ };
12
+ type InstallCase = [
13
+ scenario: string,
14
+ input: PlanInput,
15
+ expected: RegistryInstallPlan,
16
+ ];
17
+
18
+ const cases: InstallCase[] = [
19
+ [
20
+ "TEST-1 REQ-1 REQ-6: returns the unchanged HTTPS config",
21
+ {
22
+ url: "https://mcp.notion.com/mcp",
23
+ },
24
+ {
25
+ ok: true,
26
+ kind: "http",
27
+ config: {
28
+ type: "http",
29
+ url: "https://mcp.notion.com/mcp",
30
+ },
31
+ },
32
+ ],
33
+ [
34
+ "TEST-2 REQ-1 REQ-6: returns the unchanged HTTP config",
35
+ {
36
+ url: "http://localhost:8080/mcp",
37
+ },
38
+ {
39
+ ok: true,
40
+ kind: "http",
41
+ config: {
42
+ type: "http",
43
+ url: "http://localhost:8080/mcp",
44
+ },
45
+ },
46
+ ],
47
+ [
48
+ "TEST-3 REQ-1 REQ-6: accepts an upper-case HTTPS scheme without rewriting the URL",
49
+ {
50
+ url: "HTTPS://Host/x",
51
+ },
52
+ {
53
+ ok: true,
54
+ kind: "http",
55
+ config: {
56
+ type: "http",
57
+ url: "HTTPS://Host/x",
58
+ },
59
+ },
60
+ ],
61
+ [
62
+ "TEST-4 REQ-1 REQ-6: trims surrounding whitespace from the HTTP URL",
63
+ {
64
+ url: " https://h/x ",
65
+ },
66
+ {
67
+ ok: true,
68
+ kind: "http",
69
+ config: {
70
+ type: "http",
71
+ url: "https://h/x",
72
+ },
73
+ },
74
+ ],
75
+ [
76
+ "TEST-5 REQ-1 REQ-4 REQ-6 REQ-7: refuses HTTPS without a host as unknown",
77
+ {
78
+ url: "https:",
79
+ },
80
+ {
81
+ ok: false,
82
+ kind: "unknown",
83
+ reason: "magus can't read this server's address.",
84
+ },
85
+ ],
86
+ [
87
+ "TEST-6 REQ-1 REQ-4 REQ-6 REQ-7: refuses HTTPS with an empty host as unknown",
88
+ {
89
+ url: "https://",
90
+ },
91
+ {
92
+ ok: false,
93
+ kind: "unknown",
94
+ reason: "magus can't read this server's address.",
95
+ },
96
+ ],
97
+ [
98
+ "TEST-7 REQ-2 REQ-6 REQ-14: returns uvx with only the PyPI package and no type or env",
99
+ {
100
+ url: "pypi:mcparmory-notion",
101
+ },
102
+ {
103
+ ok: true,
104
+ kind: "pypi",
105
+ config: {
106
+ command: "uvx",
107
+ args: ["mcparmory-notion"],
108
+ },
109
+ },
110
+ ],
111
+ [
112
+ "TEST-8 REQ-2 REQ-6: accepts a mixed-case PyPI scheme and trims the package",
113
+ {
114
+ url: "PyPI: pkg ",
115
+ },
116
+ {
117
+ ok: true,
118
+ kind: "pypi",
119
+ config: {
120
+ command: "uvx",
121
+ args: ["pkg"],
122
+ },
123
+ },
124
+ ],
125
+ [
126
+ "TEST-9 REQ-5 REQ-6 REQ-7: refuses an empty PyPI package with the no-package reason",
127
+ {
128
+ url: "pypi:",
129
+ },
130
+ {
131
+ ok: false,
132
+ kind: "pypi",
133
+ reason:
134
+ "This pypi server has no package name, so there's nothing to run.",
135
+ },
136
+ ],
137
+ [
138
+ "TEST-10 REQ-5 REQ-6 REQ-7: refuses a whitespace-only PyPI package with the no-package reason",
139
+ {
140
+ url: "pypi: ",
141
+ },
142
+ {
143
+ ok: false,
144
+ kind: "pypi",
145
+ reason:
146
+ "This pypi server has no package name, so there's nothing to run.",
147
+ },
148
+ ],
149
+ [
150
+ "TEST-11 REQ-5 REQ-6 REQ-7: refuses PyPI extras with the bad-package reason",
151
+ {
152
+ url: "pypi:pkg[extra]",
153
+ },
154
+ {
155
+ ok: false,
156
+ kind: "pypi",
157
+ reason:
158
+ "The package name of this pypi server looks wrong, so magus won't run it.",
159
+ },
160
+ ],
161
+ [
162
+ "TEST-12 REQ-5 REQ-6 REQ-7: refuses a PyPI equals pin with the bad-package reason",
163
+ {
164
+ url: "pypi:pkg==1.0",
165
+ },
166
+ {
167
+ ok: false,
168
+ kind: "pypi",
169
+ reason:
170
+ "The package name of this pypi server looks wrong, so magus won't run it.",
171
+ },
172
+ ],
173
+ [
174
+ "TEST-13 REQ-5 REQ-6 REQ-7: refuses embedded whitespace in a PyPI package",
175
+ {
176
+ url: "pypi:a b",
177
+ },
178
+ {
179
+ ok: false,
180
+ kind: "pypi",
181
+ reason:
182
+ "The package name of this pypi server looks wrong, so magus won't run it.",
183
+ },
184
+ ],
185
+ [
186
+ "TEST-14 REQ-2 REQ-6 REQ-14: passes a PyPI package at-version through verbatim",
187
+ {
188
+ url: "pypi:pkg@1.2.3",
189
+ },
190
+ {
191
+ ok: true,
192
+ kind: "pypi",
193
+ config: {
194
+ command: "uvx",
195
+ args: ["pkg@1.2.3"],
196
+ },
197
+ },
198
+ ],
199
+ [
200
+ "TEST-15 REQ-3 REQ-6 REQ-14: returns npx -y with an unscoped npm package and no type or env",
201
+ {
202
+ url: "npm:npm-search-mcp",
203
+ },
204
+ {
205
+ ok: true,
206
+ kind: "npm",
207
+ config: {
208
+ command: "npx",
209
+ args: ["-y", "npm-search-mcp"],
210
+ },
211
+ },
212
+ ],
213
+ [
214
+ "TEST-16 REQ-3 REQ-6: returns npx -y with a scoped npm package",
215
+ {
216
+ url: "npm:@yawlabs/npmjs-mcp",
217
+ },
218
+ {
219
+ ok: true,
220
+ kind: "npm",
221
+ config: {
222
+ command: "npx",
223
+ args: ["-y", "@yawlabs/npmjs-mcp"],
224
+ },
225
+ },
226
+ ],
227
+ [
228
+ "TEST-17 REQ-3 REQ-6 REQ-14: passes an unscoped npm version through verbatim",
229
+ {
230
+ url: "npm:pkg@1.2.3",
231
+ },
232
+ {
233
+ ok: true,
234
+ kind: "npm",
235
+ config: {
236
+ command: "npx",
237
+ args: ["-y", "pkg@1.2.3"],
238
+ },
239
+ },
240
+ ],
241
+ [
242
+ "TEST-18 REQ-3 REQ-6 REQ-14: passes a scoped npm latest tag through verbatim",
243
+ {
244
+ url: "npm:@s/p@latest",
245
+ },
246
+ {
247
+ ok: true,
248
+ kind: "npm",
249
+ config: {
250
+ command: "npx",
251
+ args: ["-y", "@s/p@latest"],
252
+ },
253
+ },
254
+ ],
255
+ [
256
+ "TEST-19 REQ-5 REQ-6 REQ-7: refuses an npm scope without a package",
257
+ {
258
+ url: "npm:@scope",
259
+ },
260
+ {
261
+ ok: false,
262
+ kind: "npm",
263
+ reason:
264
+ "The package name of this npm server looks wrong, so magus won't run it.",
265
+ },
266
+ ],
267
+ [
268
+ "TEST-20 REQ-5 REQ-6 REQ-7: refuses an npm package with an empty scope",
269
+ {
270
+ url: "npm:@/x",
271
+ },
272
+ {
273
+ ok: false,
274
+ kind: "npm",
275
+ reason:
276
+ "The package name of this npm server looks wrong, so magus won't run it.",
277
+ },
278
+ ],
279
+ [
280
+ "TEST-21 REQ-5 REQ-6 REQ-7: refuses an npm name that would become the -y option",
281
+ {
282
+ url: "npm:-y",
283
+ },
284
+ {
285
+ ok: false,
286
+ kind: "npm",
287
+ reason:
288
+ "The package name of this npm server looks wrong, so magus won't run it.",
289
+ },
290
+ ],
291
+ [
292
+ "TEST-22 REQ-5 REQ-6 REQ-7: refuses npm --package option injection",
293
+ {
294
+ url: "npm:--package=x",
295
+ },
296
+ {
297
+ ok: false,
298
+ kind: "npm",
299
+ reason:
300
+ "The package name of this npm server looks wrong, so magus won't run it.",
301
+ },
302
+ ],
303
+ [
304
+ "TEST-23 REQ-4 REQ-6 REQ-7: refuses oci with the exact unsupported-kind reason",
305
+ {
306
+ url: "oci:ghcr.io/x/y:1",
307
+ },
308
+ {
309
+ ok: false,
310
+ kind: "oci",
311
+ reason:
312
+ "magus can't install oci servers. It can install https, pypi and npm ones.",
313
+ },
314
+ ],
315
+ [
316
+ "TEST-24 REQ-4 REQ-6 REQ-7: refuses nuget with the exact unsupported-kind reason",
317
+ {
318
+ url: "nuget:Foo",
319
+ },
320
+ {
321
+ ok: false,
322
+ kind: "nuget",
323
+ reason:
324
+ "magus can't install nuget servers. It can install https, pypi and npm ones.",
325
+ },
326
+ ],
327
+ [
328
+ "TEST-25 REQ-4 REQ-6 REQ-7: refuses an unsupported foo scheme and names it",
329
+ {
330
+ url: "foo:bar",
331
+ },
332
+ {
333
+ ok: false,
334
+ kind: "foo",
335
+ reason:
336
+ "magus can't install foo servers. It can install https, pypi and npm ones.",
337
+ },
338
+ ],
339
+ [
340
+ "TEST-26 REQ-4 REQ-6 REQ-7: refuses a bare localhost port as unknown",
341
+ {
342
+ url: "localhost:8080",
343
+ },
344
+ {
345
+ ok: false,
346
+ kind: "unknown",
347
+ reason: "magus can't read this server's address.",
348
+ },
349
+ ],
350
+ [
351
+ "TEST-27 REQ-4 REQ-6 REQ-7: refuses bare text as an unreadable address",
352
+ {
353
+ url: "just-text",
354
+ },
355
+ {
356
+ ok: false,
357
+ kind: "unknown",
358
+ reason: "magus can't read this server's address.",
359
+ },
360
+ ],
361
+ [
362
+ "TEST-28 REQ-4 REQ-6 REQ-7: refuses an empty address as unknown",
363
+ {
364
+ url: "",
365
+ },
366
+ {
367
+ ok: false,
368
+ kind: "unknown",
369
+ reason: "magus can't read this server's address.",
370
+ },
371
+ ],
372
+ [
373
+ "TEST-29 REQ-5 REQ-6 REQ-7: refuses an empty npm package with the no-package reason",
374
+ {
375
+ url: "npm:",
376
+ },
377
+ {
378
+ ok: false,
379
+ kind: "npm",
380
+ reason: "This npm server has no package name, so there's nothing to run.",
381
+ },
382
+ ],
383
+ [
384
+ "TEST-30 REQ-5 REQ-6 REQ-7: refuses a whitespace-only npm package with the no-package reason",
385
+ {
386
+ url: "npm: ",
387
+ },
388
+ {
389
+ ok: false,
390
+ kind: "npm",
391
+ reason: "This npm server has no package name, so there's nothing to run.",
392
+ },
393
+ ],
394
+ [
395
+ "TEST-31 REQ-3 REQ-6: accepts an upper-case npm scheme and trims the scoped package",
396
+ {
397
+ url: "NPM: @yawlabs/npmjs-mcp ",
398
+ },
399
+ {
400
+ ok: true,
401
+ kind: "npm",
402
+ config: {
403
+ command: "npx",
404
+ args: ["-y", "@yawlabs/npmjs-mcp"],
405
+ },
406
+ },
407
+ ],
408
+ [
409
+ "TEST-32 REQ-4 REQ-6 REQ-7: lower-cases the refused OCI kind and its wording",
410
+ {
411
+ url: "OCI:ghcr.io/x/y:1",
412
+ },
413
+ {
414
+ ok: false,
415
+ kind: "oci",
416
+ reason:
417
+ "magus can't install oci servers. It can install https, pypi and npm ones.",
418
+ },
419
+ ],
420
+ [
421
+ "TEST-33 REQ-4 REQ-6 REQ-7: refuses a whitespace-only address as unknown",
422
+ {
423
+ url: " ",
424
+ },
425
+ {
426
+ ok: false,
427
+ kind: "unknown",
428
+ reason: "magus can't read this server's address.",
429
+ },
430
+ ],
431
+ [
432
+ "TEST-34 REQ-1 REQ-4 REQ-6 REQ-7: refuses HTTP without a host as unknown",
433
+ {
434
+ url: "http:",
435
+ },
436
+ {
437
+ ok: false,
438
+ kind: "unknown",
439
+ reason: "magus can't read this server's address.",
440
+ },
441
+ ],
442
+ [
443
+ "TEST-35 REQ-1 REQ-4 REQ-6 REQ-7: refuses HTTP with an empty host as unknown",
444
+ {
445
+ url: "http://",
446
+ },
447
+ {
448
+ ok: false,
449
+ kind: "unknown",
450
+ reason: "magus can't read this server's address.",
451
+ },
452
+ ],
453
+ [
454
+ "TEST-36 REQ-1 REQ-6: keeps a hosted HTTPS server as HTTP despite npm metadata",
455
+ {
456
+ url: "https://mcp.notion.com/mcp",
457
+ package_registry: "npm",
458
+ },
459
+ {
460
+ ok: true,
461
+ kind: "http",
462
+ config: {
463
+ type: "http",
464
+ url: "https://mcp.notion.com/mcp",
465
+ },
466
+ },
467
+ ],
468
+ [
469
+ "TEST-37 REQ-1 REQ-6: keeps a hosted HTTPS server as HTTP despite PyPI metadata",
470
+ {
471
+ url: "https://mcp.notion.com/mcp",
472
+ package_registry: "pypi",
473
+ },
474
+ {
475
+ ok: true,
476
+ kind: "http",
477
+ config: {
478
+ type: "http",
479
+ url: "https://mcp.notion.com/mcp",
480
+ },
481
+ },
482
+ ],
483
+ [
484
+ "TEST-38 REQ-2 REQ-6: uses the PyPI URL rather than conflicting npm metadata",
485
+ {
486
+ url: "pypi:mcparmory-notion",
487
+ package_registry: "npm",
488
+ },
489
+ {
490
+ ok: true,
491
+ kind: "pypi",
492
+ config: {
493
+ command: "uvx",
494
+ args: ["mcparmory-notion"],
495
+ },
496
+ },
497
+ ],
498
+ [
499
+ "TEST-39 REQ-3 REQ-6: uses the npm URL rather than conflicting PyPI metadata",
500
+ {
501
+ url: "npm:@yawlabs/npmjs-mcp",
502
+ package_registry: "pypi",
503
+ },
504
+ {
505
+ ok: true,
506
+ kind: "npm",
507
+ config: {
508
+ command: "npx",
509
+ args: ["-y", "@yawlabs/npmjs-mcp"],
510
+ },
511
+ },
512
+ ],
513
+ [
514
+ "TEST-40 REQ-4 REQ-6 REQ-7: does not turn an unsupported OCI URL into npm using metadata",
515
+ {
516
+ url: "oci:ghcr.io/x/y:1",
517
+ package_registry: "npm",
518
+ },
519
+ {
520
+ ok: false,
521
+ kind: "oci",
522
+ reason:
523
+ "magus can't install oci servers. It can install https, pypi and npm ones.",
524
+ },
525
+ ],
526
+ [
527
+ "TEST-41 REQ-4 REQ-6 REQ-7: does not turn an unreadable address into npm using metadata",
528
+ {
529
+ url: "just-text",
530
+ package_registry: "npm",
531
+ },
532
+ {
533
+ ok: false,
534
+ kind: "unknown",
535
+ reason: "magus can't read this server's address.",
536
+ },
537
+ ],
538
+ ];
539
+
540
+ describe("registryInstallPlan: a registry address becomes an install config or a plain refusal", () => {
541
+ test.each(cases)("%s", (_scenario, input, expected) => {
542
+ const frozen = Object.freeze({ ...input });
543
+
544
+ const first = registryInstallPlan(frozen);
545
+ const repeated = registryInstallPlan(frozen);
546
+
547
+ expect(first).toStrictEqual(expected);
548
+ expect(repeated).toStrictEqual(expected);
549
+ expect(frozen).toStrictEqual(input);
550
+ });
551
+ });