dsh-agent-plugins-market 0.8.1 → 0.8.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.
Files changed (157) hide show
  1. package/README.md +2 -2
  2. package/README.zh.md +2 -2
  3. package/client/client.js +1591 -1726
  4. package/client/client.js.map +1 -1
  5. package/lib/application/agent-roles.js +53 -0
  6. package/lib/application/catalog-context.js +2 -2
  7. package/lib/application/catalog.js +4 -8
  8. package/lib/{runtime → application}/deadline.js +1 -1
  9. package/lib/application/details.js +3 -3
  10. package/lib/application/json-file.js +43 -0
  11. package/lib/{runtime → application/lsp}/lsp-direct-config.js +9 -7
  12. package/lib/application/lsp/lsp-server-state.js +17 -0
  13. package/lib/{runtime → application/lsp}/lsp-status.js +22 -7
  14. package/lib/{runtime → application/lsp}/profile-seam.js +1 -1
  15. package/lib/application/lsp-service.js +5 -5
  16. package/lib/{runtime → application/mcp}/mcp-backend.js +3 -19
  17. package/lib/{runtime → application/mcp}/mcp-config.js +4 -4
  18. package/lib/{runtime → application/mcp}/mcp-direct-config.js +8 -49
  19. package/lib/{runtime → application/mcp}/mcp-overrides.js +3 -5
  20. package/lib/{runtime → application/mcp}/mcp-status.js +13 -7
  21. package/lib/application/mcp-service.js +11 -35
  22. package/lib/application/panel-resources.js +48 -11
  23. package/lib/{runtime → application/panels}/user-hooks.js +2 -2
  24. package/lib/{runtime → application/panels}/user-store.js +2 -2
  25. package/lib/application/ports.js +1 -0
  26. package/lib/application/project-agent-roles.js +1 -1
  27. package/lib/{runtime → application}/server-config.js +7 -17
  28. package/lib/application/snapshot-cache.js +1 -1
  29. package/lib/application/source-store.js +3 -4
  30. package/lib/{runtime → application/state}/state-store.js +5 -7
  31. package/lib/{runtime → application/state}/storage-migration.js +4 -4
  32. package/lib/catalog/source-catalog.js +0 -4
  33. package/lib/contracts/lsp.js +1 -0
  34. package/lib/contracts/market.js +0 -1
  35. package/lib/contracts/mcp.js +1 -0
  36. package/lib/index.js +42 -33
  37. package/lib/routes.js +2 -9
  38. package/lib/runtime/{agent-role-router.js → agents/agent-role-router.js} +7 -53
  39. package/lib/runtime/{subagent-catalog.js → agents/subagent-catalog.js} +24 -16
  40. package/lib/runtime/core/context.js +31 -0
  41. package/lib/runtime/{reconciler.js → core/reconciler.js} +5 -5
  42. package/lib/runtime/host/failure-detail.js +58 -0
  43. package/lib/runtime/{feedback-tool.js → host/feedback-tool.js} +1 -1
  44. package/lib/runtime/host/host-locale.js +83 -0
  45. package/lib/runtime/{settings-namespace.js → host/settings-namespace.js} +1 -1
  46. package/lib/runtime/host/shell-path.js +196 -0
  47. package/lib/runtime/{tool-registry-observer.js → host/tool-registry-observer.js} +2 -1
  48. package/lib/runtime/{lsp-mounts.js → lsp/lsp-mounts.js} +69 -20
  49. package/lib/runtime/{mcp-client → mcp/bridge}/bridge.js +52 -22
  50. package/lib/runtime/{mcp-client → mcp/bridge}/connection.js +2 -2
  51. package/lib/runtime/{mcp-client → mcp/bridge}/oauth.js +1 -1
  52. package/lib/runtime/{mcp-client → mcp/bridge}/tools.js +2 -2
  53. package/lib/runtime/{mcp-auth-record.js → mcp/mcp-auth-record.js} +1 -1
  54. package/lib/runtime/{mcp-mounts.js → mcp/mcp-mounts.js} +14 -7
  55. package/lib/runtime/{user-commands.js → panels/user-commands.js} +3 -3
  56. package/lib/runtime/{user-panels.js → panels/user-panels.js} +2 -2
  57. package/lib/runtime/{commands-mounts.js → surfaces/commands-mounts.js} +7 -7
  58. package/lib/runtime/{hooks-mounts.js → surfaces/hooks-mounts.js} +2 -2
  59. package/lib/runtime/{project-runtime.js → surfaces/project-runtime.js} +5 -5
  60. package/lib/runtime/{skills-provider.js → surfaces/skills-provider.js} +4 -4
  61. package/lib/types/application/agent-roles.d.ts +30 -0
  62. package/lib/types/application/catalog.d.ts +4 -7
  63. package/lib/types/application/details.d.ts +1 -1
  64. package/lib/types/application/json-file.d.ts +12 -0
  65. package/lib/types/{runtime → application/lsp}/lsp-direct-config.d.ts +1 -1
  66. package/lib/types/{runtime → application/lsp}/lsp-status.d.ts +3 -3
  67. package/lib/types/{runtime → application/mcp}/mcp-backend.d.ts +4 -9
  68. package/lib/types/{runtime/mcp-client/config.d.ts → application/mcp/mcp-bridge-config.d.ts} +11 -1
  69. package/lib/types/{runtime → application/mcp}/mcp-config.d.ts +6 -7
  70. package/lib/types/application/mcp/mcp-direct-config.d.ts +17 -0
  71. package/lib/types/{runtime → application/mcp}/mcp-overrides.d.ts +1 -1
  72. package/lib/types/{runtime → application/mcp}/mcp-status.d.ts +7 -7
  73. package/lib/types/application/mcp-service.d.ts +4 -11
  74. package/lib/types/application/panel-resources.d.ts +9 -2
  75. package/lib/types/{runtime → application/panels}/user-hooks.d.ts +1 -1
  76. package/lib/types/{runtime → application/panels}/user-store.d.ts +1 -1
  77. package/lib/types/application/ports.d.ts +17 -3
  78. package/lib/types/application/project-agent-roles.d.ts +1 -1
  79. package/lib/types/application/queries.d.ts +3 -6
  80. package/lib/types/{runtime → application}/regions.d.ts +1 -1
  81. package/lib/types/{runtime → application/state}/state-store.d.ts +2 -2
  82. package/lib/types/catalog/source-catalog.d.ts +0 -2
  83. package/lib/types/contracts/lsp-status.d.ts +6 -0
  84. package/lib/types/contracts/lsp.d.ts +16 -0
  85. package/lib/types/contracts/market.d.ts +1 -4
  86. package/lib/types/contracts/mcp-status.d.ts +21 -2
  87. package/lib/types/contracts/mcp.d.ts +20 -0
  88. package/lib/types/model/types.d.ts +0 -14
  89. package/lib/types/runtime/{agent-role-router.d.ts → agents/agent-role-router.d.ts} +3 -28
  90. package/lib/types/runtime/{subagent-catalog.d.ts → agents/subagent-catalog.d.ts} +6 -6
  91. package/lib/types/runtime/core/context.d.ts +14 -0
  92. package/lib/types/runtime/{reconcile-scheduler.d.ts → core/reconcile-scheduler.d.ts} +2 -2
  93. package/lib/types/runtime/{reconciler.d.ts → core/reconciler.d.ts} +8 -8
  94. package/lib/types/runtime/host/failure-detail.d.ts +6 -0
  95. package/lib/types/runtime/host/host-locale.d.ts +66 -0
  96. package/lib/types/runtime/{model-catalog.d.ts → host/model-catalog.d.ts} +1 -1
  97. package/lib/types/runtime/{settings-namespace.d.ts → host/settings-namespace.d.ts} +2 -2
  98. package/lib/types/runtime/host/shell-path.d.ts +70 -0
  99. package/lib/types/runtime/{lsp-mounts.d.ts → lsp/lsp-mounts.d.ts} +17 -11
  100. package/lib/types/runtime/{mcp-client → mcp/bridge}/bridge.d.ts +1 -1
  101. package/lib/types/runtime/{mcp-client → mcp/bridge}/connection.d.ts +3 -11
  102. package/lib/types/runtime/{mcp-client → mcp/bridge}/host-contract.d.ts +1 -1
  103. package/lib/types/runtime/{mcp-client → mcp/bridge}/oauth.d.ts +1 -1
  104. package/lib/types/runtime/{mcp-client → mcp/bridge}/transport.d.ts +1 -1
  105. package/lib/types/runtime/{mcp-auth-record.d.ts → mcp/mcp-auth-record.d.ts} +1 -1
  106. package/lib/types/runtime/{mcp-credentials.d.ts → mcp/mcp-credentials.d.ts} +1 -1
  107. package/lib/types/runtime/{mcp-mounts.d.ts → mcp/mcp-mounts.d.ts} +5 -11
  108. package/lib/types/runtime/{user-commands.d.ts → panels/user-commands.d.ts} +1 -1
  109. package/lib/types/runtime/{user-panels.d.ts → panels/user-panels.d.ts} +1 -1
  110. package/lib/types/runtime/{commands-mounts.d.ts → surfaces/commands-mounts.d.ts} +2 -2
  111. package/lib/types/runtime/{hooks-mounts.d.ts → surfaces/hooks-mounts.d.ts} +1 -1
  112. package/lib/types/runtime/{project-runtime.d.ts → surfaces/project-runtime.d.ts} +3 -3
  113. package/lib/types/runtime/{skills-provider.d.ts → surfaces/skills-provider.d.ts} +1 -1
  114. package/package.json +35 -32
  115. package/lib/runtime/host-locale.js +0 -89
  116. package/lib/runtime/legacy-root-migration.js +0 -12
  117. package/lib/runtime/lsp-server-state.js +0 -19
  118. package/lib/runtime/mcp-client/json-schema-subset.js +0 -181
  119. package/lib/types/runtime/host-locale.d.ts +0 -31
  120. package/lib/types/runtime/legacy-root-migration.d.ts +0 -2
  121. package/lib/types/runtime/mcp-client/json-schema-subset.d.ts +0 -58
  122. package/lib/types/runtime/mcp-direct-config.d.ts +0 -39
  123. /package/lib/{runtime/mcp-client/config.js → application/mcp/mcp-bridge-config.js} +0 -0
  124. /package/lib/{runtime → application/mcp}/mcp-redaction.js +0 -0
  125. /package/lib/{runtime → application}/regions.js +0 -0
  126. /package/lib/runtime/{agent-role-names.js → agents/agent-role-names.js} +0 -0
  127. /package/lib/runtime/{mount-lifecycle.js → core/mount-lifecycle.js} +0 -0
  128. /package/lib/runtime/{reconcile-scheduler.js → core/reconcile-scheduler.js} +0 -0
  129. /package/lib/runtime/{source-auto-update.js → core/source-auto-update.js} +0 -0
  130. /package/lib/runtime/{timer-seat.js → core/timer-seat.js} +0 -0
  131. /package/lib/runtime/{model-catalog.js → host/model-catalog.js} +0 -0
  132. /package/lib/runtime/{plugin-message-source.js → host/plugin-message-source.js} +0 -0
  133. /package/lib/runtime/{mcp-client → mcp/bridge}/host-contract.js +0 -0
  134. /package/lib/runtime/{mcp-client → mcp/bridge}/host-seams.js +0 -0
  135. /package/lib/runtime/{mcp-client → mcp/bridge}/plugin-identity.js +0 -0
  136. /package/lib/runtime/{mcp-client → mcp/bridge}/projection.js +0 -0
  137. /package/lib/runtime/{mcp-client → mcp/bridge}/transport.js +0 -0
  138. /package/lib/runtime/{mcp-credentials.js → mcp/mcp-credentials.js} +0 -0
  139. /package/lib/runtime/{dynamic-context.js → surfaces/dynamic-context.js} +0 -0
  140. /package/lib/types/{runtime → application}/deadline.d.ts +0 -0
  141. /package/lib/types/{runtime → application/lsp}/lsp-server-state.d.ts +0 -0
  142. /package/lib/types/{runtime → application/lsp}/profile-seam.d.ts +0 -0
  143. /package/lib/types/{runtime → application/mcp}/mcp-redaction.d.ts +0 -0
  144. /package/lib/types/{runtime → application}/server-config.d.ts +0 -0
  145. /package/lib/types/{runtime → application/state}/storage-migration.d.ts +0 -0
  146. /package/lib/types/runtime/{agent-role-names.d.ts → agents/agent-role-names.d.ts} +0 -0
  147. /package/lib/types/runtime/{mount-lifecycle.d.ts → core/mount-lifecycle.d.ts} +0 -0
  148. /package/lib/types/runtime/{source-auto-update.d.ts → core/source-auto-update.d.ts} +0 -0
  149. /package/lib/types/runtime/{timer-seat.d.ts → core/timer-seat.d.ts} +0 -0
  150. /package/lib/types/runtime/{feedback-tool.d.ts → host/feedback-tool.d.ts} +0 -0
  151. /package/lib/types/runtime/{plugin-message-source.d.ts → host/plugin-message-source.d.ts} +0 -0
  152. /package/lib/types/runtime/{tool-registry-observer.d.ts → host/tool-registry-observer.d.ts} +0 -0
  153. /package/lib/types/runtime/{mcp-client → mcp/bridge}/host-seams.d.ts +0 -0
  154. /package/lib/types/runtime/{mcp-client → mcp/bridge}/plugin-identity.d.ts +0 -0
  155. /package/lib/types/runtime/{mcp-client → mcp/bridge}/projection.d.ts +0 -0
  156. /package/lib/types/runtime/{mcp-client → mcp/bridge}/tools.d.ts +0 -0
  157. /package/lib/types/runtime/{dynamic-context.d.ts → surfaces/dynamic-context.d.ts} +0 -0
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Agent-role card parsing: the frontmatter grammar and the policy it yields.
3
+ * Pure text processing shared by the project-role projection and the runtime
4
+ * delegation router; the executing half stays in runtime.
5
+ * @module application/agent-roles
6
+ */
7
+ import { parse as parseYaml } from 'yaml';
8
+ function optionalText(value, key) {
9
+ if (value === undefined)
10
+ return undefined;
11
+ if (typeof value !== 'string' || value.trim() === '')
12
+ throw new Error(`agent metadata ${key} must be a non-empty string`);
13
+ return value.trim();
14
+ }
15
+ /** Strict YAML parsing prevents malformed routing metadata from silently inheriting wider privileges. */
16
+ export function parseAgentRole(text) {
17
+ const match = /^(?:\uFEFF)?---\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)/.exec(text);
18
+ if (match === null) {
19
+ if (/^(?:\uFEFF)?---(?:\r?\n|$)/.test(text))
20
+ throw new Error('agent frontmatter is not closed');
21
+ return { content: text, disabled: false };
22
+ }
23
+ // A matched block always carries its body; the check keeps the type honest.
24
+ const frontmatter = match[1];
25
+ if (frontmatter === undefined)
26
+ throw new Error('agent frontmatter is not closed');
27
+ const parsed = parseYaml(frontmatter);
28
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed))
29
+ throw new Error('agent frontmatter must be a YAML object');
30
+ const metadata = parsed;
31
+ if (metadata.disabled !== undefined && typeof metadata.disabled !== 'boolean')
32
+ throw new Error('agent metadata disabled must be a boolean');
33
+ const model = optionalText(metadata.model, 'model');
34
+ const provider = optionalText(metadata.provider, 'provider');
35
+ const effort = optionalText(metadata.reasoning_effort, 'reasoning_effort');
36
+ const camelEffort = optionalText(metadata.reasoningEffort, 'reasoningEffort');
37
+ if (effort !== undefined && camelEffort !== undefined && effort !== camelEffort)
38
+ throw new Error('agent metadata reasoning_effort and reasoningEffort conflict');
39
+ const reasoningEffort = effort ?? camelEffort;
40
+ const title = optionalText(metadata.name, 'name');
41
+ const description = optionalText(metadata.description, 'description');
42
+ if (model === 'inherit' && provider !== undefined)
43
+ throw new Error('agent model inherit cannot specify a provider');
44
+ return {
45
+ content: text.slice(match[0].length),
46
+ disabled: metadata.disabled === true,
47
+ ...(model === undefined || model === 'inherit' ? {} : { model }),
48
+ ...(provider === undefined ? {} : { provider }),
49
+ ...(reasoningEffort === undefined ? {} : { reasoningEffort }),
50
+ ...(title === undefined ? {} : { title }),
51
+ ...(description === undefined ? {} : { description })
52
+ };
53
+ }
@@ -10,8 +10,8 @@
10
10
  import { join } from 'node:path';
11
11
  import { MARKET_SETTINGS_DEFAULTS } from '../contracts/settings.js';
12
12
  import { STATE_FILE_NAME } from '../catalog/paths.js';
13
- import { settlesWithin } from '../runtime/deadline.js';
14
- import { EMPTY_STATE, loadState, saveState } from '../runtime/state-store.js';
13
+ import { settlesWithin } from './deadline.js';
14
+ import { EMPTY_STATE, loadState, saveState } from './state/state-store.js';
15
15
  import { effectiveSurfaces } from '../model/types.js';
16
16
  import { SCAN_CACHE_TTL_MS, SnapshotCache } from './snapshot-cache.js';
17
17
  /** Key of one install entry in the persisted `installed` record. */
@@ -10,10 +10,10 @@
10
10
  * know which collaborator owns a given invariant.
11
11
  */
12
12
  import { qualifiedSuiteId } from '../catalog/paths.js';
13
- import { loadUserMcpSuite } from '../runtime/mcp-direct-config.js';
14
- import { loadSuiteOverrides } from '../runtime/mcp-overrides.js';
15
- import { loadUserHooksSuite } from '../runtime/user-hooks.js';
16
- import { applyLspOverrides } from '../runtime/server-config.js';
13
+ import { loadUserMcpSuite } from './mcp/mcp-direct-config.js';
14
+ import { loadSuiteOverrides } from './mcp/mcp-overrides.js';
15
+ import { loadUserHooksSuite } from './panels/user-hooks.js';
16
+ import { applyLspOverrides } from './server-config.js';
17
17
  import { buildSuiteDetail, readSkillContent } from './details.js';
18
18
  import { CatalogContext } from './catalog-context.js';
19
19
  import { InstallStore } from './install-store.js';
@@ -233,10 +233,6 @@ export class Catalog {
233
233
  async addMcpServer(name, server) {
234
234
  await this.mcp.addServer(name, server);
235
235
  }
236
- /** Import several pasted services in one write; each entry reports its own outcome. */
237
- async importMcpServers(servers, overwrite) {
238
- return this.mcp.importServers(servers, overwrite);
239
- }
240
236
  /** Read effective service configuration without exposing credential literals. */
241
237
  async serverConfig(kind, id) {
242
238
  return this.mcp.serverConfig(kind, id);
@@ -11,7 +11,7 @@
11
11
  * only notifies. This module adds the one thing that primitive does not answer
12
12
  * and these callers need: whether the work settled at all.
13
13
  *
14
- * @module runtime/deadline
14
+ * @module application/deadline
15
15
  */
16
16
  import { deadline } from '@deepseek-ai/dsh-timeout';
17
17
  /** Identifies this plugin's bounded waits inside a timeout reason. */
@@ -3,9 +3,9 @@ import { readFile, stat } from 'node:fs/promises';
3
3
  import { parse as parseYaml } from 'yaml';
4
4
  import { discoverLspEntries } from '../catalog/surfaces.js';
5
5
  import { defaultMarkdownResources, resourceText } from '../catalog/component-files.js';
6
- import { credentialRefsInServer } from '../runtime/mcp-config.js';
7
- import { redactMcpConfig, redactMcpOverrides } from '../runtime/mcp-redaction.js';
8
- import { applyOverride } from '../runtime/mcp-overrides.js';
6
+ import { credentialRefsInServer } from './mcp/mcp-config.js';
7
+ import { redactMcpConfig, redactMcpOverrides } from './mcp/mcp-redaction.js';
8
+ import { applyOverride } from './mcp/mcp-overrides.js';
9
9
  import { effectiveSurfaces } from '../model/types.js';
10
10
  /** The checkout's last modification, or null when the path cannot be read. */
11
11
  async function rootModifiedAt(root) {
@@ -0,0 +1,43 @@
1
+ /**
2
+ * The one JSON persistence loop every plugin-owned document runs: read with
3
+ * ENOENT tolerance, validate through a caller hook, and write back through the
4
+ * harness atomic-write helper with the private-file modes. Files here are
5
+ * plugin state under `$DSH_HOME`, written by the host process only; a caller
6
+ * needing shared or user-authored content keeps its own rules.
7
+ *
8
+ * The read resolves `undefined` when the file does not exist or parses to
9
+ * nothing usable, and rethrows anything else — a permission problem must
10
+ * surface, not read as an empty document. What counts as usable is the
11
+ * caller's `parse`: it receives the parsed value and returns the typed
12
+ * document, so each store keeps its own tolerance policy (fail-closed to a
13
+ * default, or reject) instead of sharing one.
14
+ * @module application/json-file
15
+ */
16
+ import { readFile } from 'node:fs/promises';
17
+ import { writeFileAtomic } from '@deepseek-ai/dsh-atomic-write';
18
+ /**
19
+ * Read one JSON document. Resolves `undefined` when the file is absent or its
20
+ * text is empty; a `parse` that wants an absent file to read as a default
21
+ * handles `undefined` itself.
22
+ */
23
+ export async function readJsonFile(path) {
24
+ try {
25
+ const text = await readFile(path, 'utf8');
26
+ if (text.trim() === '')
27
+ return undefined;
28
+ return JSON.parse(text);
29
+ }
30
+ catch (error) {
31
+ if (error.code === 'ENOENT')
32
+ return undefined;
33
+ throw error;
34
+ }
35
+ }
36
+ /**
37
+ * Write one JSON document atomically: two-space indent, trailing newline, and
38
+ * the private-file modes the plugin's state files carry (0o600 file, 0o700
39
+ * directory).
40
+ */
41
+ export async function writeJsonDocument(path, document) {
42
+ await writeFileAtomic(path, `${JSON.stringify(document, null, 2)}\n`, { mode: 0o600, dirMode: 0o700 });
43
+ }
@@ -8,10 +8,9 @@
8
8
  * `lsp-spec` rules as suite declarations; a broken file degrades to no direct
9
9
  * servers plus a diagnostic, never a thrown discovery.
10
10
  */
11
- import { readFile } from 'node:fs/promises';
12
11
  import { join } from 'node:path';
13
- import { writeFileAtomic } from '@deepseek-ai/dsh-atomic-write';
14
- import { parseLspServers } from '../catalog/lsp-spec.js';
12
+ import { readJsonFile, writeJsonDocument } from '../json-file.js';
13
+ import { parseLspServers } from '../../catalog/lsp-spec.js';
15
14
  /** The direct LSP server configuration file path. */
16
15
  export function lspServersPath(agentsRoot) {
17
16
  return join(agentsRoot, 'lsp.json');
@@ -20,11 +19,15 @@ export function lspServersPath(agentsRoot) {
20
19
  export async function loadLspServers(agentsRoot) {
21
20
  let raw;
22
21
  try {
23
- raw = JSON.parse(await readFile(lspServersPath(agentsRoot), 'utf8'));
22
+ raw = await readJsonFile(lspServersPath(agentsRoot));
24
23
  }
25
24
  catch (error) {
26
- return { servers: {}, errors: error.code === 'ENOENT' ? [] : [`lsp.json: ${error instanceof Error ? error.message : String(error)}`] };
25
+ return { servers: {}, errors: [`lsp.json: ${error instanceof Error ? error.message : String(error)}`] };
27
26
  }
27
+ // An absent file is an empty table, not a broken one; only a present file
28
+ // with a wrong shape carries a diagnostic.
29
+ if (raw === undefined)
30
+ return { servers: {}, errors: [] };
28
31
  const errors = [];
29
32
  const table = raw?.['lspServers'];
30
33
  if (table === undefined || table === null || Array.isArray(table) || typeof table !== 'object')
@@ -40,8 +43,7 @@ export async function saveLspServers(agentsRoot, raw) {
40
43
  if (errors.length > 0) {
41
44
  throw new Error(`invalid lspServers: ${errors[0]}`);
42
45
  }
43
- const path = lspServersPath(agentsRoot);
44
46
  const persisted = Object.fromEntries(Object.entries(servers).map(([name, { key: _key, ...config }]) => [name, config]));
45
- await writeFileAtomic(path, `${JSON.stringify({ lspServers: persisted }, null, 2)}\n`, { mode: 0o600, dirMode: 0o700 });
47
+ await writeJsonDocument(lspServersPath(agentsRoot), { lspServers: persisted });
46
48
  return { servers };
47
49
  }
@@ -0,0 +1,17 @@
1
+ import { join } from 'node:path';
2
+ import { readJsonFile, writeJsonDocument } from '../json-file.js';
3
+ export function lspServerStatePath(dataRoot) {
4
+ return join(dataRoot, 'lsp-server-state.json');
5
+ }
6
+ export async function loadDisabledLspServers(dataRoot) {
7
+ try {
8
+ const value = (await readJsonFile(lspServerStatePath(dataRoot)));
9
+ return new Set(Array.isArray(value?.disabled) ? value.disabled.filter((id) => typeof id === 'string') : []);
10
+ }
11
+ catch {
12
+ return new Set();
13
+ }
14
+ }
15
+ export async function saveDisabledLspServers(dataRoot, disabled) {
16
+ await writeJsonDocument(lspServerStatePath(dataRoot), { disabled: [...new Set(disabled)].sort() });
17
+ }
@@ -1,6 +1,16 @@
1
- import { qualifiedSuiteId } from '../catalog/paths.js';
1
+ import { qualifiedSuiteId } from '../../catalog/paths.js';
2
2
  /** Sentinel suite id for user-configured (non-suite) LSP servers. */
3
3
  export const DIRECT_LSP_SUITE_ID = 'direct';
4
+ /** The fields one diagnostic contributes to its status row. */
5
+ function diagnosticReport(diagnostic) {
6
+ if (diagnostic === undefined)
7
+ return {};
8
+ return {
9
+ reason: diagnostic.reason,
10
+ ...(diagnostic.code === undefined ? {} : { code: diagnostic.code }),
11
+ ...(diagnostic.causes === undefined ? {} : { causes: diagnostic.causes })
12
+ };
13
+ }
4
14
  /**
5
15
  * Derive one row's state from its suite's diagnostic and the live-mount flag.
6
16
  * Shared by suite rows and direct rows so both kinds report identically.
@@ -8,15 +18,16 @@ export const DIRECT_LSP_SUITE_ID = 'direct';
8
18
  function deriveState(disabled, diagnostic, anyLive) {
9
19
  if (disabled)
10
20
  return { state: 'disabled' };
21
+ const reported = diagnosticReport(diagnostic);
11
22
  if (diagnostic?.code === 'host-missing')
12
- return { state: 'host-missing', reason: diagnostic.reason };
23
+ return { state: 'host-missing', ...reported };
13
24
  if (diagnostic?.code === 'seam-conflict')
14
- return { state: 'conflict', reason: diagnostic.reason };
25
+ return { state: 'conflict', ...reported };
15
26
  if (diagnostic?.code === 'mount-failed' || diagnostic?.code === 'unmount-failed') {
16
- return { state: 'failed', reason: diagnostic.reason, retryable: true };
27
+ return { state: 'failed', ...reported, retryable: true };
17
28
  }
18
29
  if (diagnostic !== undefined)
19
- return { state: 'failed', reason: diagnostic.reason };
30
+ return { state: 'failed', ...reported };
20
31
  if (anyLive)
21
32
  return { state: 'mounted' };
22
33
  // A declaration with neither a diagnostic nor a live mount is mid-flight:
@@ -42,7 +53,7 @@ export function buildLspStatus(suites, registry, direct = { servers: {}, errors:
42
53
  const suiteKey = qualifiedSuiteId(suite.sourceId, suite.id);
43
54
  const disabled = suite.activeSurfaces.lsp === false;
44
55
  const diagnostic = diagnostics.get(suiteKey);
45
- const { state, reason, retryable } = deriveState(disabled, diagnostic, anyLive);
56
+ const { state, reason, code, causes, retryable } = deriveState(disabled, diagnostic, anyLive);
46
57
  if (state === 'mounted')
47
58
  mountedSuiteIds.add(suiteKey);
48
59
  for (const spec of Object.values(suite.lsp.servers)) {
@@ -58,6 +69,8 @@ export function buildLspStatus(suites, registry, direct = { servers: {}, errors:
58
69
  extensions: spec.extensionToLanguage,
59
70
  state: disabled || disabledServers.has(`${suiteKey}/${spec.key}`) ? 'disabled' : state,
60
71
  ...(reason === undefined ? {} : { reason }),
72
+ ...(code === undefined ? {} : { code }),
73
+ ...(causes === undefined ? {} : { causes }),
61
74
  ...(retryable === undefined ? {} : { retryable })
62
75
  });
63
76
  }
@@ -65,7 +78,7 @@ export function buildLspStatus(suites, registry, direct = { servers: {}, errors:
65
78
  // Direct user-configured rows: no install state or surface toggle of their
66
79
  // own — configured means on; the mount diagnostic decides the state.
67
80
  const directDiagnostic = diagnostics.get(DIRECT_LSP_SUITE_ID);
68
- const { state: directState, reason: directReason, retryable: directRetryable } = deriveState(false, directDiagnostic, anyLive);
81
+ const { state: directState, reason: directReason, code: directCode, causes: directCauses, retryable: directRetryable } = deriveState(false, directDiagnostic, anyLive);
69
82
  if (directState === 'mounted')
70
83
  mountedSuiteIds.add(DIRECT_LSP_SUITE_ID);
71
84
  for (const spec of Object.values(direct.servers)) {
@@ -81,6 +94,8 @@ export function buildLspStatus(suites, registry, direct = { servers: {}, errors:
81
94
  extensions: spec.extensionToLanguage,
82
95
  state: disabledServers.has(`${DIRECT_LSP_SUITE_ID}/${spec.key}`) ? 'disabled' : directState,
83
96
  ...(directReason === undefined ? {} : { reason: directReason }),
97
+ ...(directCode === undefined ? {} : { code: directCode }),
98
+ ...(directCauses === undefined ? {} : { causes: directCauses }),
84
99
  ...(directRetryable === undefined ? {} : { retryable: directRetryable })
85
100
  });
86
101
  }
@@ -28,7 +28,7 @@ import { readdir, readFile, stat } from 'node:fs/promises';
28
28
  import { join } from 'node:path';
29
29
  import { isMap, isNode, isScalar, isSeq, parseDocument } from 'yaml';
30
30
  import { writeFileAtomic } from '@deepseek-ai/dsh-atomic-write';
31
- import { resolveDshHome } from '../catalog/paths.js';
31
+ import { resolveDshHome } from '../../catalog/paths.js';
32
32
  /** Every package the plugin provisions for LSP, in declaration order. */
33
33
  export const LSP_PACKAGE_SPECIFIERS = ['@deepseek-ai/dsh-lsp', '@deepseek-ai/dsh-lsp-stdio', '@deepseek-ai/dsh-tool-lsp'];
34
34
  /**
@@ -1,8 +1,8 @@
1
- import { loadLspServers, saveLspServers } from '../runtime/lsp-direct-config.js';
2
- import { loadDisabledLspServers, saveDisabledLspServers } from '../runtime/lsp-server-state.js';
3
- import { buildLspStatus } from '../runtime/lsp-status.js';
4
- import { describeLegacySeam, findLegacyLspSeams, migrateLegacyLspSeam } from '../runtime/profile-seam.js';
5
- import { applyLspOverrides, lspConfig, validateServerLsp } from '../runtime/server-config.js';
1
+ import { loadLspServers, saveLspServers } from './lsp/lsp-direct-config.js';
2
+ import { loadDisabledLspServers, saveDisabledLspServers } from './lsp/lsp-server-state.js';
3
+ import { buildLspStatus } from './lsp/lsp-status.js';
4
+ import { describeLegacySeam, findLegacyLspSeams, migrateLegacyLspSeam } from './lsp/profile-seam.js';
5
+ import { applyLspOverrides, lspConfig, validateServerLsp } from './server-config.js';
6
6
  export class LspService {
7
7
  context;
8
8
  ports;
@@ -8,16 +8,14 @@
8
8
  * namespace (see `contracts/settings.ts`) registered by the plugin's node half:
9
9
  * the registration is what makes the host 插件配置 tab serve our card, the
10
10
  * client card binds it for state, and the node half watches it to remount
11
- * servers when the switch flips. `readMcpBackend` remains only as the
12
- * one-time migration from the earlier data-root `settings.json` choice.
11
+ * servers when the switch flips.
13
12
  *
14
- * @module runtime/mcp-backend
13
+ * @module application/mcp-backend
15
14
  */
16
15
  import { readFile } from 'node:fs/promises';
17
16
  import { createRequire } from 'node:module';
18
- import { join } from 'node:path';
19
17
  import z from '@deepseek-ai/schemastery';
20
- import { MARKET_SETTINGS_DEFAULTS } from '../contracts/settings.js';
18
+ import { MARKET_SETTINGS_DEFAULTS } from '../../contracts/settings.js';
21
19
  /**
22
20
  * Schema of the market settings namespace: the switches this plugin's config
23
21
  * card serves, with the defaults declared to the host. The values themselves
@@ -32,20 +30,6 @@ export const MarketSettingsFields = {
32
30
  autoUpdateSources: z.boolean().default(MARKET_SETTINGS_DEFAULTS.autoUpdateSources)
33
31
  };
34
32
  export const MarketSettingsSchema = z.object(MarketSettingsFields);
35
- /** Path of the legacy persisted settings file under the plugin data root. */
36
- export function marketSettingsPath(dataRoot) {
37
- return join(dataRoot, 'settings.json');
38
- }
39
- /** Read the legacy persisted backend; absent or invalid values read as the default. */
40
- export async function readMcpBackend(dataRoot) {
41
- try {
42
- const parsed = JSON.parse(await readFile(marketSettingsPath(dataRoot), 'utf8'));
43
- return parsed.mcpBackend === 'host' ? 'host' : 'builtin';
44
- }
45
- catch {
46
- return 'builtin';
47
- }
48
- }
49
33
  /**
50
34
  * Probe whether the host's `dsh-mcp-client` is resolvable from this plugin's
51
35
  * module context, and at which version. Best effort: any failure reads as
@@ -20,11 +20,11 @@
20
20
  * server instead of becoming empty strings.
21
21
  */
22
22
  import { createHash } from 'node:crypto';
23
- import { DEFAULT_STARTUP_TIMEOUT_MS, DEFAULT_TOOL_CALL_TIMEOUT_MS } from './mcp-client/config.js';
24
- import { resolveCwd } from '../catalog/validate.js';
25
- import { qualifiedSuiteId, suiteDataDir } from '../catalog/paths.js';
23
+ import { DEFAULT_STARTUP_TIMEOUT_MS, DEFAULT_TOOL_CALL_TIMEOUT_MS } from './mcp-bridge-config.js';
24
+ import { resolveCwd } from '../../catalog/validate.js';
25
+ import { qualifiedSuiteId, suiteDataDir } from '../../catalog/paths.js';
26
26
  import { applyOverride } from './mcp-overrides.js';
27
- import { PLUGIN_ROOT_VARIABLES, PLUGIN_DATA_VARIABLES } from '../model/layouts.js';
27
+ import { PLUGIN_ROOT_VARIABLES, PLUGIN_DATA_VARIABLES } from '../../model/layouts.js';
28
28
  /** The max length the bridge accepts for a serverName. */
29
29
  const SERVER_NAME_MAX = 32;
30
30
  const PLACEHOLDER = /\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\}/g;
@@ -1,8 +1,7 @@
1
- import { readFile } from 'node:fs/promises';
2
1
  import { join } from 'node:path';
3
- import { writeFileAtomic } from '@deepseek-ai/dsh-atomic-write';
4
- import { MCP_SCHEMA_ID, validateMcpJson } from '../catalog/validate.js';
5
- import { effectiveSurfaces } from '../model/types.js';
2
+ import { readJsonFile, writeJsonDocument } from '../json-file.js';
3
+ import { MCP_SCHEMA_ID, validateMcpJson } from '../../catalog/validate.js';
4
+ import { effectiveSurfaces } from '../../model/types.js';
6
5
  export const USER_MCP_SOURCE = '@user-mcp';
7
6
  export const USER_MCP_SUITE = 'user-mcp';
8
7
  /**
@@ -17,11 +16,12 @@ export async function loadUserMcpSuite(agentsRoot) {
17
16
  let mcp = { schema: MCP_SCHEMA_ID, servers: {} };
18
17
  const errors = [];
19
18
  try {
20
- mcp = await validateUserMcp(agentsRoot, JSON.parse(await readFile(path, 'utf8')));
19
+ const raw = await readJsonFile(path);
20
+ if (raw !== undefined)
21
+ mcp = await validateUserMcp(agentsRoot, raw);
21
22
  }
22
23
  catch (error) {
23
- if (error.code !== 'ENOENT')
24
- errors.push(`mcp.json: ${error instanceof Error ? error.message : String(error)}`);
24
+ errors.push(`mcp.json: ${error instanceof Error ? error.message : String(error)}`);
25
25
  }
26
26
  return {
27
27
  sourceId: USER_MCP_SOURCE,
@@ -76,50 +76,9 @@ export async function addUserMcpServer(agentsRoot, name, server) {
76
76
  throw new Error(`MCP server "${name}" already exists`);
77
77
  const document = { $schema: MCP_SCHEMA_ID, mcpServers: { ...suite.mcp.servers, [name]: server } };
78
78
  await validateUserMcp(agentsRoot, document);
79
- const path = userMcpPath(agentsRoot);
80
- await writeFileAtomic(path, `${JSON.stringify(document, null, 2)}\n`, { mode: 0o600, dirMode: 0o700 });
79
+ await writeJsonDocument(userMcpPath(agentsRoot), document);
81
80
  }
82
81
  /** The user's hand-written MCP declaration file: `<agentsRoot>/mcp.json`. */
83
82
  export function userMcpPath(agentsRoot) {
84
83
  return join(agentsRoot, 'mcp.json');
85
84
  }
86
- const SERVER_NAME_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/;
87
- /**
88
- * Import several pasted servers in one write.
89
- *
90
- * Every entry is judged on its own so one bad definition cannot take the rest
91
- * of the paste with it, and an existing name is kept unless the caller asked to
92
- * overwrite. The file is written once, after every entry has been checked, so a
93
- * rejected entry never lands half-written beside the accepted ones.
94
- */
95
- export async function importUserMcpServers(agentsRoot, entries, overwrite = false) {
96
- const suite = await loadUserMcpSuite(agentsRoot);
97
- if (suite.errors.length > 0)
98
- throw new Error(suite.errors.join('; '));
99
- const servers = { ...suite.mcp.servers };
100
- const imported = [];
101
- const skipped = [];
102
- for (const entry of entries) {
103
- if (!SERVER_NAME_PATTERN.test(entry.name)) {
104
- skipped.push({ name: entry.name, reason: 'invalid name' });
105
- continue;
106
- }
107
- if (Object.hasOwn(servers, entry.name) && !overwrite) {
108
- skipped.push({ name: entry.name, reason: 'already exists' });
109
- continue;
110
- }
111
- try {
112
- await validateUserMcp(agentsRoot, { $schema: MCP_SCHEMA_ID, mcpServers: { ...servers, [entry.name]: entry.server } });
113
- }
114
- catch (reason) {
115
- skipped.push({ name: entry.name, reason: reason instanceof Error ? reason.message : String(reason) });
116
- continue;
117
- }
118
- servers[entry.name] = entry.server;
119
- imported.push(entry.name);
120
- }
121
- if (imported.length > 0) {
122
- await writeFileAtomic(userMcpPath(agentsRoot), `${JSON.stringify({ $schema: MCP_SCHEMA_ID, mcpServers: servers }, null, 2)}\n`, { mode: 0o600, dirMode: 0o700 });
123
- }
124
- return { imported, skipped };
125
- }
@@ -11,9 +11,8 @@
11
11
  * resolved by the Host credentials service (or launch-environment fallback)
12
12
  * so keys never persist in plain text.
13
13
  */
14
- import { readFile } from 'node:fs/promises';
15
14
  import { join } from 'node:path';
16
- import { writeFileAtomic } from '@deepseek-ai/dsh-atomic-write';
15
+ import { readJsonFile, writeJsonDocument } from '../json-file.js';
17
16
  import { isSensitiveKey } from './mcp-redaction.js';
18
17
  /** The largest timeout a bridge config and the host timer can hold. */
19
18
  export const MAX_TIMEOUT_MS = 2_147_483_647;
@@ -28,7 +27,7 @@ export function suiteOverridePath(dataRoot, suiteId) {
28
27
  /** Load one suite's persisted overrides; unreadable files yield none. */
29
28
  export async function loadSuiteOverrides(dataRoot, suiteId) {
30
29
  try {
31
- const raw = JSON.parse(await readFile(suiteOverridePath(dataRoot, suiteId), 'utf8'));
30
+ const raw = await readJsonFile(suiteOverridePath(dataRoot, suiteId));
32
31
  if (typeof raw !== 'object' || raw === null)
33
32
  return {};
34
33
  const parsed = sanitizeOverrides(raw);
@@ -40,8 +39,7 @@ export async function loadSuiteOverrides(dataRoot, suiteId) {
40
39
  }
41
40
  /** Persist one suite's overrides through the harness atomic write. */
42
41
  export async function saveSuiteOverrides(dataRoot, suiteId, overrides) {
43
- const path = suiteOverridePath(dataRoot, suiteId);
44
- await writeFileAtomic(path, `${JSON.stringify(overrides, null, 2)}\n`, { mode: 0o600, dirMode: 0o700 });
42
+ await writeJsonDocument(suiteOverridePath(dataRoot, suiteId), overrides);
45
43
  }
46
44
  /** Keep only recognized fields with correct shapes; drop everything else. */
47
45
  export function sanitizeOverrides(raw) {
@@ -1,8 +1,6 @@
1
1
  import { credentialRefsInServer, effectiveMcpServers, deriveServerName } from './mcp-config.js';
2
- import { inspectToolRegistry } from './tool-registry-observer.js';
3
2
  import { redactMcpConfig, redactUrl } from './mcp-redaction.js';
4
- import { qualifiedSuiteId } from '../catalog/paths.js';
5
- export { inspectToolRegistry };
3
+ import { qualifiedSuiteId } from '../../catalog/paths.js';
6
4
  /** Build status rows from discovered plugin MCP definitions and observed tool names. */
7
5
  export function buildMcpStatus(suites, diagnostics, observed, overrides = new Map()) {
8
6
  const entries = [];
@@ -55,9 +53,15 @@ export function buildMcpStatus(suites, diagnostics, observed, overrides = new Ma
55
53
  : tools.length > 0
56
54
  ? 'connected'
57
55
  : 'degraded';
58
- const reason = orphaned
59
- ? 'MCP tools remain after this plugin surface was disabled'
60
- : (diagnostic?.reason ?? (override === undefined || (state !== 'disabled' && tools.length > 0) ? undefined : disabled ? 'disabled by override' : 'modified by override'));
56
+ // A diagnostic's reason keeps the code the mount pipeline gave it. The
57
+ // notes this builder adds describe the declaration's own state — an
58
+ // override switched it off, an override rewrote it, or its surface is
59
+ // gone while its tools remain — so each carries the matching state code
60
+ // for the panel to localize.
61
+ const orphanNote = orphaned ? 'MCP tools remain after this plugin surface was disabled' : undefined;
62
+ const overrideNote = override === undefined || (state !== 'disabled' && tools.length > 0) ? undefined : disabled ? 'disabled by override' : 'modified by override';
63
+ const reason = orphanNote ?? diagnostic?.reason ?? overrideNote;
64
+ const code = orphanNote !== undefined ? 'orphaned-tools' : (diagnostic?.code ?? (overrideNote === undefined ? undefined : disabled ? 'disabled-override' : 'modified-override'));
61
65
  entries.push({
62
66
  id: `plugin:${suiteKey}/${serverKey}`,
63
67
  name: serverName,
@@ -76,8 +80,9 @@ export function buildMcpStatus(suites, diagnostics, observed, overrides = new Ma
76
80
  ...(policy.enabledTools === undefined ? {} : { suiteEnabledTools: policy.enabledTools }),
77
81
  ...(policy.suiteDisabledTools === undefined ? {} : { suiteDisabledTools: policy.suiteDisabledTools }),
78
82
  ...(override?.disabledTools === undefined ? {} : { userDisabledTools: override.disabledTools }),
79
- ...(diagnostic?.code === undefined ? {} : { code: diagnostic.code }),
83
+ ...(code === undefined ? {} : { code }),
80
84
  ...(reason === undefined ? {} : { reason }),
85
+ ...(diagnostic?.causes === undefined ? {} : { causes: diagnostic.causes }),
81
86
  ...(credentialRefs.length === 0 ? {} : { credentialRefs })
82
87
  });
83
88
  }
@@ -102,6 +107,7 @@ export function buildMcpStatus(suites, diagnostics, observed, overrides = new Ma
102
107
  config: redactMcpConfig(stale.server),
103
108
  tools: observedTools(tools),
104
109
  reason: 'MCP tools remain after this plugin was disabled or uninstalled',
110
+ code: 'orphaned-tools',
105
111
  ...(staleRefs.length === 0 ? {} : { credentialRefs: staleRefs })
106
112
  });
107
113
  continue;
@@ -9,16 +9,15 @@
9
9
  import { qualifiedSuiteId } from '../catalog/paths.js';
10
10
  import { isDirectory } from '../catalog/fs-probes.js';
11
11
  import { discoverSuitesInSource } from '../catalog/suite-scanner.js';
12
- import { readLocalePreference } from '../runtime/host-locale.js';
13
- import { probeHostMcpClient } from '../runtime/mcp-backend.js';
14
- import { declaredMcpPolicy, namespaceMcpPolicy, resolveMcpPolicy } from '../runtime/mcp-config.js';
15
- import { loadLspServers, saveLspServers } from '../runtime/lsp-direct-config.js';
16
- import { addUserMcpServer, importUserMcpServers, loadUserMcpSuite, USER_MCP_SOURCE, USER_MCP_SUITE } from '../runtime/mcp-direct-config.js';
17
- import { applyOverride, loadSuiteOverrides, mergeOverridePatch, parseMcpPolicyPatch, saveSuiteOverrides, withoutPolicyFields } from '../runtime/mcp-overrides.js';
18
- import { redactMcpConfig, redactMcpOverrides } from '../runtime/mcp-redaction.js';
19
- import { buildMcpStatus } from '../runtime/mcp-status.js';
20
- import { resolveRegion } from '../runtime/regions.js';
21
- import { applyLspOverrides, lspConfig, restoreRedactedConfig, saveLspOverride, validateServerLsp, validateServerMcp } from '../runtime/server-config.js';
12
+ import { probeHostMcpClient } from './mcp/mcp-backend.js';
13
+ import { declaredMcpPolicy, namespaceMcpPolicy, resolveMcpPolicy } from './mcp/mcp-config.js';
14
+ import { loadLspServers, saveLspServers } from './lsp/lsp-direct-config.js';
15
+ import { addUserMcpServer, loadUserMcpSuite, USER_MCP_SOURCE, USER_MCP_SUITE } from './mcp/mcp-direct-config.js';
16
+ import { applyOverride, loadSuiteOverrides, mergeOverridePatch, parseMcpPolicyPatch, saveSuiteOverrides, withoutPolicyFields } from './mcp/mcp-overrides.js';
17
+ import { redactMcpConfig, redactMcpOverrides } from './mcp/mcp-redaction.js';
18
+ import { buildMcpStatus } from './mcp/mcp-status.js';
19
+ import { resolveRegion } from './regions.js';
20
+ import { applyLspOverrides, lspConfig, restoreRedactedConfig, saveLspOverride, validateServerLsp, validateServerMcp } from './server-config.js';
22
21
  /** The host compatibility client mounts servers without a startup timeout or tool filters. */
23
22
  const HOST_STARTUP_TIMEOUT_UNSUPPORTED = 'the host MCP backend cannot enforce a startup timeout; switch the MCP backend to the built-in client to set one';
24
23
  const HOST_TOOL_FILTER_UNSUPPORTED = 'the host MCP backend cannot enforce tool filters; switch the MCP backend to the built-in client to choose tools';
@@ -64,29 +63,6 @@ export class McpService {
64
63
  await this.context.notifyChanged(true);
65
64
  });
66
65
  }
67
- /**
68
- * Import several pasted services in one write. Each entry is checked on its
69
- * own and reports back with its own outcome, so a paste of many services
70
- * never fails as a block.
71
- */
72
- async importServers(servers, overwrite) {
73
- if (!Array.isArray(servers))
74
- throw new Error('servers must be an array');
75
- const entries = servers.map(entry => {
76
- if (typeof entry !== 'object' || entry === null)
77
- throw new Error('each server must be an object');
78
- const { name, config } = entry;
79
- if (typeof name !== 'string' || name === '')
80
- throw new Error('each server needs a name');
81
- return { name, server: config };
82
- });
83
- return this.context.enqueue(async () => {
84
- const result = await importUserMcpServers(this.context.agentsRoot, entries, overwrite);
85
- if (result.imported.length > 0)
86
- await this.context.notifyChanged(true);
87
- return result;
88
- });
89
- }
90
66
  /** Read effective service configuration without exposing credential literals. */
91
67
  async serverConfig(kind, id) {
92
68
  const config = await this.resolveServerConfig(kind, id);
@@ -272,8 +248,8 @@ export class McpService {
272
248
  * everything the plugin-config card renders.
273
249
  */
274
250
  async backendInfo() {
275
- const [backend, hostClient, regionSetting, locale] = await Promise.all([this.ports.mcpBackend(), probeHostMcpClient(), this.ports.downloadRegion(), readLocalePreference()]);
276
- return { backend, hostClient, downloadRegion: { setting: regionSetting, effective: resolveRegion(regionSetting, locale) } };
251
+ const [backend, hostClient, regionSetting] = await Promise.all([this.ports.mcpBackend(), probeHostMcpClient(), this.ports.downloadRegion()]);
252
+ return { backend, hostClient, downloadRegion: { setting: regionSetting, effective: resolveRegion(regionSetting, this.ports.localePreference()) } };
277
253
  }
278
254
  /**
279
255
  * Switch the MCP mount backend and remount every suite server through it.