@adea-ai/plugins 1.0.0

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 (79) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +74 -0
  3. package/dist/agent-plugins-catalog.d.ts +26 -0
  4. package/dist/agent-plugins-catalog.js +699 -0
  5. package/dist/agent-plugins-catalog.js.map +1 -0
  6. package/dist/agent-plugins-harness.d.ts +54 -0
  7. package/dist/agent-plugins-harness.js +212 -0
  8. package/dist/agent-plugins-harness.js.map +1 -0
  9. package/dist/agent-plugins-schema.d.ts +402 -0
  10. package/dist/agent-plugins-schema.js +200 -0
  11. package/dist/agent-plugins-schema.js.map +1 -0
  12. package/dist/consumer-index.d.ts +217 -0
  13. package/dist/consumer-index.js +358 -0
  14. package/dist/consumer-index.js.map +1 -0
  15. package/dist/display-name.d.ts +8 -0
  16. package/dist/display-name.js +160 -0
  17. package/dist/display-name.js.map +1 -0
  18. package/dist/harness.d.ts +13 -0
  19. package/dist/harness.js +169 -0
  20. package/dist/harness.js.map +1 -0
  21. package/dist/icon-mirror.d.ts +69 -0
  22. package/dist/icon-mirror.js +147 -0
  23. package/dist/icon-mirror.js.map +1 -0
  24. package/dist/icons.d.ts +65 -0
  25. package/dist/icons.js +268 -0
  26. package/dist/icons.js.map +1 -0
  27. package/dist/index.d.ts +304 -0
  28. package/dist/index.js +1877 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/portable-catalog.d.ts +9 -0
  31. package/dist/portable-catalog.js +175 -0
  32. package/dist/portable-catalog.js.map +1 -0
  33. package/dist/publication.d.ts +98 -0
  34. package/dist/publication.js +121 -0
  35. package/dist/publication.js.map +1 -0
  36. package/dist/schema.d.ts +705 -0
  37. package/dist/schema.js +267 -0
  38. package/dist/schema.js.map +1 -0
  39. package/dist/site-icons.d.ts +55 -0
  40. package/dist/site-icons.js +155 -0
  41. package/dist/site-icons.js.map +1 -0
  42. package/dist/sources.d.ts +54 -0
  43. package/dist/sources.js +306 -0
  44. package/dist/sources.js.map +1 -0
  45. package/package.json +102 -0
  46. package/schemas/agent-package.v1.json +387 -0
  47. package/schemas/catalog.v1.schema.json +407 -0
  48. package/schemas/compatibility.v1.schema.json +23 -0
  49. package/schemas/harness-profile.v1.json +75 -0
  50. package/schemas/installation-plan.v2.json +695 -0
  51. package/schemas/sources-lock.v1.schema.json +61 -0
  52. package/src/agent-plugins-catalog.test.ts +402 -0
  53. package/src/agent-plugins-catalog.ts +800 -0
  54. package/src/agent-plugins-harness.test.ts +272 -0
  55. package/src/agent-plugins-harness.ts +281 -0
  56. package/src/agent-plugins-schema.test.ts +27 -0
  57. package/src/agent-plugins-schema.ts +219 -0
  58. package/src/artifacts.test.ts +545 -0
  59. package/src/catalog-installation.test.ts +42 -0
  60. package/src/consumer-index.test.ts +465 -0
  61. package/src/consumer-index.ts +592 -0
  62. package/src/display-name.test.ts +56 -0
  63. package/src/display-name.ts +157 -0
  64. package/src/harness.ts +202 -0
  65. package/src/icon-mirror.ts +196 -0
  66. package/src/icons.test.ts +185 -0
  67. package/src/icons.ts +297 -0
  68. package/src/index.test.ts +888 -0
  69. package/src/index.ts +2538 -0
  70. package/src/portable-catalog.test.ts +186 -0
  71. package/src/portable-catalog.ts +203 -0
  72. package/src/publication.test.ts +117 -0
  73. package/src/publication.ts +171 -0
  74. package/src/schema.ts +309 -0
  75. package/src/site-icon-fallback.test.ts +267 -0
  76. package/src/site-icons.test.ts +171 -0
  77. package/src/site-icons.ts +161 -0
  78. package/src/sources.ts +360 -0
  79. package/src/upstream-headers.test.ts +148 -0
@@ -0,0 +1,61 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://agent-hq.example/schemas/sources-lock.v1.schema.json",
4
+ "type": "object",
5
+ "additionalProperties": false,
6
+ "required": ["schemaVersion", "lockId", "sources"],
7
+ "properties": {
8
+ "schemaVersion": { "const": 1 },
9
+ "lockId": { "type": "string", "pattern": "^lock:[a-f0-9]{64}$" },
10
+ "sources": {
11
+ "type": "array",
12
+ "items": {
13
+ "type": "object",
14
+ "additionalProperties": false,
15
+ "required": [
16
+ "sourceId",
17
+ "repositoryUrl",
18
+ "defaultBranch",
19
+ "resolvedCommitSha",
20
+ "manifestPath",
21
+ "marketplaceDialect",
22
+ "sourceManifestDigest",
23
+ "retrievedAt",
24
+ "synchronizationStatus",
25
+ "pluginPins"
26
+ ],
27
+ "properties": {
28
+ "sourceId": { "type": "string" },
29
+ "repositoryUrl": { "type": "string", "format": "uri" },
30
+ "defaultBranch": { "type": "string" },
31
+ "resolvedCommitSha": { "type": "string", "pattern": "^[a-f0-9]{40}$" },
32
+ "manifestPath": { "type": "string" },
33
+ "marketplaceDialect": { "type": "string" },
34
+ "sourceManifestDigest": { "type": "string", "pattern": "^sha256:[a-f0-9]{64}$" },
35
+ "retrievedAt": { "type": "string", "format": "date-time" },
36
+ "synchronizationStatus": { "type": "string" },
37
+ "pluginPins": {
38
+ "type": "array",
39
+ "maxItems": 4096,
40
+ "items": {
41
+ "type": "object",
42
+ "additionalProperties": false,
43
+ "required": [
44
+ "pluginName",
45
+ "repositoryUrl",
46
+ "pluginSubdirectory",
47
+ "resolvedCommitSha"
48
+ ],
49
+ "properties": {
50
+ "pluginName": { "type": "string", "minLength": 1 },
51
+ "repositoryUrl": { "type": "string", "format": "uri" },
52
+ "pluginSubdirectory": { "type": "string" },
53
+ "resolvedCommitSha": { "type": "string", "pattern": "^[a-f0-9]{40}$" }
54
+ }
55
+ }
56
+ }
57
+ }
58
+ }
59
+ }
60
+ }
61
+ }
@@ -0,0 +1,402 @@
1
+ import { describe, it } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { PLUGIN_SCHEMA_ID, MCP_SCHEMA_ID } from './agent-plugins-schema.js'
4
+ import {
5
+ compileAgentPackage,
6
+ materializeAgentPackage,
7
+ verifyAgentPackage,
8
+ parsePluginJson,
9
+ validateServer,
10
+ sourceDigest,
11
+ packageDigest,
12
+ sha256,
13
+ } from './agent-plugins-catalog.js'
14
+
15
+ const bytes = (value: string) => new TextEncoder().encode(value)
16
+ const text = (value: Uint8Array | undefined) => new TextDecoder().decode(value)
17
+ const skill = (name = 'review') =>
18
+ `---\nname: ${name}\ndescription: Review changes.\n---\nSee [guide](references/guide.md).\n`
19
+ function fixture(extra: Record<string, string> = {}): Map<string, Uint8Array> {
20
+ return new Map(
21
+ Object.entries({
22
+ 'plugin.json': JSON.stringify({ $schema: PLUGIN_SCHEMA_ID, name: 'review-kit' }),
23
+ 'skills/review/SKILL.md': skill(),
24
+ 'skills/review/references/guide.md': 'A reference.\n',
25
+ 'skills/review/scripts/check.py': 'print("fixture only")\n',
26
+ 'bin/server.mjs': '// An inert fixture; tests never execute it.\n',
27
+ 'mcp.json': JSON.stringify({
28
+ $schema: MCP_SCHEMA_ID,
29
+ mcpServers: {
30
+ local: {
31
+ type: 'stdio',
32
+ command: 'node',
33
+ args: ['${PLUGIN_ROOT}/bin/server.mjs'],
34
+ env: { CACHE: '${PLUGIN_DATA}/cache' },
35
+ },
36
+ remote: { type: 'streamable-http', url: 'https://example.test/mcp' },
37
+ },
38
+ }),
39
+ ...extra,
40
+ }).map(([path, value]) => [path, bytes(value)])
41
+ )
42
+ }
43
+ function compile(files = fixture()) {
44
+ return compileAgentPackage({ files, name: 'review-kit' })
45
+ }
46
+
47
+ describe('canonical Agent Plugins package compiler', () => {
48
+ it('preserves complete skill resources and source file modes without executing helpers', () => {
49
+ const source = fixture()
50
+ const pkg = compile(source)
51
+ const materialized = materializeAgentPackage(pkg, source)
52
+ assert.equal(pkg.status, 'portable')
53
+ assert.equal(pkg.skills.length, 1)
54
+ assert.equal(pkg.sourceDigest, sourceDigest(source))
55
+ assert.equal(pkg.packageDigest, packageDigest(pkg.files))
56
+ for (const path of [
57
+ 'skills/review/references/guide.md',
58
+ 'skills/review/scripts/check.py',
59
+ 'bin/server.mjs',
60
+ ]) {
61
+ assert.deepEqual(materialized.get(path), source.get(path))
62
+ assert.equal(pkg.files.find((file) => file.targetPath === path)?.action, 'copy')
63
+ }
64
+ assert.equal(
65
+ pkg.files.some((file) => file.targetPath.includes('skills/skills/')),
66
+ false
67
+ )
68
+ assert.equal(pkg.requirements.environmentReview, true)
69
+ })
70
+ it('is byte-deterministic independent of snapshot insertion order', () => {
71
+ const source = fixture()
72
+ const reversed = new Map([...source].toReversed())
73
+ assert.deepEqual(compile(source), compile(reversed))
74
+ })
75
+ it('treats canonical root metadata as authoritative over nested client manifests', () => {
76
+ const pkg = compile(
77
+ fixture({
78
+ '.claude-plugin/plugin.json': '{invalid',
79
+ 'plugin.json': JSON.stringify({
80
+ $schema: PLUGIN_SCHEMA_ID,
81
+ name: 'root-wins',
82
+ version: 'dev-build',
83
+ homepage: 'not a url',
84
+ author: { email: 'not an email' },
85
+ }),
86
+ })
87
+ )
88
+ assert.equal(pkg.manifest?.name, 'root-wins')
89
+ assert.equal(pkg.manifest?.version, 'dev-build')
90
+ })
91
+ it('reports and ignores unknown manifest fields rather than using alternate discovery paths', () => {
92
+ const pkg = compile(
93
+ fixture({
94
+ 'plugin.json': JSON.stringify({
95
+ $schema: PLUGIN_SCHEMA_ID,
96
+ name: 'review-kit',
97
+ skills: './elsewhere',
98
+ mcpServers: { forged: { command: 'no' } },
99
+ }),
100
+ })
101
+ )
102
+ assert.equal(pkg.skills.length, 1)
103
+ assert.equal(Object.keys(pkg.mcpServers).length, 2)
104
+ assert.equal(pkg.diagnostics.filter((item) => item.code === 'UNKNOWN_MANIFEST_FIELD').length, 2)
105
+ assert.equal('skills' in pkg.manifest!, false)
106
+ })
107
+ for (const raw of [
108
+ { name: 'bad', $schema: 'https://agent-plugins.org/schemas/9/plugin.schema.json' },
109
+ { name: 'Bad', $schema: PLUGIN_SCHEMA_ID },
110
+ { name: 'good', $schema: PLUGIN_SCHEMA_ID, version: 5 },
111
+ [],
112
+ { name: 'good', $schema: PLUGIN_SCHEMA_ID, author: { unknown: 'x' } },
113
+ ]) {
114
+ it(`rejects invalid native metadata without a legacy fallback: ${JSON.stringify(raw)}`, () => {
115
+ const pkg = compile(fixture({ 'plugin.json': JSON.stringify(raw) }))
116
+ // A root array is a malformed legacy manifest, also unavailable.
117
+ assert.equal(pkg.status, 'unavailable')
118
+ assert.deepEqual(pkg.files, [])
119
+ })
120
+ }
121
+ it('ignores non-object extensions and does not recursively validate unknown namespace data', () => {
122
+ assert.notEqual(
123
+ compile(
124
+ fixture({
125
+ 'plugin.json': JSON.stringify({
126
+ $schema: PLUGIN_SCHEMA_ID,
127
+ name: 'x',
128
+ extensions: false,
129
+ }),
130
+ })
131
+ ).status,
132
+ 'unavailable'
133
+ )
134
+ const pkg = compile(
135
+ fixture({
136
+ 'plugin.json': JSON.stringify({
137
+ $schema: PLUGIN_SCHEMA_ID,
138
+ name: 'x',
139
+ extensions: { 'com.example.client': { arbitrary: [1, false] } },
140
+ }),
141
+ })
142
+ )
143
+ assert.equal(pkg.status, 'partial')
144
+ assert.equal(pkg.nonPortable[0]?.kind, 'client-extension')
145
+ })
146
+ it('discovers only immediate skill directories and retains deeper SKILL.md as support content', () => {
147
+ const pkg = compile(fixture({ 'skills/review/references/nested/SKILL.md': skill('nested') }))
148
+ assert.deepEqual(
149
+ pkg.skills.map((item) => item.name),
150
+ ['review']
151
+ )
152
+ assert.ok(
153
+ pkg.files.some((file) => file.targetPath === 'skills/review/references/nested/SKILL.md')
154
+ )
155
+ })
156
+ for (const bad of [
157
+ '---\nname: wrong\ndescription: x\n---\n',
158
+ '---\nname: review\nname: review\ndescription: x\n---\n',
159
+ '---\nname: review\ndescription: x\nmetadata:\n enabled: true\n---\n',
160
+ '---\nname: review\ndescription: !untrusted x\n---\n',
161
+ 'no yaml',
162
+ ]) {
163
+ it(`isolates invalid skill frontmatter: ${bad.slice(0, 32)}`, () => {
164
+ const pkg = compile(
165
+ fixture({ 'skills/review/SKILL.md': bad, 'skills/good/SKILL.md': skill('good') })
166
+ )
167
+ assert.equal(pkg.status, 'partial')
168
+ assert.deepEqual(
169
+ pkg.skills.map((item) => item.name),
170
+ ['good']
171
+ )
172
+ assert.equal(
173
+ pkg.files.some((file) => file.targetPath === 'skills/review/SKILL.md'),
174
+ false
175
+ )
176
+ assert.equal(Object.keys(pkg.mcpServers).length, 2)
177
+ })
178
+ }
179
+ it('supports folded YAML descriptions and CRLF without rewriting skill bytes', () => {
180
+ const raw =
181
+ '---\r\nname: review\r\ndescription: >-\r\n Review changes\r\n carefully.\r\n---\r\nbody\r\n'
182
+ const files = fixture({ 'skills/review/SKILL.md': raw })
183
+ const pkg = compile(files)
184
+ assert.equal(pkg.skills[0]?.description, 'Review changes carefully.')
185
+ assert.equal(text(materializeAgentPackage(pkg, files).get('skills/review/SKILL.md')), raw)
186
+ })
187
+ it('disables only MCP on top-level schema mismatch', () => {
188
+ const pkg = compile(
189
+ fixture({ 'mcp.json': JSON.stringify({ $schema: 'unsupported', mcpServers: {} }) })
190
+ )
191
+ assert.equal(pkg.skills.length, 1)
192
+ assert.deepEqual(pkg.mcpServers, {})
193
+ assert.ok(pkg.diagnostics.some((item) => item.code === 'INVALID_MCP_CONFIGURATION'))
194
+ })
195
+ it('isolates individual invalid MCP entries', () => {
196
+ const pkg = compile(
197
+ fixture({
198
+ 'mcp.json': JSON.stringify({
199
+ $schema: MCP_SCHEMA_ID,
200
+ mcpServers: {
201
+ good: { type: 'stdio', command: 'node' },
202
+ bad: { type: 'stdio', command: 'node --flag' },
203
+ mixed: { type: 'stdio', command: 'node', url: 'https://example.test' },
204
+ },
205
+ }),
206
+ })
207
+ )
208
+ assert.deepEqual(Object.keys(pkg.mcpServers), ['good'])
209
+ assert.equal(pkg.skills.length, 1)
210
+ assert.equal(pkg.diagnostics.filter((item) => item.code === 'INVALID_MCP_SERVER').length, 2)
211
+ })
212
+ it('does not treat missing component locations as invalid', () => {
213
+ const files = new Map([
214
+ ['plugin.json', bytes(JSON.stringify({ $schema: PLUGIN_SCHEMA_ID, name: 'minimal' }))],
215
+ ])
216
+ const pkg = compile(files)
217
+ assert.equal(pkg.status, 'portable')
218
+ assert.equal(pkg.diagnostics.length, 0)
219
+ })
220
+ it('handles wrong component filesystem kinds without invalidating other component types', () => {
221
+ const files = fixture()
222
+ files.delete('mcp.json')
223
+ files.set('mcp.json/not-a-file', bytes('x'))
224
+ const pkg = compile(files)
225
+ assert.equal(pkg.skills.length, 1)
226
+ assert.equal(pkg.status, 'partial')
227
+ assert.doesNotThrow(() => verifyAgentPackage(pkg))
228
+ })
229
+ it('normalizes explicit legacy HTTP and package-root stdio references', () => {
230
+ const files = fixture()
231
+ files.delete('plugin.json')
232
+ files.delete('mcp.json')
233
+ files.set(
234
+ '.claude-plugin/plugin.json',
235
+ bytes(JSON.stringify({ name: 'review-kit', version: '1' }))
236
+ )
237
+ files.set(
238
+ '.mcp.json',
239
+ bytes(
240
+ JSON.stringify({
241
+ mcpServers: {
242
+ remote: { type: 'http', url: 'https://example.test/mcp' },
243
+ local: { command: 'node', args: ['${CLAUDE_PLUGIN_ROOT}/bin/server.mjs'] },
244
+ },
245
+ })
246
+ )
247
+ )
248
+ const pkg = compile(files)
249
+ assert.equal(pkg.originFormat, 'legacy')
250
+ assert.equal(pkg.status, 'portable')
251
+ assert.equal(pkg.mcpServers.remote?.type, 'streamable-http')
252
+ assert.equal(
253
+ pkg.mcpServers.local?.type === 'stdio' && pkg.mcpServers.local.args?.[0],
254
+ '${PLUGIN_ROOT}/bin/server.mjs'
255
+ )
256
+ assert.doesNotThrow(() => materializeAgentPackage(pkg, files))
257
+ })
258
+ it('does not silently convert credential interpolation or infer a URL transport', () => {
259
+ const files = fixture()
260
+ files.delete('plugin.json')
261
+ files.delete('mcp.json')
262
+ files.set(
263
+ '.mcp.json',
264
+ bytes(
265
+ JSON.stringify({
266
+ mcpServers: {
267
+ credential: {
268
+ type: 'http',
269
+ url: 'https://example.test',
270
+ headers: { Authorization: 'Bearer ${TOKEN}' },
271
+ },
272
+ ambiguous: { url: 'https://example.test/sse' },
273
+ },
274
+ })
275
+ )
276
+ )
277
+ const pkg = compile(files)
278
+ assert.deepEqual(pkg.mcpServers, {})
279
+ assert.equal(pkg.status, 'partial')
280
+ })
281
+ it('reports nonportable hooks/commands without disabling skills or granting execution', () => {
282
+ const pkg = compile(fixture({ 'hooks/hooks.json': '{}', 'commands/run.md': 'legacy command' }))
283
+ assert.equal(pkg.status, 'partial')
284
+ assert.equal(pkg.skills.length, 1)
285
+ assert.deepEqual(
286
+ pkg.nonPortable.map((item) => item.kind),
287
+ ['commands', 'hooks']
288
+ )
289
+ })
290
+ it('rejects source and generated-byte tampering', () => {
291
+ const source = fixture()
292
+ const pkg = compile(source)
293
+ source.set('bin/server.mjs', bytes('changed'))
294
+ assert.throws(() => materializeAgentPackage(pkg, source), /digest/)
295
+ const changed = structuredClone(pkg)
296
+ const file = changed.files.find((item) => item.action === 'write')!
297
+ if (file.action === 'write') file.content += ' '
298
+ assert.throws(() => verifyAgentPackage(changed), /digest/)
299
+ })
300
+ it('rejects a hidden skill inventory even if the file-set digest has not changed', () => {
301
+ const pkg = compile()
302
+ pkg.skills = []
303
+ pkg.requirements.skills = false
304
+ assert.throws(() => verifyAgentPackage(pkg), /inventory/)
305
+ })
306
+ for (const path of ['../escape', '/absolute', 'C:/windows', 'a\\b', 'a/../b', 'null\0byte']) {
307
+ it(`rejects unsafe snapshot path ${JSON.stringify(path)}`, () => {
308
+ assert.equal(compile(fixture({ [path]: 'x' })).status, 'unavailable')
309
+ })
310
+ }
311
+ it('rejects case aliases, symlinks and file/directory collisions', () => {
312
+ assert.equal(compile(fixture({ 'BIN/server.mjs': 'x' })).status, 'unavailable')
313
+ assert.equal(compile(fixture({ bin: 'not a directory' })).status, 'unavailable')
314
+ assert.equal(
315
+ compileAgentPackage({ files: fixture(), name: 'x', symlinks: ['skills/outside'] }).status,
316
+ 'unavailable'
317
+ )
318
+ })
319
+ it('isolates generated control-file aliases and cross-platform path collisions', () => {
320
+ for (const path of [
321
+ 'Plugin.json',
322
+ 'Mcp.json',
323
+ 'BIN/other.mjs',
324
+ 'nul.txt',
325
+ 'resources/a.',
326
+ 'resources/a ',
327
+ ]) {
328
+ const pkg = compile(fixture({ [path]: 'x' }))
329
+ assert.equal(pkg.status, 'unavailable', path)
330
+ assert.doesNotThrow(() => verifyAgentPackage(pkg))
331
+ }
332
+ })
333
+ it('rejects duplicate escaped JSON keys without prototype mutation', () => {
334
+ assert.throws(() => parsePluginJson('{"x":1,"\\u0078":2}'))
335
+ assert.throws(() => parsePluginJson('{"a":{"x":1,"x":2}}'))
336
+ const value = parsePluginJson('{"__proto__":{"polluted":true}}') as Record<string, unknown>
337
+ assert.equal(Object.prototype.hasOwnProperty.call(value, '__proto__'), true)
338
+ assert.equal(Object.prototype.hasOwnProperty.call({}, 'polluted'), false)
339
+ })
340
+ })
341
+
342
+ describe('MCP configuration semantics', () => {
343
+ for (const server of [
344
+ { type: 'stdio', command: '/usr/bin/node' },
345
+ { type: 'stdio', command: '${PLUGIN_ROOT}/server' },
346
+ { type: 'stdio', command: 'node --flag' },
347
+ { type: 'stdio', command: 'node', env: { PLUGIN_DATA: 'override' } },
348
+ { type: 'stdio', command: 'node', env: { plugin_root: 'override' } },
349
+ { type: 'stdio', command: 'node', cwd: '${PLUGIN_DATA}/../../escape' },
350
+ { type: 'stdio', command: 'node', cwd: 'relative' },
351
+ { type: 'streamable-http', url: 'http://example.test/mcp' },
352
+ { type: 'streamable-http', url: 'https://user:password@example.test/mcp' },
353
+ { type: 'streamable-http', url: 'https://example.test/mcp#' },
354
+ {
355
+ type: 'streamable-http',
356
+ url: 'https://example.test/mcp',
357
+ headers: { Tenant: 'a', tenant: 'b' },
358
+ },
359
+ {
360
+ type: 'streamable-http',
361
+ url: 'https://example.test/mcp',
362
+ headers: { Authorization: 'Bearer secret' },
363
+ },
364
+ { type: 'streamable-http', url: 'https://example.test/mcp', headers: { Host: 'other.test' } },
365
+ { type: 'streamable-http', url: 'https://example.test/mcp', headers: { Tenant: 'a\r\nb' } },
366
+ ])
367
+ it(`rejects invalid server ${JSON.stringify(server)}`, () =>
368
+ assert.throws(() => validateServer(server)))
369
+ it('accepts loopback and keeps declared transport, args, and literal remote headers', () => {
370
+ for (const url of ['http://localhost/mcp', 'http://127.0.0.2/mcp', 'http://[::1]/mcp'])
371
+ assert.equal(validateServer({ type: 'sse', url }).type, 'sse')
372
+ assert.deepEqual(
373
+ validateServer({
374
+ type: 'streamable-http',
375
+ url: 'https://example.test/mcp',
376
+ headers: { 'X-Literal': '${PLUGIN_DATA}' },
377
+ }),
378
+ {
379
+ type: 'streamable-http',
380
+ url: 'https://example.test/mcp',
381
+ headers: { 'X-Literal': '${PLUGIN_DATA}' },
382
+ }
383
+ )
384
+ assert.doesNotThrow(() =>
385
+ validateServer({
386
+ type: 'stdio',
387
+ command: './bin/../server',
388
+ args: ['space in argument', '--flag'],
389
+ })
390
+ )
391
+ })
392
+ it('detects rehashed control-file/descriptor disagreement', () => {
393
+ const pkg = compile()
394
+ const file = pkg.files.find((item) => item.targetPath === 'mcp.json')!
395
+ if (file.action === 'write') {
396
+ file.content = '{}\n'
397
+ file.digest = sha256(file.content)
398
+ }
399
+ pkg.packageDigest = packageDigest(pkg.files)
400
+ assert.throws(() => verifyAgentPackage(pkg), /MCP descriptor/)
401
+ })
402
+ })