@opvs-ai/mcp 0.7.4 → 2.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +140 -20
- package/dist/index.d.ts +19 -9
- package/dist/index.js +907 -74
- package/dist/index.js.map +1 -1
- package/package.json +11 -9
- package/dist/generated-tools.d.ts +0 -30
- package/dist/generated-tools.js +0 -4099
- package/dist/generated-tools.js.map +0 -1
- package/dist/handlers/agent.d.ts +0 -6
- package/dist/handlers/agent.js +0 -93
- package/dist/handlers/agent.js.map +0 -1
- package/dist/handlers/auth.d.ts +0 -6
- package/dist/handlers/auth.js +0 -86
- package/dist/handlers/auth.js.map +0 -1
- package/dist/handlers/board.d.ts +0 -6
- package/dist/handlers/board.js +0 -232
- package/dist/handlers/board.js.map +0 -1
- package/dist/handlers/docs.d.ts +0 -6
- package/dist/handlers/docs.js +0 -140
- package/dist/handlers/docs.js.map +0 -1
- package/dist/handlers/protocol.d.ts +0 -14
- package/dist/handlers/protocol.js +0 -448
- package/dist/handlers/protocol.js.map +0 -1
- package/dist/handlers/stages.d.ts +0 -6
- package/dist/handlers/stages.js +0 -66
- package/dist/handlers/stages.js.map +0 -1
- package/dist/handlers/types.d.ts +0 -31
- package/dist/handlers/types.js +0 -25
- package/dist/handlers/types.js.map +0 -1
- package/dist/handlers/views.d.ts +0 -6
- package/dist/handlers/views.js +0 -74
- package/dist/handlers/views.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,97 +1,930 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* @opvs-ai/mcp — MCP
|
|
3
|
+
* @opvs-ai/mcp — MCP server for the OPVS on-demand model
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* Exposes 11 tools — the 8 that mirror the gateway's `skills-meta` plugin (so
|
|
6
|
+
* external MCP hosts like Claude Desktop, Cursor, Antigravity and Windsurf get
|
|
7
|
+
* the same surface OPVS-hosted agents see), plus `board_as_markdown` (Topic 1 /
|
|
8
|
+
* S1.4 — a whole AgentBoard as a Markdown plan in one call; it has no
|
|
9
|
+
* `tool_call` route because the agentboard skill ships no schema.yaml), plus
|
|
10
|
+
* `tool_read` (DLRT 3.2 — read any published skill's actual SKILL.md + its
|
|
11
|
+
* `references/` layers on-demand from the catalog bundle, with nothing
|
|
12
|
+
* installed), plus `sandbox_chat` (vendor test mode — drive your marketplace
|
|
13
|
+
* sandbox test agent through the real gateway, per Vendor-Upstream-and-Testing
|
|
14
|
+
* decision 4: a MODE of this package, not a new one). Every other capability is
|
|
15
|
+
* reached through `tool_call` indirection so the package fits under the
|
|
16
|
+
* ~100-tool cap that all current MCP clients enforce.
|
|
7
17
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
18
|
+
* Tool names match OpenAI's `^[a-zA-Z0-9_-]+$` (see
|
|
19
|
+
* `feedback-openai-tool-name-regex` memory). Dots, `@`, and `/` are rejected.
|
|
10
20
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
21
|
+
* Transport: stdio (JSON-RPC). Never write to stdout outside the SDK.
|
|
22
|
+
* Debug logs go to stderr via console.error().
|
|
13
23
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
24
|
+
* Auth: PAT from `~/.opvs/config.json` (shared with `@opvs-ai/cli`). Brand is
|
|
25
|
+
* pinned server-side from the PAT — never trust LLM-supplied brand_id.
|
|
16
26
|
*/
|
|
17
27
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
18
28
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
19
29
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
20
|
-
import { createClient,
|
|
21
|
-
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
30
|
+
import { createClient, getEffectiveConfig, getResponseText } from "@opvs-ai/core";
|
|
31
|
+
import { readFileSync } from "node:fs";
|
|
32
|
+
import { fileURLToPath } from "node:url";
|
|
33
|
+
import { dirname, join } from "node:path";
|
|
34
|
+
// Read the version from package.json at runtime so the MCP `initialize`
|
|
35
|
+
// handshake never drifts from the published version (AgentCLI LEARNINGS #13).
|
|
36
|
+
// dist/index.js sits next to package.json at the installed package root.
|
|
37
|
+
const PKG_VERSION = (() => {
|
|
38
|
+
try {
|
|
39
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
40
|
+
return JSON.parse(readFileSync(join(here, "..", "package.json"), "utf8")).version;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return "0.0.0";
|
|
44
|
+
}
|
|
45
|
+
})();
|
|
46
|
+
function ok(body) {
|
|
47
|
+
const text = typeof body === "string" ? body : JSON.stringify(body);
|
|
48
|
+
return { content: [{ type: "text", text }] };
|
|
49
|
+
}
|
|
50
|
+
/** Standard error envelope: flat-sibling `_guidance` (W1.A discipline,
|
|
51
|
+
* confirmed with SpiderIQ 2026-05-15 PM). Never wrap. */
|
|
52
|
+
function err(message, guidance) {
|
|
53
|
+
const body = { error: message };
|
|
54
|
+
if (guidance)
|
|
55
|
+
body._guidance = guidance;
|
|
56
|
+
return { content: [{ type: "text", text: JSON.stringify(body) }] };
|
|
57
|
+
}
|
|
58
|
+
// ── Skill-ref helpers ─────────────────────────────────────────────────
|
|
59
|
+
function normalizeSkillRef(ref) {
|
|
60
|
+
// Accept "@vendor/name", "vendor/name", or bare "name".
|
|
61
|
+
const cleaned = ref.replace(/^@/, "");
|
|
62
|
+
if (cleaned.includes("/")) {
|
|
63
|
+
const [vendor, ...rest] = cleaned.split("/");
|
|
64
|
+
return { full: `${vendor}/${rest.join("/")}`, vendor, name: rest.join("/") };
|
|
65
|
+
}
|
|
66
|
+
return { full: cleaned, vendor: "", name: cleaned };
|
|
67
|
+
}
|
|
68
|
+
// ── API client ────────────────────────────────────────────────────────
|
|
69
|
+
function client() {
|
|
70
|
+
return createClient();
|
|
71
|
+
}
|
|
72
|
+
async function getJson(path) {
|
|
73
|
+
const res = await client().get(path);
|
|
74
|
+
const text = await getResponseText(res);
|
|
75
|
+
try {
|
|
76
|
+
return JSON.parse(text);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return text;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
async function postJson(path, body) {
|
|
83
|
+
const res = await client().post(path, body);
|
|
84
|
+
const text = await getResponseText(res);
|
|
85
|
+
try {
|
|
86
|
+
return JSON.parse(text);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return text;
|
|
51
90
|
}
|
|
91
|
+
}
|
|
92
|
+
/** GET a path and return the raw response body as text (no JSON parse). Used
|
|
93
|
+
* for endpoints that emit Markdown / plain text such as the board export. */
|
|
94
|
+
async function getText(path) {
|
|
95
|
+
const res = await client().get(path);
|
|
52
96
|
return getResponseText(res);
|
|
53
97
|
}
|
|
54
|
-
function
|
|
55
|
-
|
|
98
|
+
async function del204(path) {
|
|
99
|
+
// The OPVS DELETE endpoints we hit return 204 No Content; createClient
|
|
100
|
+
// throws on non-2xx, so reaching here = success.
|
|
101
|
+
await client().delete(path);
|
|
102
|
+
}
|
|
103
|
+
/** Fetch the package descriptor from the registry and return the canonical
|
|
104
|
+
* `manifest.skills[].id` array. Mirrors `SkillsMetaClient.fetchManifestSkillIds`
|
|
105
|
+
* in the gateway plugin (W2.0.A). */
|
|
106
|
+
async function fetchManifestSkillIds(vendor, packageName) {
|
|
107
|
+
const cleanedVendor = vendor.replace(/^@+/, "");
|
|
108
|
+
const canonicalVendor = `@${cleanedVendor}`;
|
|
109
|
+
const path = `/api/v1/registry/packages/${encodeURIComponent(canonicalVendor)}/${encodeURIComponent(packageName)}`;
|
|
110
|
+
try {
|
|
111
|
+
const data = await getJson(path);
|
|
112
|
+
if (typeof data !== "object" || data === null) {
|
|
113
|
+
return { ok: false, status: 502, detail: String(data).slice(0, 200) };
|
|
114
|
+
}
|
|
115
|
+
const manifest = data.manifest ??
|
|
116
|
+
{};
|
|
117
|
+
const rawSkills = Array.isArray(manifest.skills) ? manifest.skills : [];
|
|
118
|
+
const ids = rawSkills
|
|
119
|
+
.map((s) => (s && typeof s === "object" ? s.id : null))
|
|
120
|
+
.filter((id) => typeof id === "string" && id.length > 0);
|
|
121
|
+
return { ok: true, ids };
|
|
122
|
+
}
|
|
123
|
+
catch (e) {
|
|
124
|
+
const error = e;
|
|
125
|
+
return {
|
|
126
|
+
ok: false,
|
|
127
|
+
status: error.status ?? 502,
|
|
128
|
+
detail: (error.detail ?? "registry fetch failed").slice(0, 200),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
56
131
|
}
|
|
57
|
-
|
|
132
|
+
/** Resolve a skill name → active install_id for the brand pinned to the PAT. */
|
|
133
|
+
async function resolveInstallId(skillRef) {
|
|
134
|
+
try {
|
|
135
|
+
const data = await getJson("/api/v1/marketplace/installed");
|
|
136
|
+
const items = data.items ?? [];
|
|
137
|
+
const wanted = normalizeSkillRef(skillRef);
|
|
138
|
+
for (const row of items) {
|
|
139
|
+
if (row.state !== "active")
|
|
140
|
+
continue;
|
|
141
|
+
const vendor = String(row.vendor ?? "");
|
|
142
|
+
const name = String(row.name ?? "");
|
|
143
|
+
const full = vendor ? `${vendor}/${name}` : name;
|
|
144
|
+
if (full === wanted.full ||
|
|
145
|
+
name === wanted.name ||
|
|
146
|
+
full === skillRef ||
|
|
147
|
+
name === skillRef) {
|
|
148
|
+
return { install_id: String(row.id), vendor, name };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
// ── Tool definitions ──────────────────────────────────────────────────
|
|
158
|
+
/**
|
|
159
|
+
* Appended to the discovery tool descriptions so an agent can self-recover from
|
|
160
|
+
* the ONE failure we can't fix server-side: an IDE (e.g. Antigravity, 2026-07)
|
|
161
|
+
* that discovers OPVS tools but blocks their execution ("not enabled" / "unknown
|
|
162
|
+
* tool"). A tool's DESCRIPTION is in the prompt even when the tool can't be
|
|
163
|
+
* called — so this hint is reachable when nothing else is.
|
|
164
|
+
*/
|
|
165
|
+
const IDE_TOOL_HINT = " IDE note: if your IDE reports this tool as 'not enabled' or 'unknown tool' at call time, " +
|
|
166
|
+
"enable the OPVS server's tools in your IDE's MCP settings — or fall back to the CLI: " +
|
|
167
|
+
"`opvs-skills install <@vendor/name> --runtime <claude-code|antigravity|cursor|windsurf> --dest .`.";
|
|
58
168
|
const TOOLS = [
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
169
|
+
{
|
|
170
|
+
name: "tool_search",
|
|
171
|
+
description: "Semantic search across the OPVS marketplace registry for skills matching your intent. " +
|
|
172
|
+
"Returns ranked packages with vendor/name/description plus a `telemetry_signal` " +
|
|
173
|
+
"(absent|cold|warm|hot) reflecting 30-day usage on this brand. Call this BEFORE " +
|
|
174
|
+
"tool_call when you don't yet know which skill handles a job. Pair with tool_help " +
|
|
175
|
+
"for deep parameter docs. Response carries a sibling `_guidance` string. " +
|
|
176
|
+
"For board / DAG / factory work (task pipelines, claim loops, run-a-board), the " +
|
|
177
|
+
"`agentboard` skill is the one — then call tool_help(@opvs-ai/agentboard) for its " +
|
|
178
|
+
"operating model." +
|
|
179
|
+
IDE_TOOL_HINT,
|
|
180
|
+
inputSchema: {
|
|
181
|
+
type: "object",
|
|
182
|
+
properties: {
|
|
183
|
+
intent: {
|
|
184
|
+
type: "string",
|
|
185
|
+
description: "Free-text intent — what the agent is trying to do (e.g. 'find restaurant " +
|
|
186
|
+
"leads in Aarhus', 'send a Telegram message').",
|
|
187
|
+
},
|
|
188
|
+
limit: {
|
|
189
|
+
type: "number",
|
|
190
|
+
description: "Max results (1–25). Default 5.",
|
|
191
|
+
},
|
|
192
|
+
additional_filters: {
|
|
193
|
+
type: "object",
|
|
194
|
+
description: "Optional structured filter map. Reserved keys: `package` (vendor/name string), " +
|
|
195
|
+
"`vendor`, `category`. Other keys are forwarded as-is to future filter slots.",
|
|
196
|
+
additionalProperties: true,
|
|
197
|
+
},
|
|
198
|
+
include_schemas: {
|
|
199
|
+
type: "boolean",
|
|
200
|
+
description: "If true, the registry response shape includes method schemas (heavier payload, " +
|
|
201
|
+
"useful for one-shot describe + call without a follow-up tool_help round-trip).",
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
required: ["intent"],
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
name: "tool_help",
|
|
209
|
+
description: "Fetch the marketplace package descriptor for a skill: description, methods, " +
|
|
210
|
+
"manifest, examples. Use BEFORE tool_call to understand parameter shapes. " +
|
|
211
|
+
"`section` narrows the payload (description | methods | manifest | all). Default: all. " +
|
|
212
|
+
"The `methods` section returns the rich `methods_index` (full param shapes incl. nested " +
|
|
213
|
+
"object `properties`) when the package has one, else the compact `manifest.methods` list. " +
|
|
214
|
+
"The `description` section is the skill's SKILL.md — for a board/factory skill it carries " +
|
|
215
|
+
"the operating model + a references index (the run-a-factory-board recipe). When the " +
|
|
216
|
+
"descriptor is a board/factory skill, tool_help attaches a `_guidance` sibling routing you " +
|
|
217
|
+
"to those facts so you don't drive a factory board blind.",
|
|
218
|
+
inputSchema: {
|
|
219
|
+
type: "object",
|
|
220
|
+
properties: {
|
|
221
|
+
name: {
|
|
222
|
+
type: "string",
|
|
223
|
+
description: "Skill reference: '@vendor/name', 'vendor/name', or bare 'name' " +
|
|
224
|
+
"(resolves to the @opvs-ai vendor when bare).",
|
|
225
|
+
},
|
|
226
|
+
section: {
|
|
227
|
+
type: "string",
|
|
228
|
+
enum: ["description", "methods", "manifest", "all"],
|
|
229
|
+
description: "Slice of the descriptor to return. Default: all.",
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
required: ["name"],
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
name: "tool_read",
|
|
237
|
+
description: "Read any PUBLISHED skill's actual SKILL.md guidance on-demand — with nothing " +
|
|
238
|
+
"installed. Unlike tool_help (which returns the registry manifest's `description` " +
|
|
239
|
+
"field), tool_read extracts the real on-disk SKILL.md body from the signed CATALOG " +
|
|
240
|
+
"bundle PLUS its shippable `references/` layer files, so you get the full operating " +
|
|
241
|
+
"model an agent sees after install. Catalog-global + inert (no auth trust boundary — " +
|
|
242
|
+
"same as download/changelog): works for UNINSTALLED skills. Default returns " +
|
|
243
|
+
"`{vendor, name, version, skill_md, layers[]}`; `layers` lists deeper files you can " +
|
|
244
|
+
"pull one at a time by re-calling with `path` (e.g. path='references/run-a-factory-board.md'). " +
|
|
245
|
+
"Use this to LEARN a skill's guidance before deciding whether to tool_install it." +
|
|
246
|
+
IDE_TOOL_HINT,
|
|
247
|
+
inputSchema: {
|
|
248
|
+
type: "object",
|
|
249
|
+
properties: {
|
|
250
|
+
name: {
|
|
251
|
+
type: "string",
|
|
252
|
+
description: "Skill reference: '@vendor/name', 'vendor/name', or bare 'name' " +
|
|
253
|
+
"(resolves to the @opvs-ai vendor when bare).",
|
|
254
|
+
},
|
|
255
|
+
path: {
|
|
256
|
+
type: "string",
|
|
257
|
+
description: "Optional layer file to read instead of the SKILL.md body — one of the entries " +
|
|
258
|
+
"from a prior tool_read's `layers[]` (e.g. 'references/foo.md'). Returns that " +
|
|
259
|
+
"file's raw markdown. Restricted to the skill's shippable layer subdirs; an " +
|
|
260
|
+
"unknown path or a traversal attempt yields 404.",
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
required: ["name"],
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
name: "tool_call",
|
|
268
|
+
description: "Universal proxy to any installed skill on this brand. Routes through the " +
|
|
269
|
+
"marketplace runtime forwarder (W1.9). The `skill` segment is resolved server-side " +
|
|
270
|
+
"from `manifest.skills[].id` (W2.0.A) — pass either '@vendor/name' or a bare " +
|
|
271
|
+
"skill id. Response carries a sibling `_guidance` string with #audit/#next/#dont " +
|
|
272
|
+
"tags — parse `_guidance` before acting on the data.",
|
|
273
|
+
inputSchema: {
|
|
274
|
+
type: "object",
|
|
275
|
+
properties: {
|
|
276
|
+
skill: {
|
|
277
|
+
type: "string",
|
|
278
|
+
description: "Skill reference. Either '@vendor/name', 'vendor/name', or a bare " +
|
|
279
|
+
"manifest.skills[].id. Resolved against this brand's active installs.",
|
|
280
|
+
},
|
|
281
|
+
method: {
|
|
282
|
+
type: "string",
|
|
283
|
+
description: "Method to invoke (defined in the skill's `methods:` block). Example: " +
|
|
284
|
+
"'list_tasks', 'send_email'. Use tool_help first if unsure.",
|
|
285
|
+
},
|
|
286
|
+
params: {
|
|
287
|
+
type: "object",
|
|
288
|
+
description: "Method parameters. The runtime forwarder splits these into path/query/body " +
|
|
289
|
+
"based on the skill's schema. Never set `brand_id` here — it's pinned server-side.",
|
|
290
|
+
additionalProperties: true,
|
|
291
|
+
},
|
|
292
|
+
},
|
|
293
|
+
required: ["skill", "method"],
|
|
294
|
+
},
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
name: "tool_pin",
|
|
298
|
+
description: "Pin or unpin a skill in an OPVS agent's `## Pinned Skills` section (TOOLS.md). " +
|
|
299
|
+
"Pinned skills surface as native typed tools the next time that agent boots a session. " +
|
|
300
|
+
"Idempotent. NOTE: in MCP this is a stub today — TOOLS.md persistence requires " +
|
|
301
|
+
"Management API access not exposed to the PAT auth surface. Until W2.MCP.PIN ships the " +
|
|
302
|
+
"PAT-gated passthrough, use the OPVS dashboard's agent profile editor to manage pins.",
|
|
303
|
+
inputSchema: {
|
|
304
|
+
type: "object",
|
|
305
|
+
properties: {
|
|
306
|
+
name: {
|
|
307
|
+
type: "string",
|
|
308
|
+
description: "Skill reference (e.g. '@opvs-ai/agentboard').",
|
|
309
|
+
},
|
|
310
|
+
agent_id: {
|
|
311
|
+
type: "string",
|
|
312
|
+
description: "Target OPVS agent id whose TOOLS.md to edit.",
|
|
313
|
+
},
|
|
314
|
+
action: {
|
|
315
|
+
type: "string",
|
|
316
|
+
enum: ["pin", "unpin"],
|
|
317
|
+
description: "'pin' (default) or 'unpin'.",
|
|
318
|
+
},
|
|
319
|
+
},
|
|
320
|
+
required: ["name", "agent_id"],
|
|
321
|
+
},
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
name: "tool_list_installed",
|
|
325
|
+
description: "List every marketplace package installed on this brand: vendor, name, version, " +
|
|
326
|
+
"state, install_id. Use to confirm what's already available before tool_install. " +
|
|
327
|
+
"Brand is read server-side from the PAT.",
|
|
328
|
+
inputSchema: {
|
|
329
|
+
type: "object",
|
|
330
|
+
properties: {},
|
|
331
|
+
},
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
name: "tool_install",
|
|
335
|
+
description: "Install a marketplace package onto this brand. Requires the registry `package_id` " +
|
|
336
|
+
"(UUID, get it from tool_search). Grants the entitlements you pass; missing required " +
|
|
337
|
+
"entitlements yield a structured 422 with the gap list in `_guidance`. Credentials " +
|
|
338
|
+
"must already exist on the brand or the install enters `pending` state.",
|
|
339
|
+
inputSchema: {
|
|
340
|
+
type: "object",
|
|
341
|
+
properties: {
|
|
342
|
+
package_id: {
|
|
343
|
+
type: "string",
|
|
344
|
+
description: "Registry package UUID (from tool_search result `id` field).",
|
|
345
|
+
},
|
|
346
|
+
granted_entitlements: {
|
|
347
|
+
type: "array",
|
|
348
|
+
description: "Entitlement strings the brand is consenting to. Must cover the manifest's " +
|
|
349
|
+
"required set. Omit to install with no grants (skill will refuse to run).",
|
|
350
|
+
items: { type: "string" },
|
|
351
|
+
},
|
|
352
|
+
credential_bindings: {
|
|
353
|
+
type: "object",
|
|
354
|
+
description: "Optional credential resolutions. Keyed by credential name from the manifest. " +
|
|
355
|
+
"Each value is the `api_integration_id` to bind, or an object with provider/id.",
|
|
356
|
+
additionalProperties: true,
|
|
357
|
+
},
|
|
358
|
+
},
|
|
359
|
+
required: ["package_id"],
|
|
360
|
+
},
|
|
361
|
+
},
|
|
362
|
+
{
|
|
363
|
+
name: "tool_uninstall",
|
|
364
|
+
description: "Uninstall a marketplace package from this brand. Flips `installed_packages.state` " +
|
|
365
|
+
"to 'uninstalled' and revokes all granted entitlements. Gateway picks up the flip " +
|
|
366
|
+
"within 60s. Use tool_list_installed to find the install_id.",
|
|
367
|
+
inputSchema: {
|
|
368
|
+
type: "object",
|
|
369
|
+
properties: {
|
|
370
|
+
install_id: {
|
|
371
|
+
type: "string",
|
|
372
|
+
description: "The install row UUID (from tool_list_installed).",
|
|
373
|
+
},
|
|
374
|
+
},
|
|
375
|
+
required: ["install_id"],
|
|
376
|
+
},
|
|
377
|
+
},
|
|
378
|
+
{
|
|
379
|
+
name: "whoami",
|
|
380
|
+
description: "Probe the current PAT context: workspace slug, brand_id, agent role (if any), " +
|
|
381
|
+
"API base URL. Use this BEFORE sensitive operations to confirm which brand the " +
|
|
382
|
+
"PAT is pinned to. Pure session probe — emits one upstream GET against /auth/me.",
|
|
383
|
+
inputSchema: {
|
|
384
|
+
type: "object",
|
|
385
|
+
properties: {},
|
|
386
|
+
},
|
|
387
|
+
},
|
|
388
|
+
{
|
|
389
|
+
name: "board_as_markdown",
|
|
390
|
+
description: "Read a WHOLE AgentBoard (or a chosen subset of its columns) as a single " +
|
|
391
|
+
"Markdown plan/PRD document — one call instead of N task fetches. Cheaper on " +
|
|
392
|
+
"tokens and gives narrative context, not raw rows. Returns the rendered " +
|
|
393
|
+
"Markdown string verbatim (read-only, deterministic — same board state → same " +
|
|
394
|
+
"bytes, each task row carries an HTML comment with its UUID for diffing). " +
|
|
395
|
+
"Brand is pinned server-side from the PAT — never pass a brand_id. Pick `detail`: " +
|
|
396
|
+
"`summary` = compact one-row-per-task table (token-cheapest); `standard` " +
|
|
397
|
+
"(default) = the nested plan format with task bodies, subtasks and stages; " +
|
|
398
|
+
"`full` = the exhaustive dump including comments.",
|
|
399
|
+
inputSchema: {
|
|
400
|
+
type: "object",
|
|
401
|
+
properties: {
|
|
402
|
+
board_id: {
|
|
403
|
+
type: "string",
|
|
404
|
+
description: "The board UUID to render.",
|
|
405
|
+
},
|
|
406
|
+
detail: {
|
|
407
|
+
type: "string",
|
|
408
|
+
enum: ["summary", "standard", "full"],
|
|
409
|
+
description: "Render depth. Default `standard` (the nested plan/PRD shape). " +
|
|
410
|
+
"`summary` is the compact table; `full` adds comments.",
|
|
411
|
+
},
|
|
412
|
+
columns: {
|
|
413
|
+
type: "string",
|
|
414
|
+
description: "Optional comma-separated column filter — names OR ids, mixed. Each token " +
|
|
415
|
+
"is resolved name-first-then-id; an unknown token fails loud (400). Omit to " +
|
|
416
|
+
"render every column.",
|
|
417
|
+
},
|
|
418
|
+
},
|
|
419
|
+
required: ["board_id"],
|
|
420
|
+
},
|
|
421
|
+
},
|
|
422
|
+
{
|
|
423
|
+
name: "sandbox_chat",
|
|
424
|
+
description: "VENDOR TEST MODE — live-chat your marketplace sandbox test agent through the " +
|
|
425
|
+
"REAL OPVS gateway. The agent is `skills:[]`, so the MODEL reads your published " +
|
|
426
|
+
"SKILL.md guidance and decides whether/how to call a method — this is the only " +
|
|
427
|
+
"level that validates 'does my guidance make an agent pick the right tool?'. " +
|
|
428
|
+
"Returns the agent's reply PLUS `tools_fired` — the tool calls captured from the " +
|
|
429
|
+
"agent's own session transcript (the load-bearing proof a skill fired; a reply " +
|
|
430
|
+
"alone can be produced with no call). Auth is your sandbox-scoped PAT from " +
|
|
431
|
+
"`~/.opvs/config.json`; the sandbox brand is pinned SERVER-SIDE from it — never " +
|
|
432
|
+
"pass a brand_id. Provision the sandbox + install your package first (dashboard " +
|
|
433
|
+
"or `opvs-skills`).",
|
|
434
|
+
inputSchema: {
|
|
435
|
+
type: "object",
|
|
436
|
+
properties: {
|
|
437
|
+
vendor: {
|
|
438
|
+
type: "string",
|
|
439
|
+
description: "Your vendor id (the sandbox owner). Example: the UUID or slug from your " +
|
|
440
|
+
"vendor onboarding.",
|
|
441
|
+
},
|
|
442
|
+
message: {
|
|
443
|
+
type: "string",
|
|
444
|
+
description: "The message to send to the sandbox agent this turn.",
|
|
445
|
+
},
|
|
446
|
+
timeout: {
|
|
447
|
+
type: "number",
|
|
448
|
+
description: "Per-turn timeout in seconds (1–300, default 120).",
|
|
449
|
+
},
|
|
450
|
+
},
|
|
451
|
+
required: ["vendor", "message"],
|
|
452
|
+
},
|
|
453
|
+
},
|
|
66
454
|
];
|
|
67
|
-
// ──
|
|
68
|
-
|
|
455
|
+
// ── Tool execution ────────────────────────────────────────────────────
|
|
456
|
+
/** Build the exact `@vendor/name` from a search-result row (defensive on field names). */
|
|
457
|
+
function resultPackageName(item) {
|
|
458
|
+
if (typeof item.name === "string" && item.name.startsWith("@"))
|
|
459
|
+
return item.name;
|
|
460
|
+
const vendorRaw = typeof item.vendor === "string"
|
|
461
|
+
? item.vendor
|
|
462
|
+
: typeof item.vendor_name === "string"
|
|
463
|
+
? item.vendor_name
|
|
464
|
+
: null;
|
|
465
|
+
const name = typeof item.name === "string" ? item.name : null;
|
|
466
|
+
if (vendorRaw && name) {
|
|
467
|
+
const v = vendorRaw.startsWith("@") ? vendorRaw : `@${vendorRaw}`;
|
|
468
|
+
return `${v}/${name}`;
|
|
469
|
+
}
|
|
470
|
+
return null;
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* Make tool_search results copy-paste-proof: attach the exact `install` command
|
|
474
|
+
* + `tool_read` call to every row, plus a `_guidance` line telling the agent to
|
|
475
|
+
* pass the name VERBATIM — a model "helpfully" singularizing/pluralizing a
|
|
476
|
+
* package name is the #1 cause of a marketplace 404.
|
|
477
|
+
*/
|
|
478
|
+
function enrichSearchResults(data) {
|
|
479
|
+
if (!data || typeof data !== "object")
|
|
480
|
+
return data;
|
|
481
|
+
const d = data;
|
|
482
|
+
const rows = (Array.isArray(d.results) && d.results) ||
|
|
483
|
+
(Array.isArray(d.packages) && d.packages) ||
|
|
484
|
+
(Array.isArray(d.items) && d.items) ||
|
|
485
|
+
null;
|
|
486
|
+
if (rows) {
|
|
487
|
+
for (const item of rows) {
|
|
488
|
+
if (!item || typeof item !== "object")
|
|
489
|
+
continue;
|
|
490
|
+
const pkg = resultPackageName(item);
|
|
491
|
+
if (!pkg)
|
|
492
|
+
continue;
|
|
493
|
+
item.install = `opvs-skills install ${pkg} --runtime <claude-code|antigravity|cursor|windsurf> --dest .`;
|
|
494
|
+
item.read = `tool_read("${pkg}")`;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
const prior = typeof d._guidance === "string" ? d._guidance + " " : "";
|
|
498
|
+
d._guidance =
|
|
499
|
+
prior +
|
|
500
|
+
"#names:pass a result's `.name` (or the ready `.install`/`.read`) VERBATIM to " +
|
|
501
|
+
"tool_read/tool_install/CLI — never singularize, pluralize, or paraphrase it " +
|
|
502
|
+
"(the #1 cause of a marketplace 404).";
|
|
503
|
+
return d;
|
|
504
|
+
}
|
|
505
|
+
async function handleToolSearch(args) {
|
|
506
|
+
if (!args.intent || typeof args.intent !== "string") {
|
|
507
|
+
return err("intent is required", "#audit.error:input=missing — tool_search needs a free-text intent string.");
|
|
508
|
+
}
|
|
509
|
+
const q = new URLSearchParams({
|
|
510
|
+
intent: args.intent,
|
|
511
|
+
limit: String(args.limit ?? 5),
|
|
512
|
+
});
|
|
513
|
+
if (args.additional_filters && typeof args.additional_filters === "object") {
|
|
514
|
+
// Forward filters individually so future server-side handling can pick them
|
|
515
|
+
// up by name. The locked contract reserves `package` (vendor/name string).
|
|
516
|
+
// Other keys pass through untouched.
|
|
517
|
+
for (const [k, v] of Object.entries(args.additional_filters)) {
|
|
518
|
+
if (v == null)
|
|
519
|
+
continue;
|
|
520
|
+
q.set(`filter_${k}`, typeof v === "string" ? v : JSON.stringify(v));
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
if (args.include_schemas)
|
|
524
|
+
q.set("include_schemas", "true");
|
|
525
|
+
try {
|
|
526
|
+
const data = await getJson(`/api/v1/registry/search?${q.toString()}`);
|
|
527
|
+
return ok(enrichSearchResults(data));
|
|
528
|
+
}
|
|
529
|
+
catch (e) {
|
|
530
|
+
const error = e;
|
|
531
|
+
return err(`tool_search failed: ${error.status ?? "?"} ${error.detail ?? "unknown"}`, "#audit.error:registry-search " +
|
|
532
|
+
"#next:fall back to tool_list_installed for a flat catalog of what this brand has.");
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
/** Is this descriptor a board / DAG / factory skill? Keyed on the registry
|
|
536
|
+
* category first (stable), falling back to the skill name. Drives the
|
|
537
|
+
* factory-operating-model `_guidance` nudge below. */
|
|
538
|
+
function isBoardFactorySkill(refName, data) {
|
|
539
|
+
const category = String(data.category ?? "").toLowerCase();
|
|
540
|
+
if (category === "board")
|
|
541
|
+
return true;
|
|
542
|
+
return /agentboard|\bboard\b/.test(refName.toLowerCase());
|
|
543
|
+
}
|
|
544
|
+
/** Routing nudge for board/factory skills (GS.2). A POINTER to the canonical
|
|
545
|
+
* sources (the SKILL.md `description` section + its references index + the live
|
|
546
|
+
* claimTask/getRunnable `_guidance` envelopes), carrying the few platform
|
|
547
|
+
* invariants an MCP agent needs at decision time so it can't drive a factory
|
|
548
|
+
* board blind. Deliberately terse — the full recipe is run-a-factory-board.md,
|
|
549
|
+
* surfaced via section='description'; this never restates it. */
|
|
550
|
+
const FACTORY_HELP_GUIDANCE = "#use:board/DAG/factory task pipelines. " +
|
|
551
|
+
"#read:section='description' for the operating model + references index (the " +
|
|
552
|
+
"run-a-factory-board recipe). " +
|
|
553
|
+
"#factory:new cards land in Backlog (staging, NOT claimable) → move to Ready " +
|
|
554
|
+
"(claimable by role 'builder') → a worker claims and it auto-moves to Executing. " +
|
|
555
|
+
"A factory board runs NOTHING until an opvs-sidecar worker is pointed at its " +
|
|
556
|
+
"board_id; set a `mode:` label (auto|assisted|manual) per card for build autonomy. " +
|
|
557
|
+
"#next:the live claimTask + getRunnable responses carry their own `_guidance` " +
|
|
558
|
+
"envelope (e.g. a claim that returns task_id:null+reason is a worker misconfig, " +
|
|
559
|
+
"not no-work) — read it.";
|
|
560
|
+
/** Inject the factory `_guidance` sibling into a tool_help payload when the
|
|
561
|
+
* descriptor is a board/factory skill. Object bodies only; never wraps. */
|
|
562
|
+
function okWithFactoryGuidance(body, refName, data) {
|
|
563
|
+
if (isBoardFactorySkill(refName, data)) {
|
|
564
|
+
body._guidance = FACTORY_HELP_GUIDANCE;
|
|
565
|
+
}
|
|
566
|
+
return ok(body);
|
|
567
|
+
}
|
|
568
|
+
async function handleToolHelp(args) {
|
|
569
|
+
if (!args.name) {
|
|
570
|
+
return err("name is required", "#audit.error:input=missing — pass a skill reference like '@opvs-ai/agentboard'.");
|
|
571
|
+
}
|
|
572
|
+
const ref = normalizeSkillRef(args.name);
|
|
573
|
+
const vendor = ref.vendor || "opvs-ai";
|
|
574
|
+
const canonicalVendor = `@${vendor}`;
|
|
575
|
+
const path = `/api/v1/registry/packages/${encodeURIComponent(canonicalVendor)}/${encodeURIComponent(ref.name)}`;
|
|
576
|
+
try {
|
|
577
|
+
const data = await getJson(path);
|
|
578
|
+
const section = (args.section || "all").toLowerCase();
|
|
579
|
+
if (section === "all")
|
|
580
|
+
return okWithFactoryGuidance(data, ref.name, data);
|
|
581
|
+
// The registry's package_to_summary_dict emits `vendor_name`, not
|
|
582
|
+
// `vendor`. Expose both keys so callers don't have to know which
|
|
583
|
+
// surface they're talking to.
|
|
584
|
+
const subset = {
|
|
585
|
+
vendor: data.vendor_name ?? data.vendor,
|
|
586
|
+
vendor_name: data.vendor_name ?? data.vendor,
|
|
587
|
+
name: data.name,
|
|
588
|
+
version: data.version,
|
|
589
|
+
};
|
|
590
|
+
if (section === "description") {
|
|
591
|
+
subset.description = data.description;
|
|
592
|
+
subset.category = data.category;
|
|
593
|
+
}
|
|
594
|
+
else if (section === "methods") {
|
|
595
|
+
// Prefer the rich `methods_index` embedded at publish (skill-object-schema
|
|
596
|
+
// slice 1.2): {skill_id: {method: {description, http, params}}} where
|
|
597
|
+
// `params` carries the full nested object `properties` verbatim. Mirrors the
|
|
598
|
+
// gateway `skill_describe(section=methods)` logic (skills-meta/index.ts) so
|
|
599
|
+
// MCP hosts get the same nested-param detail. Falls back to the compact
|
|
600
|
+
// `manifest.methods` (name/http/description, no params) for legacy packages
|
|
601
|
+
// whose registry row has no fetchable bundle yet.
|
|
602
|
+
const methodsIndex = data.methods_index;
|
|
603
|
+
const hasIndex = methodsIndex != null &&
|
|
604
|
+
typeof methodsIndex === "object" &&
|
|
605
|
+
Object.values(methodsIndex).some((m) => m != null &&
|
|
606
|
+
typeof m === "object" &&
|
|
607
|
+
Object.keys(m).length > 0);
|
|
608
|
+
if (hasIndex) {
|
|
609
|
+
subset.methods_index = methodsIndex;
|
|
610
|
+
}
|
|
611
|
+
else {
|
|
612
|
+
subset.methods =
|
|
613
|
+
data.manifest?.methods ?? null;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
else if (section === "manifest") {
|
|
617
|
+
subset.manifest = data.manifest ?? null;
|
|
618
|
+
}
|
|
619
|
+
else {
|
|
620
|
+
return okWithFactoryGuidance(data, ref.name, data);
|
|
621
|
+
}
|
|
622
|
+
return okWithFactoryGuidance(subset, ref.name, data);
|
|
623
|
+
}
|
|
624
|
+
catch (e) {
|
|
625
|
+
const error = e;
|
|
626
|
+
if (error.status === 404) {
|
|
627
|
+
return err(`package '${canonicalVendor}/${ref.name}' not found in registry`, "#audit.error:package=missing #next:try tool_search for similar skills.");
|
|
628
|
+
}
|
|
629
|
+
return err(`tool_help failed: ${error.status ?? "?"} ${error.detail ?? ""}`);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
async function handleToolRead(args) {
|
|
633
|
+
if (!args.name) {
|
|
634
|
+
return err("name is required", "#audit.error:input=missing — pass a skill reference like '@opvs-ai/agentboard'.");
|
|
635
|
+
}
|
|
636
|
+
const ref = normalizeSkillRef(args.name);
|
|
637
|
+
const vendor = ref.vendor || "opvs-ai";
|
|
638
|
+
const canonicalVendor = `@${vendor}`;
|
|
639
|
+
const base = `/api/v1/registry/packages/${encodeURIComponent(canonicalVendor)}/${encodeURIComponent(ref.name)}/skill-guide`;
|
|
640
|
+
// Specific layer-file read → the 3.1 endpoint emits raw text/markdown (not
|
|
641
|
+
// JSON), so read it as text and return the body verbatim.
|
|
642
|
+
if (args.path) {
|
|
643
|
+
const q = new URLSearchParams({ path: args.path });
|
|
644
|
+
try {
|
|
645
|
+
const body = await getText(`${base}?${q.toString()}`);
|
|
646
|
+
return ok(body);
|
|
647
|
+
}
|
|
648
|
+
catch (e) {
|
|
649
|
+
const error = e;
|
|
650
|
+
if (error.status === 404) {
|
|
651
|
+
return err(`layer file '${args.path}' not found in ${canonicalVendor}/${ref.name}`, "#audit.error:layer=missing #next:call tool_read without `path` to see the " +
|
|
652
|
+
"valid `layers[]` list for this skill.");
|
|
653
|
+
}
|
|
654
|
+
return err(`tool_read failed: ${error.status ?? "?"} ${error.detail ?? ""}`, "#audit.error:upstream");
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
// Default → the JSON payload with the SKILL.md body + discoverable layers[].
|
|
658
|
+
try {
|
|
659
|
+
const data = await getJson(base);
|
|
660
|
+
if (typeof data === "object" && data !== null) {
|
|
661
|
+
const layers = Array.isArray(data.layers)
|
|
662
|
+
? data.layers
|
|
663
|
+
: [];
|
|
664
|
+
data._guidance =
|
|
665
|
+
layers.length > 0
|
|
666
|
+
? `#audit.ok:skill_md read + ${layers.length} layer file(s) available. ` +
|
|
667
|
+
"#next:re-call tool_read with `path` set to one of `layers[]` to read a " +
|
|
668
|
+
"reference file. #dont:tool_install isn't required to READ guidance — only to " +
|
|
669
|
+
"CALL the skill's methods."
|
|
670
|
+
: "#audit.ok:skill_md read (no extra layer files). " +
|
|
671
|
+
"#next:tool_help for the method schemas; tool_install to enable calling them.";
|
|
672
|
+
}
|
|
673
|
+
return ok(data);
|
|
674
|
+
}
|
|
675
|
+
catch (e) {
|
|
676
|
+
const error = e;
|
|
677
|
+
if (error.status === 404) {
|
|
678
|
+
return err(`no published guidance for '${canonicalVendor}/${ref.name}' ` +
|
|
679
|
+
"(package/version/SKILL.md missing)", "#audit.error:guide=missing #next:tool_search to find published skills; " +
|
|
680
|
+
"tool_help to confirm the package exists in the registry.");
|
|
681
|
+
}
|
|
682
|
+
return err(`tool_read failed: ${error.status ?? "?"} ${error.detail ?? ""}`, "#audit.error:upstream #next:tool_help to verify the package is reachable.");
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
async function handleToolCall(args) {
|
|
686
|
+
if (!args.skill || !args.method) {
|
|
687
|
+
return err("skill and method are required", "#audit.error:input=missing — tool_call needs both a skill reference and a method name.");
|
|
688
|
+
}
|
|
689
|
+
const resolved = await resolveInstallId(args.skill);
|
|
690
|
+
if (!resolved) {
|
|
691
|
+
return err(`skill '${args.skill}' not installed on this brand`, "#audit.error:skill=not-installed " +
|
|
692
|
+
"#next:try tool_search to discover candidates → tool_install to add one.");
|
|
693
|
+
}
|
|
694
|
+
// W2.0.A — resolve the URL skill segment from `manifest.skills[].id`, not the
|
|
695
|
+
// package name. Three branches when input is @vendor/name. Bare-name inputs
|
|
696
|
+
// pass through as-is (existing behaviour where sub-skill id == package name).
|
|
697
|
+
const inputRef = normalizeSkillRef(args.skill);
|
|
698
|
+
let routeSkillId = resolved.name;
|
|
699
|
+
if (inputRef.vendor !== "") {
|
|
700
|
+
const lookup = await fetchManifestSkillIds(resolved.vendor || inputRef.vendor, resolved.name);
|
|
701
|
+
if (!lookup.ok) {
|
|
702
|
+
return err(`tool_call route resolution failed: registry returned ${lookup.status} for ${resolved.vendor || `@${inputRef.vendor}`}/${resolved.name}`, "#audit.error:manifest-fetch " +
|
|
703
|
+
"#next:tool_help to verify the package is reachable.");
|
|
704
|
+
}
|
|
705
|
+
if (lookup.ids.length === 0) {
|
|
706
|
+
return err(`package ${resolved.vendor || `@${inputRef.vendor}`}/${resolved.name} declares no sub-skills (likely a meta-package — install a leaf package)`, "#audit.error:no-sub-skills " +
|
|
707
|
+
"#next:tool_list_installed to find the leaf packages it pulled in.");
|
|
708
|
+
}
|
|
709
|
+
if (lookup.ids.length === 1) {
|
|
710
|
+
routeSkillId = lookup.ids[0];
|
|
711
|
+
}
|
|
712
|
+
else {
|
|
713
|
+
return err(`package ${resolved.vendor || `@${inputRef.vendor}`}/${resolved.name} has ${lookup.ids.length} sub-skills, specify one of: ${lookup.ids.join(", ")}`, "#audit.error:ambiguous-sub-skill " +
|
|
714
|
+
"#next:re-call tool_call with `skill` set to one of the sub-skill ids above (these come from manifest.skills[].id).");
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
const path = `/api/v1/marketplace/runtime/${resolved.install_id}/${encodeURIComponent(routeSkillId)}/${encodeURIComponent(args.method)}`;
|
|
718
|
+
try {
|
|
719
|
+
const data = await postJson(path, args.params ?? {});
|
|
720
|
+
return ok(data);
|
|
721
|
+
}
|
|
722
|
+
catch (e) {
|
|
723
|
+
const error = e;
|
|
724
|
+
return err(`tool_call failed: ${error.status ?? "?"} ${error.detail ?? ""}`, "#audit.error:upstream " +
|
|
725
|
+
"#next:tool_help for the method's parameters; tool_list_installed to verify entitlements.");
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
async function handleToolPin(args) {
|
|
729
|
+
// W2.1 TOOLS.md persistence is live in the gateway's skills-meta plugin, but
|
|
730
|
+
// the underlying Management API endpoint is admin-key gated and not yet
|
|
731
|
+
// reachable from a PAT-authenticated MCP client. Returning an explicit
|
|
732
|
+
// not-yet-supported envelope (vs silent success) keeps the agent honest.
|
|
733
|
+
if (!args.name || !args.agent_id) {
|
|
734
|
+
return err("name and agent_id are required", "#audit.error:input=missing");
|
|
735
|
+
}
|
|
736
|
+
const ref = normalizeSkillRef(args.name);
|
|
737
|
+
const fullRef = ref.vendor ? `@${ref.vendor}/${ref.name}` : ref.name;
|
|
738
|
+
return ok({
|
|
739
|
+
action: args.action || "pin",
|
|
740
|
+
skill: fullRef,
|
|
741
|
+
agent_id: args.agent_id,
|
|
742
|
+
changed: false,
|
|
743
|
+
_guidance: "#audit.warn:not-supported-via-mcp — tool_pin's Management API surface " +
|
|
744
|
+
"isn't PAT-gated yet. Use the OPVS dashboard agent profile editor at " +
|
|
745
|
+
"/admin/agents/personas to pin/unpin skills, or run skill_pin via the " +
|
|
746
|
+
"gateway plugin if you have an OpenClaw agent session. The MCP shim " +
|
|
747
|
+
"will start writing through once W2.MCP.PIN ships the PAT passthrough.",
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
async function handleToolListInstalled() {
|
|
751
|
+
try {
|
|
752
|
+
const data = await getJson("/api/v1/marketplace/installed");
|
|
753
|
+
const items = data.items ?? [];
|
|
754
|
+
const active = items.filter((i) => i.state === "active").length;
|
|
755
|
+
return ok({
|
|
756
|
+
...data,
|
|
757
|
+
_guidance: `#audit.ok:installs=${items.length} active=${active}. ` +
|
|
758
|
+
"#next:tool_help on any of these to see methods, or tool_call to invoke.",
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
catch (e) {
|
|
762
|
+
const error = e;
|
|
763
|
+
return err(`tool_list_installed failed: ${error.status ?? "?"} ${error.detail ?? ""}`, "#audit.error:upstream");
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
async function handleToolInstall(args) {
|
|
767
|
+
if (!args.package_id) {
|
|
768
|
+
return err("package_id is required", "#audit.error:input=missing — call tool_search first to get a package_id (the `id` field).");
|
|
769
|
+
}
|
|
770
|
+
const body = {};
|
|
771
|
+
if (args.granted_entitlements)
|
|
772
|
+
body.granted_entitlements = args.granted_entitlements;
|
|
773
|
+
if (args.credential_bindings)
|
|
774
|
+
body.credential_bindings = args.credential_bindings;
|
|
775
|
+
try {
|
|
776
|
+
const data = await postJson(`/api/v1/marketplace/packages/${encodeURIComponent(args.package_id)}/install`, body);
|
|
777
|
+
return ok(data);
|
|
778
|
+
}
|
|
779
|
+
catch (e) {
|
|
780
|
+
const error = e;
|
|
781
|
+
return err(`tool_install failed: ${error.status ?? "?"} ${error.detail ?? ""}`, "#audit.error:install " +
|
|
782
|
+
"#next:if 422, inspect `detail` for the missing entitlements/credentials list.");
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
async function handleToolUninstall(args) {
|
|
786
|
+
if (!args.install_id) {
|
|
787
|
+
return err("install_id is required", "#audit.error:input=missing — call tool_list_installed to find the install_id.");
|
|
788
|
+
}
|
|
789
|
+
try {
|
|
790
|
+
await del204(`/api/v1/marketplace/installed/${encodeURIComponent(args.install_id)}`);
|
|
791
|
+
return ok({
|
|
792
|
+
install_id: args.install_id,
|
|
793
|
+
state: "uninstalled",
|
|
794
|
+
_guidance: "#audit.ok:uninstalled — entitlement grants revoked; gateway picks up the flip within 60s.",
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
catch (e) {
|
|
798
|
+
const error = e;
|
|
799
|
+
return err(`tool_uninstall failed: ${error.status ?? "?"} ${error.detail ?? ""}`, "#audit.error:uninstall");
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
async function handleWhoami() {
|
|
803
|
+
try {
|
|
804
|
+
const cfg = getEffectiveConfig();
|
|
805
|
+
const me = await getJson("/api/v1/auth/me");
|
|
806
|
+
return ok({
|
|
807
|
+
api_url: cfg.api_url,
|
|
808
|
+
workspace: cfg.workspace_slug ?? null,
|
|
809
|
+
brand_id: cfg.brand_id ?? null,
|
|
810
|
+
identity: me,
|
|
811
|
+
_guidance: cfg.brand_id != null
|
|
812
|
+
? `#audit.ok:brand_id=${cfg.brand_id} workspace=${cfg.workspace_slug ?? "?"}`
|
|
813
|
+
: "#audit.warn:brand=missing — this PAT isn't brand-scoped; brand-pinned " +
|
|
814
|
+
"tools (tool_call, tool_list_installed) will refuse. Run `opvs auth request` " +
|
|
815
|
+
"with a brand-scoped workspace.",
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
catch (e) {
|
|
819
|
+
const error = e;
|
|
820
|
+
return err(`whoami failed: ${error.status ?? "?"} ${error.detail ?? ""}`, "#audit.error:auth — PAT may be missing or expired. Run `opvs auth request`.");
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
async function handleBoardAsMarkdown(args) {
|
|
824
|
+
if (!args.board_id || typeof args.board_id !== "string") {
|
|
825
|
+
return err("board_id is required", "#audit.error:input=missing — board_as_markdown needs the board UUID. " +
|
|
826
|
+
"Use tool_call(@opvs-ai/agentboard, list_boards) to find one.");
|
|
827
|
+
}
|
|
828
|
+
if (args.detail != null &&
|
|
829
|
+
!["summary", "standard", "full"].includes(args.detail)) {
|
|
830
|
+
return err(`invalid detail '${args.detail}'`, "#audit.error:input=bad-detail — detail must be summary | standard | full. " +
|
|
831
|
+
"#next:omit it for the default (standard).");
|
|
832
|
+
}
|
|
833
|
+
// Mirror the export contract: format=md is fixed; detail/columns are forwarded
|
|
834
|
+
// only when the caller set them (don't synthesize defaults — let the backend
|
|
835
|
+
// own them; LEARNINGS #38, the silent-drop tax cuts both ways).
|
|
836
|
+
const q = new URLSearchParams({ format: "md" });
|
|
837
|
+
if (args.detail)
|
|
838
|
+
q.set("detail", args.detail);
|
|
839
|
+
if (args.columns)
|
|
840
|
+
q.set("columns", args.columns);
|
|
841
|
+
const path = `/api/v1/board/boards/${encodeURIComponent(args.board_id)}/export?${q.toString()}`;
|
|
842
|
+
try {
|
|
843
|
+
const md = await getText(path);
|
|
844
|
+
return ok(md);
|
|
845
|
+
}
|
|
846
|
+
catch (e) {
|
|
847
|
+
const error = e;
|
|
848
|
+
if (error.status === 400) {
|
|
849
|
+
return err(`board_as_markdown: ${error.detail ?? "bad request"}`, "#audit.error:bad-columns — an unknown column name/id was passed. " +
|
|
850
|
+
"#next:tool_call(@opvs-ai/agentboard, list_columns) to see valid columns, or omit `columns`.");
|
|
851
|
+
}
|
|
852
|
+
if (error.status === 404) {
|
|
853
|
+
return err(`board '${args.board_id}' not found (or not visible to this brand)`, "#audit.error:board=missing #next:whoami to confirm the PAT's brand; " +
|
|
854
|
+
"tool_call(@opvs-ai/agentboard, list_boards) to list boards.");
|
|
855
|
+
}
|
|
856
|
+
return err(`board_as_markdown failed: ${error.status ?? "?"} ${error.detail ?? ""}`, "#audit.error:upstream #next:whoami to verify auth/brand; check the board_id.");
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
async function handleSandboxChat(args) {
|
|
860
|
+
if (!args.vendor || !args.message) {
|
|
861
|
+
return err("vendor and message are required", "#audit.error:input=missing — sandbox_chat needs your vendor id and a message. " +
|
|
862
|
+
"The sandbox brand is pinned server-side from your PAT.");
|
|
863
|
+
}
|
|
864
|
+
const body = { message: args.message };
|
|
865
|
+
if (args.timeout != null)
|
|
866
|
+
body.timeout = args.timeout;
|
|
867
|
+
const path = `/api/v1/marketplace/vendors/${encodeURIComponent(args.vendor)}/sandbox/chat`;
|
|
868
|
+
try {
|
|
869
|
+
const data = await postJson(path, body);
|
|
870
|
+
const fired = Array.isArray(data.tools_fired) ? data.tools_fired : [];
|
|
871
|
+
// Attach the tool-fire verdict as a sibling `_guidance` string (W1.A) so the
|
|
872
|
+
// driving agent parses the EVIDENCE, not just the reply text.
|
|
873
|
+
data._guidance =
|
|
874
|
+
fired.length > 0
|
|
875
|
+
? `#audit.ok:tools_fired=${fired.join(",")} — verified from the sandbox agent's ` +
|
|
876
|
+
"session transcript (authoritative). #next:inspect tool_calls for ids/timestamps."
|
|
877
|
+
: "#audit.warn:no-tool-fired — the model answered from guidance alone this turn. " +
|
|
878
|
+
"If you expected a skill call, sharpen the SKILL.md decision rules or the " +
|
|
879
|
+
"method `guidance.use`, then retry.";
|
|
880
|
+
return ok(data);
|
|
881
|
+
}
|
|
882
|
+
catch (e) {
|
|
883
|
+
const error = e;
|
|
884
|
+
if (error.status === 403) {
|
|
885
|
+
return err(`sandbox_chat forbidden: ${error.detail ?? "PAT is not sandbox-scoped"}`, "#audit.error:auth — this tool needs a SANDBOX-scoped PAT (from sandbox " +
|
|
886
|
+
"provision) or a platform admin key. A real-brand PAT is refused.");
|
|
887
|
+
}
|
|
888
|
+
if (error.status === 404) {
|
|
889
|
+
return err(`sandbox_chat: no sandbox for vendor '${args.vendor}'`, "#audit.error:no-sandbox #next:provision the sandbox + install your package first.");
|
|
890
|
+
}
|
|
891
|
+
return err(`sandbox_chat failed: ${error.status ?? "?"} ${error.detail ?? ""}`, "#audit.error:upstream #next:confirm the gateway is up and the sandbox agent exists.");
|
|
892
|
+
}
|
|
893
|
+
}
|
|
69
894
|
async function handleTool(name, args) {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
return
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
895
|
+
switch (name) {
|
|
896
|
+
case "tool_search":
|
|
897
|
+
return handleToolSearch(args);
|
|
898
|
+
case "tool_help":
|
|
899
|
+
return handleToolHelp(args);
|
|
900
|
+
case "tool_read":
|
|
901
|
+
return handleToolRead(args);
|
|
902
|
+
case "tool_call":
|
|
903
|
+
return handleToolCall(args);
|
|
904
|
+
case "tool_pin":
|
|
905
|
+
return handleToolPin(args);
|
|
906
|
+
case "tool_list_installed":
|
|
907
|
+
return handleToolListInstalled();
|
|
908
|
+
case "tool_install":
|
|
909
|
+
return handleToolInstall(args);
|
|
910
|
+
case "tool_uninstall":
|
|
911
|
+
return handleToolUninstall(args);
|
|
912
|
+
case "whoami":
|
|
913
|
+
return handleWhoami();
|
|
914
|
+
case "board_as_markdown":
|
|
915
|
+
return handleBoardAsMarkdown(args);
|
|
916
|
+
case "sandbox_chat":
|
|
917
|
+
return handleSandboxChat(args);
|
|
918
|
+
default:
|
|
919
|
+
return err(`unknown tool: ${name}`);
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
// ── Server setup ──────────────────────────────────────────────────────
|
|
923
|
+
const server = new Server({ name: "opvs", version: PKG_VERSION }, { capabilities: { tools: {} } });
|
|
924
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
|
|
87
925
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
88
926
|
const { name, arguments: args } = request.params;
|
|
89
927
|
try {
|
|
90
|
-
// Try generated tools first (opvsHUB skills)
|
|
91
|
-
const generatedResult = await handleGeneratedTool(name, (args ?? {}), callApi, buildUrl, ok);
|
|
92
|
-
if (generatedResult !== null)
|
|
93
|
-
return generatedResult;
|
|
94
|
-
// Fall back to hand-written tools
|
|
95
928
|
return await handleTool(name, (args ?? {}));
|
|
96
929
|
}
|
|
97
930
|
catch (e) {
|