dsh-agent-plugins-market 0.7.1 → 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.
- package/README.md +29 -24
- package/README.zh.md +29 -24
- package/client/client.js +1172 -159
- package/client/client.js.map +1 -1
- package/lib/application/catalog.js +14 -2
- package/lib/application/install-store.js +1 -1
- package/lib/application/mcp-service.js +185 -30
- package/lib/catalog/archive.js +15 -0
- package/lib/catalog/manifests.js +124 -13
- package/lib/catalog/scan-resolvers.js +117 -12
- package/lib/catalog/surfaces.js +73 -27
- package/lib/catalog/validate.js +205 -54
- package/lib/contracts/market.js +2 -0
- package/lib/index.js +6 -2
- package/lib/model/layouts.js +34 -0
- package/lib/model/preset-source.js +8 -0
- package/lib/routes.js +31 -2
- package/lib/runtime/agent-role-router.js +176 -51
- package/lib/runtime/host-locale.js +6 -12
- package/lib/runtime/mcp-client/connection.js +3 -2
- package/lib/runtime/mcp-client/oauth.js +82 -14
- package/lib/runtime/mcp-client/tools.js +2 -1
- package/lib/runtime/mcp-config.js +178 -34
- package/lib/runtime/mcp-direct-config.js +49 -6
- package/lib/runtime/mcp-mounts.js +15 -3
- package/lib/runtime/mcp-overrides.js +131 -1
- package/lib/runtime/mcp-redaction.js +8 -0
- package/lib/runtime/mcp-status.js +21 -5
- package/lib/runtime/server-config.js +49 -8
- package/lib/runtime/subagent-catalog.js +62 -22
- package/lib/runtime/tool-registry-observer.js +8 -4
- package/lib/types/application/catalog.d.ts +10 -1
- package/lib/types/application/install-store.d.ts +1 -1
- package/lib/types/application/mcp-service.d.ts +32 -3
- package/lib/types/application/queries.d.ts +6 -1
- package/lib/types/catalog/manifests.d.ts +6 -2
- package/lib/types/catalog/surfaces.d.ts +14 -4
- package/lib/types/catalog/validate.d.ts +72 -11
- package/lib/types/contracts/market.d.ts +67 -0
- package/lib/types/contracts/mcp-status.d.ts +11 -0
- package/lib/types/model/layouts.d.ts +20 -0
- package/lib/types/model/preset-source.d.ts +18 -0
- package/lib/types/model/types.d.ts +37 -1
- package/lib/types/runtime/agent-role-router.d.ts +90 -16
- package/lib/types/runtime/host-locale.d.ts +1 -1
- package/lib/types/runtime/mcp-client/oauth.d.ts +4 -0
- package/lib/types/runtime/mcp-config.d.ts +75 -13
- package/lib/types/runtime/mcp-direct-config.d.ts +22 -0
- package/lib/types/runtime/mcp-overrides.d.ts +44 -0
- package/lib/types/runtime/mcp-redaction.d.ts +6 -0
- package/lib/types/runtime/server-config.d.ts +26 -1
- package/lib/types/runtime/subagent-catalog.d.ts +14 -2
- package/lib/types/runtime/tool-registry-observer.d.ts +5 -3
- package/package.json +8 -8
- package/schemas/1.0.0/mcp.schema.json +0 -22
- package/schemas/1.1.0/mcp.schema.json +120 -0
- package/schemas/1.1.0/plugin.schema.json +65 -0
- package/schemas/README.md +25 -19
- package/schemas/agent-plugins/spec.md +23 -9
- package/schemas/com.deepseek.harness/namespace.schema.json +42 -0
- package/schemas/com.deepseek.harness/spec.md +52 -0
|
@@ -18,14 +18,17 @@
|
|
|
18
18
|
* 3. `FlatCollectionsStrategy` — the terminal fallback: manifest-less
|
|
19
19
|
* `<root>/<name>/SKILL.md` collections.
|
|
20
20
|
*/
|
|
21
|
-
import { realpath } from 'node:fs/promises';
|
|
21
|
+
import { realpath, readFile } from 'node:fs/promises';
|
|
22
22
|
import { join, resolve } from 'node:path';
|
|
23
23
|
import { isWithin, sanitizeId } from './paths.js';
|
|
24
24
|
import { isDirectory, isFile, listChildDirs } from './fs-probes.js';
|
|
25
|
+
import { EXTENSION_NAMESPACE, NAMESPACE_DIR, NAMESPACE_HOOKS_FILE, NAMESPACE_LSP_FILE } from '../model/layouts.js';
|
|
25
26
|
import { componentDeclarations, hasSuiteManifest, readManifest, readMarketplaces, syntheticManifestName } from './manifests.js';
|
|
26
27
|
import { countSurfaces, discoverMcp, discoverSkills, listMdFiles } from './surfaces.js';
|
|
27
28
|
import { discoverMarkdownResources, isUnknownArray } from './component-files.js';
|
|
28
29
|
import { discoverSuiteHooks, discoverSuiteLsp, discoverSystemPrompt } from './suite-components.js';
|
|
30
|
+
import { parseLspServers } from './lsp-spec.js';
|
|
31
|
+
import { normalizeHookDocuments } from './project-hooks.js';
|
|
29
32
|
import { runScanChain } from './scan-pipeline.js';
|
|
30
33
|
const CONTAINER_DIRS = ['plugins', 'external_plugins', 'skills'];
|
|
31
34
|
/**
|
|
@@ -327,10 +330,11 @@ export async function hasSkillFiles(dir) {
|
|
|
327
330
|
/** Read one suite root into the normalized shape, or undefined when no manifest parses. */
|
|
328
331
|
export async function readSuite(root, sourceId, dimension, hint, notes = []) {
|
|
329
332
|
const errors = [];
|
|
333
|
+
const manifestNotes = [];
|
|
330
334
|
const fallbacks = [];
|
|
331
335
|
const declaredManifest = await hasSuiteManifest(root);
|
|
332
|
-
let manifest = declaredManifest ? await readManifest(root, errors, hint, fallbacks) : await syntheticManifest(root);
|
|
333
|
-
if (declaredManifest &&
|
|
336
|
+
let manifest = declaredManifest ? await readManifest(root, errors, hint, fallbacks, manifestNotes) : await syntheticManifest(root);
|
|
337
|
+
if (declaredManifest && manifest === undefined) {
|
|
334
338
|
notes.push(...errors, ...fallbacks, `suite ${root}: rejected declared manifest`);
|
|
335
339
|
return undefined;
|
|
336
340
|
}
|
|
@@ -338,6 +342,7 @@ export async function readSuite(root, sourceId, dimension, hint, notes = []) {
|
|
|
338
342
|
// higher-priority declaration was skipped visible without failing the suite.
|
|
339
343
|
if (fallbacks.length > 0)
|
|
340
344
|
notes.push(`suite ${root}: a higher-priority manifest was rejected`, ...fallbacks);
|
|
345
|
+
notes.push(...manifestNotes);
|
|
341
346
|
// A declaration-only suite (official CC lsp plugins ship just a README):
|
|
342
347
|
// the marketplace entry's inline lspServers are its manifest.
|
|
343
348
|
if (manifest === undefined && hint !== undefined && Object.keys(componentDeclarations(hint)).length > 0) {
|
|
@@ -354,24 +359,41 @@ export async function readSuite(root, sourceId, dimension, hint, notes = []) {
|
|
|
354
359
|
}
|
|
355
360
|
if (manifest === undefined)
|
|
356
361
|
return undefined;
|
|
362
|
+
// The portable v1 dialect reads only the spec's fixed locations plus this
|
|
363
|
+
// client's extension namespace (§6.1, §8.2): skills and MCP from their
|
|
364
|
+
// fixed locations, extension surfaces from `com.deepseek.harness/`.
|
|
365
|
+
// Marketplace-entry and hint declarations carry no inline components for
|
|
366
|
+
// this dialect, so `components` stays empty and every discovery call below
|
|
367
|
+
// takes its namespace path.
|
|
368
|
+
const portable = manifest.layout === 'agent-plugin-v1';
|
|
369
|
+
// The namespace seats (manifest data and directory) open only when the
|
|
370
|
+
// suite declares our namespace with a supported contract version; an
|
|
371
|
+
// unsupported or missing declaration reads the portable core alone.
|
|
372
|
+
const namespaceOpen = portable && (manifest.harness !== undefined || manifest.schemaVersion !== undefined) && namespaceReadable(manifest);
|
|
357
373
|
let declared = manifest.components?.skills;
|
|
358
374
|
if (declared !== undefined && (manifest.layout === 'claude-code' || manifest.layout === 'codex') && (await isDirectory(join(root, 'skills')))) {
|
|
359
375
|
declared = ['skills', ...(isUnknownArray(declared) ? declared : [declared])];
|
|
360
376
|
}
|
|
361
|
-
|
|
377
|
+
if (portable)
|
|
378
|
+
await noteUnreadRootComponents(root, notes);
|
|
379
|
+
const skills = await discoverSkills(root, errors, declared, portable);
|
|
362
380
|
if (manifest.layout === 'skill-collection' && skills.length === 0) {
|
|
363
381
|
notes.push(...errors);
|
|
364
382
|
return undefined;
|
|
365
383
|
}
|
|
366
|
-
const mcp = await discoverMcp(root, errors, manifest);
|
|
384
|
+
const mcp = await discoverMcp(root, errors, manifest, portable);
|
|
367
385
|
// Effective component declarations include marketplace fallbacks and manifest overrides.
|
|
368
|
-
const lsp = await discoverSuiteLsp(root, manifest, errors);
|
|
369
|
-
const hooks = await discoverSuiteHooks(root, manifest, errors);
|
|
370
|
-
const resources =
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
386
|
+
const lsp = namespaceOpen ? await discoverNamespaceLsp(root, manifest, errors, notes) : await discoverSuiteLsp(root, manifest, errors);
|
|
387
|
+
const hooks = namespaceOpen ? await discoverNamespaceHooks(root, manifest, errors, notes) : await discoverSuiteHooks(root, manifest, errors);
|
|
388
|
+
const resources = namespaceOpen
|
|
389
|
+
? await discoverNamespaceMarkdown(root, notes)
|
|
390
|
+
: portable
|
|
391
|
+
? { commands: [], agents: [] }
|
|
392
|
+
: {
|
|
393
|
+
commands: await discoverMarkdownResources(root, 'commands', manifest.components?.commands, manifest.path, errors, manifest.layout === 'cursor' ? ['.md', '.mdc', '.markdown', '.txt'] : ['.md']),
|
|
394
|
+
agents: await discoverMarkdownResources(root, 'agents', manifest.components?.agents, manifest.path, errors)
|
|
395
|
+
};
|
|
396
|
+
const systemPrompt = portable ? undefined : await discoverSystemPrompt(root, manifest, errors);
|
|
375
397
|
const surfaces = await countSurfaces(root, skills, mcp, lsp);
|
|
376
398
|
surfaces.commands = resources.commands.length;
|
|
377
399
|
surfaces.agents = resources.agents.length;
|
|
@@ -428,3 +450,86 @@ async function syntheticManifest(root) {
|
|
|
428
450
|
name
|
|
429
451
|
};
|
|
430
452
|
}
|
|
453
|
+
// ---------------------------------------------------------------------------
|
|
454
|
+
// Extension namespace surfaces (Agent Plugins §8.2)
|
|
455
|
+
//
|
|
456
|
+
// A portable v1 suite carries this client's extension surfaces in the
|
|
457
|
+
// top-level directory named after the namespace. Contents outside it — the
|
|
458
|
+
// dialect root directories other layouts read — are package files this
|
|
459
|
+
// manager does not mount for the portable dialect, and their presence is
|
|
460
|
+
// reported through scan notes rather than read silently.
|
|
461
|
+
// ---------------------------------------------------------------------------
|
|
462
|
+
/** Root component locations other dialects read; under v1 their presence is reported, never read. */
|
|
463
|
+
const ROOT_COMPONENT_DIRS = ['commands', 'agents', 'hooks'];
|
|
464
|
+
/** `.mcp.json` is an alternative MCP path the portable format forbids; `mcp.json` itself is the fixed location. */
|
|
465
|
+
const ROOT_COMPONENT_FILES = ['hooks.json', 'lsp.json', '.lsp.json', '.mcp.json'];
|
|
466
|
+
/**
|
|
467
|
+
* Whether the namespace contract allows reading the extension seats: the
|
|
468
|
+
* manifest must carry our namespace with a supported `schemaVersion`. A
|
|
469
|
+
* directory without manifest data stays closed until the suite declares the
|
|
470
|
+
* contract, so a name collision on the filesystem cannot silently activate
|
|
471
|
+
* extension reading.
|
|
472
|
+
*/
|
|
473
|
+
function namespaceReadable(manifest) {
|
|
474
|
+
return manifest.harness !== undefined;
|
|
475
|
+
}
|
|
476
|
+
/** Report portable-suite content this manager does not mount, so nothing fails silently. */
|
|
477
|
+
async function noteUnreadRootComponents(root, notes) {
|
|
478
|
+
for (const dir of ROOT_COMPONENT_DIRS) {
|
|
479
|
+
if (await isDirectory(join(root, dir)))
|
|
480
|
+
notes.push(`suite ${root}: "${dir}/" is outside the portable layout and the ${EXTENSION_NAMESPACE} namespace; not read for this dialect`);
|
|
481
|
+
}
|
|
482
|
+
for (const file of ROOT_COMPONENT_FILES) {
|
|
483
|
+
if (await isFile(join(root, file)))
|
|
484
|
+
notes.push(`suite ${root}: "${file}" is outside the portable layout; not read for this dialect`);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
/** Commands and agents from `<namespace>/commands/` and `<namespace>/agents/`. */
|
|
488
|
+
async function discoverNamespaceMarkdown(root, notes) {
|
|
489
|
+
const namespaceRoot = join(root, NAMESPACE_DIR);
|
|
490
|
+
if (!(await isDirectory(namespaceRoot)))
|
|
491
|
+
return { commands: [], agents: [] };
|
|
492
|
+
const errors = [];
|
|
493
|
+
const commands = await discoverMarkdownResources(namespaceRoot, 'commands', undefined, join(namespaceRoot, 'plugin.json'), errors);
|
|
494
|
+
const agents = await discoverMarkdownResources(namespaceRoot, 'agents', undefined, join(namespaceRoot, 'plugin.json'), errors);
|
|
495
|
+
notes.push(...errors.map(error => `${EXTENSION_NAMESPACE}: ${error}`));
|
|
496
|
+
return { commands, agents };
|
|
497
|
+
}
|
|
498
|
+
/** Hooks from `<namespace>/hooks/hooks.json`, normalized through the command-hook bridge. */
|
|
499
|
+
async function discoverNamespaceHooks(root, manifest, errors, notes) {
|
|
500
|
+
const file = join(root, NAMESPACE_HOOKS_FILE);
|
|
501
|
+
if (!(await isFile(file)))
|
|
502
|
+
return undefined;
|
|
503
|
+
let value;
|
|
504
|
+
try {
|
|
505
|
+
value = JSON.parse(await readFile(file, 'utf8'));
|
|
506
|
+
}
|
|
507
|
+
catch (error) {
|
|
508
|
+
notes.push(`${NAMESPACE_HOOKS_FILE}: unparsable (${error instanceof Error ? error.message : String(error)})`);
|
|
509
|
+
return undefined;
|
|
510
|
+
}
|
|
511
|
+
const events = value?.hooks ?? value;
|
|
512
|
+
if (typeof events !== 'object' || events === null) {
|
|
513
|
+
notes.push(`${NAMESPACE_HOOKS_FILE}: must contain an event table`);
|
|
514
|
+
return undefined;
|
|
515
|
+
}
|
|
516
|
+
return normalizeHookDocuments(root, [{ file: NAMESPACE_HOOKS_FILE, settings: { hooks: events } }], errors);
|
|
517
|
+
}
|
|
518
|
+
/** LSP servers from `<namespace>/lsp.json`. */
|
|
519
|
+
async function discoverNamespaceLsp(root, manifest, errors, notes) {
|
|
520
|
+
const file = join(root, NAMESPACE_LSP_FILE);
|
|
521
|
+
if (!(await isFile(file)))
|
|
522
|
+
return undefined;
|
|
523
|
+
let value;
|
|
524
|
+
try {
|
|
525
|
+
value = JSON.parse(await readFile(file, 'utf8'));
|
|
526
|
+
}
|
|
527
|
+
catch (error) {
|
|
528
|
+
notes.push(`${NAMESPACE_LSP_FILE}: unparsable (${error instanceof Error ? error.message : String(error)})`);
|
|
529
|
+
return undefined;
|
|
530
|
+
}
|
|
531
|
+
const servers = Object.create(null);
|
|
532
|
+
const document = value.lspServers ?? value;
|
|
533
|
+
Object.assign(servers, parseLspServers(document, notes));
|
|
534
|
+
return Object.keys(servers).length > 0 ? { servers } : undefined;
|
|
535
|
+
}
|
package/lib/catalog/surfaces.js
CHANGED
|
@@ -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
|
-
/**
|
|
42
|
-
|
|
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 (
|
|
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
|
-
/**
|
|
149
|
-
|
|
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 =
|
|
153
|
-
? ['
|
|
154
|
-
: layout === '
|
|
155
|
-
? ['.mcp.json'
|
|
156
|
-
: layout === '
|
|
157
|
-
? ['.mcp.json', '
|
|
158
|
-
: layout === '
|
|
159
|
-
? ['.mcp.json', 'mcp.json']
|
|
160
|
-
: layout === '
|
|
161
|
-
? ['mcp.json']
|
|
162
|
-
:
|
|
163
|
-
|
|
164
|
-
|
|
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, {
|
|
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;
|