dsh-agent-plugins-market 0.7.0 → 0.7.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 (114) hide show
  1. package/README.md +31 -26
  2. package/README.zh.md +31 -26
  3. package/client/client.js +29164 -1136
  4. package/client/client.js.map +1 -1
  5. package/lib/application/catalog-context.js +8 -1
  6. package/lib/application/catalog.js +27 -2
  7. package/lib/application/details.js +13 -2
  8. package/lib/application/install-store.js +1 -1
  9. package/lib/application/mcp-service.js +191 -19
  10. package/lib/application/panel-resources.js +16 -4
  11. package/lib/application/project-agent-roles.js +7 -1
  12. package/lib/catalog/archive.js +16 -1
  13. package/lib/catalog/manifests.js +124 -13
  14. package/lib/catalog/paths.js +8 -0
  15. package/lib/catalog/plugin-variables.js +38 -0
  16. package/lib/catalog/project-hooks.js +14 -1
  17. package/lib/catalog/scan-resolvers.js +117 -12
  18. package/lib/catalog/skills-parse.js +47 -0
  19. package/lib/catalog/source-catalog.js +8 -3
  20. package/lib/catalog/suite-components.js +2 -2
  21. package/lib/catalog/surfaces.js +73 -27
  22. package/lib/catalog/validate.js +205 -54
  23. package/lib/contracts/market.js +3 -0
  24. package/lib/contracts/settings.js +63 -0
  25. package/lib/index.js +32 -13
  26. package/lib/model/layouts.js +44 -1
  27. package/lib/model/preset-source.js +8 -0
  28. package/lib/routes.js +43 -2
  29. package/lib/runtime/agent-role-router.js +203 -56
  30. package/lib/runtime/commands-mounts.js +50 -9
  31. package/lib/runtime/deadline.js +32 -6
  32. package/lib/runtime/dynamic-context.js +140 -0
  33. package/lib/runtime/feedback-tool.js +7 -3
  34. package/lib/runtime/host-locale.js +45 -15
  35. package/lib/runtime/lsp-mounts.js +23 -2
  36. package/lib/runtime/mcp-backend.js +14 -15
  37. package/lib/runtime/mcp-client/config.js +8 -0
  38. package/lib/runtime/mcp-client/connection.js +9 -8
  39. package/lib/runtime/mcp-client/host-seams.js +8 -88
  40. package/lib/runtime/mcp-client/oauth.js +82 -14
  41. package/lib/runtime/mcp-client/tools.js +2 -1
  42. package/lib/runtime/mcp-config.js +182 -35
  43. package/lib/runtime/mcp-direct-config.js +49 -6
  44. package/lib/runtime/mcp-mounts.js +15 -3
  45. package/lib/runtime/mcp-overrides.js +131 -1
  46. package/lib/runtime/mcp-redaction.js +8 -0
  47. package/lib/runtime/mcp-status.js +21 -5
  48. package/lib/runtime/project-runtime.js +23 -9
  49. package/lib/runtime/reconcile-scheduler.js +6 -8
  50. package/lib/runtime/reconciler.js +2 -1
  51. package/lib/runtime/regions.js +0 -19
  52. package/lib/runtime/server-config.js +49 -8
  53. package/lib/runtime/settings-namespace.js +21 -13
  54. package/lib/runtime/skills-provider.js +33 -11
  55. package/lib/runtime/source-auto-update.js +10 -14
  56. package/lib/runtime/subagent-catalog.js +62 -22
  57. package/lib/runtime/timer-seat.js +80 -0
  58. package/lib/runtime/tool-registry-observer.js +24 -20
  59. package/lib/runtime/user-commands.js +5 -1
  60. package/lib/runtime/user-panels.js +110 -41
  61. package/lib/runtime/user-store.js +114 -58
  62. package/lib/types/application/catalog-context.d.ts +6 -0
  63. package/lib/types/application/catalog.d.ts +19 -1
  64. package/lib/types/application/install-store.d.ts +1 -1
  65. package/lib/types/application/mcp-service.d.ts +38 -3
  66. package/lib/types/application/ports.d.ts +1 -1
  67. package/lib/types/application/queries.d.ts +8 -1
  68. package/lib/types/catalog/manifests.d.ts +6 -2
  69. package/lib/types/catalog/paths.d.ts +6 -0
  70. package/lib/types/catalog/plugin-variables.d.ts +16 -0
  71. package/lib/types/catalog/skills-parse.d.ts +13 -0
  72. package/lib/types/catalog/source-catalog.d.ts +7 -2
  73. package/lib/types/catalog/surfaces.d.ts +14 -4
  74. package/lib/types/catalog/validate.d.ts +72 -11
  75. package/lib/types/contracts/market.d.ts +77 -0
  76. package/lib/types/contracts/mcp-status.d.ts +11 -0
  77. package/lib/types/contracts/settings.d.ts +59 -0
  78. package/lib/types/model/layouts.d.ts +30 -0
  79. package/lib/types/model/preset-source.d.ts +18 -0
  80. package/lib/types/model/types.d.ts +37 -1
  81. package/lib/types/runtime/agent-role-router.d.ts +96 -18
  82. package/lib/types/runtime/commands-mounts.d.ts +15 -7
  83. package/lib/types/runtime/deadline.d.ts +10 -11
  84. package/lib/types/runtime/dynamic-context.d.ts +81 -0
  85. package/lib/types/runtime/host-locale.d.ts +20 -2
  86. package/lib/types/runtime/lsp-mounts.d.ts +7 -0
  87. package/lib/types/runtime/mcp-backend.d.ts +8 -15
  88. package/lib/types/runtime/mcp-client/config.d.ts +8 -0
  89. package/lib/types/runtime/mcp-client/host-seams.d.ts +20 -64
  90. package/lib/types/runtime/mcp-client/oauth.d.ts +4 -0
  91. package/lib/types/runtime/mcp-config.d.ts +75 -13
  92. package/lib/types/runtime/mcp-direct-config.d.ts +22 -0
  93. package/lib/types/runtime/mcp-overrides.d.ts +44 -0
  94. package/lib/types/runtime/mcp-redaction.d.ts +6 -0
  95. package/lib/types/runtime/project-runtime.d.ts +10 -3
  96. package/lib/types/runtime/reconcile-scheduler.d.ts +1 -1
  97. package/lib/types/runtime/regions.d.ts +1 -4
  98. package/lib/types/runtime/server-config.d.ts +26 -1
  99. package/lib/types/runtime/settings-namespace.d.ts +10 -10
  100. package/lib/types/runtime/skills-provider.d.ts +11 -2
  101. package/lib/types/runtime/source-auto-update.d.ts +3 -4
  102. package/lib/types/runtime/subagent-catalog.d.ts +14 -2
  103. package/lib/types/runtime/timer-seat.d.ts +51 -0
  104. package/lib/types/runtime/tool-registry-observer.d.ts +14 -6
  105. package/lib/types/runtime/user-panels.d.ts +65 -12
  106. package/lib/types/runtime/user-store.d.ts +65 -16
  107. package/package.json +23 -6
  108. package/schemas/1.0.0/mcp.schema.json +0 -22
  109. package/schemas/1.1.0/mcp.schema.json +120 -0
  110. package/schemas/1.1.0/plugin.schema.json +65 -0
  111. package/schemas/README.md +25 -19
  112. package/schemas/agent-plugins/spec.md +23 -9
  113. package/schemas/com.deepseek.harness/namespace.schema.json +42 -0
  114. package/schemas/com.deepseek.harness/spec.md +52 -0
@@ -10,10 +10,10 @@
10
10
  * thrown discovery.
11
11
  */
12
12
  import { readFile, readdir, stat } from 'node:fs/promises';
13
- import { dirname, join } from 'node:path';
13
+ import { dirname, join, relative } from 'node:path';
14
14
  import { parseSkillFrontmatter } from './skills-parse.js';
15
15
  import { isDirectory, isFile, listChildDirs } from './fs-probes.js';
16
- import { validateMcpJson } from './validate.js';
16
+ import { validateMcpJson, pathContainmentError, recognizedSpecVersion } from './validate.js';
17
17
  import { readManifest } from './manifests.js';
18
18
  import { componentDocuments, componentPath, firstComponentFile, isUnknownArray } from './component-files.js';
19
19
  /**
@@ -38,16 +38,29 @@ async function declaredSkillDirs(root, declared, errors) {
38
38
  }
39
39
  return dirs;
40
40
  }
41
- /** Discover SKILL.md files under the suite's skills directory, up to 3 levels deep. */
42
- export async function discoverSkills(root, errors, declared) {
41
+ /**
42
+ * Discover SKILL.md files under the suite's skills directory, up to 3 levels
43
+ * deep. Portable v1 suites (§7.1) instead discover exactly one level of
44
+ * `skills/` subdirectories each carrying a `SKILL.md` — the spec forbids
45
+ * recursive deeper search and root-level or flat skill files are outside the
46
+ * portable discovery shape.
47
+ */
48
+ export async function discoverSkills(root, errors, declared, portable = false) {
43
49
  const skills = [];
44
- const rootSkill = join(root, 'SKILL.md');
45
- const rootName = root.split(/[\\/]/).at(-1) ?? 'plugin';
46
- const rootParsed = await parseOneSkill(rootSkill, root, rootName, errors);
47
- if (rootParsed !== undefined)
48
- skills.push(rootParsed);
49
50
  const skillsDirs = await declaredSkillDirs(root, declared, errors);
50
- if (declared === undefined) {
51
+ if (!portable) {
52
+ const rootSkill = join(root, 'SKILL.md');
53
+ const rootName = root.split(/[\\/]/).at(-1) ?? 'plugin';
54
+ const rootParsed = await parseOneSkill(rootSkill, root, rootName, errors);
55
+ if (rootParsed !== undefined)
56
+ skills.push(rootParsed);
57
+ if (declared === undefined) {
58
+ const fallback = join(root, 'skills');
59
+ if (await isDirectory(fallback))
60
+ skillsDirs.push(fallback);
61
+ }
62
+ }
63
+ else if (declared === undefined) {
51
64
  const fallback = join(root, 'skills');
52
65
  if (await isDirectory(fallback))
53
66
  skillsDirs.push(fallback);
@@ -60,6 +73,25 @@ export async function discoverSkills(root, errors, declared) {
60
73
  skills.push(skill);
61
74
  };
62
75
  for (const skillsDir of skillsDirs) {
76
+ if (portable) {
77
+ // §7.1: each immediate child directory containing `SKILL.md` is one
78
+ // skill; no deeper descendants are searched and flat files are not
79
+ // skills. Each candidate also passes a realpath containment check so a
80
+ // symlink pointing outside the plugin root is skipped (§4.1 boundary 3).
81
+ for (const child of await listChildDirs(skillsDir)) {
82
+ const skillFile = join(child, 'SKILL.md');
83
+ if (!(await isFile(skillFile)))
84
+ continue;
85
+ const reason = await pathContainmentError(root, `./${relative(root, skillFile).replace(/\\/g, '/')}`);
86
+ if (reason !== undefined) {
87
+ errors.push(`skill "${child.split(/[\\/]/).at(-1) ?? ''}": ${reason}`);
88
+ continue;
89
+ }
90
+ const name = child.split(/[\\/]/).at(-1) ?? '';
91
+ pushUnique(await parseOneSkill(skillFile, child, name, errors));
92
+ }
93
+ continue;
94
+ }
63
95
  if (await isFile(skillsDir)) {
64
96
  pushUnique(await parseOneSkill(skillsDir, dirname(skillsDir), '', errors));
65
97
  continue;
@@ -145,23 +177,34 @@ async function parseOneSkill(file, directory, fallbackName, errors) {
145
177
  }
146
178
  return { ...verdict, directory };
147
179
  }
148
- /** Read the suite's MCP config: `mcp.json` or `.mcp.json`, else the winning manifest's inline `mcpServers`. */
149
- export async function discoverMcp(root, errors, manifest) {
180
+ /**
181
+ * Read the suite's MCP config: `mcp.json` or `.mcp.json`, else the winning
182
+ * manifest's inline `mcpServers`. Portable v1 suites read only `mcp.json` at
183
+ * the plugin root (§7.2.1: no alternative core path, no inline declarations).
184
+ */
185
+ export async function discoverMcp(root, errors, manifest, portable = false) {
150
186
  const resolved = manifest ?? (await readManifest(root, errors, undefined));
151
187
  const layout = resolved?.layout;
152
- const names = layout === 'zcode'
153
- ? ['.mcp.json']
154
- : layout === 'qoder'
155
- ? ['.mcp.json', 'mcp.json']
156
- : layout === 'github-copilot' || layout === 'universal'
157
- ? ['.mcp.json', '.github/mcp.json', 'mcp.json']
158
- : layout === 'codex' || layout === 'claude-code'
159
- ? ['.mcp.json', 'mcp.json']
160
- : layout === 'cursor'
161
- ? ['mcp.json']
162
- : ['mcp.json', '.mcp.json'];
163
- const declared = resolved?.components?.mcpServers;
164
- const fallback = layout === 'kimi' ? undefined : await firstComponentFile(root, names);
188
+ const names = portable
189
+ ? ['mcp.json']
190
+ : layout === 'zcode'
191
+ ? ['.mcp.json']
192
+ : layout === 'qoder'
193
+ ? ['.mcp.json', 'mcp.json']
194
+ : layout === 'github-copilot' || layout === 'universal'
195
+ ? ['.mcp.json', '.github/mcp.json', 'mcp.json']
196
+ : layout === 'codex' || layout === 'claude-code'
197
+ ? ['.mcp.json', 'mcp.json']
198
+ : layout === 'cursor'
199
+ ? ['mcp.json']
200
+ : ['mcp.json', '.mcp.json'];
201
+ const declared = portable ? undefined : resolved?.components?.mcpServers;
202
+ // Portable mode reads exactly the fixed location `mcp.json` (§7.2.1), so
203
+ // the candidate list is the location itself rather than a fallback probe.
204
+ // The file is optional for portable suites too — §7.2 fixes its location,
205
+ // not its presence — so an absent file resolves to zero servers without a
206
+ // diagnostic; an existing but unreadable path still fails loudly below.
207
+ const fallback = portable ? ((await isFile(join(root, 'mcp.json'))) ? 'mcp.json' : undefined) : layout === 'kimi' ? undefined : await firstComponentFile(root, names);
165
208
  const additive = layout === 'zcode' || layout === 'claude-code';
166
209
  const values = declared === undefined
167
210
  ? fallback === undefined
@@ -177,8 +220,11 @@ export async function discoverMcp(root, errors, manifest) {
177
220
  let schema = '';
178
221
  for (const document of documents) {
179
222
  const isBareFile = document.path === join(root, 'mcp.json');
180
- const strict = isBareFile && (layout === 'agent-plugin-v1' || (declared === undefined && layout !== 'cursor' && layout !== 'qoder'));
181
- const result = await validateMcpJson(root, document.value, { strict });
223
+ const strict = portable || (isBareFile && (layout === 'agent-plugin-v1' || (declared === undefined && layout !== 'cursor' && layout !== 'qoder')));
224
+ const result = await validateMcpJson(root, document.value, {
225
+ strict,
226
+ ...(portable && resolved?.schemaVersion !== undefined ? { manifestVersion: recognizedSpecVersion(resolved.schemaVersion)?.version } : {})
227
+ });
182
228
  errors.push(...result.errors);
183
229
  if (result.config === undefined)
184
230
  return undefined;
@@ -1,12 +1,17 @@
1
1
  /**
2
- * agent-plugins.org v1.0.0 validation: vendored JSON Schemas plus the
3
- * specification's semantic filesystem rules.
2
+ * agent-plugins.org v1 validation: vendored JSON Schemas per supported
3
+ * specification version plus the specification's semantic rules.
4
4
  *
5
- * The schemas live under `schemas/1.0.0/` and are bundled into the package —
6
- * the spec forbids retrieving a schema while loading a plugin. Schema
7
- * validation is one failure boundary (per server / per manifest); the
8
- * semantic pass then enforces §4 path containment, `$schema` recognition, and
9
- * the `${PLUGIN_ROOT}` / `${PLUGIN_DATA}` expansion the spec makes mandatory.
5
+ * The schemas live under `schemas/<version>/` and are bundled into the
6
+ * package — the spec forbids retrieving a schema while loading a plugin
7
+ * (§5.2, §7.2.1). Validation follows the specification's failure boundaries:
8
+ * manifest unknown top-level fields and the whole `extensions` subtree are
9
+ * reported and ignored (§5.2, §8.1) while any other manifest violation is
10
+ * fatal; `mcp.json` file-level violations disable MCP for the plugin (§7.2.2
11
+ * rule 2) and per-server violations skip only their server (§7.2.2 rule 3).
12
+ * The semantic pass then enforces §4 path containment, §7.2.1 remote URL and
13
+ * header rules, §9.2 placeholder discipline, and the `${PLUGIN_ROOT}` /
14
+ * `${PLUGIN_DATA}` expansion the spec makes mandatory.
10
15
  */
11
16
  import { readFile, realpath } from 'node:fs/promises';
12
17
  import { dirname, join, resolve } from 'node:path';
@@ -16,35 +21,62 @@ import { fileURLToPath } from 'node:url';
16
21
  import Ajv2020Default from 'ajv/dist/2020.js';
17
22
  import { PLUGIN_ROOT_VARIABLES, PLUGIN_DATA_VARIABLES } from '../model/layouts.js';
18
23
  import { isWithin } from './paths.js';
19
- const SCHEMAS_DIR = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'schemas', '1.0.0');
24
+ const SUPPORTED_VERSIONS = [
25
+ { version: '1.0.0', pluginSchemaId: 'https://agent-plugins.org/schemas/1.0.0/plugin.schema.json', mcpSchemaId: 'https://agent-plugins.org/schemas/1.0.0/mcp.schema.json' },
26
+ // 1.1.0 is explicitly recognized as compatible: its two published schemas
27
+ // differ from 1.0.0 only in the version string (§5.2 allows mapping
28
+ // explicitly recognized compatible versions onto one implementation).
29
+ { version: '1.1.0', pluginSchemaId: 'https://agent-plugins.org/schemas/1.1.0/plugin.schema.json', mcpSchemaId: 'https://agent-plugins.org/schemas/1.1.0/mcp.schema.json' }
30
+ ];
31
+ const SCHEMAS_DIR = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'schemas');
20
32
  const Ajv2020Class = Ajv2020Default;
21
- export const PLUGIN_SCHEMA_ID = 'https://agent-plugins.org/schemas/1.0.0/plugin.schema.json';
22
- export const MCP_SCHEMA_ID = 'https://agent-plugins.org/schemas/1.0.0/mcp.schema.json';
33
+ const BASELINE = SUPPORTED_VERSIONS[0];
34
+ /** The canonical plugin manifest schema identifier of the baseline release. */
35
+ export const PLUGIN_SCHEMA_ID = BASELINE.pluginSchemaId;
36
+ /** The canonical MCP configuration schema identifier of the baseline release. */
37
+ export const MCP_SCHEMA_ID = BASELINE.mcpSchemaId;
38
+ /** The spec version a recognized `$schema` id selects, or undefined when unrecognized. */
39
+ export function recognizedSpecVersion(schemaId) {
40
+ if (typeof schemaId !== 'string')
41
+ return undefined;
42
+ return SUPPORTED_VERSIONS.find(version => version.pluginSchemaId === schemaId || version.mcpSchemaId === schemaId);
43
+ }
44
+ /** Whether a `$schema` value selects a locally supported ruleset. */
45
+ export function isRecognizedSchema(value) {
46
+ return recognizedSpecVersion(value) !== undefined;
47
+ }
23
48
  let ajvPromise;
24
- /** Lazily build one Ajv instance holding the two vendored schemas. */
49
+ /** Lazily build one Ajv instance holding every vendored schema. */
25
50
  async function ajv() {
26
51
  ajvPromise ??= (async () => {
27
- const instance = new Ajv2020Class({ strict: false });
28
- for (const name of ['plugin.schema.json', 'mcp.schema.json']) {
29
- const schemaText = await readFile(join(SCHEMAS_DIR, name), 'utf8');
30
- instance.addSchema(JSON.parse(schemaText));
52
+ const instance = new Ajv2020Class({ strict: false, allErrors: true });
53
+ for (const version of SUPPORTED_VERSIONS) {
54
+ for (const name of ['plugin.schema.json', 'mcp.schema.json']) {
55
+ const schemaText = await readFile(join(SCHEMAS_DIR, version.version, name), 'utf8');
56
+ instance.addSchema(JSON.parse(schemaText));
57
+ }
31
58
  }
32
59
  return instance;
33
60
  })();
34
61
  return ajvPromise;
35
62
  }
36
- /** Validate one JSON document against a vendored schema; errors joined for surfacing. */
63
+ /** Validate one JSON document against a vendored schema; structured errors. */
37
64
  export async function validateAgainstSchema(schemaId, document) {
38
65
  const instance = await ajv();
39
66
  const validate = instance.getSchema(schemaId);
40
67
  if (validate === undefined)
41
- return [`no bundled validator for ${schemaId}`];
68
+ return [{ keyword: 'schema', instancePath: '', message: `no bundled validator for ${schemaId}` }];
42
69
  validate(document);
43
- return (validate.errors ?? []).map(error => `${error.instancePath === '' ? 'root' : error.instancePath} ${error.message ?? 'invalid'}`);
70
+ return (validate.errors ?? []).map(error => ({
71
+ keyword: error.keyword ?? 'schema',
72
+ instancePath: error.instancePath ?? '',
73
+ message: error.message ?? 'invalid',
74
+ ...(error.params === undefined ? {} : { params: error.params })
75
+ }));
44
76
  }
45
- /** Whether a `$schema` value selects the local v1.0.0 ruleset. */
46
- export function isRecognizedSchema(value) {
47
- return value === PLUGIN_SCHEMA_ID || value === MCP_SCHEMA_ID;
77
+ /** Render structured errors as one diagnostic string per error for surfacing. */
78
+ export function formatSchemaErrors(errors) {
79
+ return errors.map(error => `${error.instancePath === '' ? 'root' : error.instancePath} ${error.message}`);
48
80
  }
49
81
  /**
50
82
  * Spec §4 containment: a plugin-relative path must begin with `./` and, after
@@ -76,39 +108,124 @@ export async function pathContainmentError(pluginRoot, value) {
76
108
  }
77
109
  return undefined;
78
110
  }
79
- /** Validate a plugin manifest per the vendored schema plus `$schema` recognition. */
111
+ const EXTENSIONS_PATH = '/extensions';
112
+ function isExtensionPath(instancePath) {
113
+ return instancePath === EXTENSIONS_PATH || instancePath.startsWith(`${EXTENSIONS_PATH}/`);
114
+ }
115
+ /** Validate a plugin manifest against its declared spec version's schema and split the verdict. */
80
116
  export async function validatePluginManifest(raw) {
81
- const errors = [];
82
- if (typeof raw !== 'object' || raw === null)
83
- return ['manifest is not a JSON object'];
117
+ if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
118
+ return { fatal: [{ keyword: 'type', instancePath: '', message: 'manifest is not a JSON object' }], ignored: [] };
119
+ }
84
120
  const record = raw;
85
- if (!isRecognizedSchema(record['$schema'])) {
86
- errors.push(`unrecognized $schema ${JSON.stringify(record['$schema'])}; this manager supports agent-plugins 1.0.0 only`);
121
+ const version = recognizedSpecVersion(record['$schema']);
122
+ if (version === undefined) {
123
+ return {
124
+ fatal: [{ keyword: 'const', instancePath: '', message: `unrecognized $schema ${JSON.stringify(record['$schema'])}; supported: agent-plugins 1.0.0, 1.1.0` }],
125
+ ignored: []
126
+ };
87
127
  }
88
- errors.push(...(await validateAgainstSchema(PLUGIN_SCHEMA_ID, raw)));
89
- return errors;
128
+ const errors = await validateAgainstSchema(version.pluginSchemaId, raw);
129
+ const fatal = [];
130
+ const ignored = [];
131
+ for (const error of errors) {
132
+ if (isExtensionPath(error.instancePath))
133
+ ignored.push(error);
134
+ else if (error.keyword === 'additionalProperties' && error.instancePath === '') {
135
+ const name = typeof error.params?.additionalProperty === 'string' ? error.params.additionalProperty : 'unknown field';
136
+ ignored.push({ ...error, message: `unknown top-level field "${name}" is not part of the portable manifest; ignored` });
137
+ }
138
+ else
139
+ fatal.push(error);
140
+ }
141
+ return { fatal, ignored };
90
142
  }
143
+ const KNOWN_MCP_TRANSPORTS = new Set(['stdio', 'streamable-http', 'sse']);
91
144
  /**
92
- * Parse and validate an `mcp.json` document: schema first, then §4 path
93
- * containment for `command` and `cwd` of every stdio server.
94
- * @returns the validated config plus per-problem strings; a failed `$schema`
95
- * or schema body drops the whole file, while per-server path problems
96
- * invalidate only their server.
145
+ * §7.2.1 remote URL rules: absolute http/https, no user information, no
146
+ * fragment, HTTPS everywhere except loopback hosts.
147
+ */
148
+ export function remoteUrlError(value) {
149
+ let url;
150
+ try {
151
+ url = new URL(value);
152
+ }
153
+ catch {
154
+ return `url "${value}" is not an absolute URL`;
155
+ }
156
+ if (url.protocol !== 'http:' && url.protocol !== 'https:')
157
+ return `url "${value}" must use http or https`;
158
+ if (url.username !== '' || url.password !== '')
159
+ return `url "${value}" must not contain user information`;
160
+ if (url.hash !== '')
161
+ return `url "${value}" must not contain a fragment`;
162
+ if (url.protocol === 'http:') {
163
+ const host = url.hostname;
164
+ const loopback = host === 'localhost' || host === '::1' || host === '[::1]' || host.endsWith('.localhost') || /^127\.\d+\.\d+\.\d+$/.test(host);
165
+ if (!loopback)
166
+ return `url "${value}" must use https (http is allowed only for loopback)`;
167
+ }
168
+ return undefined;
169
+ }
170
+ /**
171
+ * §7.2.1 header rules: valid field names, and a duplicate name under
172
+ * different casing makes the entry invalid.
173
+ */
174
+ export function headersError(headers) {
175
+ const seen = new Set();
176
+ for (const [name, value] of Object.entries(headers)) {
177
+ if (!/^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/.test(name))
178
+ return `header name ${JSON.stringify(name)} is not a valid HTTP header field name`;
179
+ if (typeof value !== 'string')
180
+ return `header ${JSON.stringify(name)} must have a string value`;
181
+ const lower = name.toLowerCase();
182
+ if (seen.has(lower))
183
+ return `header "${name}" is declared more than once under different casing`;
184
+ seen.add(lower);
185
+ }
186
+ return undefined;
187
+ }
188
+ /**
189
+ * Parse and validate an `mcp.json` document.
190
+ *
191
+ * Strict mode applies the two-level failure boundary of §7.2.2: a file-level
192
+ * violation (bad `$schema`, version mismatch with the manifest, non-object
193
+ * `mcpServers`) disables MCP for the whole plugin, while a per-server
194
+ * violation skips only that server. Lenient mode keeps the native-client
195
+ * behavior: no `$schema` requirement, unknown transports skipped per server.
196
+ *
197
+ * @returns the validated config plus per-problem strings; `config` stays
198
+ * defined whenever at least the file shell was valid, even when some
199
+ * servers were skipped.
97
200
  */
98
- const KNOWN_MCP_TRANSPORTS = new Set(['stdio', 'streamable-http', 'sse']);
99
201
  export async function validateMcpJson(pluginRoot, raw, options) {
100
202
  if (typeof raw !== 'object' || raw === null)
101
203
  return { errors: ['mcp.json is not a JSON object'] };
102
204
  const record = raw;
103
205
  const errors = [];
104
206
  const strict = options?.strict !== false;
207
+ const packageRules = strict && options?.packageRules !== false;
208
+ let version;
105
209
  if (strict) {
106
- if (!isRecognizedSchema(record['$schema'])) {
107
- return { errors: [`unrecognized mcp.json $schema ${JSON.stringify(record['$schema'])}; this manager supports agent-plugins 1.0.0 only`] };
210
+ version = recognizedSpecVersion(record['$schema']);
211
+ if (version === undefined) {
212
+ return { errors: [`unrecognized mcp.json $schema ${JSON.stringify(record['$schema'])}; supported: agent-plugins 1.0.0, 1.1.0`] };
108
213
  }
109
- const schemaErrors = await validateAgainstSchema(MCP_SCHEMA_ID, raw);
110
- if (schemaErrors.length > 0)
111
- return { errors: schemaErrors };
214
+ if (options?.manifestVersion !== undefined && options.manifestVersion !== version.version) {
215
+ return {
216
+ errors: [
217
+ `mcp.json declares agent-plugins ${version.version} while plugin.json declares ${options.manifestVersion}; the versions must match (§10.1) and MCP is disabled for this plugin`
218
+ ]
219
+ };
220
+ }
221
+ // File-level shape only: `$schema`, `mcpServers`, unknown top-level
222
+ // fields. Server entries are validated per server below (§7.2.2 rule 3).
223
+ const shellErrors = (await validateAgainstSchema(version.mcpSchemaId, raw)).filter(error => !error.instancePath.startsWith('/mcpServers/'));
224
+ // User-owned data may carry keys this client does not know; they ride along
225
+ // rather than failing the file.
226
+ const fatal = packageRules ? shellErrors : shellErrors.filter(error => error.keyword !== 'additionalProperties');
227
+ if (fatal.length > 0)
228
+ return { errors: formatSchemaErrors(fatal) };
112
229
  }
113
230
  // Lenient native-client files may omit the `mcpServers` wrapper and put the
114
231
  // server map at the top level (Claude Code `.mcp.json` shorthand).
@@ -125,6 +242,27 @@ export async function validateMcpJson(pluginRoot, raw, options) {
125
242
  errors.push(`server "${name}": not an object`);
126
243
  continue;
127
244
  }
245
+ if (strict && packageRules) {
246
+ // §7.2.1 exposes `#/$defs/server` so each entry can be validated
247
+ // independently, preserving §7.2.2 rule 3: a violation skips only its
248
+ // own server. Validate the bare entry against that ref directly —
249
+ // errors are then rooted at the entry itself (`/env`, `` for required).
250
+ // User-owned data goes straight to the per-transport checks below: its
251
+ // closed-set shape is deliberately not the package one.
252
+ const serverErrors = await validateAgainstSchema(`${version.mcpSchemaId}#/$defs/server`, value);
253
+ if (serverErrors.length > 0) {
254
+ const rendered = new Set();
255
+ for (const error of serverErrors) {
256
+ if (error.keyword === 'oneOf' || error.keyword === 'required')
257
+ continue;
258
+ rendered.add(`${error.instancePath === '' ? '' : `${error.instancePath} `}${error.message}`);
259
+ }
260
+ if (rendered.size === 0)
261
+ rendered.add('does not match any supported server variant');
262
+ errors.push(...[...rendered].map(problem => `server "${name}": ${problem || 'invalid server entry'}`));
263
+ continue;
264
+ }
265
+ }
128
266
  const server = value;
129
267
  // Claude Code dialects: `http`/`streamable-http` are the same remote
130
268
  // transport, and `local` or a missing `type` (with `command`) mean stdio.
@@ -138,6 +276,28 @@ export async function validateMcpJson(pluginRoot, raw, options) {
138
276
  continue;
139
277
  }
140
278
  if (type !== 'stdio') {
279
+ if (strict) {
280
+ if (typeof server.url !== 'string') {
281
+ errors.push(`server "${name}": remote servers require a url`);
282
+ continue;
283
+ }
284
+ const urlProblem = remoteUrlError(server.url);
285
+ if (urlProblem !== undefined) {
286
+ errors.push(`server "${name}": ${urlProblem}`);
287
+ continue;
288
+ }
289
+ if (server.headers !== undefined) {
290
+ if (typeof server.headers !== 'object' || server.headers === null) {
291
+ errors.push(`server "${name}": headers must be an object`);
292
+ continue;
293
+ }
294
+ const headerProblem = headersError(server.headers);
295
+ if (headerProblem !== undefined) {
296
+ errors.push(`server "${name}": ${headerProblem}`);
297
+ continue;
298
+ }
299
+ }
300
+ }
141
301
  valid[name] = { ...server, type };
142
302
  continue;
143
303
  }
@@ -147,8 +307,11 @@ export async function validateMcpJson(pluginRoot, raw, options) {
147
307
  errors.push(`server "${name}": stdio servers require a command`);
148
308
  continue;
149
309
  }
310
+ if (packageRules && /\$\{/.test(stdioServer.command)) {
311
+ problems.push(`command ${JSON.stringify(stdioServer.command)} must not contain placeholders (§9.2: command is resolved as one token, not expanded)`);
312
+ }
150
313
  const command = stdioServer.command.replace(/^\$\{([A-Z_]+)\}\//, (match, name) => (PLUGIN_ROOT_VARIABLES.has(name) ? './' : match));
151
- if (options?.pathMode !== 'project' && command.includes('/')) {
314
+ if (packageRules && options?.pathMode !== 'project' && command.includes('/')) {
152
315
  if (!command.startsWith('./')) {
153
316
  problems.push(`command "${command}" must be a bare executable name or a plugin-relative path beginning with "./"`);
154
317
  }
@@ -162,7 +325,7 @@ export async function validateMcpJson(pluginRoot, raw, options) {
162
325
  if (declaredCwd !== undefined && typeof declaredCwd !== 'string') {
163
326
  problems.push('cwd must be a string');
164
327
  }
165
- else if (options?.pathMode !== 'project' && declaredCwd !== undefined && ![...PLUGIN_DATA_VARIABLES].some(name => declaredCwd.startsWith(`\${${name}}`))) {
328
+ else if (packageRules && options?.pathMode !== 'project' && declaredCwd !== undefined && ![...PLUGIN_DATA_VARIABLES].some(name => declaredCwd.startsWith(`\${${name}}`))) {
166
329
  // `.` is the Codex dialect spelling for the plugin root.
167
330
  const cwd = declaredCwd === '.' ? './' : declaredCwd;
168
331
  const reason = await pathContainmentError(pluginRoot, cwd);
@@ -178,18 +341,6 @@ export async function validateMcpJson(pluginRoot, raw, options) {
178
341
  }
179
342
  return { config: { schema: typeof record['$schema'] === 'string' ? record['$schema'] : 'native-client', servers: valid }, errors };
180
343
  }
181
- /** Expand `${PLUGIN_ROOT}`, `${PLUGIN_DATA}`, and `${NAME}` (process env) in one string.
182
- * Claude Code aliases `${CLAUDE_PLUGIN_ROOT}` / `${CLAUDE_PLUGIN_DATA}` and
183
- * `${NAME:-default}` fallbacks are honored for unset/empty env vars. */
184
- export function expandPlaceholders(value, pluginRoot, pluginData, env = process.env) {
185
- return value.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\}/g, (match, name, fallback) => {
186
- if (PLUGIN_ROOT_VARIABLES.has(name))
187
- return pluginRoot;
188
- if (PLUGIN_DATA_VARIABLES.has(name))
189
- return pluginData;
190
- return env[name] || fallback || '';
191
- });
192
- }
193
344
  /** Resolve an expanded plugin-relative cwd to an absolute path. */
194
345
  export function resolveCwd(value, pluginRoot, pluginData) {
195
346
  if (value.startsWith('${PLUGIN_DATA}'))
@@ -6,6 +6,7 @@ export const MARKET_ROUTES = {
6
6
  overview: `${MARKET_API_PREFIX}overview`,
7
7
  mcpStatus: `${MARKET_API_PREFIX}mcp-status`,
8
8
  addMcpServer: `${MARKET_API_PREFIX}mcp-servers/add`,
9
+ importMcpServers: `${MARKET_API_PREFIX}mcp-servers/import`,
9
10
  lspStatus: `${MARKET_API_PREFIX}lsp-status`,
10
11
  lspServers: `${MARKET_API_PREFIX}lsp-servers`,
11
12
  addLspServer: `${MARKET_API_PREFIX}lsp-servers/add`,
@@ -28,6 +29,8 @@ export const MARKET_ROUTES = {
28
29
  setSurface: `${MARKET_API_PREFIX}set-surface`,
29
30
  mcpOverrides: `${MARKET_API_PREFIX}mcp-overrides`,
30
31
  setMcpOverride: `${MARKET_API_PREFIX}set-mcp-override`,
32
+ setMcpServerEnabled: `${MARKET_API_PREFIX}set-mcp-server-enabled`,
33
+ setMcpServerTool: `${MARKET_API_PREFIX}set-mcp-server-tool`,
31
34
  mcpRetry: `${MARKET_API_PREFIX}mcp-retry`,
32
35
  mcpReauthorize: `${MARKET_API_PREFIX}mcp-reauthorize`,
33
36
  mcpBackend: `${MARKET_API_PREFIX}mcp-backend`,
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Browser-safe settings contract of the market's host settings namespace: the
3
+ * namespace key, the fields with their defaults, and the one reader that turns
4
+ * a stored section into resolved values.
5
+ *
6
+ * The defaults live here and nowhere else. The namespace schema declares them
7
+ * to the host, the node half reads them back through {@link resolveMarketSettings},
8
+ * and the browser half — which cannot import `src/runtime` — reads the same
9
+ * resolved values off the settings snapshot it is handed. Changing a default is
10
+ * therefore a one-line edit in this module.
11
+ *
12
+ * @module contracts/settings
13
+ */
14
+ /** Settings namespace this plugin registers; the key the plugin-config tab pairs our card by. */
15
+ export const MARKET_SETTINGS_NAMESPACE = 'dsh-agent-plugins-market';
16
+ /**
17
+ * The default of every market setting — the single place one is written.
18
+ *
19
+ * These are user-visible behavior: a flip here is a behavior change that ships
20
+ * with its documentation, not a private implementation detail.
21
+ */
22
+ export const MARKET_SETTINGS_DEFAULTS = {
23
+ mcpEnhanced: true,
24
+ scanProjectLayouts: false,
25
+ downloadRegion: 'auto',
26
+ feedbackEnabled: true,
27
+ autoUpdateSources: false
28
+ };
29
+ /**
30
+ * Narrow an untrusted stored region to a setting; anything else reads as the
31
+ * declared default.
32
+ * @param value - the stored `downloadRegion` value.
33
+ * @param fallback - the value an unrecognized input reads as.
34
+ */
35
+ export function narrowDownloadRegion(value, fallback = MARKET_SETTINGS_DEFAULTS.downloadRegion) {
36
+ return value === 'auto' || value === 'global' || value === 'china' ? value : fallback;
37
+ }
38
+ /** One boolean field: a stored boolean, else the field's default. */
39
+ function narrowBoolean(value, fallback) {
40
+ return typeof value === 'boolean' ? value : fallback;
41
+ }
42
+ /**
43
+ * Resolve a stored settings section the way the host resolves it: each field
44
+ * takes its stored value when it is one this field accepts, and its declared
45
+ * default otherwise.
46
+ *
47
+ * The host hands the resolved section back on its own, but not every caller has
48
+ * one — the plugin's own halves read a section they may not have received yet,
49
+ * and a test hands in a bare literal. Funnelling all of them through this
50
+ * function is what keeps a second reading of "what does missing mean" from
51
+ * growing somewhere else.
52
+ * @param section - the stored (or absent) namespace section, untrusted.
53
+ */
54
+ export function resolveMarketSettings(section) {
55
+ const stored = (typeof section === 'object' && section !== null ? section : {});
56
+ return {
57
+ mcpEnhanced: narrowBoolean(stored['mcpEnhanced'], MARKET_SETTINGS_DEFAULTS.mcpEnhanced),
58
+ scanProjectLayouts: narrowBoolean(stored['scanProjectLayouts'], MARKET_SETTINGS_DEFAULTS.scanProjectLayouts),
59
+ downloadRegion: narrowDownloadRegion(stored['downloadRegion']),
60
+ feedbackEnabled: narrowBoolean(stored['feedbackEnabled'], MARKET_SETTINGS_DEFAULTS.feedbackEnabled),
61
+ autoUpdateSources: narrowBoolean(stored['autoUpdateSources'], MARKET_SETTINGS_DEFAULTS.autoUpdateSources)
62
+ };
63
+ }