@vantageos/vantage-registry-mcp 1.6.0 → 1.7.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/README.md +45 -2
- package/package.json +2 -2
- package/server.js +1714 -1515
- package/server.ts +448 -133
package/server.ts
CHANGED
|
@@ -19,10 +19,15 @@
|
|
|
19
19
|
* Orchestrator: Omega — VantageOS Team | 2026-06-01
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
+
import { createServer } from "node:http";
|
|
23
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
24
|
+
import type { AddressInfo } from "node:net";
|
|
25
|
+
import { timingSafeEqual } from "node:crypto";
|
|
22
26
|
import { readFileSync } from "node:fs";
|
|
23
27
|
import { resolve } from "node:path";
|
|
24
28
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
25
29
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
30
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
26
31
|
import { ConvexHttpClient } from "convex/browser";
|
|
27
32
|
import { z } from "zod";
|
|
28
33
|
import { api } from "../convex/_generated/api.js";
|
|
@@ -121,23 +126,330 @@ const linkTypeSchema = z
|
|
|
121
126
|
"Link type — uses: template consumed during execution, produces: output document generated, references: informational cross-reference",
|
|
122
127
|
);
|
|
123
128
|
|
|
129
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
130
|
+
// Admin write secret — ported from scripts/backfill-vr-content.ts pattern (lines 58-68)
|
|
131
|
+
// All Convex write mutations require this secret since PR #93 (write-auth-guard).
|
|
132
|
+
// Only injected when the MCP server is in ADMIN mode (MCP_BEARER_TOKEN auth).
|
|
133
|
+
// Read-only instances (MCP_BEARER_TOKEN_READONLY) must never inject or execute writes.
|
|
134
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Injects adminSecret into mutation args, gated on ADMIN mode AND secret presence.
|
|
138
|
+
* Mirrors the pattern from scripts/backfill-vr-content.ts lines 58-68 / 231.
|
|
139
|
+
*
|
|
140
|
+
* VR_ADMIN_WRITE_SECRET is read from process.env at call time (not module load)
|
|
141
|
+
* so tests can set it after import and live servers pick up rotated values.
|
|
142
|
+
*
|
|
143
|
+
* Two failure modes (both throw — zero mutations fired):
|
|
144
|
+
* 1. isAdmin=false → read-only mode; write forbidden regardless of secret presence.
|
|
145
|
+
* 2. VR_ADMIN_WRITE_SECRET unset → admin mode but server not configured for writes.
|
|
146
|
+
*
|
|
147
|
+
* @param args - Mutation args the caller would pass without the secret.
|
|
148
|
+
* @param isAdmin - true when authenticated via MCP_BEARER_TOKEN (admin role).
|
|
149
|
+
* Defaults to true for the stdio path (no HTTP layer);
|
|
150
|
+
* HTTP write tools pass role==="admin" from resolveRole().
|
|
151
|
+
*/
|
|
152
|
+
export function adminWriteArgs<T extends Record<string, unknown>>(
|
|
153
|
+
args: T,
|
|
154
|
+
isAdmin = true,
|
|
155
|
+
): T & { adminSecret: string } {
|
|
156
|
+
if (!isAdmin) {
|
|
157
|
+
throw new Error(
|
|
158
|
+
"This MCP instance is in read-only mode. Write tools are not available. " +
|
|
159
|
+
"Use an admin token (MCP_BEARER_TOKEN) to enable write operations.",
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
const secret = process.env.VR_ADMIN_WRITE_SECRET;
|
|
163
|
+
if (!secret || secret.length === 0) {
|
|
164
|
+
throw new Error(
|
|
165
|
+
"This MCP instance is not configured for writes: VR_ADMIN_WRITE_SECRET is not set. " +
|
|
166
|
+
"Set VR_ADMIN_WRITE_SECRET in the server environment to enable write operations.",
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
return { ...args, adminSecret: secret };
|
|
170
|
+
}
|
|
171
|
+
|
|
124
172
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
125
173
|
// Server setup
|
|
126
174
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
127
175
|
|
|
128
|
-
|
|
129
|
-
|
|
176
|
+
// Lazy ConvexHttpClient — deferred until first use so tests can mock
|
|
177
|
+
// convex/browser before any instantiation occurs at module load time.
|
|
178
|
+
let _convex: ConvexHttpClient | null = null;
|
|
179
|
+
function getConvex(): ConvexHttpClient {
|
|
180
|
+
if (!_convex) {
|
|
181
|
+
_convex = new ConvexHttpClient(loadConvexUrl());
|
|
182
|
+
}
|
|
183
|
+
return _convex;
|
|
184
|
+
}
|
|
185
|
+
// Proxy object so existing `convex.query(...)` / `convex.mutation(...)` calls
|
|
186
|
+
// in tool handlers continue to work without modification.
|
|
187
|
+
const convex = new Proxy({} as ConvexHttpClient, {
|
|
188
|
+
get(_target, prop) {
|
|
189
|
+
const client = getConvex();
|
|
190
|
+
const value = (client as unknown as Record<string | symbol, unknown>)[prop];
|
|
191
|
+
if (typeof value === "function") {
|
|
192
|
+
return value.bind(client);
|
|
193
|
+
}
|
|
194
|
+
return value;
|
|
195
|
+
},
|
|
196
|
+
});
|
|
130
197
|
|
|
131
198
|
const server = new McpServer({
|
|
132
199
|
name: "vantage-registry",
|
|
133
|
-
version: "1.
|
|
200
|
+
version: "1.7.0",
|
|
134
201
|
});
|
|
135
202
|
|
|
203
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
204
|
+
// HTTP transport — Bearer auth + read-only scope
|
|
205
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Write tools — any tools/call for these names is blocked for read-only tokens.
|
|
209
|
+
* Derived from the full set of `server.tool(...)` registrations above.
|
|
210
|
+
*/
|
|
211
|
+
export const WRITE_TOOLS: ReadonlySet<string> = new Set([
|
|
212
|
+
"upsert_team",
|
|
213
|
+
"upsert_agent",
|
|
214
|
+
"upsert_skill",
|
|
215
|
+
"upsert_skill_content",
|
|
216
|
+
"upsert_agent_content",
|
|
217
|
+
"upsert_plugin_content",
|
|
218
|
+
"upsert_hook_content",
|
|
219
|
+
"upsert_command_content",
|
|
220
|
+
"upsert_plugin",
|
|
221
|
+
"upsert_hook",
|
|
222
|
+
"upsert_prompt",
|
|
223
|
+
"upsert_template",
|
|
224
|
+
"upsert_test_run",
|
|
225
|
+
"upsert_skill_eval_corpus",
|
|
226
|
+
"upsert_runbook",
|
|
227
|
+
"delete_runbook",
|
|
228
|
+
"link_runbook_template",
|
|
229
|
+
"unlink_runbook_template",
|
|
230
|
+
"register_component",
|
|
231
|
+
"update_component",
|
|
232
|
+
"delete_component",
|
|
233
|
+
]);
|
|
234
|
+
|
|
235
|
+
/** Resolve incoming bearer token to a role or null (unauthorized). */
|
|
236
|
+
function resolveRole(
|
|
237
|
+
authHeader: string | undefined,
|
|
238
|
+
): "admin" | "readonly" | null {
|
|
239
|
+
if (!authHeader) return null;
|
|
240
|
+
const prefix = "Bearer ";
|
|
241
|
+
if (!authHeader.startsWith(prefix)) return null;
|
|
242
|
+
const token = authHeader.slice(prefix.length);
|
|
243
|
+
|
|
244
|
+
const adminToken = process.env.MCP_BEARER_TOKEN;
|
|
245
|
+
const roToken = process.env.MCP_BEARER_TOKEN_READONLY;
|
|
246
|
+
|
|
247
|
+
const enc = new TextEncoder();
|
|
248
|
+
const tokenBuf = enc.encode(token);
|
|
249
|
+
|
|
250
|
+
if (adminToken) {
|
|
251
|
+
const adminBuf = enc.encode(adminToken);
|
|
252
|
+
// timingSafeEqual requires same length — pad shorter with zeros
|
|
253
|
+
const len = Math.max(tokenBuf.length, adminBuf.length);
|
|
254
|
+
const a = Buffer.alloc(len);
|
|
255
|
+
const b = Buffer.alloc(len);
|
|
256
|
+
Buffer.from(tokenBuf).copy(a);
|
|
257
|
+
Buffer.from(adminBuf).copy(b);
|
|
258
|
+
if (timingSafeEqual(a, b) && token.length === adminToken.length) {
|
|
259
|
+
return "admin";
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (roToken) {
|
|
264
|
+
const roBuf = enc.encode(roToken);
|
|
265
|
+
const len = Math.max(tokenBuf.length, roBuf.length);
|
|
266
|
+
const a = Buffer.alloc(len);
|
|
267
|
+
const b = Buffer.alloc(len);
|
|
268
|
+
Buffer.from(tokenBuf).copy(a);
|
|
269
|
+
Buffer.from(roBuf).copy(b);
|
|
270
|
+
if (timingSafeEqual(a, b) && token.length === roToken.length) {
|
|
271
|
+
return "readonly";
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** Send a JSON error response. */
|
|
279
|
+
function sendJsonError(
|
|
280
|
+
res: ServerResponse,
|
|
281
|
+
status: number,
|
|
282
|
+
body: Record<string, unknown>,
|
|
283
|
+
extraHeaders?: Record<string, string>,
|
|
284
|
+
): void {
|
|
285
|
+
const payload = JSON.stringify(body);
|
|
286
|
+
res.writeHead(status, {
|
|
287
|
+
"Content-Type": "application/json",
|
|
288
|
+
"Content-Length": Buffer.byteLength(payload),
|
|
289
|
+
...extraHeaders,
|
|
290
|
+
});
|
|
291
|
+
res.end(payload);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** Read the full request body as a Buffer. */
|
|
295
|
+
async function readBody(req: IncomingMessage): Promise<Buffer> {
|
|
296
|
+
return new Promise((resolve, reject) => {
|
|
297
|
+
const chunks: Buffer[] = [];
|
|
298
|
+
req.on("data", (c: Buffer) => chunks.push(c));
|
|
299
|
+
req.on("end", () => resolve(Buffer.concat(chunks)));
|
|
300
|
+
req.on("error", reject);
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Create the node:http request handler with Bearer auth + read-only scope gate.
|
|
306
|
+
* Returns an app-like object with a `fetch`-compatible `.handler` property.
|
|
307
|
+
*/
|
|
308
|
+
export function createHttpApp(): (
|
|
309
|
+
req: IncomingMessage,
|
|
310
|
+
res: ServerResponse,
|
|
311
|
+
) => void {
|
|
312
|
+
return async function handler(
|
|
313
|
+
req: IncomingMessage,
|
|
314
|
+
res: ServerResponse,
|
|
315
|
+
): Promise<void> {
|
|
316
|
+
const url = new URL(req.url ?? "/", `http://localhost`);
|
|
317
|
+
|
|
318
|
+
// Health check — unauthenticated
|
|
319
|
+
if (req.method === "GET" && url.pathname === "/health") {
|
|
320
|
+
sendJsonError(res, 200, {
|
|
321
|
+
status: "ok",
|
|
322
|
+
service: "vantage-registry-mcp",
|
|
323
|
+
transport: "streamable-http",
|
|
324
|
+
});
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Only handle /mcp
|
|
329
|
+
if (url.pathname !== "/mcp") {
|
|
330
|
+
sendJsonError(res, 404, { error: "not_found" });
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// ── Bearer auth ──────────────────────────────────────────────────────
|
|
335
|
+
// Only enforce bearer auth when MCP_BEARER_TOKEN is configured.
|
|
336
|
+
// If neither token is set, the server runs in open mode (e.g., local dev).
|
|
337
|
+
const adminToken = process.env.MCP_BEARER_TOKEN;
|
|
338
|
+
const roToken = process.env.MCP_BEARER_TOKEN_READONLY;
|
|
339
|
+
const bearerEnabled = Boolean(adminToken || roToken);
|
|
340
|
+
|
|
341
|
+
let role: "admin" | "readonly" | "open" = "open";
|
|
342
|
+
if (bearerEnabled) {
|
|
343
|
+
const resolved = resolveRole(req.headers["authorization"]);
|
|
344
|
+
if (resolved === null) {
|
|
345
|
+
sendJsonError(
|
|
346
|
+
res,
|
|
347
|
+
401,
|
|
348
|
+
{ error: "Unauthorized", message: "Missing or invalid bearer token" },
|
|
349
|
+
{ "WWW-Authenticate": 'Bearer realm="vantage-registry"' },
|
|
350
|
+
);
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
role = resolved;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// ── Read body once (needed for scope gate + forwarding to transport) ──
|
|
357
|
+
const rawBody = await readBody(req);
|
|
358
|
+
let parsedBody: unknown = null;
|
|
359
|
+
try {
|
|
360
|
+
parsedBody = JSON.parse(rawBody.toString("utf-8"));
|
|
361
|
+
} catch {
|
|
362
|
+
sendJsonError(res, 400, { error: "invalid_json" });
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// ── Read-only scope gate: block write tools ──────────────────────────
|
|
367
|
+
if (role === "readonly") {
|
|
368
|
+
const body = parsedBody as Record<string, unknown>;
|
|
369
|
+
if (
|
|
370
|
+
body &&
|
|
371
|
+
typeof body === "object" &&
|
|
372
|
+
body.method === "tools/call" &&
|
|
373
|
+
body.params &&
|
|
374
|
+
typeof body.params === "object"
|
|
375
|
+
) {
|
|
376
|
+
const toolName = (body.params as Record<string, unknown>).name;
|
|
377
|
+
if (typeof toolName === "string" && WRITE_TOOLS.has(toolName)) {
|
|
378
|
+
sendJsonError(res, 403, {
|
|
379
|
+
error: "Forbidden",
|
|
380
|
+
message: `Tool '${toolName}' requires admin scope. Read-only token cannot call write tools.`,
|
|
381
|
+
});
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// ── Fresh MCP server + stateless transport per request ───────────────
|
|
388
|
+
const mcpServer = new McpServer({
|
|
389
|
+
name: "vantage-registry",
|
|
390
|
+
version: "1.7.0",
|
|
391
|
+
});
|
|
392
|
+
// Re-register all tools on the per-request server, passing ADMIN mode flag
|
|
393
|
+
// so write tools know whether to inject adminSecret or refuse.
|
|
394
|
+
registerAllTools(mcpServer, role === "admin");
|
|
395
|
+
|
|
396
|
+
const transport = new StreamableHTTPServerTransport({
|
|
397
|
+
sessionIdGenerator: undefined, // stateless
|
|
398
|
+
enableJsonResponse: true, // return plain JSON, not SSE
|
|
399
|
+
});
|
|
400
|
+
await mcpServer.connect(transport);
|
|
401
|
+
|
|
402
|
+
// Inject the already-read body so transport doesn't need to re-read it
|
|
403
|
+
await transport.handleRequest(req, res, parsedBody);
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Start an HTTP server on the given port.
|
|
409
|
+
* Returns { close(), address() } matching the test contract.
|
|
410
|
+
*/
|
|
411
|
+
export function startHttpServer(
|
|
412
|
+
port: number,
|
|
413
|
+
): Promise<{ close(): void; address(): AddressInfo }> {
|
|
414
|
+
return new Promise((resolve, reject) => {
|
|
415
|
+
const handler = createHttpApp();
|
|
416
|
+
const httpServer = createServer(handler);
|
|
417
|
+
httpServer.on("error", reject);
|
|
418
|
+
httpServer.listen(port, "0.0.0.0", () => {
|
|
419
|
+
resolve({
|
|
420
|
+
close() {
|
|
421
|
+
httpServer.close();
|
|
422
|
+
},
|
|
423
|
+
address() {
|
|
424
|
+
return httpServer.address() as AddressInfo;
|
|
425
|
+
},
|
|
426
|
+
});
|
|
427
|
+
});
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
432
|
+
// Tool registration — called once for stdio server, and per-request for HTTP
|
|
433
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Register all MCP tools on the given server instance.
|
|
437
|
+
*
|
|
438
|
+
* @param s - The McpServer to register tools on.
|
|
439
|
+
* @param isAdmin - Whether the caller has admin privileges.
|
|
440
|
+
* - HTTP path: pass role==="admin" (resolved from MCP_BEARER_TOKEN).
|
|
441
|
+
* - stdio path: defaults to true — stdio has no token layer;
|
|
442
|
+
* adminWriteArgs() still enforces VR_ADMIN_WRITE_SECRET presence.
|
|
443
|
+
* Write tools call adminWriteArgs(args, isAdmin) which throws (zero
|
|
444
|
+
* mutation) if isAdmin=false OR VR_ADMIN_WRITE_SECRET is unset.
|
|
445
|
+
*/
|
|
446
|
+
function registerAllTools(s: McpServer, isAdmin = true): void {
|
|
447
|
+
|
|
136
448
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
137
449
|
// TEAMS
|
|
138
450
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
139
451
|
|
|
140
|
-
|
|
452
|
+
s.tool(
|
|
141
453
|
"upsert_team",
|
|
142
454
|
"Create or update a team in VantageRegistry. Upserts by name — if a team with the same name exists, it is updated.",
|
|
143
455
|
{
|
|
@@ -152,7 +464,7 @@ server.tool(
|
|
|
152
464
|
.describe("Associated project — e.g. 'vantage-starter'"),
|
|
153
465
|
},
|
|
154
466
|
async (args) => {
|
|
155
|
-
const id = await convex.mutation(api.teams.upsert, args);
|
|
467
|
+
const id = await convex.mutation(api.teams.upsert, adminWriteArgs(args, isAdmin));
|
|
156
468
|
return {
|
|
157
469
|
content: [
|
|
158
470
|
{ type: "text", text: JSON.stringify({ id, ...args }, null, 2) },
|
|
@@ -161,7 +473,7 @@ server.tool(
|
|
|
161
473
|
},
|
|
162
474
|
);
|
|
163
475
|
|
|
164
|
-
|
|
476
|
+
s.tool(
|
|
165
477
|
"list_teams",
|
|
166
478
|
"List all teams in VantageRegistry. Optionally filter by status (active, planned, deprecated).",
|
|
167
479
|
{
|
|
@@ -177,7 +489,7 @@ server.tool(
|
|
|
177
489
|
},
|
|
178
490
|
);
|
|
179
491
|
|
|
180
|
-
|
|
492
|
+
s.tool(
|
|
181
493
|
"get_team",
|
|
182
494
|
"Get a single team by its Convex document ID.",
|
|
183
495
|
{
|
|
@@ -195,7 +507,7 @@ server.tool(
|
|
|
195
507
|
// AGENTS
|
|
196
508
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
197
509
|
|
|
198
|
-
|
|
510
|
+
s.tool(
|
|
199
511
|
"upsert_agent",
|
|
200
512
|
"Create or update an agent in VantageRegistry. Upserts by name+team — if an agent with the same name and team exists, it is updated.",
|
|
201
513
|
{
|
|
@@ -223,7 +535,7 @@ server.tool(
|
|
|
223
535
|
.describe("Categories beyond team name"),
|
|
224
536
|
},
|
|
225
537
|
async (args) => {
|
|
226
|
-
const id = await convex.mutation(api.agents.upsert, args);
|
|
538
|
+
const id = await convex.mutation(api.agents.upsert, adminWriteArgs(args, isAdmin));
|
|
227
539
|
return {
|
|
228
540
|
content: [
|
|
229
541
|
{
|
|
@@ -239,7 +551,7 @@ server.tool(
|
|
|
239
551
|
},
|
|
240
552
|
);
|
|
241
553
|
|
|
242
|
-
|
|
554
|
+
s.tool(
|
|
243
555
|
"list_agents",
|
|
244
556
|
"List all agents in VantageRegistry. Optionally filter by status, team, or category. " +
|
|
245
557
|
"fields='lite' returns compact {_id, name, team, status}. fields='full' returns the complete document including content.",
|
|
@@ -276,7 +588,7 @@ server.tool(
|
|
|
276
588
|
},
|
|
277
589
|
);
|
|
278
590
|
|
|
279
|
-
|
|
591
|
+
s.tool(
|
|
280
592
|
"list_agents_by_team",
|
|
281
593
|
"List all agents belonging to a specific team.",
|
|
282
594
|
{
|
|
@@ -297,7 +609,7 @@ server.tool(
|
|
|
297
609
|
},
|
|
298
610
|
);
|
|
299
611
|
|
|
300
|
-
|
|
612
|
+
s.tool(
|
|
301
613
|
"get_agent",
|
|
302
614
|
"Get a single agent by its Convex document ID.",
|
|
303
615
|
{
|
|
@@ -315,7 +627,7 @@ server.tool(
|
|
|
315
627
|
// SKILLS
|
|
316
628
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
317
629
|
|
|
318
|
-
|
|
630
|
+
s.tool(
|
|
319
631
|
"upsert_skill",
|
|
320
632
|
"Create or update a skill in VantageRegistry. Upserts by name+team — if a skill with the same name and team exists, it is updated. " +
|
|
321
633
|
"Pass vrBody to write the full SKILL.md body (vrContent/vrContentHash/vrContentVersion) in the SAME call — no separate upsert_skill_content needed (that tool remains for back-compat). Omit vrBody for metadata-only upsert.",
|
|
@@ -353,7 +665,7 @@ server.tool(
|
|
|
353
665
|
),
|
|
354
666
|
},
|
|
355
667
|
async (args) => {
|
|
356
|
-
const id = await convex.mutation(api.skills.upsert, args);
|
|
668
|
+
const id = await convex.mutation(api.skills.upsert, adminWriteArgs(args, isAdmin));
|
|
357
669
|
return {
|
|
358
670
|
content: [
|
|
359
671
|
{
|
|
@@ -369,7 +681,7 @@ server.tool(
|
|
|
369
681
|
},
|
|
370
682
|
);
|
|
371
683
|
|
|
372
|
-
|
|
684
|
+
s.tool(
|
|
373
685
|
"list_skills",
|
|
374
686
|
"List all skills in VantageRegistry. Optionally filter by status, team, or category. " +
|
|
375
687
|
"fields='lite' returns compact {_id, name, team, category, status}. fields='full' returns the complete document including content.",
|
|
@@ -410,7 +722,7 @@ server.tool(
|
|
|
410
722
|
},
|
|
411
723
|
);
|
|
412
724
|
|
|
413
|
-
|
|
725
|
+
s.tool(
|
|
414
726
|
"list_skills_by_team",
|
|
415
727
|
"List all skills belonging to a specific team.",
|
|
416
728
|
{
|
|
@@ -431,7 +743,7 @@ server.tool(
|
|
|
431
743
|
},
|
|
432
744
|
);
|
|
433
745
|
|
|
434
|
-
|
|
746
|
+
s.tool(
|
|
435
747
|
"list_skills_by_category",
|
|
436
748
|
"List all skills of a specific category (capability, composite, playbook, root, external).",
|
|
437
749
|
{
|
|
@@ -452,7 +764,7 @@ server.tool(
|
|
|
452
764
|
},
|
|
453
765
|
);
|
|
454
766
|
|
|
455
|
-
|
|
767
|
+
s.tool(
|
|
456
768
|
"get_skill",
|
|
457
769
|
"Get a single skill by its Convex document ID.",
|
|
458
770
|
{
|
|
@@ -470,7 +782,7 @@ server.tool(
|
|
|
470
782
|
// SKILL CONTENT — VR Single-Source-of-Truth (D72-VR-SoT)
|
|
471
783
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
472
784
|
|
|
473
|
-
|
|
785
|
+
s.tool(
|
|
474
786
|
"get_skill_content",
|
|
475
787
|
"Fetch the VR-hosted canonical SKILL.md body for a skill. " +
|
|
476
788
|
"Returns content, sha256 hash, semver version, and last sync timestamp. " +
|
|
@@ -490,7 +802,7 @@ server.tool(
|
|
|
490
802
|
},
|
|
491
803
|
);
|
|
492
804
|
|
|
493
|
-
|
|
805
|
+
s.tool(
|
|
494
806
|
"upsert_skill_content",
|
|
495
807
|
"Set or update the canonical SKILL.md body in VantageRegistry. " +
|
|
496
808
|
"Computes sha256; if content is unchanged from the stored version the call is idempotent " +
|
|
@@ -513,11 +825,7 @@ server.tool(
|
|
|
513
825
|
async ({ name, content, createIfMissing }) => {
|
|
514
826
|
const result = await convex.mutation(
|
|
515
827
|
api.skillContentDb.upsertSkillContent,
|
|
516
|
-
{
|
|
517
|
-
name,
|
|
518
|
-
content,
|
|
519
|
-
createIfMissing,
|
|
520
|
-
},
|
|
828
|
+
adminWriteArgs({ name, content, createIfMissing }, isAdmin),
|
|
521
829
|
);
|
|
522
830
|
return {
|
|
523
831
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
@@ -525,7 +833,7 @@ server.tool(
|
|
|
525
833
|
},
|
|
526
834
|
);
|
|
527
835
|
|
|
528
|
-
|
|
836
|
+
s.tool(
|
|
529
837
|
"detect_skill_drift",
|
|
530
838
|
"Return the VR-side sha256 hash and filePath for each skill in scope. " +
|
|
531
839
|
"IMPORTANT: Convex queries have no filesystem access. This tool returns the VR " +
|
|
@@ -556,7 +864,7 @@ server.tool(
|
|
|
556
864
|
// AGENT CONTENT — VR Single-Source-of-Truth (D72-VR-SoT Phase A)
|
|
557
865
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
558
866
|
|
|
559
|
-
|
|
867
|
+
s.tool(
|
|
560
868
|
"get_agent_content",
|
|
561
869
|
"Fetch the VR-hosted canonical agent content body for an agent. " +
|
|
562
870
|
"Returns content, sha256 hash, semver version, and last sync timestamp. " +
|
|
@@ -576,7 +884,7 @@ server.tool(
|
|
|
576
884
|
},
|
|
577
885
|
);
|
|
578
886
|
|
|
579
|
-
|
|
887
|
+
s.tool(
|
|
580
888
|
"upsert_agent_content",
|
|
581
889
|
"Set or update the canonical agent content body in VantageRegistry. " +
|
|
582
890
|
"Computes sha256; if content is unchanged the call is idempotent (version not bumped). " +
|
|
@@ -600,11 +908,7 @@ server.tool(
|
|
|
600
908
|
async ({ name, content, createIfMissing }) => {
|
|
601
909
|
const result = await convex.mutation(
|
|
602
910
|
api.agentContentDb.upsertAgentContent,
|
|
603
|
-
{
|
|
604
|
-
name,
|
|
605
|
-
content,
|
|
606
|
-
createIfMissing,
|
|
607
|
-
},
|
|
911
|
+
adminWriteArgs({ name, content, createIfMissing }, isAdmin),
|
|
608
912
|
);
|
|
609
913
|
return {
|
|
610
914
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
@@ -612,7 +916,7 @@ server.tool(
|
|
|
612
916
|
},
|
|
613
917
|
);
|
|
614
918
|
|
|
615
|
-
|
|
919
|
+
s.tool(
|
|
616
920
|
"detect_agent_drift",
|
|
617
921
|
"Return the VR-side sha256 hash and filePath for each agent in scope. " +
|
|
618
922
|
"IMPORTANT: Convex queries have no filesystem access. This tool returns the VR " +
|
|
@@ -643,7 +947,7 @@ server.tool(
|
|
|
643
947
|
// PLUGIN CONTENT — VR Single-Source-of-Truth (D72-VR-SoT Phase A)
|
|
644
948
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
645
949
|
|
|
646
|
-
|
|
950
|
+
s.tool(
|
|
647
951
|
"get_plugin_content",
|
|
648
952
|
"Fetch the VR-hosted canonical plugin content body for a plugin. " +
|
|
649
953
|
"Returns content, sha256 hash, semver version, and last sync timestamp. " +
|
|
@@ -663,7 +967,7 @@ server.tool(
|
|
|
663
967
|
},
|
|
664
968
|
);
|
|
665
969
|
|
|
666
|
-
|
|
970
|
+
s.tool(
|
|
667
971
|
"upsert_plugin_content",
|
|
668
972
|
"Set or update the canonical plugin content body in VantageRegistry. " +
|
|
669
973
|
"Computes sha256; if content is unchanged the call is idempotent (version not bumped). " +
|
|
@@ -685,11 +989,7 @@ server.tool(
|
|
|
685
989
|
async ({ name, content, createIfMissing }) => {
|
|
686
990
|
const result = await convex.mutation(
|
|
687
991
|
api.pluginContentDb.upsertPluginContent,
|
|
688
|
-
{
|
|
689
|
-
name,
|
|
690
|
-
content,
|
|
691
|
-
createIfMissing,
|
|
692
|
-
},
|
|
992
|
+
adminWriteArgs({ name, content, createIfMissing }, isAdmin),
|
|
693
993
|
);
|
|
694
994
|
return {
|
|
695
995
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
@@ -697,7 +997,7 @@ server.tool(
|
|
|
697
997
|
},
|
|
698
998
|
);
|
|
699
999
|
|
|
700
|
-
|
|
1000
|
+
s.tool(
|
|
701
1001
|
"detect_plugin_drift",
|
|
702
1002
|
"Return the VR-side sha256 hash and filePath for each plugin in scope. " +
|
|
703
1003
|
"IMPORTANT: Convex queries have no filesystem access. This tool returns the VR " +
|
|
@@ -728,7 +1028,7 @@ server.tool(
|
|
|
728
1028
|
// HOOK CONTENT — VR Single-Source-of-Truth (D72-VR-SoT Phase A)
|
|
729
1029
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
730
1030
|
|
|
731
|
-
|
|
1031
|
+
s.tool(
|
|
732
1032
|
"get_hook_content",
|
|
733
1033
|
"Fetch the VR-hosted canonical hook content body for a hook. " +
|
|
734
1034
|
"Returns content, sha256 hash, semver version, and last sync timestamp. " +
|
|
@@ -748,7 +1048,7 @@ server.tool(
|
|
|
748
1048
|
},
|
|
749
1049
|
);
|
|
750
1050
|
|
|
751
|
-
|
|
1051
|
+
s.tool(
|
|
752
1052
|
"upsert_hook_content",
|
|
753
1053
|
"Set or update the canonical hook content body in VantageRegistry. " +
|
|
754
1054
|
"Computes sha256; if content is unchanged the call is idempotent (version not bumped). " +
|
|
@@ -769,18 +1069,16 @@ server.tool(
|
|
|
769
1069
|
),
|
|
770
1070
|
},
|
|
771
1071
|
async ({ name, content, createIfMissing }) => {
|
|
772
|
-
const result = await convex.mutation(api.hookContentDb.upsertHookContent,
|
|
773
|
-
name,
|
|
774
|
-
|
|
775
|
-
createIfMissing,
|
|
776
|
-
});
|
|
1072
|
+
const result = await convex.mutation(api.hookContentDb.upsertHookContent,
|
|
1073
|
+
adminWriteArgs({ name, content, createIfMissing }, isAdmin),
|
|
1074
|
+
);
|
|
777
1075
|
return {
|
|
778
1076
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
779
1077
|
};
|
|
780
1078
|
},
|
|
781
1079
|
);
|
|
782
1080
|
|
|
783
|
-
|
|
1081
|
+
s.tool(
|
|
784
1082
|
"detect_hook_drift",
|
|
785
1083
|
"Return the VR-side sha256 hash and filePath for each hook in scope. " +
|
|
786
1084
|
"IMPORTANT: Convex queries have no filesystem access. This tool returns the VR " +
|
|
@@ -808,7 +1106,7 @@ server.tool(
|
|
|
808
1106
|
// COMMAND CONTENT — VR Single-Source-of-Truth (D72-VR-SoT Phase A)
|
|
809
1107
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
810
1108
|
|
|
811
|
-
|
|
1109
|
+
s.tool(
|
|
812
1110
|
"get_command_content",
|
|
813
1111
|
"Fetch the VR-hosted canonical command content body for a command. " +
|
|
814
1112
|
"Returns content, sha256 hash, semver version, and last sync timestamp. " +
|
|
@@ -828,7 +1126,7 @@ server.tool(
|
|
|
828
1126
|
},
|
|
829
1127
|
);
|
|
830
1128
|
|
|
831
|
-
|
|
1129
|
+
s.tool(
|
|
832
1130
|
"upsert_command_content",
|
|
833
1131
|
"Set or update the canonical command content body in VantageRegistry. " +
|
|
834
1132
|
"Computes sha256; if content is unchanged the call is idempotent (version not bumped). " +
|
|
@@ -842,10 +1140,7 @@ server.tool(
|
|
|
842
1140
|
async ({ name, content }) => {
|
|
843
1141
|
const result = await convex.mutation(
|
|
844
1142
|
api.commandContentDb.upsertCommandContent,
|
|
845
|
-
{
|
|
846
|
-
name,
|
|
847
|
-
content,
|
|
848
|
-
},
|
|
1143
|
+
adminWriteArgs({ name, content }, isAdmin),
|
|
849
1144
|
);
|
|
850
1145
|
return {
|
|
851
1146
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
@@ -853,7 +1148,7 @@ server.tool(
|
|
|
853
1148
|
},
|
|
854
1149
|
);
|
|
855
1150
|
|
|
856
|
-
|
|
1151
|
+
s.tool(
|
|
857
1152
|
"detect_command_drift",
|
|
858
1153
|
"Return the VR-side sha256 hash and filePath for each command in scope. " +
|
|
859
1154
|
"IMPORTANT: Convex queries have no filesystem access. This tool returns the VR " +
|
|
@@ -884,7 +1179,7 @@ server.tool(
|
|
|
884
1179
|
// PLUGINS
|
|
885
1180
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
886
1181
|
|
|
887
|
-
|
|
1182
|
+
s.tool(
|
|
888
1183
|
"upsert_plugin",
|
|
889
1184
|
"Create or update a plugin in VantageRegistry. Upserts by name.",
|
|
890
1185
|
{
|
|
@@ -912,7 +1207,7 @@ server.tool(
|
|
|
912
1207
|
.describe("Categories beyond team name"),
|
|
913
1208
|
},
|
|
914
1209
|
async (args) => {
|
|
915
|
-
const id = await convex.mutation(api.plugins.upsert, args);
|
|
1210
|
+
const id = await convex.mutation(api.plugins.upsert, adminWriteArgs(args, isAdmin));
|
|
916
1211
|
return {
|
|
917
1212
|
content: [
|
|
918
1213
|
{
|
|
@@ -924,7 +1219,7 @@ server.tool(
|
|
|
924
1219
|
},
|
|
925
1220
|
);
|
|
926
1221
|
|
|
927
|
-
|
|
1222
|
+
s.tool(
|
|
928
1223
|
"list_plugins",
|
|
929
1224
|
"List all plugins in VantageRegistry. Optionally filter by status, source, or team. " +
|
|
930
1225
|
"fields='lite' returns compact {_id, name, status, source}. fields='full' returns the complete document.",
|
|
@@ -954,7 +1249,7 @@ server.tool(
|
|
|
954
1249
|
},
|
|
955
1250
|
);
|
|
956
1251
|
|
|
957
|
-
|
|
1252
|
+
s.tool(
|
|
958
1253
|
"get_plugin",
|
|
959
1254
|
"Get a single plugin by its Convex document ID.",
|
|
960
1255
|
{
|
|
@@ -972,7 +1267,7 @@ server.tool(
|
|
|
972
1267
|
// HOOKS
|
|
973
1268
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
974
1269
|
|
|
975
|
-
|
|
1270
|
+
s.tool(
|
|
976
1271
|
"upsert_hook",
|
|
977
1272
|
"Create or update a hook in VantageRegistry. Upserts by name.",
|
|
978
1273
|
{
|
|
@@ -1000,7 +1295,7 @@ server.tool(
|
|
|
1000
1295
|
),
|
|
1001
1296
|
},
|
|
1002
1297
|
async (args) => {
|
|
1003
|
-
const id = await convex.mutation(api.hooks.upsert, args);
|
|
1298
|
+
const id = await convex.mutation(api.hooks.upsert, adminWriteArgs(args, isAdmin));
|
|
1004
1299
|
return {
|
|
1005
1300
|
content: [
|
|
1006
1301
|
{
|
|
@@ -1016,7 +1311,7 @@ server.tool(
|
|
|
1016
1311
|
},
|
|
1017
1312
|
);
|
|
1018
1313
|
|
|
1019
|
-
|
|
1314
|
+
s.tool(
|
|
1020
1315
|
"list_hooks",
|
|
1021
1316
|
"List all hooks in VantageRegistry. Optionally filter by status or lifecycle event. " +
|
|
1022
1317
|
"fields='lite' returns compact {_id, name, status, event, registered}. fields='full' returns the complete document including content.",
|
|
@@ -1048,7 +1343,7 @@ server.tool(
|
|
|
1048
1343
|
},
|
|
1049
1344
|
);
|
|
1050
1345
|
|
|
1051
|
-
|
|
1346
|
+
s.tool(
|
|
1052
1347
|
"get_hook",
|
|
1053
1348
|
"Get a single hook by its Convex document ID.",
|
|
1054
1349
|
{
|
|
@@ -1066,7 +1361,7 @@ server.tool(
|
|
|
1066
1361
|
// PROMPTS
|
|
1067
1362
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
1068
1363
|
|
|
1069
|
-
|
|
1364
|
+
s.tool(
|
|
1070
1365
|
"upsert_prompt",
|
|
1071
1366
|
"Create or update a prompt in VantageRegistry. Upserts by name.",
|
|
1072
1367
|
{
|
|
@@ -1082,7 +1377,7 @@ server.tool(
|
|
|
1082
1377
|
version: z.string().optional().describe("Semantic version — e.g. '1.0.0'"),
|
|
1083
1378
|
},
|
|
1084
1379
|
async (args) => {
|
|
1085
|
-
const id = await convex.mutation(api.prompts.upsert, args);
|
|
1380
|
+
const id = await convex.mutation(api.prompts.upsert, adminWriteArgs(args, isAdmin));
|
|
1086
1381
|
return {
|
|
1087
1382
|
content: [
|
|
1088
1383
|
{
|
|
@@ -1094,7 +1389,7 @@ server.tool(
|
|
|
1094
1389
|
},
|
|
1095
1390
|
);
|
|
1096
1391
|
|
|
1097
|
-
|
|
1392
|
+
s.tool(
|
|
1098
1393
|
"list_prompts",
|
|
1099
1394
|
"List all prompts in VantageRegistry. Optionally filter by team.",
|
|
1100
1395
|
{
|
|
@@ -1108,7 +1403,7 @@ server.tool(
|
|
|
1108
1403
|
},
|
|
1109
1404
|
);
|
|
1110
1405
|
|
|
1111
|
-
|
|
1406
|
+
s.tool(
|
|
1112
1407
|
"get_prompt",
|
|
1113
1408
|
"Get a single prompt by its Convex document ID.",
|
|
1114
1409
|
{
|
|
@@ -1126,7 +1421,7 @@ server.tool(
|
|
|
1126
1421
|
// TEMPLATES
|
|
1127
1422
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
1128
1423
|
|
|
1129
|
-
|
|
1424
|
+
s.tool(
|
|
1130
1425
|
"upsert_template",
|
|
1131
1426
|
"Create or update a template in VantageRegistry. Upserts by name. " +
|
|
1132
1427
|
"contentHash is auto-computed server-side (sha256 of content) when omitted.",
|
|
@@ -1162,7 +1457,7 @@ server.tool(
|
|
|
1162
1457
|
.describe("Git repo the template content was sourced from"),
|
|
1163
1458
|
},
|
|
1164
1459
|
async (args) => {
|
|
1165
|
-
const id = await convex.mutation(api.templates.upsert, args);
|
|
1460
|
+
const id = await convex.mutation(api.templates.upsert, adminWriteArgs(args, isAdmin));
|
|
1166
1461
|
return {
|
|
1167
1462
|
content: [
|
|
1168
1463
|
{
|
|
@@ -1174,7 +1469,7 @@ server.tool(
|
|
|
1174
1469
|
},
|
|
1175
1470
|
);
|
|
1176
1471
|
|
|
1177
|
-
|
|
1472
|
+
s.tool(
|
|
1178
1473
|
"list_templates",
|
|
1179
1474
|
"List all templates in VantageRegistry. Optionally filter by team and/or template_type.",
|
|
1180
1475
|
{
|
|
@@ -1194,7 +1489,7 @@ server.tool(
|
|
|
1194
1489
|
},
|
|
1195
1490
|
);
|
|
1196
1491
|
|
|
1197
|
-
|
|
1492
|
+
s.tool(
|
|
1198
1493
|
"get_template",
|
|
1199
1494
|
"Get a single template by its Convex document ID.",
|
|
1200
1495
|
{
|
|
@@ -1208,7 +1503,7 @@ server.tool(
|
|
|
1208
1503
|
},
|
|
1209
1504
|
);
|
|
1210
1505
|
|
|
1211
|
-
|
|
1506
|
+
s.tool(
|
|
1212
1507
|
"detect_template_drift",
|
|
1213
1508
|
"Return the VR-side sha256 contentHash + provenance (sourceCommit, sourceRepo) " +
|
|
1214
1509
|
"for each template in scope. IMPORTANT: Convex queries have no filesystem access. " +
|
|
@@ -1231,7 +1526,7 @@ server.tool(
|
|
|
1231
1526
|
},
|
|
1232
1527
|
);
|
|
1233
1528
|
|
|
1234
|
-
|
|
1529
|
+
s.tool(
|
|
1235
1530
|
"list_templates_by_category",
|
|
1236
1531
|
"List templates for a specific category using the byCategory index.",
|
|
1237
1532
|
{
|
|
@@ -1253,7 +1548,7 @@ server.tool(
|
|
|
1253
1548
|
// SKILL TEST RUNS
|
|
1254
1549
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
1255
1550
|
|
|
1256
|
-
|
|
1551
|
+
s.tool(
|
|
1257
1552
|
"upsert_test_run",
|
|
1258
1553
|
"Insert a skill quality test run and update denormalized quality fields on the parent skill " +
|
|
1259
1554
|
"(lastTestedAt, lastReviewerScore, lastEvalDelta, testStatus). " +
|
|
@@ -1311,10 +1606,9 @@ server.tool(
|
|
|
1311
1606
|
.describe("Override test status — defaults to 'tested'"),
|
|
1312
1607
|
},
|
|
1313
1608
|
async (args) => {
|
|
1314
|
-
const id = await convex.mutation(api.skillTestRuns.upsertTestRun,
|
|
1315
|
-
...args,
|
|
1316
|
-
|
|
1317
|
-
});
|
|
1609
|
+
const id = await convex.mutation(api.skillTestRuns.upsertTestRun,
|
|
1610
|
+
adminWriteArgs({ ...args, skillId: args.skillId as any }, isAdmin),
|
|
1611
|
+
);
|
|
1318
1612
|
return {
|
|
1319
1613
|
content: [
|
|
1320
1614
|
{
|
|
@@ -1330,7 +1624,7 @@ server.tool(
|
|
|
1330
1624
|
},
|
|
1331
1625
|
);
|
|
1332
1626
|
|
|
1333
|
-
|
|
1627
|
+
s.tool(
|
|
1334
1628
|
"get_skill_test_history",
|
|
1335
1629
|
"Return test run history for a skill, ordered newest-first.",
|
|
1336
1630
|
{
|
|
@@ -1351,7 +1645,7 @@ server.tool(
|
|
|
1351
1645
|
},
|
|
1352
1646
|
);
|
|
1353
1647
|
|
|
1354
|
-
|
|
1648
|
+
s.tool(
|
|
1355
1649
|
"list_skills_by_freshness",
|
|
1356
1650
|
"Return skills that have never been tested or were last tested more than staleDays ago. " +
|
|
1357
1651
|
"Use this to find skills that need a quality test run. " +
|
|
@@ -1385,7 +1679,7 @@ server.tool(
|
|
|
1385
1679
|
},
|
|
1386
1680
|
);
|
|
1387
1681
|
|
|
1388
|
-
|
|
1682
|
+
s.tool(
|
|
1389
1683
|
"list_skills_below_threshold",
|
|
1390
1684
|
"Return skills whose last test run was below a score ratio (scoreMin as 0-1 fraction of 'X/Y') " +
|
|
1391
1685
|
"or below a delta threshold (deltaMin in pp). Omit a param to skip that filter. " +
|
|
@@ -1421,7 +1715,7 @@ server.tool(
|
|
|
1421
1715
|
},
|
|
1422
1716
|
);
|
|
1423
1717
|
|
|
1424
|
-
|
|
1718
|
+
s.tool(
|
|
1425
1719
|
"upsert_skill_eval_corpus",
|
|
1426
1720
|
"Insert or update an eval corpus version for a skill. Upserts by (skillId, version).",
|
|
1427
1721
|
{
|
|
@@ -1452,10 +1746,7 @@ server.tool(
|
|
|
1452
1746
|
async (args) => {
|
|
1453
1747
|
const id = await convex.mutation(
|
|
1454
1748
|
api.skillEvalCorpus.upsertSkillEvalCorpus,
|
|
1455
|
-
{
|
|
1456
|
-
...args,
|
|
1457
|
-
skillId: args.skillId as any,
|
|
1458
|
-
},
|
|
1749
|
+
adminWriteArgs({ ...args, skillId: args.skillId as any }, isAdmin),
|
|
1459
1750
|
);
|
|
1460
1751
|
return {
|
|
1461
1752
|
content: [
|
|
@@ -1523,7 +1814,7 @@ const linkedTemplateSchema = z.object({
|
|
|
1523
1814
|
description: z.string().describe("How this template is used in this runbook"),
|
|
1524
1815
|
});
|
|
1525
1816
|
|
|
1526
|
-
|
|
1817
|
+
s.tool(
|
|
1527
1818
|
"upsert_runbook",
|
|
1528
1819
|
"Create or update a runbook in VantageRegistry. Upserts by name — if a runbook with the same name exists, it is updated.",
|
|
1529
1820
|
{
|
|
@@ -1560,7 +1851,7 @@ server.tool(
|
|
|
1560
1851
|
},
|
|
1561
1852
|
async (args) => {
|
|
1562
1853
|
try {
|
|
1563
|
-
const result = await convex.mutation(api.runbooks.upsertRunbook, args);
|
|
1854
|
+
const result = await convex.mutation(api.runbooks.upsertRunbook, adminWriteArgs(args, isAdmin));
|
|
1564
1855
|
return {
|
|
1565
1856
|
content: [
|
|
1566
1857
|
{
|
|
@@ -1586,7 +1877,7 @@ server.tool(
|
|
|
1586
1877
|
},
|
|
1587
1878
|
);
|
|
1588
1879
|
|
|
1589
|
-
|
|
1880
|
+
s.tool(
|
|
1590
1881
|
"get_runbook",
|
|
1591
1882
|
"Get a single runbook by name. Optionally guard by exact version.",
|
|
1592
1883
|
{
|
|
@@ -1627,7 +1918,7 @@ server.tool(
|
|
|
1627
1918
|
},
|
|
1628
1919
|
);
|
|
1629
1920
|
|
|
1630
|
-
|
|
1921
|
+
s.tool(
|
|
1631
1922
|
"list_runbooks",
|
|
1632
1923
|
"List runbooks in VantageRegistry. Defaults to published status. Supports applicability filters.",
|
|
1633
1924
|
{
|
|
@@ -1685,7 +1976,7 @@ server.tool(
|
|
|
1685
1976
|
},
|
|
1686
1977
|
);
|
|
1687
1978
|
|
|
1688
|
-
|
|
1979
|
+
s.tool(
|
|
1689
1980
|
"list_runbooks_by_category",
|
|
1690
1981
|
"List runbooks for a specific category using the byCategory index.",
|
|
1691
1982
|
{
|
|
@@ -1729,7 +2020,7 @@ server.tool(
|
|
|
1729
2020
|
},
|
|
1730
2021
|
);
|
|
1731
2022
|
|
|
1732
|
-
|
|
2023
|
+
s.tool(
|
|
1733
2024
|
"list_runbooks_by_team",
|
|
1734
2025
|
"List runbooks for a specific team using the byTeam index.",
|
|
1735
2026
|
{
|
|
@@ -1769,7 +2060,7 @@ server.tool(
|
|
|
1769
2060
|
},
|
|
1770
2061
|
);
|
|
1771
2062
|
|
|
1772
|
-
|
|
2063
|
+
s.tool(
|
|
1773
2064
|
"delete_runbook",
|
|
1774
2065
|
"Soft-delete a runbook by name — sets status to 'deprecated'. Returns {deleted: true} if found, {deleted: false} if not found.",
|
|
1775
2066
|
{
|
|
@@ -1777,9 +2068,9 @@ server.tool(
|
|
|
1777
2068
|
},
|
|
1778
2069
|
async ({ name }) => {
|
|
1779
2070
|
try {
|
|
1780
|
-
const result = await convex.mutation(api.runbooks.deleteRunbook,
|
|
1781
|
-
name,
|
|
1782
|
-
|
|
2071
|
+
const result = await convex.mutation(api.runbooks.deleteRunbook,
|
|
2072
|
+
adminWriteArgs({ name }, isAdmin),
|
|
2073
|
+
);
|
|
1783
2074
|
return {
|
|
1784
2075
|
content: [
|
|
1785
2076
|
{
|
|
@@ -1805,7 +2096,7 @@ server.tool(
|
|
|
1805
2096
|
},
|
|
1806
2097
|
);
|
|
1807
2098
|
|
|
1808
|
-
|
|
2099
|
+
s.tool(
|
|
1809
2100
|
"detect_runbook_drift",
|
|
1810
2101
|
"Return the VR-side sha256 hash (vrHash) plus category and version for each runbook in scope. " +
|
|
1811
2102
|
"IMPORTANT: Convex queries have no filesystem access. This tool computes sha256(runbook.content) " +
|
|
@@ -1831,7 +2122,7 @@ server.tool(
|
|
|
1831
2122
|
// RUNBOOK ↔ TEMPLATE LINKS (Sprint B T4 — junction table typed links)
|
|
1832
2123
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
1833
2124
|
|
|
1834
|
-
|
|
2125
|
+
s.tool(
|
|
1835
2126
|
"link_runbook_template",
|
|
1836
2127
|
"Create or update a typed link between a runbook and a template in the runbook_template_links junction table. " +
|
|
1837
2128
|
"Upserts by (runbookId, templateId) pair — at-most-one link per pair. " +
|
|
@@ -1865,14 +2156,17 @@ server.tool(
|
|
|
1865
2156
|
try {
|
|
1866
2157
|
const result = await convex.mutation(
|
|
1867
2158
|
api.runbookTemplateLinks.linkRunbookTemplate,
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
2159
|
+
adminWriteArgs(
|
|
2160
|
+
{
|
|
2161
|
+
runbookId: args.runbookId as any,
|
|
2162
|
+
templateId: args.templateId as any,
|
|
2163
|
+
linkType: args.linkType,
|
|
2164
|
+
usagePhase: args.usagePhase,
|
|
2165
|
+
order: args.order,
|
|
2166
|
+
createdBy: args.createdBy,
|
|
2167
|
+
},
|
|
2168
|
+
isAdmin,
|
|
2169
|
+
),
|
|
1876
2170
|
);
|
|
1877
2171
|
return {
|
|
1878
2172
|
content: [
|
|
@@ -1899,7 +2193,7 @@ server.tool(
|
|
|
1899
2193
|
},
|
|
1900
2194
|
);
|
|
1901
2195
|
|
|
1902
|
-
|
|
2196
|
+
s.tool(
|
|
1903
2197
|
"unlink_runbook_template",
|
|
1904
2198
|
"Hard-delete the typed link between a runbook and a template. " +
|
|
1905
2199
|
"Links have no lifecycle state — they either exist or they don't (no soft-delete). " +
|
|
@@ -1912,10 +2206,13 @@ server.tool(
|
|
|
1912
2206
|
try {
|
|
1913
2207
|
const result = await convex.mutation(
|
|
1914
2208
|
api.runbookTemplateLinks.unlinkRunbookTemplate,
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
2209
|
+
adminWriteArgs(
|
|
2210
|
+
{
|
|
2211
|
+
runbookId: args.runbookId as any,
|
|
2212
|
+
templateId: args.templateId as any,
|
|
2213
|
+
},
|
|
2214
|
+
isAdmin,
|
|
2215
|
+
),
|
|
1919
2216
|
);
|
|
1920
2217
|
return {
|
|
1921
2218
|
content: [
|
|
@@ -1942,7 +2239,7 @@ server.tool(
|
|
|
1942
2239
|
},
|
|
1943
2240
|
);
|
|
1944
2241
|
|
|
1945
|
-
|
|
2242
|
+
s.tool(
|
|
1946
2243
|
"list_templates_for_runbook",
|
|
1947
2244
|
"List all templates linked to a runbook, with enriched template metadata. " +
|
|
1948
2245
|
"Returns enriched [{link, template}] objects sorted by link.order ASC then link.createdAt ASC. " +
|
|
@@ -1990,7 +2287,7 @@ server.tool(
|
|
|
1990
2287
|
},
|
|
1991
2288
|
);
|
|
1992
2289
|
|
|
1993
|
-
|
|
2290
|
+
s.tool(
|
|
1994
2291
|
"list_runbooks_for_template",
|
|
1995
2292
|
"List all runbooks linked to a template (reverse lookup), with enriched runbook metadata. " +
|
|
1996
2293
|
"Symmetric reverse of list_templates_for_runbook — uses the byTemplate index. " +
|
|
@@ -2060,7 +2357,7 @@ const componentStatusSchema = z
|
|
|
2060
2357
|
.enum(["active", "deprecated", "experimental"])
|
|
2061
2358
|
.describe("Component status");
|
|
2062
2359
|
|
|
2063
|
-
|
|
2360
|
+
s.tool(
|
|
2064
2361
|
"register_component",
|
|
2065
2362
|
"Create or update a component in the VantageRegistry components catalog. " +
|
|
2066
2363
|
"Upserts by name+kind — if a component with the same name and kind exists it is updated. " +
|
|
@@ -2096,7 +2393,7 @@ server.tool(
|
|
|
2096
2393
|
},
|
|
2097
2394
|
async (args) => {
|
|
2098
2395
|
try {
|
|
2099
|
-
const id = await convex.mutation(api.components.registerComponent, args);
|
|
2396
|
+
const id = await convex.mutation(api.components.registerComponent, adminWriteArgs(args, isAdmin));
|
|
2100
2397
|
return {
|
|
2101
2398
|
content: [
|
|
2102
2399
|
{
|
|
@@ -2126,7 +2423,7 @@ server.tool(
|
|
|
2126
2423
|
},
|
|
2127
2424
|
);
|
|
2128
2425
|
|
|
2129
|
-
|
|
2426
|
+
s.tool(
|
|
2130
2427
|
"list_components",
|
|
2131
2428
|
"List components in the VantageRegistry catalog. " +
|
|
2132
2429
|
"Supports filtering by kind, ownerTeam, status, and tags. " +
|
|
@@ -2189,7 +2486,7 @@ server.tool(
|
|
|
2189
2486
|
},
|
|
2190
2487
|
);
|
|
2191
2488
|
|
|
2192
|
-
|
|
2489
|
+
s.tool(
|
|
2193
2490
|
"get_component",
|
|
2194
2491
|
"Get a single component by its Convex document ID. Returns the full document or null if not found.",
|
|
2195
2492
|
{
|
|
@@ -2220,7 +2517,7 @@ server.tool(
|
|
|
2220
2517
|
},
|
|
2221
2518
|
);
|
|
2222
2519
|
|
|
2223
|
-
|
|
2520
|
+
s.tool(
|
|
2224
2521
|
"search_components",
|
|
2225
2522
|
"Search components by name prefix. Returns up to `limit` lite results whose name starts with the query string. " +
|
|
2226
2523
|
"Case-sensitive index range scan. Use list_components with filters for kind/status/tags filtering.",
|
|
@@ -2260,7 +2557,7 @@ server.tool(
|
|
|
2260
2557
|
},
|
|
2261
2558
|
);
|
|
2262
2559
|
|
|
2263
|
-
|
|
2560
|
+
s.tool(
|
|
2264
2561
|
"update_component",
|
|
2265
2562
|
"Patch individual fields on an existing component. Only provided fields are updated. Throws if not found.",
|
|
2266
2563
|
{
|
|
@@ -2279,10 +2576,9 @@ server.tool(
|
|
|
2279
2576
|
},
|
|
2280
2577
|
async (args) => {
|
|
2281
2578
|
try {
|
|
2282
|
-
await convex.mutation(api.components.updateComponent,
|
|
2283
|
-
...args,
|
|
2284
|
-
|
|
2285
|
-
});
|
|
2579
|
+
await convex.mutation(api.components.updateComponent,
|
|
2580
|
+
adminWriteArgs({ ...args, id: args.id as any }, isAdmin),
|
|
2581
|
+
);
|
|
2286
2582
|
return {
|
|
2287
2583
|
content: [
|
|
2288
2584
|
{
|
|
@@ -2308,7 +2604,7 @@ server.tool(
|
|
|
2308
2604
|
},
|
|
2309
2605
|
);
|
|
2310
2606
|
|
|
2311
|
-
|
|
2607
|
+
s.tool(
|
|
2312
2608
|
"delete_component",
|
|
2313
2609
|
"Soft-delete a component by ID — sets status to 'deprecated'. " +
|
|
2314
2610
|
"The document remains in the database and is still retrievable. " +
|
|
@@ -2320,9 +2616,9 @@ server.tool(
|
|
|
2320
2616
|
},
|
|
2321
2617
|
async ({ id }) => {
|
|
2322
2618
|
try {
|
|
2323
|
-
const result = await convex.mutation(api.components.deleteComponent,
|
|
2324
|
-
id: id as any,
|
|
2325
|
-
|
|
2619
|
+
const result = await convex.mutation(api.components.deleteComponent,
|
|
2620
|
+
adminWriteArgs({ id: id as any }, isAdmin),
|
|
2621
|
+
);
|
|
2326
2622
|
return {
|
|
2327
2623
|
content: [
|
|
2328
2624
|
{
|
|
@@ -2352,7 +2648,7 @@ server.tool(
|
|
|
2352
2648
|
// STATS
|
|
2353
2649
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
2354
2650
|
|
|
2355
|
-
|
|
2651
|
+
s.tool(
|
|
2356
2652
|
"get_stats",
|
|
2357
2653
|
"Get counts per table in VantageRegistry — teams, agents, skills, plugins, hooks, and runbooks. " +
|
|
2358
2654
|
"Runbooks include breakdown by_status (draft/published/deprecated) and by_category. " +
|
|
@@ -2424,14 +2720,33 @@ server.tool(
|
|
|
2424
2720
|
},
|
|
2425
2721
|
);
|
|
2426
2722
|
|
|
2723
|
+
} // end registerAllTools
|
|
2724
|
+
|
|
2725
|
+
// Register tools on the module-level stdio server.
|
|
2726
|
+
// stdio path has no HTTP bearer layer; adminWriteArgs() still enforces
|
|
2727
|
+
// VR_ADMIN_WRITE_SECRET presence at call time.
|
|
2728
|
+
registerAllTools(server, true);
|
|
2729
|
+
|
|
2427
2730
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
2428
2731
|
// Start
|
|
2429
2732
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
2430
2733
|
|
|
2734
|
+
const PORT = Number(process.env.PORT ?? 3000);
|
|
2735
|
+
|
|
2431
2736
|
async function main() {
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2737
|
+
if (process.env.MCP_TRANSPORT === "http") {
|
|
2738
|
+
const instance = await startHttpServer(PORT);
|
|
2739
|
+
const addr = instance.address();
|
|
2740
|
+
console.error(
|
|
2741
|
+
`VantageRegistry MCP server running on HTTP port ${addr.port}`,
|
|
2742
|
+
);
|
|
2743
|
+
console.error(`Health: http://0.0.0.0:${addr.port}/health`);
|
|
2744
|
+
console.error(`MCP: http://0.0.0.0:${addr.port}/mcp`);
|
|
2745
|
+
} else {
|
|
2746
|
+
const transport = new StdioServerTransport();
|
|
2747
|
+
await server.connect(transport);
|
|
2748
|
+
console.error("VantageRegistry MCP server running on stdio");
|
|
2749
|
+
}
|
|
2435
2750
|
}
|
|
2436
2751
|
|
|
2437
2752
|
main().catch((err) => {
|