@opvs-ai/mcp 0.7.4 → 2.6.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/dist/index.js CHANGED
@@ -1,97 +1,930 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * @opvs-ai/mcp — MCP Server for AgentBoard + AgentDocs
3
+ * @opvs-ai/mcp — MCP server for the OPVS on-demand model
4
4
  *
5
- * Gives Claude Code, Cursor, and Claude Desktop native tool access
6
- * to OPVS AgentBoard task management and AgentDocs documentation.
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
- * Transport: stdio (JSON-RPC over stdin/stdout)
9
- * Auth: reads token from ~/.opvs/config.json (shared with @opvs-ai/cli)
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
- * Multi-workspace: every tool accepts an optional `workspace` parameter.
12
- * If omitted, uses OPVS_WORKSPACE env var or the current workspace from config.
21
+ * Transport: stdio (JSON-RPC). Never write to stdout outside the SDK.
22
+ * Debug logs go to stderr via console.error().
13
23
  *
14
- * IMPORTANT: Never use console.log() — it corrupts the stdio transport.
15
- * Use console.error() for debug logging.
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, buildUrl, getResponseText } from "@opvs-ai/core";
21
- // ── Handler modules ─────────────────────────────────────────────
22
- import { TOOLS as BOARD_TOOLS, handleTool as handleBoard } from "./handlers/board.js";
23
- import { TOOLS as AGENT_TOOLS, handleTool as handleAgent } from "./handlers/agent.js";
24
- import { TOOLS as DOCS_TOOLS, handleTool as handleDocs } from "./handlers/docs.js";
25
- import { TOOLS as VIEWS_TOOLS, handleTool as handleViews } from "./handlers/views.js";
26
- import { TOOLS as STAGES_TOOLS, handleTool as handleStages } from "./handlers/stages.js";
27
- import { TOOLS as AUTH_TOOLS, handleTool as handleAuth } from "./handlers/auth.js";
28
- import { TOOLS as PROTOCOL_TOOLS, handleTool as handleProtocol } from "./handlers/protocol.js";
29
- import { GENERATED_TOOLS, handleGeneratedTool } from "./generated-tools.js";
30
- import { err } from "./handlers/types.js";
31
- // ── API client ──────────────────────────────────────────────────
32
- async function callApi(method, path, wsSlug, body, headers) {
33
- const client = createClient(wsSlug);
34
- let res;
35
- switch (method) {
36
- case "GET":
37
- res = await client.get(path, headers);
38
- break;
39
- case "POST":
40
- res = await client.post(path, body, headers);
41
- break;
42
- case "PATCH":
43
- res = await client.patch(path, body, headers);
44
- break;
45
- case "PUT":
46
- res = await client.put(path, body, headers);
47
- break;
48
- case "DELETE":
49
- res = await client.delete(path, headers);
50
- break;
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 ok(text) {
55
- return { content: [{ type: "text", text }] };
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
- // ── Tool registry ───────────────────────────────────────────────
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
- ...BOARD_TOOLS,
60
- ...AGENT_TOOLS,
61
- ...DOCS_TOOLS,
62
- ...VIEWS_TOOLS,
63
- ...STAGES_TOOLS,
64
- ...AUTH_TOOLS,
65
- ...PROTOCOL_TOOLS,
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
- // ── Handler router ──────────────────────────────────────────────
68
- const handlers = [handleBoard, handleAgent, handleDocs, handleViews, handleStages, handleProtocol];
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
- // Try each domain handler
71
- for (const handler of handlers) {
72
- const result = await handler(name, args, callApi, buildUrl);
73
- if (result !== null)
74
- return result;
75
- }
76
- // Auth handler has a different signature (no buildUrl)
77
- const authResult = await handleAuth(name, args, callApi);
78
- if (authResult !== null)
79
- return authResult;
80
- return err(`Unknown tool: ${name}`);
81
- }
82
- // ── Server setup ────────────────────────────────────────────────
83
- const server = new Server({ name: "opvs", version: "0.7.4" }, { capabilities: { tools: {} } });
84
- server.setRequestHandler(ListToolsRequestSchema, async () => ({
85
- tools: [...TOOLS, ...GENERATED_TOOLS],
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) {