@auggieteo/dsh-mcp-adapter 0.2.0 → 0.2.2

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 CHANGED
@@ -4,6 +4,31 @@ All notable changes to this project are documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [v0.2.2] - 2026-09-06
8
+
9
+ ### Added
10
+
11
+ - `skillInstall` Config field (`file` by default, `runtime`, or `off`) controlling how the bundled `mcp-adapter` agent skill is installed. `file` copies `skills/mcp-adapter/SKILL.md` verbatim to `$DSH_HOME/skills/mcp-adapter/SKILL.md` at startup (atomic temp-file-then-rename write, never a symlink), so the DSH filesystem skill provider lists it under Settings > Skills > DSH skills. `runtime` keeps the previous in-memory registration; `off` installs nothing. The mode is read once at Adapter startup.
12
+
13
+ ### Changed
14
+
15
+ - The bundled agent skill now installs as a real file by default instead of registering only in memory. The installed file is never overwritten: an identical file is left untouched, and a file edited by a human wins with one Host warning naming the installed path and the package version — delete `$DSH_HOME/skills/mcp-adapter/` and restart DSH to refresh it from the package. A failed file install (for example a read-only `$DSH_HOME`) falls back to the runtime registration with a warning; the skill stays a convenience and never throws.
16
+
17
+ ## [v0.2.1] - 2026-09-06
18
+
19
+ ### Added
20
+
21
+ - `scripts/patch-dsh-settings-nav-icon.mjs`: patches the installed DSH settings shell so the `mcp` nav row in Settings shows a connection icon (`IconLinkOutline16`) instead of the generic settings-gear fallback. The icon mapping is DSH shell chrome that this plugin cannot reach through the `settings.section` slot (it accepts only `id`, `order`, `label`). The insert is idempotent and marked `// dsh-mcp-adapter`; `--revert` restores the original bytes. A DSH upgrade replaces the patched file, so re-run the script afterwards, then restart `dsh web` and refresh the page.
22
+
23
+ ### Changed
24
+
25
+ - Settings > MCP has a single Add Server entry point: the dialog now offers an Input fields mode for one Server and a Paste JSON mode for a standard `mcpServers` object (existing names are replaced), replacing the separate Import JSON button and dialog in both the toolbar and the empty state.
26
+
27
+ ### Fixed
28
+
29
+ - The plugin failed to load on DSH 0.1.2-rc.1 with `failed to apply loader entry … cannot get property "remote.settings" without inject`: the runner mounts the settings namespace as the traced dotted service `remote.settings`, so the plain `ctx.remote?.settings` read is governed and throws for plugins that do not declare it — while declaring it would park the plugin forever on 0.1.1-rc.2, where the namespace is never mounted. The write face is now resolved through the inject-free `ctx.get('remote.settings')`, which works on both generations ([ADR 0009](docs/adr/0009-dual-generation-settings-api.md)).
30
+ - The Add Server and Import JSON dialogs follow the host dialog theme again: the modal card now surfaces on `bg-layer-2` with the shared `--dsw-elevation-prominent` treatment and the overlay mask blurs like the host Modal primitive, instead of the off-palette `bg-overlay` fill and a hardcoded drop shadow.
31
+
7
32
  ## [v0.2.0] - 2026-09-05
8
33
 
9
34
  Everything from v0.1.2 through this release: the six deferred v1 features (#8–#12, #14), the dual-generation DSH compatibility fix, and the standard-readme documentation pass.
@@ -41,5 +66,6 @@ Everything from v0.1.2 through this release: the six deferred v1 features (#8–
41
66
 
42
67
  Initial releases of the Adapter through v1 issues #1–#7: proxy-first tool surface, Config in the DSH settings namespace, promotion registry, lazy lifecycle, output guard, settings page, and the `/mcp-adapter` RPC channel. See [docs/verification/v1-e2e.md](docs/verification/v1-e2e.md).
43
68
 
69
+ [v0.2.2]: https://github.com/auggie246/dsh-mcp-adapter/compare/v0.2.1...v0.2.2
44
70
  [v0.2.0]: https://github.com/auggie246/dsh-mcp-adapter/compare/v0.1.2...v0.2.0
45
71
  [v0.1.2]: https://github.com/auggie246/dsh-mcp-adapter/releases/tag/v0.1.2
package/README.md CHANGED
@@ -130,10 +130,13 @@ mcp:
130
130
  url: https://mcp.example.com/other
131
131
  auth: oauth
132
132
  scopes: [repo, read:org]
133
+ skillInstall: file
133
134
  ```
134
135
 
135
136
  Each Server configures exactly one Transport: `command` for stdio, or `url` for streamable HTTP with SSE fallback. Adapter extension fields are `auth` (`headers` by default, or `oauth` for HTTP Servers), `scopes` (OAuth scopes, requires `auth: oauth`), `disabled`, `autoAllow`, `lifecycle` (`lazy`, `eager`, `keep-alive`, or `lazy-keep-alive`; default `lazy`), `idleTimeoutMinutes` (default `10`), and `promotedTools`.
136
137
 
138
+ The one top-level Adapter field is `skillInstall` (`file` by default, `runtime`, or `off`); see [Agent skill](#agent-skill).
139
+
137
140
  The schema rejects unknown fields and invalid transport combinations before they reach `$DSH_HOME/settings.yaml`. Each `env` and `headers` value has the DSH `secret` schema role. Wire views retain each key and redact its value.
138
141
 
139
142
  #### Workspace Config layer
@@ -211,7 +214,13 @@ The SDK transports refresh expired access tokens through the stored refresh toke
211
214
 
212
215
  ### Agent skill
213
216
 
214
- The package bundles a model-facing skill in `skills/mcp-adapter/SKILL.md`. When the DSH `skills` service is present, the Adapter registers it at startup as a runtime skill named `mcp-adapter`, so agents learn the search → describe → call workflow, how to read failure messages (including the OAuth sign-in hint), and which commands belong to the human. The frontmatter drives routing; edit the file to change the guidance, and a DSH restart picks the edit up.
217
+ The package bundles a model-facing skill in `skills/mcp-adapter/SKILL.md`, so agents learn the search → describe → call workflow, how to read failure messages (including the OAuth sign-in hint), and which commands belong to the human. The top-level `skillInstall` Config field controls how the Adapter installs it; the mode is read once at Adapter startup, so a change takes effect on the next DSH restart.
218
+
219
+ - `file` (default): on startup the Adapter copies the bundled `SKILL.md` verbatim to `$DSH_HOME/skills/mcp-adapter/SKILL.md` (an atomic temp-file-then-rename write, never a symlink). The DSH filesystem skill provider discovers the file, so the skill shows under **Settings > Skills > DSH skills** and joins the session catalog without a restart of an already-running session. When the file install fails, the Adapter falls back to the runtime registration below and logs one warning.
220
+ - `runtime`: the Adapter registers the skill in memory on the DSH `skills` service only. It reaches agents but never appears on the Settings > Skills page.
221
+ - `off`: the Adapter installs no skill.
222
+
223
+ The installed file is never overwritten. If `$DSH_HOME/skills/mcp-adapter/SKILL.md` already exists, your edit wins: an identical file is left untouched, and a differing file gets one Host warning naming the installed path and the package version. To refresh the installed copy from the package, delete `$DSH_HOME/skills/mcp-adapter/` and restart DSH. Editing the installed file changes what the session catalog serves; the edit survives plugin restarts.
215
224
 
216
225
  ## Architecture
217
226
 
@@ -4,6 +4,8 @@ The Adapter's client bundle once read `ctx.connection.api.settings` directly. DS
4
4
 
5
5
  We resolve the write face at apply time (`src/client/settings-api.js`) and prefer `ctx.remote.settings` when it exists; otherwise we fall back to the 0.1.1-rc.2 `ctx.connection.api.settings` face. Both normalize to the controller's existing contract — `update`/`mutate` take one request object and answer `{ result: { ok, value | error } }` — so `McpSettingsController` and the host stay generation-agnostic. The client declares `remote` in its `inject` (the service exists in both generations; `remote.settings` does not exist on 0.1.1-rc.2 and stays undeclared, so the runner never parks waiting for it), and `package.json` adds `@deepseek-ai/dsh-api-remotes` to `dsh.client.inject` so the typed settings namespace mounts before consumers apply. On the host, `MCP_SETTINGS_NAMESPACE` becomes the plain `'mcp'` string: branding was type-only on 0.1.1-rc.2, and 0.1.2-rc.1 accepts the validated raw string. The peer dependency widens to `^0.1.1-rc.2 || ^0.1.2-rc.1`.
6
6
 
7
+ *Correction:* the original resolution read the rc.1 face as `ctx.remote?.settings`, assuming a plain property. The rc.1 runner instead mounts the namespace as the traced dotted service `remote.settings`: reading through `ctx.remote` composes that key, and cordis throws `cannot get property "remote.settings" without inject` for any plugin that did not declare the dotted service — which crashed this plugin at apply with a failed loader entry. Declaring it is not an option either: a declared-but-never-mounted service keeps the fiber from ever applying, which is exactly the 0.1.1-rc.2 case. The face is therefore resolved through cordis's inject-free `ctx.get('remote.settings')`, which answers the mounted namespace on 0.1.2-rc.1 via the root isolate and undefined on 0.1.1-rc.2, keeping the fallback reachable on both generations; the plain-property read remains as a legacy fallback for runners that expose the face as a plain property.
8
+
7
9
  Considered options: shipping a 0.1.2-only release and telling 0.1.1-rc.2 users to upgrade was rejected because the harness is in flux and plugin releases should not force harness moves; probing `ctx.remote.$host` or version strings was rejected because face presence is the actual capability signal and survives future renames better. A hard failure with upgrade guidance is kept for a hypothetical third shape, so a future break surfaces as one clear message instead of an `undefined` read.
8
10
 
9
11
  Consequences: the `apply`-level test suite (`test/client-apply.test.js`) compiles the real client entry and drives both fake generations, locking the fallback contract. When DSH drops 0.1.1-rc.2 support, the fallback branch, the legacy fake, and the README note are the removal surface — the controller and host need no changes.
package/lib/client.js CHANGED
@@ -647,6 +647,7 @@ function useEscapeClose(onClose) {
647
647
  }, [onClose]);
648
648
  }
649
649
  function AddServerDialog({ controller, busy, onClose }) {
650
+ const [mode, setMode] = (0, import_react.useState)("form");
650
651
  const [name, setName] = (0, import_react.useState)("");
651
652
  const [transport, setTransport] = (0, import_react.useState)("stdio");
652
653
  const [command, setCommand] = (0, import_react.useState)("");
@@ -654,11 +655,28 @@ function AddServerDialog({ controller, busy, onClose }) {
654
655
  const [argsText, setArgsText] = (0, import_react.useState)("[]");
655
656
  const [auth, setAuth] = (0, import_react.useState)("headers");
656
657
  const [scopesText, setScopesText] = (0, import_react.useState)("");
658
+ const [jsonText, setJsonText] = (0, import_react.useState)('{\n "mcpServers": {\n \n }\n}');
657
659
  const [error, setError] = (0, import_react.useState)();
658
660
  useEscapeClose(onClose);
661
+ const switchMode = (next) => {
662
+ setMode(next);
663
+ setError(void 0);
664
+ };
659
665
  const submit = async (event) => {
660
666
  event.preventDefault();
661
667
  setError(void 0);
668
+ if (mode === "json") {
669
+ try {
670
+ parseMcpImport(jsonText);
671
+ } catch (nextError) {
672
+ setError(nextError instanceof Error ? nextError.message : String(nextError));
673
+ return;
674
+ }
675
+ const result = await controller.importJson(jsonText);
676
+ if (result) onClose();
677
+ else setError("The Host rejected this import. Review the page error and retry.");
678
+ return;
679
+ }
662
680
  let config;
663
681
  try {
664
682
  config = normalizeServerConfig(
@@ -677,170 +695,165 @@ function AddServerDialog({ controller, busy, onClose }) {
677
695
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "mcp-card-head", children: [
678
696
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
679
697
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { className: "mcp-modal-title", children: "Add Server" }),
680
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "mcp-muted", children: "Configure one stdio command or one HTTP URL." })
698
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "mcp-muted", children: "Fill in the fields for one Server, or paste an mcpServers JSON." })
681
699
  ] }),
682
700
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "mcp-button", onClick: onClose, children: "Close" })
683
701
  ] }),
684
- error !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "mcp-error", role: "alert", children: error }),
685
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { className: "mcp-field", children: [
686
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "mcp-label", children: "Name" }),
702
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "mcp-dialog-tabs", role: "tablist", "aria-label": "Add Server input mode", children: [
687
703
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
688
- "input",
704
+ "button",
689
705
  {
690
- autoFocus: true,
691
- className: "mcp-input",
692
- value: name,
706
+ type: "button",
707
+ role: "tab",
708
+ "aria-selected": mode === "form",
709
+ className: mode === "form" ? "mcp-dialog-tab mcp-dialog-tab-active" : "mcp-dialog-tab",
693
710
  disabled: busy,
694
- onChange: (event) => setName(event.target.value),
695
- placeholder: "github"
711
+ onClick: () => switchMode("form"),
712
+ children: "Input fields"
713
+ }
714
+ ),
715
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
716
+ "button",
717
+ {
718
+ type: "button",
719
+ role: "tab",
720
+ "aria-selected": mode === "json",
721
+ className: mode === "json" ? "mcp-dialog-tab mcp-dialog-tab-active" : "mcp-dialog-tab",
722
+ disabled: busy,
723
+ onClick: () => switchMode("json"),
724
+ children: "Paste JSON"
696
725
  }
697
726
  )
698
727
  ] }),
699
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { className: "mcp-field", children: [
700
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "mcp-label", children: "Transport" }),
701
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
702
- "select",
728
+ error !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "mcp-error", role: "alert", children: error }),
729
+ mode === "json" && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { className: "mcp-field", children: [
730
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "mcp-label", children: "MCP Config JSON" }),
731
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
732
+ "textarea",
703
733
  {
704
- className: "mcp-select",
705
- value: transport,
734
+ autoFocus: true,
735
+ className: "mcp-textarea",
736
+ style: { minHeight: 280 },
737
+ value: jsonText,
706
738
  disabled: busy,
707
- onChange: (event) => setTransport(event.target.value),
708
- children: [
709
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "stdio", children: "stdio" }),
710
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "http", children: "Streamable HTTP" })
711
- ]
739
+ spellCheck: false,
740
+ onChange: (event) => setJsonText(event.target.value)
712
741
  }
713
- )
742
+ ),
743
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "mcp-muted", children: "One or more Servers. A name that already exists is replaced." })
714
744
  ] }),
715
- transport === "stdio" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
745
+ mode === "form" && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
716
746
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { className: "mcp-field", children: [
717
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "mcp-label", children: "Command" }),
747
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "mcp-label", children: "Name" }),
718
748
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
719
749
  "input",
720
750
  {
751
+ autoFocus: true,
721
752
  className: "mcp-input",
722
- value: command,
753
+ value: name,
723
754
  disabled: busy,
724
- onChange: (event) => setCommand(event.target.value),
725
- placeholder: "npx"
755
+ onChange: (event) => setName(event.target.value),
756
+ placeholder: "github"
726
757
  }
727
758
  )
728
759
  ] }),
729
760
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { className: "mcp-field", children: [
730
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "mcp-label", children: "Args as a JSON array" }),
731
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
732
- "textarea",
761
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "mcp-label", children: "Transport" }),
762
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
763
+ "select",
733
764
  {
734
- className: "mcp-textarea",
735
- value: argsText,
765
+ className: "mcp-select",
766
+ value: transport,
736
767
  disabled: busy,
737
- placeholder: '["-y", "@modelcontextprotocol/server-github"]',
738
- onChange: (event) => setArgsText(event.target.value),
739
- spellCheck: false
740
- }
741
- )
742
- ] })
743
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
744
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { className: "mcp-field", children: [
745
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "mcp-label", children: "URL" }),
746
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
747
- "input",
748
- {
749
- className: "mcp-input",
750
- value: url,
751
- disabled: busy,
752
- onChange: (event) => setUrl(event.target.value),
753
- placeholder: "https://example.com/mcp"
768
+ onChange: (event) => setTransport(event.target.value),
769
+ children: [
770
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "stdio", children: "stdio" }),
771
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "http", children: "Streamable HTTP" })
772
+ ]
754
773
  }
755
774
  )
756
775
  ] }),
757
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "mcp-field-row", children: [
776
+ transport === "stdio" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
758
777
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { className: "mcp-field", children: [
759
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "mcp-label", children: "HTTP authentication" }),
760
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
761
- "select",
778
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "mcp-label", children: "Command" }),
779
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
780
+ "input",
762
781
  {
763
- className: "mcp-select",
764
- value: auth,
782
+ className: "mcp-input",
783
+ value: command,
765
784
  disabled: busy,
766
- title: "Static headers send fixed values on every request. OAuth 2.0 with PKCE signs in through the provider in your browser.",
767
- onChange: (event) => setAuth(event.target.value),
768
- children: [
769
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "headers", children: "Static headers" }),
770
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "oauth", children: "OAuth 2.0 with PKCE" })
771
- ]
785
+ onChange: (event) => setCommand(event.target.value),
786
+ placeholder: "npx"
772
787
  }
773
788
  )
774
789
  ] }),
775
- auth === "oauth" && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { className: "mcp-field", children: [
776
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "mcp-label", children: "OAuth scopes (optional, comma-separated)" }),
790
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { className: "mcp-field", children: [
791
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "mcp-label", children: "Args as a JSON array" }),
792
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
793
+ "textarea",
794
+ {
795
+ className: "mcp-textarea",
796
+ value: argsText,
797
+ disabled: busy,
798
+ placeholder: '["-y", "@modelcontextprotocol/server-github"]',
799
+ onChange: (event) => setArgsText(event.target.value),
800
+ spellCheck: false
801
+ }
802
+ )
803
+ ] })
804
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
805
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { className: "mcp-field", children: [
806
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "mcp-label", children: "URL" }),
777
807
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
778
808
  "input",
779
809
  {
780
810
  className: "mcp-input",
781
- value: scopesText,
811
+ value: url,
782
812
  disabled: busy,
783
- placeholder: "read write",
784
- title: "Optional. Leave empty when the provider defines no scopes \u2014 for example Context7. Request only scopes the provider grants.",
785
- onChange: (event) => setScopesText(event.target.value)
813
+ onChange: (event) => setUrl(event.target.value),
814
+ placeholder: "https://example.com/mcp"
786
815
  }
787
- ),
788
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "mcp-muted", children: "Leave empty unless the provider requires scopes." })
789
- ] })
790
- ] }),
791
- auth === "oauth" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "mcp-muted", children: "OAuth signs in through your browser. The Sign in button appears on the Server panel after you save." })
792
- ] }),
793
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "mcp-actions", children: [
794
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "submit", className: "mcp-button mcp-button-primary", disabled: busy, children: busy ? "Adding\u2026" : "Add Server" }),
795
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "mcp-button", disabled: busy, onClick: onClose, children: "Cancel" })
796
- ] })
797
- ] }) });
798
- }
799
- function ImportDialog({ controller, busy, onClose }) {
800
- const [text, setText] = (0, import_react.useState)('{\n "mcpServers": {\n \n }\n}');
801
- const [error, setError] = (0, import_react.useState)();
802
- useEscapeClose(onClose);
803
- const submit = async (event) => {
804
- event.preventDefault();
805
- setError(void 0);
806
- try {
807
- parseMcpImport(text);
808
- } catch (nextError) {
809
- setError(nextError instanceof Error ? nextError.message : String(nextError));
810
- return;
811
- }
812
- const result = await controller.importJson(text);
813
- if (result) onClose();
814
- else setError("The Host rejected this import. Review the page error and retry.");
815
- };
816
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "mcp-overlay", role: "presentation", onMouseDown: (event) => {
817
- if (event.target === event.currentTarget) onClose();
818
- }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("form", { className: "mcp-modal-card", "aria-label": "Import MCP Servers", onSubmit: submit, children: [
819
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "mcp-card-head", children: [
820
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
821
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { className: "mcp-modal-title", children: "Import JSON" }),
822
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "mcp-muted", children: "Paste a standard object with an mcpServers property." })
823
- ] }),
824
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "mcp-button", onClick: onClose, children: "Close" })
825
- ] }),
826
- error !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "mcp-error", role: "alert", children: error }),
827
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { className: "mcp-field", children: [
828
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "mcp-label", children: "MCP Config JSON" }),
829
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
830
- "textarea",
831
- {
832
- autoFocus: true,
833
- className: "mcp-textarea",
834
- style: { minHeight: 280 },
835
- value: text,
836
- disabled: busy,
837
- spellCheck: false,
838
- onChange: (event) => setText(event.target.value)
839
- }
840
- )
816
+ )
817
+ ] }),
818
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "mcp-field-row", children: [
819
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { className: "mcp-field", children: [
820
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "mcp-label", children: "HTTP authentication" }),
821
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
822
+ "select",
823
+ {
824
+ className: "mcp-select",
825
+ value: auth,
826
+ disabled: busy,
827
+ title: "Static headers send fixed values on every request. OAuth 2.0 with PKCE signs in through the provider in your browser.",
828
+ onChange: (event) => setAuth(event.target.value),
829
+ children: [
830
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "headers", children: "Static headers" }),
831
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "oauth", children: "OAuth 2.0 with PKCE" })
832
+ ]
833
+ }
834
+ )
835
+ ] }),
836
+ auth === "oauth" && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { className: "mcp-field", children: [
837
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "mcp-label", children: "OAuth scopes (optional, comma-separated)" }),
838
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
839
+ "input",
840
+ {
841
+ className: "mcp-input",
842
+ value: scopesText,
843
+ disabled: busy,
844
+ placeholder: "read write",
845
+ title: "Optional. Leave empty when the provider defines no scopes \u2014 for example Context7. Request only scopes the provider grants.",
846
+ onChange: (event) => setScopesText(event.target.value)
847
+ }
848
+ ),
849
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "mcp-muted", children: "Leave empty unless the provider requires scopes." })
850
+ ] })
851
+ ] }),
852
+ auth === "oauth" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "mcp-muted", children: "OAuth signs in through your browser. The Sign in button appears on the Server panel after you save." })
853
+ ] })
841
854
  ] }),
842
855
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "mcp-actions", children: [
843
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "submit", className: "mcp-button mcp-button-primary", disabled: busy, children: busy ? "Importing\u2026" : "Import Servers" }),
856
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "submit", className: "mcp-button mcp-button-primary", disabled: busy, children: busy ? mode === "json" ? "Importing\u2026" : "Adding\u2026" : mode === "json" ? "Import Servers" : "Add Server" }),
844
857
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "mcp-button", disabled: busy, onClick: onClose, children: "Cancel" })
845
858
  ] })
846
859
  ] }) });
@@ -1303,56 +1316,32 @@ function McpSettingsPage({ controller }) {
1303
1316
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h1", { className: "mcp-title", children: "MCP" }),
1304
1317
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "mcp-intro", children: "Connect MCP Servers and choose how their tools reach the Agent." })
1305
1318
  ] }),
1306
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "mcp-actions", children: [
1307
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1308
- "button",
1309
- {
1310
- type: "button",
1311
- className: "mcp-button",
1312
- disabled: snapshot.busy || !settings.writable,
1313
- onClick: () => setDialog("import"),
1314
- children: "Import JSON"
1315
- }
1316
- ),
1317
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1318
- "button",
1319
- {
1320
- type: "button",
1321
- className: "mcp-button mcp-button-primary",
1322
- disabled: snapshot.busy || !settings.writable,
1323
- onClick: () => setDialog("add"),
1324
- children: "Add Server"
1325
- }
1326
- )
1327
- ] })
1319
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "mcp-actions", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1320
+ "button",
1321
+ {
1322
+ type: "button",
1323
+ className: "mcp-button mcp-button-primary",
1324
+ disabled: snapshot.busy || !settings.writable,
1325
+ onClick: () => setDialog("add"),
1326
+ children: "Add Server"
1327
+ }
1328
+ ) })
1328
1329
  ] }),
1329
1330
  !settings.writable && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "mcp-warning", children: "The active Settings Provider is read-only." }),
1330
1331
  snapshot.error !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "mcp-error", role: "alert", children: snapshot.error }),
1331
1332
  names.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "mcp-empty", children: [
1332
1333
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { className: "mcp-card-title", children: "No MCP Servers" }),
1333
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "mcp-muted", children: "Add a Server or import an existing mcpServers Config." }),
1334
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "mcp-actions", children: [
1335
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1336
- "button",
1337
- {
1338
- type: "button",
1339
- className: "mcp-button mcp-button-primary",
1340
- disabled: snapshot.busy || !settings.writable,
1341
- onClick: () => setDialog("add"),
1342
- children: "Add Server"
1343
- }
1344
- ),
1345
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1346
- "button",
1347
- {
1348
- type: "button",
1349
- className: "mcp-button",
1350
- disabled: snapshot.busy || !settings.writable,
1351
- onClick: () => setDialog("import"),
1352
- children: "Import JSON"
1353
- }
1354
- )
1355
- ] })
1334
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "mcp-muted", children: "Add a Server through its input fields, or paste an existing mcpServers JSON." }),
1335
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "mcp-actions", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1336
+ "button",
1337
+ {
1338
+ type: "button",
1339
+ className: "mcp-button mcp-button-primary",
1340
+ disabled: snapshot.busy || !settings.writable,
1341
+ onClick: () => setDialog("add"),
1342
+ children: "Add Server"
1343
+ }
1344
+ ) })
1356
1345
  ] }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "mcp-layout", children: [
1357
1346
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("nav", { className: "mcp-sidebar", "aria-label": "MCP Servers", children: names.map((name) => {
1358
1347
  const status = statusFor(snapshot.overview, name);
@@ -1395,14 +1384,13 @@ function McpSettingsPage({ controller }) {
1395
1384
  selected
1396
1385
  )
1397
1386
  ] }),
1398
- dialog === "add" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AddServerDialog, { controller, busy: snapshot.busy, onClose: () => setDialog() }),
1399
- dialog === "import" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ImportDialog, { controller, busy: snapshot.busy, onClose: () => setDialog() })
1387
+ dialog === "add" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AddServerDialog, { controller, busy: snapshot.busy, onClose: () => setDialog() })
1400
1388
  ] });
1401
1389
  }
1402
1390
 
1403
1391
  // src/client/settings-api.js
1404
1392
  function createSettingsApi(ctx) {
1405
- const remoteSettings = ctx.remote?.settings;
1393
+ const remoteSettings = readRemoteSettings(ctx);
1406
1394
  if (remoteSettings !== void 0) return remoteSettingsApi(remoteSettings);
1407
1395
  const connectionSettings = ctx.connection?.api?.settings;
1408
1396
  if (connectionSettings !== void 0) return connectionSettings;
@@ -1410,6 +1398,20 @@ function createSettingsApi(ctx) {
1410
1398
  "No DSH settings write API: expected ctx.remote.settings (DSH 0.1.2+) or ctx.connection.api.settings (DSH 0.1.1-rc.2). Upgrade @auggieteo/dsh-mcp-adapter to a release supporting this harness."
1411
1399
  );
1412
1400
  }
1401
+ function readRemoteSettings(ctx) {
1402
+ if (typeof ctx.get === "function") {
1403
+ try {
1404
+ const mounted = ctx.get("remote.settings");
1405
+ if (mounted !== void 0) return mounted;
1406
+ } catch {
1407
+ }
1408
+ }
1409
+ try {
1410
+ return ctx.remote?.settings;
1411
+ } catch {
1412
+ }
1413
+ return void 0;
1414
+ }
1413
1415
  function remoteSettingsApi(remote) {
1414
1416
  return {
1415
1417
  async update({ ns, patch, expectedRevision }) {
@@ -1493,11 +1495,17 @@ var MCP_SETTINGS_CSS = `
1493
1495
  .mcp-promotion-name{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;color:var(--dsw-alias-label-primary);overflow-wrap:anywhere}
1494
1496
  .mcp-promotion-description{display:block;color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:17px}
1495
1497
  .mcp-empty{padding:30px 20px;text-align:center;display:flex;align-items:center;flex-direction:column;gap:10px}
1496
- .mcp-overlay{position:fixed;inset:0;background:var(--dsw-alias-bg-mask-1);display:flex;align-items:center;justify-content:center;padding:20px;z-index:1200;animation:mcp-fade .12s ease}
1497
- .mcp-modal-card{width:min(560px,100%);max-height:min(760px,90vh);overflow:auto;padding:18px;display:flex;flex-direction:column;gap:13px;border:1px solid var(--dsw-alias-border-l1);border-radius:14px;background:var(--dsw-alias-bg-overlay);box-shadow:0 18px 60px rgba(0,0,0,.32);animation:mcp-pop .16s ease}
1498
+ .mcp-overlay{position:fixed;inset:0;background:var(--dsw-alias-bg-mask-1);backdrop-filter:var(--dsw-mask-blur);display:flex;align-items:center;justify-content:center;padding:20px;z-index:1200;animation:mcp-fade .12s ease}
1499
+ .mcp-modal-card{width:min(560px,100%);max-height:min(760px,90vh);overflow:auto;padding:18px;display:flex;flex-direction:column;gap:13px;border:0;border-radius:14px;background:var(--dsw-alias-bg-layer-2);box-shadow:var(--dsw-elevation-prominent);animation:mcp-pop .16s ease}
1498
1500
  @keyframes mcp-fade{from{opacity:0}to{opacity:1}}
1499
1501
  @keyframes mcp-pop{from{opacity:0;transform:translateY(6px) scale(.98)}to{opacity:1;transform:none}}
1500
1502
  .mcp-modal-title{margin:0;font-size:15px;line-height:22px;font-weight:600}
1503
+ .mcp-dialog-tabs{display:inline-flex;gap:2px;align-self:flex-start;border:1px solid var(--dsw-alias-border-l2);border-radius:16px;padding:2px;background:var(--dsw-alias-bg-layer-3)}
1504
+ .mcp-dialog-tab{height:26px;border:0;border-radius:14px;background:transparent;color:var(--dsw-alias-label-secondary);padding:0 12px;font:inherit;font-size:12px;cursor:pointer;transition:background .12s ease,color .12s ease}
1505
+ .mcp-dialog-tab:hover:not(:disabled):not(.mcp-dialog-tab-active){color:var(--dsw-alias-label-primary)}
1506
+ .mcp-dialog-tab:disabled{cursor:not-allowed;opacity:.5}
1507
+ .mcp-dialog-tab-active{background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground)}
1508
+ .mcp-dialog-tab:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:1px}
1501
1509
  .mcp-confirm{border-radius:8px;padding:10px;display:flex;flex-direction:column;gap:8px;background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 10%, transparent)}
1502
1510
  @container (max-width:660px){.mcp-layout{grid-template-columns:minmax(0,1fr)}.mcp-sidebar{position:static;flex-direction:row;overflow-x:auto;padding-bottom:2px}.mcp-server-button{width:auto;flex:none;white-space:nowrap}}
1503
1511
  @container (max-width:480px){.mcp-field-row{grid-template-columns:minmax(0,1fr)}.mcp-secret-row{grid-template-columns:minmax(0,1fr) auto}.mcp-secret-row .mcp-input:first-child{grid-column:1 / -1}.mcp-card-head .mcp-actions{margin-left:0;width:100%}.mcp-toolbar{align-items:flex-start}}