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/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(listMcp(config.profileDirPath).map(row => row.serverName))
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 (listMcp(config.profileDirPath).some(row => row.serverName === server.id)) {
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 id = upsertMcp(config.profileDirPath, input)
490
- sendJson(response, 200, { ok: true, id, restartNeeded: true })
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
- sendJson(response, 200, { servers: listMcp(config.profileDirPath), restartNeeded: true })
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 id = upsertMcp(config.profileDirPath, input)
531
- sendJson(response, 200, { ok: true, id, restartNeeded: true })
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 ok = setMcpDisabled(config.profileDirPath, body.id, body.disabled)
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 ok = removeMcp(config.profileDirPath, body.id)
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
- try {
601
- sendJson(response, 200, {
602
- servers: scanAllMcp(),
603
- // Profile serverNames, so the browser can grey out existing ones.
604
- existing: listMcp(config.profileDirPath).map(row => row.serverName),
605
- })
606
- } catch (error) {
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
- const existing = listMcp(config.profileDirPath).some(row => row.serverName === server.name)
637
- if (existing) {
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 })