dsh-plugin-capabilities 0.3.7 → 0.3.9
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 +4 -2
- package/lib/client.js +158 -32
- package/lib/client.js.map +2 -2
- package/lib/index.js +245 -38
- package/lib/index.js.map +2 -2
- package/package.json +1 -1
- package/src/agents.test.ts +47 -1
- package/src/agents.ts +59 -8
- package/src/client/MarketTab.tsx +23 -7
- package/src/client/McpTab.tsx +128 -23
- package/src/client/index.ts +12 -8
- package/src/client/locales.ts +30 -8
- package/src/index.ts +2 -1
- package/src/mcp.test.ts +135 -2
- package/src/mcp.ts +156 -18
- package/src/profile.ts +6 -2
- package/src/routes.ts +116 -26
package/src/routes.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { dshLaunch, restartOwnedByShell, scheduleRestart, trustedRestartRequest
|
|
|
12
12
|
import { deleteSkill, setSkillPolicy, updateSkillFile, userSkillsDir, validateSkillInput, writeSkill, type SkillInput } from './skills.ts'
|
|
13
13
|
import { findRootByUrl, loadState, pluginStateDir, removeSkillRoot, type SkillRootEntry } from './state.ts'
|
|
14
14
|
import { removeTree } from './rmtree.ts'
|
|
15
|
-
import { listMcp, removeMcp, setMcpDisabled, upsertMcp, validateMcpInput, type McpInput } from './mcp.ts'
|
|
15
|
+
import { checkMcpRow, listMcp, listMcpScoped, mcpRowToInput, mcpScopeDir, removeMcp, setMcpDisabled, upsertMcp, validateMcpInput, type McpInput, type McpScope } from './mcp.ts'
|
|
16
16
|
import type { CapabilitiesHost, HostSkill } from './types.ts'
|
|
17
17
|
|
|
18
18
|
/**
|
|
@@ -57,10 +57,17 @@ function toRootView(entry: SkillRootEntry): SkillRootEntry & { live: boolean } {
|
|
|
57
57
|
|
|
58
58
|
export interface CapabilitiesRoutesConfig {
|
|
59
59
|
profileDirPath: string
|
|
60
|
+
/** The dsh home directory: holds the machine-wide `cordis.patch.yml`. */
|
|
61
|
+
dshHomePath: string
|
|
60
62
|
/** Remount the host-plane skill provider after root-set changes. */
|
|
61
63
|
remountProvider: () => Promise<void>
|
|
62
64
|
}
|
|
63
65
|
|
|
66
|
+
/** The request's target patch layer; unknown values fall back to the profile. */
|
|
67
|
+
function readScope(value: unknown): McpScope {
|
|
68
|
+
return value === 'global' ? 'global' : 'profile'
|
|
69
|
+
}
|
|
70
|
+
|
|
64
71
|
/** Register the manager's routes; returns the disposer removing them all. */
|
|
65
72
|
export function mountCapabilitiesRoutes(host: CapabilitiesHost, config: CapabilitiesRoutesConfig): () => void {
|
|
66
73
|
const disposers = [
|
|
@@ -408,7 +415,7 @@ export function mountCapabilitiesRoutes(host: CapabilitiesHost, config: Capabili
|
|
|
408
415
|
sendJson(response, 502, { error: 'market index unavailable (offline?)' })
|
|
409
416
|
return
|
|
410
417
|
}
|
|
411
|
-
const existing = new Set(
|
|
418
|
+
const existing = new Set(listMcpScoped(config.profileDirPath, config.dshHomePath).servers.map(row => row.serverName))
|
|
412
419
|
sendJson(response, 200, {
|
|
413
420
|
source: index.source,
|
|
414
421
|
servers: (index.servers ?? []).map(server => ({ ...server, installed: existing.has(server.id) })),
|
|
@@ -458,7 +465,7 @@ export function mountCapabilitiesRoutes(host: CapabilitiesHost, config: Capabili
|
|
|
458
465
|
return
|
|
459
466
|
}
|
|
460
467
|
try {
|
|
461
|
-
const body = (await readJsonBody(request)) as { id?: unknown }
|
|
468
|
+
const body = (await readJsonBody(request)) as { id?: unknown; scope?: unknown }
|
|
462
469
|
if (typeof body.id !== 'string') {
|
|
463
470
|
sendJson(response, 400, { error: 'id is required' })
|
|
464
471
|
return
|
|
@@ -469,7 +476,7 @@ export function mountCapabilitiesRoutes(host: CapabilitiesHost, config: Capabili
|
|
|
469
476
|
sendJson(response, 404, { error: 'server not found in the market index' })
|
|
470
477
|
return
|
|
471
478
|
}
|
|
472
|
-
if (
|
|
479
|
+
if (listMcpScoped(config.profileDirPath, config.dshHomePath).servers.some(row => row.serverName === server.id)) {
|
|
473
480
|
sendJson(response, 409, { error: 'already installed' })
|
|
474
481
|
return
|
|
475
482
|
}
|
|
@@ -486,8 +493,9 @@ export function mountCapabilitiesRoutes(host: CapabilitiesHost, config: Capabili
|
|
|
486
493
|
sendJson(response, 400, { error: invalid })
|
|
487
494
|
return
|
|
488
495
|
}
|
|
489
|
-
const
|
|
490
|
-
|
|
496
|
+
const scope = readScope(body.scope)
|
|
497
|
+
const id = upsertMcp(mcpScopeDir(scope, config.profileDirPath, config.dshHomePath), input)
|
|
498
|
+
sendJson(response, 200, { ok: true, id, scope, restartNeeded: true })
|
|
491
499
|
} catch (error) {
|
|
492
500
|
sendJson(response, 500, { error: error instanceof Error ? error.message : String(error) })
|
|
493
501
|
}
|
|
@@ -503,7 +511,12 @@ export function mountCapabilitiesRoutes(host: CapabilitiesHost, config: Capabili
|
|
|
503
511
|
response.end()
|
|
504
512
|
return
|
|
505
513
|
}
|
|
506
|
-
|
|
514
|
+
const listing = listMcpScoped(config.profileDirPath, config.dshHomePath)
|
|
515
|
+
sendJson(response, 200, {
|
|
516
|
+
servers: listing.servers,
|
|
517
|
+
...(listing.globalError !== undefined ? { globalError: listing.globalError } : {}),
|
|
518
|
+
restartNeeded: true,
|
|
519
|
+
})
|
|
507
520
|
},
|
|
508
521
|
}),
|
|
509
522
|
|
|
@@ -521,14 +534,15 @@ export function mountCapabilitiesRoutes(host: CapabilitiesHost, config: Capabili
|
|
|
521
534
|
return
|
|
522
535
|
}
|
|
523
536
|
try {
|
|
524
|
-
const input = (await readJsonBody(request)) as McpInput
|
|
537
|
+
const input = (await readJsonBody(request)) as McpInput & { scope?: unknown }
|
|
525
538
|
const invalid = validateMcpInput(input)
|
|
526
539
|
if (invalid !== null) {
|
|
527
540
|
sendJson(response, 400, { error: invalid })
|
|
528
541
|
return
|
|
529
542
|
}
|
|
530
|
-
const
|
|
531
|
-
|
|
543
|
+
const scope = readScope(input.scope)
|
|
544
|
+
const id = upsertMcp(mcpScopeDir(scope, config.profileDirPath, config.dshHomePath), input)
|
|
545
|
+
sendJson(response, 200, { ok: true, id, scope, restartNeeded: true })
|
|
532
546
|
} catch (error) {
|
|
533
547
|
sendJson(response, 500, { error: error instanceof Error ? error.message : String(error) })
|
|
534
548
|
}
|
|
@@ -549,12 +563,13 @@ export function mountCapabilitiesRoutes(host: CapabilitiesHost, config: Capabili
|
|
|
549
563
|
return
|
|
550
564
|
}
|
|
551
565
|
try {
|
|
552
|
-
const body = (await readJsonBody(request)) as { id?: unknown; disabled?: unknown }
|
|
566
|
+
const body = (await readJsonBody(request)) as { id?: unknown; disabled?: unknown; scope?: unknown }
|
|
553
567
|
if (typeof body.id !== 'string' || typeof body.disabled !== 'boolean') {
|
|
554
568
|
sendJson(response, 400, { error: 'id and disabled are required' })
|
|
555
569
|
return
|
|
556
570
|
}
|
|
557
|
-
const
|
|
571
|
+
const dir = mcpScopeDir(readScope(body.scope), config.profileDirPath, config.dshHomePath)
|
|
572
|
+
const ok = setMcpDisabled(dir, body.id, body.disabled)
|
|
558
573
|
sendJson(response, ok ? 200 : 404, ok ? { ok: true, restartNeeded: true } : { error: 'server row not found' })
|
|
559
574
|
} catch (error) {
|
|
560
575
|
sendJson(response, 500, { error: error instanceof Error ? error.message : String(error) })
|
|
@@ -576,18 +591,93 @@ export function mountCapabilitiesRoutes(host: CapabilitiesHost, config: Capabili
|
|
|
576
591
|
return
|
|
577
592
|
}
|
|
578
593
|
try {
|
|
579
|
-
const body = (await readJsonBody(request)) as { id?: unknown }
|
|
594
|
+
const body = (await readJsonBody(request)) as { id?: unknown; scope?: unknown }
|
|
580
595
|
if (typeof body.id !== 'string') {
|
|
581
596
|
sendJson(response, 400, { error: 'id is required' })
|
|
582
597
|
return
|
|
583
598
|
}
|
|
584
|
-
const
|
|
599
|
+
const dir = mcpScopeDir(readScope(body.scope), config.profileDirPath, config.dshHomePath)
|
|
600
|
+
const ok = removeMcp(dir, body.id)
|
|
585
601
|
sendJson(response, ok ? 200 : 404, ok ? { ok: true, restartNeeded: true } : { error: 'server row not found' })
|
|
586
602
|
} catch (error) {
|
|
587
603
|
sendJson(response, 500, { error: error instanceof Error ? error.message : String(error) })
|
|
588
604
|
}
|
|
589
605
|
},
|
|
590
606
|
}),
|
|
607
|
+
|
|
608
|
+
host.webServer.register({
|
|
609
|
+
kind: 'exact',
|
|
610
|
+
path: '/dsh-plugin-capabilities/mcp/check',
|
|
611
|
+
handler: async (request: IncomingMessage, response: ServerResponse) => {
|
|
612
|
+
if (request.method !== 'POST') {
|
|
613
|
+
response.writeHead(405, { allow: 'POST' })
|
|
614
|
+
response.end()
|
|
615
|
+
return
|
|
616
|
+
}
|
|
617
|
+
if (!sameOrigin(request)) {
|
|
618
|
+
sendJson(response, 403, { error: 'untrusted origin' })
|
|
619
|
+
return
|
|
620
|
+
}
|
|
621
|
+
try {
|
|
622
|
+
const body = (await readJsonBody(request)) as { id?: unknown; scope?: unknown }
|
|
623
|
+
if (typeof body.id !== 'string') {
|
|
624
|
+
sendJson(response, 400, { error: 'id is required' })
|
|
625
|
+
return
|
|
626
|
+
}
|
|
627
|
+
const dir = mcpScopeDir(readScope(body.scope), config.profileDirPath, config.dshHomePath)
|
|
628
|
+
const row = listMcp(dir).find(item => item.id === body.id)
|
|
629
|
+
if (row === undefined) {
|
|
630
|
+
sendJson(response, 404, { error: 'server row not found' })
|
|
631
|
+
return
|
|
632
|
+
}
|
|
633
|
+
// No host-level timeout override needed: the probe is bounded
|
|
634
|
+
// (5s) and side-effect-free (PATH reads or a plain GET).
|
|
635
|
+
sendJson(response, 200, await checkMcpRow(row))
|
|
636
|
+
} catch (error) {
|
|
637
|
+
sendJson(response, 500, { error: error instanceof Error ? error.message : String(error) })
|
|
638
|
+
}
|
|
639
|
+
},
|
|
640
|
+
}),
|
|
641
|
+
|
|
642
|
+
host.webServer.register({
|
|
643
|
+
kind: 'exact',
|
|
644
|
+
path: '/dsh-plugin-capabilities/mcp/copy',
|
|
645
|
+
handler: async (request: IncomingMessage, response: ServerResponse) => {
|
|
646
|
+
if (request.method !== 'POST') {
|
|
647
|
+
response.writeHead(405, { allow: 'POST' })
|
|
648
|
+
response.end()
|
|
649
|
+
return
|
|
650
|
+
}
|
|
651
|
+
if (!sameOrigin(request)) {
|
|
652
|
+
sendJson(response, 403, { error: 'untrusted origin' })
|
|
653
|
+
return
|
|
654
|
+
}
|
|
655
|
+
try {
|
|
656
|
+
const body = (await readJsonBody(request)) as { id?: unknown; scope?: unknown; toScope?: unknown }
|
|
657
|
+
if (typeof body.id !== 'string') {
|
|
658
|
+
sendJson(response, 400, { error: 'id is required' })
|
|
659
|
+
return
|
|
660
|
+
}
|
|
661
|
+
const scope = readScope(body.scope)
|
|
662
|
+
const toScope: McpScope = readScope(body.toScope)
|
|
663
|
+
const sourceRow = listMcp(mcpScopeDir(scope, config.profileDirPath, config.dshHomePath)).find(item => item.id === body.id)
|
|
664
|
+
if (sourceRow === undefined) {
|
|
665
|
+
sendJson(response, 404, { error: 'server row not found' })
|
|
666
|
+
return
|
|
667
|
+
}
|
|
668
|
+
const input = mcpRowToInput(sourceRow)
|
|
669
|
+
const invalid = validateMcpInput(input)
|
|
670
|
+
if (invalid !== null) {
|
|
671
|
+
sendJson(response, 400, { error: invalid })
|
|
672
|
+
return
|
|
673
|
+
}
|
|
674
|
+
const id = upsertMcp(mcpScopeDir(toScope, config.profileDirPath, config.dshHomePath), input)
|
|
675
|
+
sendJson(response, 200, { ok: true, id, scope: toScope, restartNeeded: true })
|
|
676
|
+
} catch (error) {
|
|
677
|
+
sendJson(response, 500, { error: error instanceof Error ? error.message : String(error) })
|
|
678
|
+
}
|
|
679
|
+
},
|
|
680
|
+
}),
|
|
591
681
|
host.webServer.register({
|
|
592
682
|
kind: 'exact',
|
|
593
683
|
path: '/dsh-plugin-capabilities/import/scan',
|
|
@@ -597,13 +687,14 @@ export function mountCapabilitiesRoutes(host: CapabilitiesHost, config: Capabili
|
|
|
597
687
|
response.end()
|
|
598
688
|
return
|
|
599
689
|
}
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
690
|
+
try {
|
|
691
|
+
sendJson(response, 200, {
|
|
692
|
+
servers: scanAllMcp(),
|
|
693
|
+
// ServerNames present in either patch layer, so the browser can
|
|
694
|
+
// grey out existing ones.
|
|
695
|
+
existing: listMcpScoped(config.profileDirPath, config.dshHomePath).servers.map(row => row.serverName),
|
|
696
|
+
})
|
|
697
|
+
} catch (error) {
|
|
607
698
|
sendJson(response, 500, { error: error instanceof Error ? error.message : String(error) })
|
|
608
699
|
}
|
|
609
700
|
},
|
|
@@ -623,7 +714,7 @@ export function mountCapabilitiesRoutes(host: CapabilitiesHost, config: Capabili
|
|
|
623
714
|
return
|
|
624
715
|
}
|
|
625
716
|
try {
|
|
626
|
-
const body = (await readJsonBody(request)) as { items?: unknown }
|
|
717
|
+
const body = (await readJsonBody(request)) as { items?: unknown; scope?: unknown }
|
|
627
718
|
const wanted = new Set(
|
|
628
719
|
(Array.isArray(body.items) ? body.items : [])
|
|
629
720
|
.filter((item): item is { agent: string; name: string } =>
|
|
@@ -633,9 +724,8 @@ export function mountCapabilitiesRoutes(host: CapabilitiesHost, config: Capabili
|
|
|
633
724
|
const results: Array<{ name: string; ok: boolean; error?: string }> = []
|
|
634
725
|
for (const server of scanAllMcp()) {
|
|
635
726
|
if (!wanted.has(`${server.agent}/${server.name}`)) continue
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
results.push({ name: server.name, ok: false, error: 'already in profile' })
|
|
727
|
+
if (listMcpScoped(config.profileDirPath, config.dshHomePath).servers.some(row => row.serverName === server.name)) {
|
|
728
|
+
results.push({ name: server.name, ok: false, error: 'already configured' })
|
|
639
729
|
continue
|
|
640
730
|
}
|
|
641
731
|
const input: McpInput = {
|
|
@@ -651,7 +741,7 @@ export function mountCapabilitiesRoutes(host: CapabilitiesHost, config: Capabili
|
|
|
651
741
|
results.push({ name: server.name, ok: false, error: invalid })
|
|
652
742
|
continue
|
|
653
743
|
}
|
|
654
|
-
upsertMcp(config.profileDirPath, input)
|
|
744
|
+
upsertMcp(mcpScopeDir(readScope(body.scope), config.profileDirPath, config.dshHomePath), input)
|
|
655
745
|
results.push({ name: server.name, ok: true })
|
|
656
746
|
}
|
|
657
747
|
sendJson(response, 200, { ok: results.every(item => item.ok), results, restartNeeded: true })
|