dsh-agent-plugins-market 0.6.2 → 0.7.1

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 (155) hide show
  1. package/README.md +117 -111
  2. package/README.zh.md +112 -98
  3. package/client/client.js +28533 -1511
  4. package/client/client.js.map +1 -1
  5. package/lib/application/catalog-context.js +188 -0
  6. package/lib/application/catalog.js +229 -1109
  7. package/lib/application/details.js +19 -8
  8. package/lib/application/install-store.js +95 -0
  9. package/lib/application/lsp-service.js +97 -0
  10. package/lib/application/mcp-service.js +263 -0
  11. package/lib/application/panel-resources.js +43 -14
  12. package/lib/application/ports.js +21 -0
  13. package/lib/application/project-agent-roles.js +8 -2
  14. package/lib/application/snapshot-cache.js +160 -0
  15. package/lib/application/source-store.js +521 -0
  16. package/lib/catalog/archive.js +35 -30
  17. package/lib/catalog/component-files.js +14 -6
  18. package/lib/catalog/fs-probes.js +43 -0
  19. package/lib/catalog/manifests.js +53 -92
  20. package/lib/catalog/native-project.js +2 -2
  21. package/lib/catalog/paths.js +74 -55
  22. package/lib/catalog/plugin-variables.js +38 -0
  23. package/lib/catalog/project-config.js +2 -1
  24. package/lib/catalog/project-hooks.js +23 -2
  25. package/lib/catalog/scan-resolvers.js +29 -45
  26. package/lib/catalog/skills-parse.js +61 -10
  27. package/lib/catalog/source-catalog.js +10 -4
  28. package/lib/catalog/suite-components.js +7 -7
  29. package/lib/catalog/suite-scanner.js +3 -1
  30. package/lib/catalog/surfaces.js +5 -26
  31. package/lib/catalog/validate.js +7 -11
  32. package/lib/contracts/market.js +2 -0
  33. package/lib/contracts/settings.js +63 -0
  34. package/lib/index.js +115 -217
  35. package/lib/model/layouts.js +10 -1
  36. package/lib/model/types.js +26 -3
  37. package/lib/routes.js +57 -15
  38. package/lib/runtime/agent-role-names.js +70 -0
  39. package/lib/runtime/agent-role-router.js +142 -106
  40. package/lib/runtime/commands-mounts.js +55 -11
  41. package/lib/runtime/deadline.js +54 -0
  42. package/lib/runtime/dynamic-context.js +140 -0
  43. package/lib/runtime/feedback-tool.js +101 -46
  44. package/lib/runtime/hooks-mounts.js +9 -11
  45. package/lib/runtime/host-locale.js +49 -24
  46. package/lib/runtime/lsp-direct-config.js +19 -19
  47. package/lib/runtime/lsp-mounts.js +182 -70
  48. package/lib/runtime/lsp-server-state.js +4 -4
  49. package/lib/runtime/lsp-status.js +1 -1
  50. package/lib/runtime/mcp-auth-record.js +42 -0
  51. package/lib/runtime/mcp-backend.js +14 -14
  52. package/lib/runtime/mcp-client/config.js +8 -1
  53. package/lib/runtime/mcp-client/connection.js +38 -43
  54. package/lib/runtime/mcp-client/host-contract.js +10 -0
  55. package/lib/runtime/mcp-client/host-seams.js +8 -88
  56. package/lib/runtime/mcp-client/oauth.js +5 -7
  57. package/lib/runtime/mcp-client/plugin-identity.js +28 -0
  58. package/lib/runtime/mcp-client/projection.js +202 -0
  59. package/lib/runtime/mcp-client/tools.js +4 -189
  60. package/lib/runtime/mcp-config.js +36 -14
  61. package/lib/runtime/mcp-direct-config.js +33 -16
  62. package/lib/runtime/mcp-mounts.js +35 -75
  63. package/lib/runtime/mcp-overrides.js +5 -5
  64. package/lib/runtime/mcp-redaction.js +3 -2
  65. package/lib/runtime/mcp-status.js +7 -8
  66. package/lib/runtime/mount-lifecycle.js +73 -0
  67. package/lib/runtime/profile-seam.js +321 -0
  68. package/lib/runtime/project-runtime.js +34 -17
  69. package/lib/runtime/reconcile-scheduler.js +97 -0
  70. package/lib/runtime/reconciler.js +6 -1
  71. package/lib/runtime/regions.js +0 -19
  72. package/lib/runtime/server-config.js +6 -4
  73. package/lib/runtime/settings-namespace.js +144 -0
  74. package/lib/runtime/skills-provider.js +38 -14
  75. package/lib/runtime/source-auto-update.js +66 -0
  76. package/lib/{model/state.js → runtime/state-store.js} +9 -11
  77. package/lib/runtime/storage-migration.js +35 -18
  78. package/lib/runtime/subagent-catalog.js +5 -4
  79. package/lib/runtime/timer-seat.js +80 -0
  80. package/lib/runtime/tool-registry-observer.js +20 -20
  81. package/lib/runtime/user-commands.js +7 -3
  82. package/lib/runtime/user-panels.js +125 -46
  83. package/lib/runtime/user-store.js +124 -69
  84. package/lib/types/application/catalog-context.d.ts +124 -0
  85. package/lib/types/application/catalog.d.ts +108 -304
  86. package/lib/types/application/install-store.d.ts +21 -0
  87. package/lib/types/application/lsp-service.d.ts +36 -0
  88. package/lib/types/application/mcp-service.d.ts +77 -0
  89. package/lib/types/application/panel-resources.d.ts +8 -0
  90. package/lib/types/application/ports.d.ts +72 -0
  91. package/lib/types/application/queries.d.ts +15 -26
  92. package/lib/types/application/snapshot-cache.d.ts +51 -0
  93. package/lib/types/application/source-store.d.ts +100 -0
  94. package/lib/types/catalog/archive.d.ts +3 -11
  95. package/lib/types/catalog/component-files.d.ts +4 -0
  96. package/lib/types/catalog/fs-probes.d.ts +10 -0
  97. package/lib/types/catalog/manifests.d.ts +17 -16
  98. package/lib/types/catalog/native-project.d.ts +2 -2
  99. package/lib/types/catalog/paths.d.ts +53 -15
  100. package/lib/types/catalog/plugin-variables.d.ts +16 -0
  101. package/lib/types/catalog/scan-pipeline.d.ts +3 -3
  102. package/lib/types/catalog/scan-resolvers.d.ts +3 -9
  103. package/lib/types/catalog/skills-parse.d.ts +13 -7
  104. package/lib/types/catalog/source-catalog.d.ts +10 -5
  105. package/lib/types/catalog/suite-scanner.d.ts +8 -6
  106. package/lib/types/catalog/validate.d.ts +0 -2
  107. package/lib/types/contracts/lsp-status.d.ts +35 -0
  108. package/lib/types/contracts/market.d.ts +13 -2
  109. package/lib/types/contracts/mcp-status.d.ts +4 -0
  110. package/lib/types/contracts/settings.d.ts +59 -0
  111. package/lib/types/model/layouts.d.ts +10 -0
  112. package/lib/types/model/types.d.ts +29 -5
  113. package/lib/types/runtime/agent-role-names.d.ts +6 -0
  114. package/lib/types/runtime/agent-role-router.d.ts +59 -52
  115. package/lib/types/runtime/commands-mounts.d.ts +15 -7
  116. package/lib/types/runtime/deadline.d.ts +17 -0
  117. package/lib/types/runtime/dynamic-context.d.ts +81 -0
  118. package/lib/types/runtime/feedback-tool.d.ts +33 -11
  119. package/lib/types/runtime/hooks-mounts.d.ts +1 -1
  120. package/lib/types/runtime/host-locale.d.ts +20 -2
  121. package/lib/types/runtime/lsp-direct-config.d.ts +3 -3
  122. package/lib/types/runtime/lsp-mounts.d.ts +69 -19
  123. package/lib/types/runtime/lsp-status.d.ts +2 -1
  124. package/lib/types/runtime/mcp-auth-record.d.ts +29 -0
  125. package/lib/types/runtime/mcp-backend.d.ts +10 -14
  126. package/lib/types/runtime/mcp-client/config.d.ts +8 -2
  127. package/lib/types/runtime/mcp-client/host-contract.d.ts +78 -0
  128. package/lib/types/runtime/mcp-client/host-seams.d.ts +20 -64
  129. package/lib/types/runtime/mcp-client/oauth.d.ts +1 -1
  130. package/lib/types/runtime/mcp-client/plugin-identity.d.ts +16 -0
  131. package/lib/types/runtime/mcp-client/projection.d.ts +45 -0
  132. package/lib/types/runtime/mcp-client/tools.d.ts +5 -72
  133. package/lib/types/runtime/mcp-config.d.ts +25 -4
  134. package/lib/types/runtime/mcp-direct-config.d.ts +18 -4
  135. package/lib/types/runtime/mcp-mounts.d.ts +10 -15
  136. package/lib/types/runtime/mcp-overrides.d.ts +1 -1
  137. package/lib/types/runtime/mcp-status.d.ts +1 -1
  138. package/lib/types/runtime/mount-lifecycle.d.ts +53 -0
  139. package/lib/types/runtime/profile-seam.d.ts +64 -0
  140. package/lib/types/runtime/project-runtime.d.ts +10 -3
  141. package/lib/types/runtime/reconcile-scheduler.d.ts +45 -0
  142. package/lib/types/runtime/reconciler.d.ts +2 -0
  143. package/lib/types/runtime/regions.d.ts +1 -4
  144. package/lib/types/runtime/settings-namespace.d.ts +76 -0
  145. package/lib/types/runtime/skills-provider.d.ts +11 -2
  146. package/lib/types/runtime/source-auto-update.d.ts +32 -0
  147. package/lib/types/{model/state.d.ts → runtime/state-store.d.ts} +2 -2
  148. package/lib/types/runtime/storage-migration.d.ts +5 -3
  149. package/lib/types/runtime/subagent-catalog.d.ts +2 -0
  150. package/lib/types/runtime/timer-seat.d.ts +51 -0
  151. package/lib/types/runtime/tool-registry-observer.d.ts +12 -6
  152. package/lib/types/runtime/user-panels.d.ts +69 -19
  153. package/lib/types/runtime/user-store.d.ts +67 -20
  154. package/package.json +68 -24
  155. package/schemas/README.md +1 -1
@@ -8,9 +8,16 @@
8
8
  *
9
9
  * @module runtime/mcp-client/config
10
10
  */
11
- export { RECONNECT_DEFAULTS } from './connection.js';
12
11
  /** Default timeout for individual MCP tool calls (ms). */
13
12
  export const DEFAULT_TOOL_CALL_TIMEOUT_MS = 60_000;
13
+ /**
14
+ * How long one connection attempt may spend initializing (ms). A server that
15
+ * never answers would otherwise hold its process, transport and browser leg
16
+ * until the SDK's own 60-second request default expired; a working server
17
+ * handshakes well under a second, and a genuinely slow one can declare its own
18
+ * `startupTimeoutMs`.
19
+ */
20
+ export const DEFAULT_STARTUP_TIMEOUT_MS = 10_000;
14
21
  /** Valid `serverName`, kept below the public tool-name budget. */
15
22
  export const SERVER_NAME_PATTERN = /^[A-Za-z0-9_-]{1,32}$/;
16
23
  /**
@@ -23,8 +23,10 @@
23
23
  import { Client } from '@modelcontextprotocol/sdk/client/index.js';
24
24
  import { ToolListChangedNotificationSchema } from '@modelcontextprotocol/sdk/types.js';
25
25
  import { MAX_TIMER_DELAY_MS } from './host-seams.js';
26
+ import { PLUGIN_NAME, PLUGIN_VERSION } from './plugin-identity.js';
26
27
  import { createTransport } from './transport.js';
27
28
  import { syncTools } from './tools.js';
29
+ import { DEFAULT_STARTUP_TIMEOUT_MS } from './config.js';
28
30
  /** Defaults shared by the config and {@link resolveReconnectPolicy}. */
29
31
  export const RECONNECT_DEFAULTS = Object.freeze({
30
32
  enabled: true,
@@ -88,9 +90,11 @@ export function startConnection(host, config, policy, credentials) {
88
90
  registrationFailure: 'contain',
89
91
  serverName: config.serverName,
90
92
  toolCallTimeoutMs: config.toolCallTimeoutMs,
93
+ // Every attempt is bounded: an unreachable server must not hold its
94
+ // process, transport and browser leg while nothing can arrive.
95
+ startupTimeoutMs: config.startupTimeoutMs ?? DEFAULT_STARTUP_TIMEOUT_MS,
91
96
  ...(config.enabledTools === undefined ? {} : { enabledTools: config.enabledTools }),
92
- ...(config.disabledTools === undefined ? {} : { disabledTools: config.disabledTools }),
93
- ...(config.startupTimeoutMs === undefined ? {} : { startupTimeoutMs: config.startupTimeoutMs })
97
+ ...(config.disabledTools === undefined ? {} : { disabledTools: config.disabledTools })
94
98
  };
95
99
  // The initial sync uses 'throw' when failOnStartupError is configured, so
96
100
  // a registration conflict propagates to the startup-await path. Re-syncs
@@ -197,7 +201,7 @@ export function startConnection(host, config, policy, credentials) {
197
201
  * @param startup - Whether this is the bridge's activation attempt.
198
202
  */
199
203
  async function connectGeneration(startup) {
200
- const generation = new Client({ name: 'dsh-agent-plugins-market', version: '0.1.0' }, { capabilities: {} });
204
+ const generation = new Client({ name: PLUGIN_NAME, version: PLUGIN_VERSION }, { capabilities: {} });
201
205
  const closed = Promise.withResolvers();
202
206
  let attemptSettled = false;
203
207
  let closeObserved = false;
@@ -228,14 +232,36 @@ export function startConnection(host, config, policy, credentials) {
228
232
  host.logger.error(`${label}: tool re-sync failed: ${String(error)}`);
229
233
  }
230
234
  });
235
+ /**
236
+ * Retire a generation whose attempt failed: close it, wait for the
237
+ * transport's own close signal, then hand it to the disconnect path. A
238
+ * generation that will not close stops reconnection outright — respawning
239
+ * would overlap the server process still running behind it.
240
+ */
241
+ async function teardownFailedAttempt() {
242
+ try {
243
+ await generation.close();
244
+ }
245
+ catch {
246
+ /* transport already gone */
247
+ }
248
+ const quiesced = hasClosed() || (await waitForClose(closed.promise));
249
+ attemptSettled = true;
250
+ if (!isCurrent(generation))
251
+ return;
252
+ if (quiesced) {
253
+ generationDown(generation);
254
+ return;
255
+ }
256
+ client = undefined;
257
+ clientClosed = undefined;
258
+ host.logger.error(`${label}: failed generation did not close within ${GENERATION_CLOSE_TIMEOUT_MS}ms — reconnect stopped to avoid overlapping server processes; reload the plugin or restart the Host to retry`);
259
+ }
231
260
  const { transport, oauthProvider } = createTransport(config, credentials, (message) => {
232
261
  host.logger.info(message);
233
262
  });
234
263
  try {
235
- if (config.startupTimeoutMs === undefined)
236
- await generation.connect(transport);
237
- else
238
- await generation.connect(transport, { timeout: config.startupTimeoutMs });
264
+ await generation.connect(transport, { timeout: opts.startupTimeoutMs });
239
265
  if (hasClosed()) {
240
266
  attemptSettled = true;
241
267
  generationDown(generation);
@@ -261,11 +287,9 @@ export function startConnection(host, config, policy, credentials) {
261
287
  attemptSettled = true;
262
288
  if (!isCurrent(generation))
263
289
  return;
264
- if (hasClosed()) {
265
- generationDown(generation);
266
- return;
267
- }
268
- if (!legSettled) {
290
+ // The leg failed, or the transport closed while it ran: no generation
291
+ // is left to finish establishing.
292
+ if (hasClosed() || !legSettled) {
269
293
  generationDown(generation);
270
294
  return;
271
295
  }
@@ -277,20 +301,7 @@ export function startConnection(host, config, policy, credentials) {
277
301
  catch (syncError) {
278
302
  if (isCurrent(generation))
279
303
  host.logger.warn(`${label}: tool sync after authorization failed: ${String(syncError)}`);
280
- try {
281
- await generation.close();
282
- }
283
- catch {
284
- /* transport already gone */
285
- }
286
- const quiescedSync = hasClosed() || (await waitForClose(closed.promise));
287
- if (!quiescedSync) {
288
- client = undefined;
289
- clientClosed = undefined;
290
- host.logger.error(`${label}: failed generation did not close within ${GENERATION_CLOSE_TIMEOUT_MS}ms — reconnect stopped to avoid overlapping server processes`);
291
- return;
292
- }
293
- generationDown(generation);
304
+ await teardownFailedAttempt();
294
305
  return;
295
306
  }
296
307
  if (hasClosed()) {
@@ -304,23 +315,7 @@ export function startConnection(host, config, policy, credentials) {
304
315
  host.logger.info(`${label}: authorized and re-synced tools (attempt ${failedAttempts}/${policy.maxAttempts})`);
305
316
  return;
306
317
  }
307
- try {
308
- await generation.close();
309
- }
310
- catch {
311
- /* transport already gone */
312
- }
313
- const quiesced = hasClosed() || (await waitForClose(closed.promise));
314
- attemptSettled = true;
315
- if (!isCurrent(generation))
316
- return;
317
- if (!quiesced) {
318
- client = undefined;
319
- clientClosed = undefined;
320
- host.logger.error(`${label}: failed generation did not close within ${GENERATION_CLOSE_TIMEOUT_MS}ms — reconnect stopped to avoid overlapping server processes; reload the plugin or restart the Host to retry`);
321
- return;
322
- }
323
- generationDown(generation);
318
+ await teardownFailedAttempt();
324
319
  return;
325
320
  }
326
321
  attemptSettled = true;
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Structural mirrors of the harness tool-registration surface the bridge
3
+ * registers against: the runtime accepts the same object shape the upstream
4
+ * `@deepseek-ai/dsh-tools` bridge registers, so the model-facing behavior is
5
+ * identical. Mirrored rather than imported so the market never loads a second
6
+ * copy of the host runtime.
7
+ *
8
+ * @module runtime/mcp-client/host-contract
9
+ */
10
+ export {};
@@ -1,88 +1,8 @@
1
- /**
2
- * Local ports of the tiny host seams the self-built MCP client needs.
3
- *
4
- * The market plugin must stay self-contained at runtime: it cannot import
5
- * host-internal packages that are not published at compatible versions, and
6
- * every one of these seams is a small pure function or constant. Each port
7
- * below is copied verbatim (semantics, not prose) from the harness source at
8
- * `0.1.1-rc.2` and carries the upstream reference it came from, so a future
9
- * host change can be re-diffed against `~/workspace/deepseek-harness`.
10
- *
11
- * Anything larger than a handful of lines (the JSON Schema subset validator)
12
- * lives in its own module.
13
- *
14
- * @module runtime/mcp-client/host-seams
15
- */
16
- // ---- dsh-timeout: MAX_TIMER_DELAY_MS (packages/util/timeout/src/index.ts) ----
17
- /**
18
- * The largest delay `setTimeout` accepts without overflowing to a 1ms clamp
19
- * (2^31 - 1). Shared guard for reconnect and callback timers.
20
- */
21
- export const MAX_TIMER_DELAY_MS = 2_147_483_647;
22
- // ---- dsh-subprocess: scrubbedParentEnv (packages/subprocess/subprocess/src/index.ts) ----
23
- /** `DSH_*` environment namespace owned by the harness. */
24
- export const DSH_ENV_PREFIX = 'DSH_';
25
- /**
26
- * Credential-shaped environment names are NOT forwarded to MCP server child
27
- * processes (the harness's own `DEEPSEEK_API_KEY`/secrets must not leak into a
28
- * spawned server implicitly). Deliberately supplied env entries survive
29
- * because the config's explicit env merges after the scrub.
30
- */
31
- export const SENSITIVE_ENV_PATTERN = /KEY|PASSWORD|SECRET|TOKEN/i;
32
- /**
33
- * The ambient parent environment minus credential-shaped names and minus all
34
- * `DSH_*` names — the canonical base every harness child starts from. `PATH`,
35
- * `HOME`, locale, and proxy variables survive, so child CLIs run normally;
36
- * harness identity never leaks implicitly. Both scrubs match case-insensitively:
37
- * Windows environment names are case-insensitive, so a parent `dsh_*` entry
38
- * would otherwise survive and read back as `$env:DSH_*` in the child.
39
- * @returns a fresh environment object safe to hand to a child spawn.
40
- */
41
- export function scrubbedParentEnv() {
42
- const env = {};
43
- for (const [key, value] of Object.entries(process.env)) {
44
- if (value !== undefined && !SENSITIVE_ENV_PATTERN.test(key) && !key.toUpperCase().startsWith(DSH_ENV_PREFIX))
45
- env[key] = value;
46
- }
47
- return env;
48
- }
49
- /** Key segments are lowercase hyphenated identifiers. */
50
- const KEY_SEGMENT_PATTERN = /^[a-z][a-z0-9-]*$/;
51
- /**
52
- * Brand a scope and an id as a {@link CredentialKey}.
53
- * @param scope - the owning plugin's registered name (here: the `mcp-auth` record scope).
54
- * @param id - the plugin's own addressing unit (here: the folded serverName).
55
- * @returns the branded key.
56
- * @throws TypeError when either segment is not a lowercase hyphenated identifier.
57
- */
58
- export function credentialKey(scope, id) {
59
- for (const segment of [scope, id]) {
60
- if (!KEY_SEGMENT_PATTERN.test(segment)) {
61
- throw new TypeError(`credential key segment "${segment}" must match ${String(KEY_SEGMENT_PATTERN)}`);
62
- }
63
- }
64
- return `${scope}/${id}`;
65
- }
66
- // ---- dsh-attachment: image admission error routing (packages/attachment/attachment/src/error.ts) ----
67
- /** Caller-correctable attachment failure codes raised while admitting image input. */
68
- const IMAGE_ADMISSION_ERROR_CODES = [
69
- 'TOO_MANY_IMAGES',
70
- 'IMAGES_TOO_LARGE',
71
- 'UNSUPPORTED_IMAGE_TYPE',
72
- 'INVALID_IMAGE_BASE64',
73
- 'INVALID_IMAGE',
74
- 'IMAGE_TYPE_MISMATCH',
75
- 'IMAGE_TOO_LARGE',
76
- 'IMAGE_TOO_MANY_PIXELS',
77
- 'IMAGE_DIMENSION_TOO_LARGE'
78
- ];
79
- /** Runtime membership for structurally compatible errors crossing package boundaries. */
80
- const IMAGE_ADMISSION_ERROR_CODE_SET = new Set(IMAGE_ADMISSION_ERROR_CODES);
81
- /**
82
- * Distinguish caller-correctable image admission failures from storage faults,
83
- * so an admission refusal projects text diagnostics while a storage fault is
84
- * reported as such.
85
- */
86
- export function isImageAdmissionError(error) {
87
- return error instanceof Error && 'code' in error && typeof error.code === 'string' && IMAGE_ADMISSION_ERROR_CODE_SET.has(error.code);
88
- }
1
+ // ---- dsh-timeout: timer bounds ----
2
+ export { MAX_TIMER_DELAY_MS } from '@deepseek-ai/dsh-timeout';
3
+ // ---- dsh-subprocess: the canonical child environment ----
4
+ export { scrubbedParentEnv } from '@deepseek-ai/dsh-subprocess';
5
+ // ---- dsh-credentials: key grammar ----
6
+ export { credentialKey } from '@deepseek-ai/dsh-credentials';
7
+ // ---- dsh-attachment: durable image vocabulary ----
8
+ export { isImageAdmissionError } from '@deepseek-ai/dsh-attachment';
@@ -22,18 +22,16 @@
22
22
  * then last exactly as long as the process.
23
23
  *
24
24
  * Ported from the harness `dsh-mcp-client` OAuth implementation (upstream
25
- * commits archived at `docs/upstream-proposal/patches/`); the record format is
25
+ * commits archived at `docs/developer/upstream-proposal/patches/`); the record format is
26
26
  * byte-compatible, so grants written by either implementation are interchangeable.
27
27
  *
28
28
  * @module runtime/mcp-client/oauth
29
29
  */
30
30
  import { createServer } from 'node:http';
31
31
  import { credentialKey, scrubbedParentEnv } from './host-seams.js';
32
+ import { PLUGIN_NAME, PLUGIN_VERSION } from './plugin-identity.js';
32
33
  /** OAuth client identity presented during dynamic registration. */
33
- const CLIENT_NAME = 'DeepSeek Harness (dsh-agent-plugins-market)';
34
- /** Software id/version advertised in RFC 7591 registration metadata. */
35
- const CLIENT_SOFTWARE_ID = 'dsh-agent-plugins-market';
36
- const CLIENT_SOFTWARE_VERSION = '0.1.0';
34
+ const CLIENT_NAME = `DeepSeek Harness (${PLUGIN_NAME})`;
37
35
  /** Credential-record scope for MCP OAuth state; per-server ids follow it. */
38
36
  export const MCP_AUTH_RECORD_SCOPE = 'mcp-auth';
39
37
  /** Loopback callback budget: the browser must land within five minutes. */
@@ -146,8 +144,8 @@ export class LoopbackOAuthClientProvider {
146
144
  // Public client: PKCE carries the proof; no client secret exists.
147
145
  token_endpoint_auth_method: 'none',
148
146
  ...(this.scope === undefined ? {} : { scope: this.scope }),
149
- software_id: CLIENT_SOFTWARE_ID,
150
- software_version: CLIENT_SOFTWARE_VERSION
147
+ software_id: PLUGIN_NAME,
148
+ software_version: PLUGIN_VERSION
151
149
  };
152
150
  }
153
151
  async clientInformation() {
@@ -0,0 +1,28 @@
1
+ /**
2
+ * This plugin's own identity as advertised to the servers it talks to: the MCP
3
+ * client implementation name and version, and the RFC 7591 `software_id` /
4
+ * `software_version` carried in OAuth dynamic-registration metadata.
5
+ *
6
+ * The version is read from the installed manifest through Node's package
7
+ * self-reference, so a release bumps exactly one number — `package.json`. A
8
+ * copy that can no longer resolve its own manifest (a bundle that inlined this
9
+ * module) reports {@link UNKNOWN_VERSION} rather than failing the connection.
10
+ *
11
+ * @module runtime/mcp-client/plugin-identity
12
+ */
13
+ import { createRequire } from 'node:module';
14
+ /** Name npm and the harness know this plugin by. */
15
+ export const PLUGIN_NAME = 'dsh-agent-plugins-market';
16
+ /** Reported when the plugin cannot read its own manifest; never a released version. */
17
+ const UNKNOWN_VERSION = '0.0.0';
18
+ function readVersion() {
19
+ try {
20
+ const manifest = createRequire(import.meta.url)('dsh-agent-plugins-market/package.json');
21
+ return typeof manifest.version === 'string' && manifest.version !== '' ? manifest.version : UNKNOWN_VERSION;
22
+ }
23
+ catch {
24
+ return UNKNOWN_VERSION;
25
+ }
26
+ }
27
+ /** This plugin's release version. */
28
+ export const PLUGIN_VERSION = readVersion();
@@ -0,0 +1,202 @@
1
+ /**
2
+ * MCP result projection: turn the content blocks an MCP server returns into
3
+ * the host content vocabulary the model sees.
4
+ *
5
+ * Text-like blocks collapse into newline-joined text runs; declared images are
6
+ * decoded, admitted against the live model route, and saved to the durable
7
+ * attachment store. Every refusal — an unsupported media type, a model that
8
+ * does not declare image input, a storage fault — projects the block as text
9
+ * explaining why, and the canonical raw value stays available to programmatic
10
+ * callers in the tool result.
11
+ *
12
+ * Everything here reads an untrusted network payload: the MCP server is an
13
+ * external process and the SDK's declared-required fields may be absent at
14
+ * runtime, so each block is narrowed field by field.
15
+ *
16
+ * @module runtime/mcp-client/projection
17
+ */
18
+ import { isImageAdmissionError } from './host-seams.js';
19
+ /** Raster formats supported by the durable attachment vocabulary. */
20
+ const IMAGE_MEDIA_TYPES = ['image/png', 'image/jpeg', 'image/webp', 'image/gif'];
21
+ /** Canonical RFC 4648 base64, excluding whitespace and URL-safe aliases. */
22
+ const CANONICAL_BASE64 = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
23
+ /** Whether an untrusted MCP content array contains a declared image block. */
24
+ export function containsImage(content) {
25
+ return content.some(value => isRecord(value) && value.type === 'image');
26
+ }
27
+ /** Narrow one JSON value to a string-keyed object. */
28
+ function isRecord(value) {
29
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
30
+ }
31
+ /** Narrow a declared MIME string to the durable image vocabulary. */
32
+ function isImageMediaType(value) {
33
+ return IMAGE_MEDIA_TYPES.includes(value);
34
+ }
35
+ /** Decode one untrusted MCP image block without accepting base64 aliases. */
36
+ function decodeImage(block) {
37
+ if (block.mimeType === undefined || !isImageMediaType(block.mimeType)) {
38
+ throw new Error('the declared media type is not PNG, JPEG, WebP, or GIF');
39
+ }
40
+ if (block.data === undefined || !CANONICAL_BASE64.test(block.data)) {
41
+ throw new Error('the image data is not canonical base64');
42
+ }
43
+ const data = Buffer.from(block.data, 'base64');
44
+ if (data.toString('base64') !== block.data) {
45
+ throw new Error('the image data is not canonical base64');
46
+ }
47
+ return { data, mediaType: block.mimeType };
48
+ }
49
+ /**
50
+ * Resolve the active model route and durable store for an image-bearing result.
51
+ * @param host - bridge host with optional services.
52
+ * @param exec - exact tool execution whose agent supplies the latest route.
53
+ * @returns the attachment store after exact positive image-capability proof.
54
+ */
55
+ async function resolveImageAdmission(host, exec) {
56
+ const attachments = host.getService?.('attachments');
57
+ if (attachments === undefined || typeof attachments.saveImages !== 'function') {
58
+ throw new Error('no attachment store is mounted');
59
+ }
60
+ const routed = exec.agent?.session?.requestHeader?.()?.config;
61
+ const provider = routed?.provider ?? exec.agent?.options?.provider;
62
+ const model = routed?.model ?? exec.agent?.options?.model;
63
+ const llm = host.getService?.('llm');
64
+ if (provider === undefined || model === undefined || llm === undefined) {
65
+ throw new Error('the current model route could not be resolved');
66
+ }
67
+ let info;
68
+ try {
69
+ info = await llm.resolveModelInfo(provider, model, exec.signal);
70
+ }
71
+ catch {
72
+ throw new Error('the current model route could not be verified');
73
+ }
74
+ if (info?.inputModalities === undefined || !info.inputModalities.includes('image')) {
75
+ throw new Error(`model "${model}" does not declare image input`);
76
+ }
77
+ if (exec.signal.aborted)
78
+ throw new Error('the tool call was canceled before image storage');
79
+ return attachments;
80
+ }
81
+ /** Stable diagnostic text for an image block that was not admitted. */
82
+ function imageDiagnostic(block, reason) {
83
+ return `[image unavailable: ${block.mimeType ?? 'unknown media type'}; ${reason}; raw image data remains available to programmatic callers]`;
84
+ }
85
+ /**
86
+ * Decode, preflight, and durably save one MCP result's ordered image batch.
87
+ * Any refusal projects every image as text while retaining the canonical raw
88
+ * value for programmatic callers.
89
+ */
90
+ export async function prepareImageProjection(host, exec, content, toolName) {
91
+ const decoded = [];
92
+ const validationErrors = new Map();
93
+ const imageIndexes = [];
94
+ for (const [index, value] of content.entries()) {
95
+ if (!isRecord(value) || value.type !== 'image')
96
+ continue;
97
+ imageIndexes.push(index);
98
+ try {
99
+ decoded.push(decodeImage(value));
100
+ }
101
+ catch (error) {
102
+ // decodeImage owns every throw above and always produces Error.
103
+ validationErrors.set(index, error.message);
104
+ }
105
+ }
106
+ if (validationErrors.size > 0) {
107
+ return projectContent(content, toolName, (block, index) => ({
108
+ type: 'text',
109
+ text: imageDiagnostic(block, validationErrors.get(index) ?? 'another image in the same result was invalid')
110
+ }));
111
+ }
112
+ let attachments;
113
+ try {
114
+ attachments = await resolveImageAdmission(host, exec);
115
+ }
116
+ catch (error) {
117
+ // resolveImageAdmission contains provider failures and throws Error only.
118
+ const reason = error.message;
119
+ return projectContent(content, toolName, block => ({ type: 'text', text: imageDiagnostic(block, reason) }));
120
+ }
121
+ try {
122
+ const refs = await attachments.saveImages(decoded);
123
+ const byIndex = new Map(imageIndexes.map((index, offset) => [index, refs[offset]]));
124
+ return projectContent(content, toolName, (_block, index) => ({
125
+ type: 'image',
126
+ attachment: byIndex.get(index)
127
+ }));
128
+ }
129
+ catch (error) {
130
+ const reason = isImageAdmissionError(error) ? `image admission rejected the result: ${error.message}` : 'durable image storage rejected the result';
131
+ return projectContent(content, toolName, block => ({
132
+ type: 'text',
133
+ text: imageDiagnostic(block, reason)
134
+ }));
135
+ }
136
+ }
137
+ /**
138
+ * Extract text from an MCP content array into a single string.
139
+ * - text blocks: join with '\n'
140
+ * - image/audio/resource blocks: replaced with a placeholder
141
+ *
142
+ * Defensive: fields that the MCP spec declares required (mimeType, text) are
143
+ * guarded with fallbacks because this is a network trust boundary.
144
+ */
145
+ export function extractText(mcpContent, toolName) {
146
+ const content = projectContent(mcpContent, toolName);
147
+ // The default image projector below also returns text, so this local call
148
+ // cannot produce a core image block.
149
+ return content.map(block => block.text).join('\n');
150
+ }
151
+ /**
152
+ * Project ordered MCP blocks into the core content vocabulary.
153
+ * Text-like runs are newline-coalesced; admitted images split those runs at
154
+ * their original position.
155
+ */
156
+ function projectContent(mcpContent, toolName, image = block => ({
157
+ type: 'text',
158
+ text: imageDiagnostic(block, 'this result was not admitted to durable model context')
159
+ })) {
160
+ const projected = [];
161
+ const text = [];
162
+ const flushText = () => {
163
+ if (text.length === 0)
164
+ return;
165
+ projected.push({ type: 'text', text: text.splice(0).join('\n') });
166
+ };
167
+ for (const [index, value] of mcpContent.entries()) {
168
+ if (!isRecord(value)) {
169
+ text.push('[unsupported MCP content block: expected an object]');
170
+ continue;
171
+ }
172
+ const block = value;
173
+ switch (block.type) {
174
+ case 'text':
175
+ if (block.text !== undefined)
176
+ text.push(block.text);
177
+ break;
178
+ case 'image':
179
+ flushText();
180
+ projected.push(image(block, index));
181
+ break;
182
+ case 'resource_link':
183
+ if (block.name === undefined || block.uri === undefined) {
184
+ text.push('[resource link unavailable: the MCP block is missing its name or URI]');
185
+ }
186
+ else {
187
+ text.push(`Resource link: ${block.name} (${block.uri})`);
188
+ }
189
+ break;
190
+ case 'audio':
191
+ text.push(`[audio result unsupported: ${block.mimeType ?? 'unknown media type'}; raw audio data remains available to programmatic callers]`);
192
+ break;
193
+ case 'resource':
194
+ text.push('[embedded resource unsupported; raw resource data remains available to programmatic callers]');
195
+ break;
196
+ default:
197
+ text.push(`[unsupported MCP content type: ${block.type}]`);
198
+ }
199
+ }
200
+ flushText();
201
+ return projected.length > 0 ? projected : [{ type: 'text', text: `(${toolName} returned no model-visible content)` }];
202
+ }