@dshforge/agent-plugins 0.0.0-stage → 0.1.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.
package/lib/rpc.js ADDED
@@ -0,0 +1,147 @@
1
+ import { clientRequestSchema } from '@deepseek-ai/dsh-client-connection'
2
+ import { readdir, stat } from 'node:fs/promises'
3
+ import { join } from 'node:path'
4
+ import { resolveExistingInside } from './paths.js'
5
+ import { errorMessage } from './validation.js'
6
+
7
+ /** Build the catalog-only Connection RPC handler without exposing config values. */
8
+ export function createAgentPluginsRpcHandler(catalog, { install } = {}) {
9
+ const readCatalog = () => typeof catalog === 'function' ? catalog() : catalog
10
+ return async (endpoint, payload, signal, peer) => {
11
+ if (peer.principal.kind !== 'owner') {
12
+ return failure('agent-plugins/forbidden', 'Agent Plugins catalog is available to owner peers only')
13
+ }
14
+ if (signal.aborted) return failure('agent-plugins/cancelled', 'Agent Plugins catalog request was cancelled')
15
+ if (endpoint === 'catalog') return { ok: true, value: projectCatalog(readCatalog()) }
16
+ if (endpoint === 'files') return listPluginDirectory(readCatalog(), payload)
17
+ if (endpoint === 'install' && install !== undefined) {
18
+ if (typeof payload?.spec !== 'string') return failure('agent-plugins/invalid-request', 'install requires a Git spec')
19
+ try {
20
+ return { ok: true, value: await install(payload.spec, signal) }
21
+ } catch (error) {
22
+ return failure(typeof error?.code === 'string' && error.code.startsWith('agent-plugins/')
23
+ ? error.code
24
+ : 'agent-plugins/install-failed', errorMessage(error))
25
+ }
26
+ }
27
+ return failure('agent-plugins/not-found', `Unknown Agent Plugins endpoint ${JSON.stringify(endpoint)}`)
28
+ }
29
+ }
30
+
31
+ async function listPluginDirectory(catalog, payload) {
32
+ if (typeof payload?.root !== 'string' || typeof payload.path !== 'string') {
33
+ return failure('agent-plugins/invalid-request', 'files requires root and path')
34
+ }
35
+ const plugin = catalog.plugins.find(entry => entry.directoryName === payload.root)
36
+ if (!plugin || plugin.status !== 'ready') return failure('agent-plugins/not-found', 'Plugin directory was not found')
37
+ const relative = payload.path
38
+ if (relative.includes('\0') || relative.includes('\\') || relative.startsWith('/') || relative.split('/').includes('..')) {
39
+ return failure('agent-plugins/invalid-path', 'Directory path must stay within the plugin root')
40
+ }
41
+ const directory = join(plugin.root, relative)
42
+ let resolved
43
+ try {
44
+ resolved = await resolveExistingInside(plugin.root, directory)
45
+ if (!(await stat(resolved)).isDirectory()) {
46
+ return failure('agent-plugins/not-directory', 'Requested path is not a directory')
47
+ }
48
+ } catch (error) {
49
+ return failure('agent-plugins/invalid-path', errorMessage(error))
50
+ }
51
+ let entries
52
+ try {
53
+ entries = await readdir(resolved, { withFileTypes: true })
54
+ } catch (error) {
55
+ return failure('agent-plugins/unavailable', errorMessage(error))
56
+ }
57
+ const rows = entries
58
+ .filter(entry => !entry.isSymbolicLink() && (entry.isDirectory() || entry.isFile()))
59
+ .map(entry => ({
60
+ name: entry.name,
61
+ kind: entry.isDirectory() ? 'directory' : 'file',
62
+ path: relative ? `${relative}/${entry.name}` : entry.name,
63
+ }))
64
+ .sort((a, b) => a.kind === b.kind ? a.name.localeCompare(b.name) : a.kind === 'directory' ? -1 : 1)
65
+ return { ok: true, value: { path: relative, entries: rows.slice(0, 200), truncated: rows.length > 200 } }
66
+ }
67
+
68
+ /** Register the catalog over the existing authenticated Connection fetch bridge. */
69
+ export function registerAgentPluginsRpc(ctx, catalog, options) {
70
+ const handler = createAgentPluginsRpcHandler(catalog, options)
71
+ return ctx.get('webServer').register({
72
+ kind: 'prefix',
73
+ path: '/agent-plugins',
74
+ handler: async (request, response) => {
75
+ const admission = ctx.connection.admit(request)
76
+ if ('rejection' in admission) {
77
+ response.writeHead(admission.rejection).end(admission.rejection === 401 ? 'unauthorized' : 'forbidden')
78
+ return
79
+ }
80
+ if (request.method !== 'POST') {
81
+ response.writeHead(404).end('not found')
82
+ return
83
+ }
84
+ const endpoint = new URL(request.url, 'http://localhost').pathname.slice('/agent-plugins/'.length)
85
+ if (!['catalog', 'files', 'install'].includes(endpoint)) {
86
+ response.writeHead(404).end('not found')
87
+ return
88
+ }
89
+ let message
90
+ try {
91
+ message = clientRequestSchema.parse(await readJson(request))
92
+ } catch {
93
+ response.writeHead(400).end('invalid client-request message')
94
+ return
95
+ }
96
+ if (message.method !== endpoint) {
97
+ response.writeHead(400).end('method does not match endpoint')
98
+ return
99
+ }
100
+ const controller = new AbortController()
101
+ response.on('close', () => controller.abort())
102
+ const result = await handler(endpoint, message.payload, controller.signal, admission.peer)
103
+ response.writeHead(200, { 'content-type': 'application/json' })
104
+ response.end(JSON.stringify({ type: 'server-response', rpcId: message.rpcId, result }))
105
+ },
106
+ })
107
+ }
108
+
109
+ async function readJson(request) {
110
+ const chunks = []
111
+ let size = 0
112
+ for await (const chunk of request) {
113
+ size += chunk.byteLength
114
+ if (size > 1024 * 1024) {
115
+ request.destroy()
116
+ throw new Error('request body exceeds 1 MiB')
117
+ }
118
+ chunks.push(chunk)
119
+ }
120
+ return JSON.parse(Buffer.concat(chunks).toString('utf8'))
121
+ }
122
+
123
+ function failure(code, message) {
124
+ return { ok: false, error: { code, message, details: {} } }
125
+ }
126
+
127
+ /** Keep transport config useful to the UI without exposing env or header values. */
128
+ function projectCatalog(catalog) {
129
+ return {
130
+ ...catalog,
131
+ plugins: catalog.plugins.map(plugin => ({
132
+ ...plugin,
133
+ mcpServers: plugin.mcpServers.map(server => ({
134
+ name: server.name,
135
+ type: server.type,
136
+ ...server.type === 'stdio' ? {
137
+ command: server.command,
138
+ args: server.args ?? [],
139
+ ...server.cwd === undefined ? {} : { cwd: server.cwd },
140
+ } : {
141
+ url: server.url,
142
+ headerNames: Object.keys(server.headers ?? {}),
143
+ },
144
+ })),
145
+ })),
146
+ }
147
+ }
package/lib/skills.js ADDED
@@ -0,0 +1,54 @@
1
+ import { BUNDLED_SKILL_RANK } from '@deepseek-ai/dsh-skill'
2
+ import { discoverAgentPlugins, loadAgentSkill } from './discovery.js'
3
+ import { INVALID_AGENT_PLUGIN, isMissingPath } from './validation.js'
4
+
5
+ const PROVIDER = 'agent-plugins'
6
+ const INVOCATION = { modelInvocable: true, userInvocable: true }
7
+
8
+ /** Create a strict Agent Plugins v1 skill provider with on-demand body loading. */
9
+ export function createAgentSkillProvider({ pluginsDirectory }) {
10
+ return {
11
+ name: PROVIDER,
12
+ async list(options = {}) {
13
+ options.signal?.throwIfAborted()
14
+ const catalog = await discoverAgentPlugins({ pluginsDirectory })
15
+ options.signal?.throwIfAborted()
16
+ return catalog.plugins.flatMap(plugin => plugin.skills.map(skill => ({
17
+ name: skill.name,
18
+ description: skill.description,
19
+ path: skill.path,
20
+ provider: PROVIDER,
21
+ source: PROVIDER,
22
+ rank: BUNDLED_SKILL_RANK,
23
+ invocation: INVOCATION,
24
+ resourceBase: { kind: 'directory', path: skill.directory },
25
+ locator: {
26
+ pluginRoot: plugin.root,
27
+ skillDirectory: skill.directory,
28
+ packageDirectory: skill.packageDirectory,
29
+ },
30
+ ...skill.metadata === undefined ? {} : { metadata: skill.metadata },
31
+ })))
32
+ },
33
+ async get(candidate, options = {}) {
34
+ try {
35
+ const document = await loadAgentSkill({ ...candidate.locator, signal: options.signal })
36
+ if (document === undefined) return undefined
37
+ return {
38
+ name: document.name,
39
+ description: document.description,
40
+ path: candidate.path,
41
+ provider: PROVIDER,
42
+ source: PROVIDER,
43
+ invocation: INVOCATION,
44
+ resourceBase: candidate.resourceBase,
45
+ content: document.content,
46
+ ...document.metadata === undefined ? {} : { metadata: document.metadata },
47
+ }
48
+ } catch (error) {
49
+ if (isMissingPath(error) || error.code === INVALID_AGENT_PLUGIN) return undefined
50
+ throw error
51
+ }
52
+ },
53
+ }
54
+ }
@@ -0,0 +1,218 @@
1
+ import Ajv2020 from 'ajv/dist/2020.js'
2
+ import { validateHeaderName, validateHeaderValue } from 'node:http'
3
+ import { isIP } from 'node:net'
4
+ import { createRequire } from 'node:module'
5
+
6
+ const require = createRequire(import.meta.url)
7
+ const pluginSchema = require('../schemas/1.0.0/plugin.schema.json')
8
+ const mcpSchema = require('../schemas/1.0.0/mcp.schema.json')
9
+
10
+ export const PLUGIN_SCHEMA = 'https://agent-plugins.org/schemas/1.0.0/plugin.schema.json'
11
+ export const MCP_SCHEMA = 'https://agent-plugins.org/schemas/1.0.0/mcp.schema.json'
12
+ export const INVALID_AGENT_PLUGIN = 'AGENT_PLUGIN_INVALID'
13
+
14
+ const MANIFEST_FIELDS = new Set([
15
+ '$schema', 'name', 'version', 'description', 'author', 'homepage', 'repository',
16
+ 'license', 'keywords', 'extensions',
17
+ ])
18
+ const SKILL_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/
19
+ const ajv = new Ajv2020({ strict: false, allErrors: true })
20
+ const validateManifestSchema = ajv.compile(pluginSchema)
21
+ ajv.addSchema(mcpSchema)
22
+ const validateMcpRootSchema = ajv.compile({
23
+ $schema: 'https://json-schema.org/draft/2020-12/schema',
24
+ type: 'object',
25
+ properties: {
26
+ $schema: { const: MCP_SCHEMA },
27
+ mcpServers: { type: 'object' },
28
+ },
29
+ required: ['$schema', 'mcpServers'],
30
+ additionalProperties: false,
31
+ })
32
+ const validateMcpServerSchema = ajv.getSchema(`${MCP_SCHEMA}#/$defs/server`)
33
+
34
+ /** Create one validation failure suitable for isolated component reporting. */
35
+ export function invalidAgentPlugin(message, path) {
36
+ const error = new Error(path ? `${path}: ${message}` : message)
37
+ error.code = INVALID_AGENT_PLUGIN
38
+ error.path = path
39
+ return error
40
+ }
41
+
42
+ export function isPlainObject(value) {
43
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
44
+ }
45
+
46
+ export function isMissingPath(error) {
47
+ return error?.code === 'ENOENT' || error?.code === 'ENOTDIR'
48
+ }
49
+
50
+ export function errorMessage(error) {
51
+ return error instanceof Error ? error.message : String(error)
52
+ }
53
+
54
+ function schemaMessage(validate) {
55
+ return ajv.errorsText(validate.errors, { separator: '; ', maxErrors: 3 })
56
+ }
57
+
58
+ /** Validate and sanitize plugin.json, including the two non-fatal exceptions. */
59
+ export function validateManifest(value, path) {
60
+ if (!isPlainObject(value)) throw invalidAgentPlugin('plugin.json must contain an object', path)
61
+ const manifest = { ...value }
62
+ const diagnostics = []
63
+ for (const key of Object.keys(manifest)) {
64
+ if (MANIFEST_FIELDS.has(key)) continue
65
+ diagnostics.push({ component: 'manifest', path, message: `ignored unknown manifest field ${JSON.stringify(key)}` })
66
+ delete manifest[key]
67
+ }
68
+ if (Object.hasOwn(manifest, 'extensions') && !isPlainObject(manifest.extensions)) {
69
+ diagnostics.push({ component: 'manifest', path, message: 'ignored non-object extensions field' })
70
+ delete manifest.extensions
71
+ }
72
+ const extensions = manifest.extensions
73
+ if (extensions !== undefined) delete manifest.extensions
74
+ if (!validateManifestSchema(manifest)) {
75
+ throw invalidAgentPlugin(`invalid plugin.json: ${schemaMessage(validateManifestSchema)}`, path)
76
+ }
77
+ if (extensions !== undefined) manifest.extensions = extensions
78
+ return { manifest, diagnostics }
79
+ }
80
+
81
+ /** Validate the MCP document envelope without rejecting independent server entries. */
82
+ export function validateMcpDocument(value, manifestSchema, path) {
83
+ if (!validateMcpRootSchema(value)) {
84
+ throw invalidAgentPlugin(`invalid mcp.json: ${schemaMessage(validateMcpRootSchema)}`, path)
85
+ }
86
+ if (schemaVersion(value.$schema) !== schemaVersion(manifestSchema)) {
87
+ throw invalidAgentPlugin('plugin.json and mcp.json target different Agent Plugins versions', path)
88
+ }
89
+ if (!isPlainObject(value.mcpServers)) throw invalidAgentPlugin('mcpServers must be an object', path)
90
+ }
91
+
92
+ function schemaVersion(value) {
93
+ return /^https:\/\/agent-plugins\.org\/schemas\/([^/]+)\//.exec(value)?.[1]
94
+ }
95
+
96
+ /** Validate one MCP server entry and its transport-level URL/header semantics. */
97
+ export function validateMcpServer(name, value, path) {
98
+ if (!validateMcpServerSchema(value)) {
99
+ throw invalidAgentPlugin(`invalid MCP server ${JSON.stringify(name)}: ${schemaMessage(validateMcpServerSchema)}`, path)
100
+ }
101
+ if (value.type === 'stdio') {
102
+ if (!value.command.startsWith('./') && /[\\/]/.test(value.command)) {
103
+ throw invalidAgentPlugin(`MCP server ${JSON.stringify(name)} command must be a bare executable or ./ plugin-relative path`, path)
104
+ }
105
+ return
106
+ }
107
+ validateRemoteUrl(value.url, path)
108
+ validateHeaders(value.headers, path)
109
+ }
110
+
111
+ function validateRemoteUrl(value, path) {
112
+ let url
113
+ try {
114
+ url = new URL(value)
115
+ } catch {
116
+ throw invalidAgentPlugin('MCP endpoint must be an absolute HTTP(S) URL', path)
117
+ }
118
+ if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password || url.hash) {
119
+ throw invalidAgentPlugin('MCP endpoint must be HTTP(S), without user information or a fragment', path)
120
+ }
121
+ if (url.protocol === 'http:' && !isLoopbackHostname(url.hostname)) {
122
+ throw invalidAgentPlugin('non-loopback MCP endpoints must use HTTPS', path)
123
+ }
124
+ }
125
+
126
+ function isLoopbackHostname(hostname) {
127
+ if (hostname === 'localhost') return true
128
+ const normalized = hostname.startsWith('[') && hostname.endsWith(']') ? hostname.slice(1, -1) : hostname
129
+ const family = isIP(normalized)
130
+ return (family === 4 && normalized.startsWith('127.')) || (family === 6 && normalized === '::1')
131
+ }
132
+
133
+ function validateHeaders(headers = {}, path) {
134
+ if (!isPlainObject(headers)) throw invalidAgentPlugin('MCP headers must be an object', path)
135
+ const names = new Set()
136
+ for (const [name, value] of Object.entries(headers)) {
137
+ try {
138
+ validateHeaderName(name)
139
+ validateHeaderValue(name, value)
140
+ } catch (error) {
141
+ throw invalidAgentPlugin(`invalid MCP header: ${errorMessage(error)}`, path)
142
+ }
143
+ const normalized = name.toLowerCase()
144
+ if (names.has(normalized)) throw invalidAgentPlugin(`duplicate MCP header ${JSON.stringify(name)}`, path)
145
+ names.add(normalized)
146
+ }
147
+ }
148
+
149
+ /** Parse and validate one SKILL.md document against the Agent Skills contract. */
150
+ export function parseSkillDocument(raw, expectedName, path) {
151
+ const { data, body } = parseFrontmatter(raw, path)
152
+ const name = requiredString(data, 'name', 64, path)
153
+ if (!SKILL_NAME.test(name) || name !== expectedName) {
154
+ throw invalidAgentPlugin(`skill name ${JSON.stringify(name)} must be kebab-case and match parent directory ${JSON.stringify(expectedName)}`, path)
155
+ }
156
+ const description = requiredString(data, 'description', 1024, path)
157
+ const result = {
158
+ name,
159
+ description,
160
+ content: body.trim(),
161
+ }
162
+ if (Object.hasOwn(data, 'license')) result.license = optionalString(data, 'license', undefined, path)
163
+ if (Object.hasOwn(data, 'compatibility')) result.compatibility = optionalString(data, 'compatibility', 500, path)
164
+ if (Object.hasOwn(data, 'allowed-tools')) result.allowedTools = optionalString(data, 'allowed-tools', undefined, path)
165
+ if (Object.hasOwn(data, 'metadata')) {
166
+ if (!isPlainObject(data.metadata) || Object.values(data.metadata).some(value => typeof value !== 'string')) {
167
+ throw invalidAgentPlugin('skill metadata must map strings to strings', path)
168
+ }
169
+ result.metadata = { ...data.metadata }
170
+ }
171
+ return result
172
+ }
173
+
174
+ function requiredString(data, key, maximum, path) {
175
+ const value = data[key]
176
+ if (typeof value !== 'string' || value.length < 1 || value.length > maximum) {
177
+ throw invalidAgentPlugin(`skill ${key} must be a string of 1-${maximum} characters`, path)
178
+ }
179
+ return value
180
+ }
181
+
182
+ function optionalString(data, key, maximum, path) {
183
+ const value = data[key]
184
+ if (typeof value !== 'string' || value.length < 1 || (maximum !== undefined && value.length > maximum)) {
185
+ throw invalidAgentPlugin(`skill ${key} has an invalid string value`, path)
186
+ }
187
+ return value
188
+ }
189
+
190
+ function parseFrontmatter(raw, path) {
191
+ const firstLineEnd = raw.indexOf('\n')
192
+ if (firstLineEnd < 0 || raw.slice(0, firstLineEnd).replace(/\r$/, '') !== '---') {
193
+ throw invalidAgentPlugin('SKILL.md requires YAML frontmatter', path)
194
+ }
195
+ const start = firstLineEnd + 1
196
+ let lineStart = start
197
+ while (lineStart <= raw.length) {
198
+ const nextNewline = raw.indexOf('\n', lineStart)
199
+ const lineEnd = nextNewline < 0 ? raw.length : nextNewline
200
+ const line = raw.slice(lineStart, lineEnd).replace(/\r$/, '')
201
+ if (line === '---') {
202
+ const yamlText = raw.slice(start, lineStart)
203
+ let data
204
+ try {
205
+ data = parseYaml(yamlText)
206
+ } catch (error) {
207
+ throw invalidAgentPlugin(`invalid YAML frontmatter: ${errorMessage(error)}`, path)
208
+ }
209
+ if (!isPlainObject(data)) throw invalidAgentPlugin('skill frontmatter must be an object', path)
210
+ return { data, body: raw.slice(nextNewline < 0 ? raw.length : nextNewline + 1) }
211
+ }
212
+ if (nextNewline < 0) break
213
+ lineStart = nextNewline + 1
214
+ }
215
+ throw invalidAgentPlugin('SKILL.md frontmatter has no closing delimiter', path)
216
+ }
217
+
218
+ import { parse as parseYaml } from 'yaml'
package/package.json CHANGED
@@ -1,6 +1,67 @@
1
1
  {
2
2
  "name": "@dshforge/agent-plugins",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "Agent Plugins v1 Skills and MCP support for DeepSeek Harness Web.",
6
+ "main": "lib/index.js",
7
+ "exports": {
8
+ ".": "./lib/index.js",
9
+ "./client": "./lib/client.js",
10
+ "./package.json": "./package.json"
11
+ },
12
+ "files": [
13
+ "lib/",
14
+ "schemas/",
15
+ "cordis.patch.yml",
16
+ "README.md"
17
+ ],
18
+ "scripts": {
19
+ "build": "node scripts/build.mjs",
20
+ "test": "node --test tests/*.test.js"
21
+ },
22
+ "engines": {
23
+ "dsh": ">=0.2.1-rc.3 <0.3.0",
24
+ "node": ">=24.9.0"
25
+ },
26
+ "dsh": {
27
+ "bundle": {
28
+ "patch": "./cordis.patch.yml"
29
+ },
30
+ "client": {
31
+ "platform": "web",
32
+ "inject": [
33
+ "@deepseek-ai/dsh-client-ui-layout",
34
+ "@deepseek-ai/dsh-client-ui-renderer",
35
+ "@deepseek-ai/dsh-client-ui-settings-general",
36
+ "@deepseek-ai/dsh-client-ui-sidebar-right",
37
+ "@deepseek-ai/dsh-client-connection",
38
+ "@deepseek-ai/dsh-client-locale"
39
+ ]
40
+ }
41
+ },
42
+ "dependencies": {
43
+ "@deepseek-ai/dsh-util-workspace-path": ">=0.2.1-rc.3 <0.3.0",
44
+ "ajv": "^8.17.1",
45
+ "execa": "^9.6.0",
46
+ "yaml": "^2.4.2"
47
+ },
48
+ "peerDependencies": {
49
+ "@deepseek-ai/cordis": "^4.0.5",
50
+ "@deepseek-ai/dsh-client-connection": ">=0.2.1-rc.3 <0.3.0",
51
+ "@deepseek-ai/dsh-mcp-client": ">=0.2.1-rc.3 <0.3.0",
52
+ "@deepseek-ai/dsh-skill": ">=0.2.1-rc.3 <0.3.0"
53
+ },
54
+ "devDependencies": {
55
+ "@deepseek-ai/cordis": "^4.0.5",
56
+ "@deepseek-ai/dsh-client-connection": ">=0.2.1-rc.3 <0.3.0",
57
+ "@deepseek-ai/dsh-mcp-client": ">=0.2.1-rc.3 <0.3.0",
58
+ "@deepseek-ai/dsh-skill": ">=0.2.1-rc.3 <0.3.0",
59
+ "@deepseek-ai/dsh-system-prompt": ">=0.2.1-rc.3 <0.3.0",
60
+ "@deepseek-ai/dsh-tools": ">=0.2.1-rc.3 <0.3.0",
61
+ "@modelcontextprotocol/client": "2.2.0",
62
+ "@modelcontextprotocol/node": "2.0.0",
63
+ "@modelcontextprotocol/server": "2.2.0",
64
+ "esbuild": "^0.25.0",
65
+ "zod": "4.4.3"
66
+ }
67
+ }
@@ -0,0 +1,120 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json",
4
+ "title": "Agent Plugins MCP Configuration",
5
+ "description": "Machine-readable schema for mcp.json in Agent Plugins 1.0.0. The Agent Plugins specification defines additional semantic and operational requirements.",
6
+ "type": "object",
7
+ "properties": {
8
+ "$schema": {
9
+ "const": "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json",
10
+ "description": "Canonical identifier of the MCP configuration schema for the Agent Plugins version targeted by this document."
11
+ },
12
+ "mcpServers": {
13
+ "type": "object",
14
+ "additionalProperties": {
15
+ "$ref": "#/$defs/server"
16
+ }
17
+ }
18
+ },
19
+ "required": ["$schema", "mcpServers"],
20
+ "additionalProperties": false,
21
+ "$defs": {
22
+ "server": {
23
+ "title": "MCP server",
24
+ "oneOf": [
25
+ {
26
+ "$ref": "#/$defs/stdioServer"
27
+ },
28
+ {
29
+ "$ref": "#/$defs/streamableHttpServer"
30
+ },
31
+ {
32
+ "$ref": "#/$defs/sseServer"
33
+ }
34
+ ]
35
+ },
36
+ "stdioServer": {
37
+ "title": "stdio MCP server",
38
+ "type": "object",
39
+ "properties": {
40
+ "type": {
41
+ "const": "stdio"
42
+ },
43
+ "command": {
44
+ "type": "string",
45
+ "minLength": 1,
46
+ "description": "Executable token. Resolution rules are defined by the Agent Plugins specification."
47
+ },
48
+ "args": {
49
+ "type": "array",
50
+ "items": {
51
+ "type": "string"
52
+ }
53
+ },
54
+ "env": {
55
+ "type": "object",
56
+ "propertyNames": {
57
+ "not": {
58
+ "enum": ["PLUGIN_ROOT", "PLUGIN_DATA"]
59
+ }
60
+ },
61
+ "additionalProperties": {
62
+ "type": "string"
63
+ }
64
+ },
65
+ "cwd": {
66
+ "type": "string",
67
+ "pattern": "^(?:\\./|\\$\\{PLUGIN_ROOT\\}(?:/|$)|\\$\\{PLUGIN_DATA\\}(?:/|$))",
68
+ "description": "Plugin-relative, PLUGIN_ROOT-rooted, or PLUGIN_DATA-rooted working directory. Filesystem containment is validated separately."
69
+ }
70
+ },
71
+ "required": ["type", "command"],
72
+ "additionalProperties": false
73
+ },
74
+ "streamableHttpServer": {
75
+ "title": "Streamable HTTP MCP server",
76
+ "type": "object",
77
+ "properties": {
78
+ "type": {
79
+ "const": "streamable-http"
80
+ },
81
+ "url": {
82
+ "type": "string",
83
+ "minLength": 1,
84
+ "description": "MCP endpoint URL. URL semantics are defined by the Agent Plugins specification."
85
+ },
86
+ "headers": {
87
+ "$ref": "#/$defs/headers"
88
+ }
89
+ },
90
+ "required": ["type", "url"],
91
+ "additionalProperties": false
92
+ },
93
+ "sseServer": {
94
+ "title": "Legacy HTTP+SSE MCP server",
95
+ "type": "object",
96
+ "properties": {
97
+ "type": {
98
+ "const": "sse"
99
+ },
100
+ "url": {
101
+ "type": "string",
102
+ "minLength": 1,
103
+ "description": "MCP endpoint URL. URL semantics are defined by the Agent Plugins specification."
104
+ },
105
+ "headers": {
106
+ "$ref": "#/$defs/headers"
107
+ }
108
+ },
109
+ "required": ["type", "url"],
110
+ "additionalProperties": false
111
+ },
112
+ "headers": {
113
+ "title": "HTTP headers",
114
+ "type": "object",
115
+ "additionalProperties": {
116
+ "type": "string"
117
+ }
118
+ }
119
+ }
120
+ }
@@ -0,0 +1,65 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
4
+ "title": "Agent Plugins Manifest",
5
+ "description": "Machine-readable schema for plugin.json in Agent Plugins 1.0.0. The Agent Plugins specification defines additional semantic and operational requirements.",
6
+ "type": "object",
7
+ "properties": {
8
+ "$schema": {
9
+ "const": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
10
+ "description": "Canonical identifier of the plugin manifest schema for the Agent Plugins version targeted by this document."
11
+ },
12
+ "name": {
13
+ "type": "string",
14
+ "minLength": 1,
15
+ "maxLength": 64,
16
+ "pattern": "^(?!.*(?:--|\\.\\.))[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$",
17
+ "description": "Human-readable plugin name."
18
+ },
19
+ "version": {
20
+ "type": "string"
21
+ },
22
+ "description": {
23
+ "type": "string"
24
+ },
25
+ "author": {
26
+ "type": "object",
27
+ "properties": {
28
+ "name": {
29
+ "type": "string"
30
+ },
31
+ "email": {
32
+ "type": "string"
33
+ },
34
+ "url": {
35
+ "type": "string"
36
+ }
37
+ },
38
+ "additionalProperties": false
39
+ },
40
+ "homepage": {
41
+ "type": "string"
42
+ },
43
+ "repository": {
44
+ "type": "string"
45
+ },
46
+ "license": {
47
+ "type": "string"
48
+ },
49
+ "keywords": {
50
+ "type": "array",
51
+ "items": {
52
+ "type": "string"
53
+ }
54
+ },
55
+ "extensions": {
56
+ "type": "object",
57
+ "description": "Client-specific manifest data keyed by reverse-domain extension namespace. Agent Plugins assigns no semantics to namespace object contents.",
58
+ "additionalProperties": {
59
+ "type": "object"
60
+ }
61
+ }
62
+ },
63
+ "required": ["$schema", "name"],
64
+ "additionalProperties": false
65
+ }