@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.
Files changed (4) hide show
  1. package/README.md +45 -2
  2. package/package.json +2 -2
  3. package/server.js +1714 -1515
  4. package/server.ts +448 -133
package/server.js CHANGED
@@ -1,10 +1,13 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
3
  // server.ts
4
+ import { createServer } from "node:http";
5
+ import { timingSafeEqual } from "node:crypto";
4
6
  import { readFileSync } from "node:fs";
5
7
  import { resolve } from "node:path";
6
8
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
7
9
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
10
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
8
11
  import { ConvexHttpClient } from "convex/browser";
9
12
  import { z } from "zod";
10
13
 
@@ -60,1017 +63,273 @@ var templateTypeSchema = z.enum(["standard", "mission", "brief", "runbook", "doc
60
63
  var linkTypeSchema = z.enum(["uses", "produces", "references"]).describe(
61
64
  "Link type \u2014 uses: template consumed during execution, produces: output document generated, references: informational cross-reference"
62
65
  );
63
- var convexUrl = loadConvexUrl();
64
- var convex = new ConvexHttpClient(convexUrl);
66
+ function adminWriteArgs(args, isAdmin = true) {
67
+ if (!isAdmin) {
68
+ throw new Error(
69
+ "This MCP instance is in read-only mode. Write tools are not available. Use an admin token (MCP_BEARER_TOKEN) to enable write operations."
70
+ );
71
+ }
72
+ const secret = process.env.VR_ADMIN_WRITE_SECRET;
73
+ if (!secret || secret.length === 0) {
74
+ throw new Error(
75
+ "This MCP instance is not configured for writes: VR_ADMIN_WRITE_SECRET is not set. Set VR_ADMIN_WRITE_SECRET in the server environment to enable write operations."
76
+ );
77
+ }
78
+ return { ...args, adminSecret: secret };
79
+ }
80
+ var _convex = null;
81
+ function getConvex() {
82
+ if (!_convex) {
83
+ _convex = new ConvexHttpClient(loadConvexUrl());
84
+ }
85
+ return _convex;
86
+ }
87
+ var convex = new Proxy({}, {
88
+ get(_target, prop) {
89
+ const client = getConvex();
90
+ const value = client[prop];
91
+ if (typeof value === "function") {
92
+ return value.bind(client);
93
+ }
94
+ return value;
95
+ }
96
+ });
65
97
  var server = new McpServer({
66
98
  name: "vantage-registry",
67
- version: "1.5.0"
99
+ version: "1.7.0"
68
100
  });
69
- server.tool(
101
+ var WRITE_TOOLS = /* @__PURE__ */ new Set([
70
102
  "upsert_team",
71
- "Create or update a team in VantageRegistry. Upserts by name \u2014 if a team with the same name exists, it is updated.",
72
- {
73
- name: z.string().describe("Team name \u2014 e.g. 'core', 'geo', 'dev'"),
74
- description: z.string().describe("What this team does"),
75
- agentCount: z.number().int().describe("Number of agents in this team"),
76
- skillCount: z.number().int().describe("Number of skills in this team"),
77
- status: teamStatusSchema,
78
- project: z.string().optional().describe("Associated project \u2014 e.g. 'vantage-starter'")
79
- },
80
- async (args) => {
81
- const id = await convex.mutation(api.teams.upsert, args);
82
- return {
83
- content: [
84
- { type: "text", text: JSON.stringify({ id, ...args }, null, 2) }
85
- ]
86
- };
87
- }
88
- );
89
- server.tool(
90
- "list_teams",
91
- "List all teams in VantageRegistry. Optionally filter by status (active, planned, deprecated).",
92
- {
93
- status: teamStatusSchema.optional().describe("Filter by status \u2014 omit to list all")
94
- },
95
- async ({ status }) => {
96
- const results = await convex.query(api.teams.list, { status });
97
- return {
98
- content: [{ type: "text", text: JSON.stringify(results, null, 2) }]
99
- };
100
- }
101
- );
102
- server.tool(
103
- "get_team",
104
- "Get a single team by its Convex document ID.",
105
- {
106
- id: z.string().describe("Convex document ID for the team")
107
- },
108
- async ({ id }) => {
109
- const result = await convex.query(api.teams.get, { id });
110
- return {
111
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
112
- };
113
- }
114
- );
115
- server.tool(
116
103
  "upsert_agent",
117
- "Create or update an agent in VantageRegistry. Upserts by name+team \u2014 if an agent with the same name and team exists, it is updated.",
118
- {
119
- name: z.string().describe("Agent name \u2014 e.g. 'copywriter', 'strategy-researcher'"),
120
- team: z.string().describe("Team this agent belongs to"),
121
- description: z.string().describe("What this agent does"),
122
- content: z.string().describe("Full agent definition content (markdown)"),
123
- filePath: z.string().describe("File path relative to project root"),
124
- lineCount: z.number().int().describe("Number of lines in the agent file"),
125
- status: agentStatusSchema,
126
- version: z.string().optional().describe("Semantic version \u2014 e.g. '1.0.0'"),
127
- source: sourceSchema,
128
- pricing: z.enum(["free", "paid"]).optional().describe("Pricing tier"),
129
- price: z.number().optional().describe("Price in EUR (if paid)"),
130
- license: z.string().optional().describe("License \u2014 e.g. 'MIT', 'proprietary'"),
131
- publisherId: z.string().optional().describe("Publisher identifier"),
132
- categories: z.array(z.string()).optional().describe("Categories beyond team name")
133
- },
134
- async (args) => {
135
- const id = await convex.mutation(api.agents.upsert, args);
136
- return {
137
- content: [
138
- {
139
- type: "text",
140
- text: JSON.stringify(
141
- { id, name: args.name, team: args.team },
142
- null,
143
- 2
144
- )
145
- }
146
- ]
147
- };
148
- }
149
- );
150
- server.tool(
151
- "list_agents",
152
- "List all agents in VantageRegistry. Optionally filter by status, team, or category. fields='lite' returns compact {_id, name, team, status}. fields='full' returns the complete document including content.",
153
- {
154
- status: agentStatusSchema.optional().describe("Filter by status \u2014 omit to list all"),
155
- team: z.string().optional().describe("Filter by team \u2014 e.g. 'dev', 'core'"),
156
- category: z.string().optional().describe("Filter by category"),
157
- fields: z.enum(["lite", "full"]).optional().describe(
158
- "Projection: 'lite' (compact) or 'full' (complete document). Omit to use legacy summary behaviour."
159
- ),
160
- summary: z.boolean().optional().describe(
161
- "Return summary only (no content). Default: true \u2014 legacy, prefer fields='lite'"
162
- )
163
- },
164
- async ({ status, team, category, fields, summary }) => {
165
- const results = await convex.query(api.agents.list, {
166
- status,
167
- team,
168
- category,
169
- fields,
170
- summary
171
- });
172
- return {
173
- content: [{ type: "text", text: JSON.stringify(results, null, 2) }]
174
- };
175
- }
176
- );
177
- server.tool(
178
- "list_agents_by_team",
179
- "List all agents belonging to a specific team.",
180
- {
181
- team: z.string().describe("Team name to filter by"),
182
- summary: z.boolean().optional().describe("Return summary only (no content). Default: true")
183
- },
184
- async ({ team, summary }) => {
185
- const results = await convex.query(api.agents.listByTeam, {
186
- team,
187
- summary
188
- });
189
- return {
190
- content: [{ type: "text", text: JSON.stringify(results, null, 2) }]
191
- };
192
- }
193
- );
194
- server.tool(
195
- "get_agent",
196
- "Get a single agent by its Convex document ID.",
197
- {
198
- id: z.string().describe("Convex document ID for the agent")
199
- },
200
- async ({ id }) => {
201
- const result = await convex.query(api.agents.get, { id });
202
- return {
203
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
204
- };
205
- }
206
- );
207
- server.tool(
208
104
  "upsert_skill",
209
- "Create or update a skill in VantageRegistry. Upserts by name+team \u2014 if a skill with the same name and team exists, it is updated. Pass vrBody to write the full SKILL.md body (vrContent/vrContentHash/vrContentVersion) in the SAME call \u2014 no separate upsert_skill_content needed (that tool remains for back-compat). Omit vrBody for metadata-only upsert.",
210
- {
211
- name: z.string().describe("Skill name \u2014 e.g. 'social-post', 'competitor-watch'"),
212
- team: z.string().describe("Team this skill belongs to"),
213
- category: skillCategorySchema,
214
- description: z.string().describe("What this skill does \u2014 use pushy trigger descriptions"),
215
- content: z.string().describe("Full SKILL.md content"),
216
- filePath: z.string().describe("File path relative to project root"),
217
- lineCount: z.number().int().describe("Number of lines in the skill file"),
218
- status: skillStatusSchema,
219
- version: z.string().optional().describe("Semantic version \u2014 e.g. '1.0.0'"),
220
- source: sourceSchema,
221
- pricing: z.enum(["free", "paid"]).optional().describe("Pricing tier"),
222
- price: z.number().optional().describe("Price in EUR (if paid)"),
223
- license: z.string().optional().describe("License \u2014 e.g. 'MIT', 'proprietary'"),
224
- publisherId: z.string().optional().describe("Publisher identifier"),
225
- categories: z.array(z.string()).optional().describe("Categories beyond team name"),
226
- vrBody: z.string().optional().describe(
227
- "Full SKILL.md body. When passed, writes vrContent/vrContentHash/vrContentVersion in the same call. Omitted \u2192 metadata-only (unchanged)."
228
- )
229
- },
230
- async (args) => {
231
- const id = await convex.mutation(api.skills.upsert, args);
232
- return {
233
- content: [
234
- {
235
- type: "text",
236
- text: JSON.stringify(
237
- { id, name: args.name, team: args.team, category: args.category },
238
- null,
239
- 2
240
- )
241
- }
242
- ]
243
- };
244
- }
245
- );
246
- server.tool(
247
- "list_skills",
248
- "List all skills in VantageRegistry. Optionally filter by status, team, or category. fields='lite' returns compact {_id, name, team, category, status}. fields='full' returns the complete document including content.",
249
- {
250
- status: skillStatusSchema.optional().describe("Filter by status \u2014 omit to list all"),
251
- team: z.string().optional().describe("Filter by team \u2014 e.g. 'dev', 'core'"),
252
- category: skillCategorySchema.optional().describe(
253
- "Filter by category: capability, composite, playbook, root, or external"
254
- ),
255
- fields: z.enum(["lite", "full"]).optional().describe(
256
- "Projection: 'lite' (compact) or 'full' (complete document). Omit to use legacy summary behaviour."
257
- ),
258
- summary: z.boolean().optional().describe(
259
- "Return summary only (no content). Default: true \u2014 legacy, prefer fields='lite'"
260
- )
261
- },
262
- async ({ status, team, category, fields, summary }) => {
263
- const results = await convex.query(api.skills.list, {
264
- status,
265
- team,
266
- category,
267
- fields,
268
- summary
269
- });
270
- return {
271
- content: [{ type: "text", text: JSON.stringify(results, null, 2) }]
272
- };
273
- }
274
- );
275
- server.tool(
276
- "list_skills_by_team",
277
- "List all skills belonging to a specific team.",
278
- {
279
- team: z.string().describe("Team name to filter by"),
280
- summary: z.boolean().optional().describe("Return summary only (no content). Default: true")
281
- },
282
- async ({ team, summary }) => {
283
- const results = await convex.query(api.skills.listByTeam, {
284
- team,
285
- summary
286
- });
287
- return {
288
- content: [{ type: "text", text: JSON.stringify(results, null, 2) }]
289
- };
290
- }
291
- );
292
- server.tool(
293
- "list_skills_by_category",
294
- "List all skills of a specific category (capability, composite, playbook, root, external).",
295
- {
296
- category: skillCategorySchema,
297
- summary: z.boolean().optional().describe("Return summary only (no content). Default: true")
298
- },
299
- async ({ category, summary }) => {
300
- const results = await convex.query(api.skills.listByCategory, {
301
- category,
302
- summary
303
- });
304
- return {
305
- content: [{ type: "text", text: JSON.stringify(results, null, 2) }]
306
- };
307
- }
308
- );
309
- server.tool(
310
- "get_skill",
311
- "Get a single skill by its Convex document ID.",
312
- {
313
- id: z.string().describe("Convex document ID for the skill")
314
- },
315
- async ({ id }) => {
316
- const result = await convex.query(api.skills.get, { id });
317
- return {
318
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
319
- };
320
- }
321
- );
322
- server.tool(
323
- "get_skill_content",
324
- "Fetch the VR-hosted canonical SKILL.md body for a skill. Returns content, sha256 hash, semver version, and last sync timestamp. Returns null fields if the skill has not been hosted in VR yet.",
325
- {
326
- name: z.string().describe("Skill slug \u2014 e.g. 'check-messages', 'blog-writer'")
327
- },
328
- async ({ name }) => {
329
- const result = await convex.query(api.skillContentDb.getSkillContent, {
330
- name
331
- });
332
- return {
333
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
334
- };
335
- }
336
- );
337
- server.tool(
338
105
  "upsert_skill_content",
339
- "Set or update the canonical SKILL.md body in VantageRegistry. Computes sha256; if content is unchanged from the stored version the call is idempotent (version is not bumped, only contentSyncedAt is updated). If content changed, patch version is bumped (X.Y.Z \u2192 X.Y.Z+1). First write sets version to '1.0.0'. If createIfMissing=true, auto-creates a placeholder skill row if it does not exist yet. Throws if the skill does not exist and createIfMissing is not set.",
340
- {
341
- name: z.string().describe("Skill slug \u2014 e.g. 'check-messages'"),
342
- content: z.string().describe("Full SKILL.md body (frontmatter + markdown)"),
343
- createIfMissing: z.boolean().optional().default(false).describe(
344
- "Auto-create a placeholder skill row if it does not exist (default: false)"
345
- )
346
- },
347
- async ({ name, content, createIfMissing }) => {
348
- const result = await convex.mutation(
349
- api.skillContentDb.upsertSkillContent,
350
- {
351
- name,
352
- content,
353
- createIfMissing
354
- }
355
- );
356
- return {
357
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
358
- };
359
- }
360
- );
361
- server.tool(
362
- "detect_skill_drift",
363
- "Return the VR-side sha256 hash and filePath for each skill in scope. IMPORTANT: Convex queries have no filesystem access. This tool returns the VR canonical hash + stored filePath so the caller can compute disk SHA256 and compare. scope='all' (default) returns every skill. name=<slug> filters to a single skill.",
364
- {
365
- name: z.string().optional().describe("Skill slug \u2014 filter to a single skill"),
366
- scope: z.string().optional().describe("'all' to return every skill (default when name is omitted)")
367
- },
368
- async ({ name, scope }) => {
369
- const result = await convex.query(api.skillContentDb.detectSkillDrift, {
370
- name,
371
- scope
372
- });
373
- return {
374
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
375
- };
376
- }
377
- );
378
- server.tool(
379
- "get_agent_content",
380
- "Fetch the VR-hosted canonical agent content body for an agent. Returns content, sha256 hash, semver version, and last sync timestamp. Returns null fields if the agent has not been hosted in VR yet.",
381
- {
382
- name: z.string().describe("Agent slug \u2014 e.g. 'dev-convex-expert', 'dev-senior-dev'")
383
- },
384
- async ({ name }) => {
385
- const result = await convex.query(api.agentContentDb.getAgentContent, {
386
- name
387
- });
388
- return {
389
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
390
- };
391
- }
392
- );
393
- server.tool(
394
106
  "upsert_agent_content",
395
- "Set or update the canonical agent content body in VantageRegistry. Computes sha256; if content is unchanged the call is idempotent (version not bumped). If content changed, patch version is bumped (X.Y.Z \u2192 X.Y.Z+1). First write sets version to '1.0.0'. If createIfMissing=true, auto-creates a placeholder agent row if it does not exist yet. Throws if the agent does not exist and createIfMissing is not set.",
396
- {
397
- name: z.string().describe("Agent slug \u2014 e.g. 'dev-convex-expert'"),
398
- content: z.string().describe("Full agent content body (AGENT.md or similar)"),
399
- createIfMissing: z.boolean().optional().default(false).describe(
400
- "Auto-create a placeholder agent row if it does not exist (default: false)"
401
- )
402
- },
403
- async ({ name, content, createIfMissing }) => {
404
- const result = await convex.mutation(
405
- api.agentContentDb.upsertAgentContent,
406
- {
407
- name,
408
- content,
409
- createIfMissing
410
- }
411
- );
412
- return {
413
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
414
- };
415
- }
416
- );
417
- server.tool(
418
- "detect_agent_drift",
419
- "Return the VR-side sha256 hash and filePath for each agent in scope. IMPORTANT: Convex queries have no filesystem access. This tool returns the VR canonical hash + stored filePath so the caller can compute disk SHA256 and compare. scope='all' (default) returns every agent. name=<slug> filters to a single agent.",
420
- {
421
- name: z.string().optional().describe("Agent slug \u2014 filter to a single agent"),
422
- scope: z.string().optional().describe("'all' to return every agent (default when name is omitted)")
423
- },
424
- async ({ name, scope }) => {
425
- const result = await convex.query(api.agentContentDb.detectAgentDrift, {
426
- name,
427
- scope
428
- });
429
- return {
430
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
431
- };
432
- }
433
- );
434
- server.tool(
435
- "get_plugin_content",
436
- "Fetch the VR-hosted canonical plugin content body for a plugin. Returns content, sha256 hash, semver version, and last sync timestamp. Returns null fields if the plugin has not been hosted in VR yet.",
437
- {
438
- name: z.string().describe("Plugin slug \u2014 e.g. 'perello-bootstrap', 'perello-identity'")
439
- },
440
- async ({ name }) => {
441
- const result = await convex.query(api.pluginContentDb.getPluginContent, {
442
- name
443
- });
444
- return {
445
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
446
- };
447
- }
448
- );
449
- server.tool(
450
107
  "upsert_plugin_content",
451
- "Set or update the canonical plugin content body in VantageRegistry. Computes sha256; if content is unchanged the call is idempotent (version not bumped). If content changed, patch version is bumped (X.Y.Z \u2192 X.Y.Z+1). First write sets version to '1.0.0'. If createIfMissing=true, auto-creates a placeholder plugin row if it does not exist yet. Throws if the plugin does not exist and createIfMissing is not set.",
452
- {
453
- name: z.string().describe("Plugin slug \u2014 e.g. 'perello-bootstrap'"),
454
- content: z.string().describe("Full plugin content body"),
455
- createIfMissing: z.boolean().optional().default(false).describe(
456
- "Auto-create a placeholder plugin row if it does not exist (default: false)"
457
- )
458
- },
459
- async ({ name, content, createIfMissing }) => {
460
- const result = await convex.mutation(
461
- api.pluginContentDb.upsertPluginContent,
462
- {
463
- name,
464
- content,
465
- createIfMissing
466
- }
467
- );
468
- return {
469
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
470
- };
471
- }
472
- );
473
- server.tool(
474
- "detect_plugin_drift",
475
- "Return the VR-side sha256 hash and filePath for each plugin in scope. IMPORTANT: Convex queries have no filesystem access. This tool returns the VR canonical hash + stored filePath so the caller can compute disk SHA256 and compare. scope='all' (default) returns every plugin. name=<slug> filters to a single plugin.",
476
- {
477
- name: z.string().optional().describe("Plugin slug \u2014 filter to a single plugin"),
478
- scope: z.string().optional().describe("'all' to return every plugin (default when name is omitted)")
479
- },
480
- async ({ name, scope }) => {
481
- const result = await convex.query(api.pluginContentDb.detectPluginDrift, {
482
- name,
483
- scope
484
- });
485
- return {
486
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
487
- };
488
- }
489
- );
490
- server.tool(
491
- "get_hook_content",
492
- "Fetch the VR-hosted canonical hook content body for a hook. Returns content, sha256 hash, semver version, and last sync timestamp. Returns null fields if the hook has not been hosted in VR yet.",
493
- {
494
- name: z.string().describe("Hook slug \u2014 e.g. 'enforce-no-task-in-message', 'check-pii'")
495
- },
496
- async ({ name }) => {
497
- const result = await convex.query(api.hookContentDb.getHookContent, {
498
- name
499
- });
500
- return {
501
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
502
- };
503
- }
504
- );
505
- server.tool(
506
108
  "upsert_hook_content",
507
- "Set or update the canonical hook content body in VantageRegistry. Computes sha256; if content is unchanged the call is idempotent (version not bumped). If content changed, patch version is bumped (X.Y.Z \u2192 X.Y.Z+1). First write sets version to '1.0.0'. If createIfMissing=true, auto-creates a placeholder hook row if it does not exist yet (event defaults to 'PreToolUse' \u2014 update via upsert_hook afterward). Throws if the hook does not exist and createIfMissing is not set.",
508
- {
509
- name: z.string().describe("Hook slug \u2014 e.g. 'enforce-no-task-in-message'"),
510
- content: z.string().describe("Full hook script content"),
511
- createIfMissing: z.boolean().optional().default(false).describe(
512
- "Auto-create a placeholder hook row if it does not exist (default: false)"
513
- )
514
- },
515
- async ({ name, content, createIfMissing }) => {
516
- const result = await convex.mutation(api.hookContentDb.upsertHookContent, {
517
- name,
518
- content,
519
- createIfMissing
520
- });
521
- return {
522
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
523
- };
524
- }
525
- );
526
- server.tool(
527
- "detect_hook_drift",
528
- "Return the VR-side sha256 hash and filePath for each hook in scope. IMPORTANT: Convex queries have no filesystem access. This tool returns the VR canonical hash + stored filePath so the caller can compute disk SHA256 and compare. scope='all' (default) returns every hook. name=<slug> filters to a single hook.",
529
- {
530
- name: z.string().optional().describe("Hook slug \u2014 filter to a single hook"),
531
- scope: z.string().optional().describe("'all' to return every hook (default when name is omitted)")
532
- },
533
- async ({ name, scope }) => {
534
- const result = await convex.query(api.hookContentDb.detectHookDrift, {
535
- name,
536
- scope
537
- });
538
- return {
539
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
540
- };
541
- }
542
- );
543
- server.tool(
544
- "get_command_content",
545
- "Fetch the VR-hosted canonical command content body for a command. Returns content, sha256 hash, semver version, and last sync timestamp. Returns null fields if the command has not been hosted in VR yet.",
546
- {
547
- name: z.string().describe("Command slug \u2014 e.g. 'sync-workspace-from-vr', 'deploy-prod'")
548
- },
549
- async ({ name }) => {
550
- const result = await convex.query(api.commandContentDb.getCommandContent, {
551
- name
552
- });
553
- return {
554
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
555
- };
556
- }
557
- );
558
- server.tool(
559
109
  "upsert_command_content",
560
- "Set or update the canonical command content body in VantageRegistry. Computes sha256; if content is unchanged the call is idempotent (version not bumped). If content changed, patch version is bumped (X.Y.Z \u2192 X.Y.Z+1). First write sets version to '1.0.0'. Auto-creates the command document if it does not exist yet.",
561
- {
562
- name: z.string().describe("Command slug \u2014 e.g. 'sync-workspace-from-vr'"),
563
- content: z.string().describe("Full command script content")
564
- },
565
- async ({ name, content }) => {
566
- const result = await convex.mutation(
567
- api.commandContentDb.upsertCommandContent,
568
- {
569
- name,
570
- content
571
- }
572
- );
573
- return {
574
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
575
- };
576
- }
577
- );
578
- server.tool(
579
- "detect_command_drift",
580
- "Return the VR-side sha256 hash and filePath for each command in scope. IMPORTANT: Convex queries have no filesystem access. This tool returns the VR canonical hash + stored filePath so the caller can compute disk SHA256 and compare. scope='all' (default) returns every command. name=<slug> filters to a single command.",
581
- {
582
- name: z.string().optional().describe("Command slug \u2014 filter to a single command"),
583
- scope: z.string().optional().describe("'all' to return every command (default when name is omitted)")
584
- },
585
- async ({ name, scope }) => {
586
- const result = await convex.query(api.commandContentDb.detectCommandDrift, {
587
- name,
588
- scope
589
- });
590
- return {
591
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
592
- };
593
- }
594
- );
595
- server.tool(
596
110
  "upsert_plugin",
597
- "Create or update a plugin in VantageRegistry. Upserts by name.",
598
- {
599
- name: z.string().describe("Plugin name \u2014 e.g. 'perello-bootstrap', 'perello-identity'"),
600
- description: z.string().describe("What this plugin provides"),
601
- agentCount: z.number().int().describe("Number of agents in this plugin"),
602
- skillCount: z.number().int().describe("Number of skills in this plugin"),
603
- hookCount: z.number().int().describe("Number of hooks in this plugin"),
604
- source: pluginSourceSchema,
605
- status: pluginStatusSchema,
606
- version: z.string().optional().describe("Semantic version \u2014 e.g. '1.0.0'"),
607
- filePath: z.string().optional().describe("Plugin directory path"),
608
- pricing: z.enum(["free", "paid"]).optional().describe("Pricing tier"),
609
- price: z.number().optional().describe("Price in EUR (if paid)"),
610
- license: z.string().optional().describe("License \u2014 e.g. 'MIT', 'proprietary'"),
611
- publisherId: z.string().optional().describe("Publisher identifier"),
612
- categories: z.array(z.string()).optional().describe("Categories beyond team name")
613
- },
614
- async (args) => {
615
- const id = await convex.mutation(api.plugins.upsert, args);
616
- return {
617
- content: [
618
- {
619
- type: "text",
620
- text: JSON.stringify({ id, name: args.name }, null, 2)
621
- }
622
- ]
623
- };
624
- }
625
- );
626
- server.tool(
627
- "list_plugins",
628
- "List all plugins in VantageRegistry. Optionally filter by status, source, or team. fields='lite' returns compact {_id, name, status, source}. fields='full' returns the complete document.",
629
- {
630
- status: pluginStatusSchema.optional().describe("Filter by status \u2014 omit to list all"),
631
- source: pluginSourceSchema.optional().describe("Filter by source: 'internal' or 'external'"),
632
- team: z.string().optional().describe("Filter by owning team"),
633
- fields: z.enum(["lite", "full"]).optional().describe("Projection: 'lite' (compact) or 'full' (complete document)")
634
- },
635
- async ({ status, source, team, fields }) => {
636
- const results = await convex.query(api.plugins.list, {
637
- status,
638
- source,
639
- team,
640
- fields
641
- });
642
- return {
643
- content: [{ type: "text", text: JSON.stringify(results, null, 2) }]
644
- };
645
- }
646
- );
647
- server.tool(
648
- "get_plugin",
649
- "Get a single plugin by its Convex document ID.",
650
- {
651
- id: z.string().describe("Convex document ID for the plugin")
652
- },
653
- async ({ id }) => {
654
- const result = await convex.query(api.plugins.get, { id });
655
- return {
656
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
657
- };
658
- }
659
- );
660
- server.tool(
661
111
  "upsert_hook",
662
- "Create or update a hook in VantageRegistry. Upserts by name.",
663
- {
664
- name: z.string().describe("Hook name \u2014 e.g. 'check-pii', 'enforce-skill-references'"),
665
- event: hookEventSchema,
666
- matcher: z.string().optional().describe("Tool matcher pattern \u2014 e.g. 'Bash', 'Agent'"),
667
- description: z.string().describe("What this hook does"),
668
- content: z.string().describe("Full hook script content"),
669
- filePath: z.string().describe("File path relative to project root"),
670
- registered: z.boolean().describe("Whether this hook is registered in settings.json"),
671
- status: hookStatusSchema,
672
- version: z.string().optional().describe("Semantic version \u2014 e.g. '1.0.0'"),
673
- tests: z.array(z.string()).optional().describe(
674
- "Hook test file paths \u2014 e.g. ['hooks/tests/check-pii.test.py']. Omitted \u2192 not sent (back-compat)."
675
- )
676
- },
677
- async (args) => {
678
- const id = await convex.mutation(api.hooks.upsert, args);
679
- return {
680
- content: [
681
- {
682
- type: "text",
683
- text: JSON.stringify(
684
- { id, name: args.name, event: args.event },
685
- null,
686
- 2
687
- )
688
- }
689
- ]
690
- };
691
- }
692
- );
693
- server.tool(
694
- "list_hooks",
695
- "List all hooks in VantageRegistry. Optionally filter by status or lifecycle event. fields='lite' returns compact {_id, name, status, event, registered}. fields='full' returns the complete document including content.",
696
- {
697
- status: hookStatusSchema.optional().describe("Filter by status \u2014 omit to list all"),
698
- event: hookEventSchema.optional().describe(
699
- "Filter by lifecycle event \u2014 e.g. 'PreToolUse', 'SessionStart'"
700
- ),
701
- fields: z.enum(["lite", "full"]).optional().describe(
702
- "Projection: 'lite' (compact) or 'full' (complete document with content)"
703
- )
704
- },
705
- async ({ status, event, fields }) => {
706
- const results = await convex.query(api.hooks.list, {
707
- status,
708
- event,
709
- fields
710
- });
711
- return {
712
- content: [{ type: "text", text: JSON.stringify(results, null, 2) }]
713
- };
714
- }
715
- );
716
- server.tool(
717
- "get_hook",
718
- "Get a single hook by its Convex document ID.",
719
- {
720
- id: z.string().describe("Convex document ID for the hook")
721
- },
722
- async ({ id }) => {
723
- const result = await convex.query(api.hooks.get, { id });
724
- return {
725
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
726
- };
727
- }
728
- );
729
- server.tool(
730
112
  "upsert_prompt",
731
- "Create or update a prompt in VantageRegistry. Upserts by name.",
732
- {
733
- name: z.string().describe("Prompt name \u2014 e.g. 'system-prompt', 'review-checklist'"),
734
- team: z.string().optional().describe("Team this prompt belongs to \u2014 defaults to 'global'"),
735
- purpose: z.string().describe("What this prompt is used for"),
736
- content: z.string().describe("Full prompt content"),
737
- version: z.string().optional().describe("Semantic version \u2014 e.g. '1.0.0'")
738
- },
739
- async (args) => {
740
- const id = await convex.mutation(api.prompts.upsert, args);
741
- return {
742
- content: [
743
- {
744
- type: "text",
745
- text: JSON.stringify({ id, name: args.name }, null, 2)
746
- }
747
- ]
748
- };
749
- }
750
- );
751
- server.tool(
752
- "list_prompts",
753
- "List all prompts in VantageRegistry. Optionally filter by team.",
754
- {
755
- team: z.string().optional().describe("Filter by team \u2014 omit to list all")
756
- },
757
- async ({ team }) => {
758
- const results = await convex.query(api.prompts.list, { team });
759
- return {
760
- content: [{ type: "text", text: JSON.stringify(results, null, 2) }]
761
- };
762
- }
763
- );
764
- server.tool(
765
- "get_prompt",
766
- "Get a single prompt by its Convex document ID.",
767
- {
768
- id: z.string().describe("Convex document ID for the prompt")
769
- },
770
- async ({ id }) => {
771
- const result = await convex.query(api.prompts.get, { id });
772
- return {
773
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
774
- };
775
- }
776
- );
777
- server.tool(
778
113
  "upsert_template",
779
- "Create or update a template in VantageRegistry. Upserts by name. contentHash is auto-computed server-side (sha256 of content) when omitted.",
780
- {
781
- name: z.string().describe("Template name \u2014 e.g. 'agent-brief', 'skill-md'"),
782
- team: z.string().optional().describe("Team this template belongs to \u2014 defaults to 'global'"),
783
- purpose: z.string().describe("What this template is used for"),
784
- content: z.string().describe("Full template content"),
785
- version: z.string().optional().describe("Semantic version \u2014 e.g. '1.0.0'"),
786
- template_type: templateTypeSchema.optional().describe(
787
- "Template type: standard, mission, brief, runbook, document, or checklist"
788
- ),
789
- category: z.string().optional().describe("Category \u2014 e.g. 'standards/fleet-shared'"),
790
- contentHash: z.string().optional().describe("sha256 of content \u2014 auto-computed server-side when omitted"),
791
- sourceCommit: z.string().optional().describe("Git commit SHA the template content was sourced from"),
792
- sourceRepo: z.string().optional().describe("Git repo the template content was sourced from")
793
- },
794
- async (args) => {
795
- const id = await convex.mutation(api.templates.upsert, args);
796
- return {
797
- content: [
798
- {
799
- type: "text",
800
- text: JSON.stringify({ id, name: args.name }, null, 2)
801
- }
802
- ]
803
- };
804
- }
805
- );
806
- server.tool(
807
- "list_templates",
808
- "List all templates in VantageRegistry. Optionally filter by team and/or template_type.",
809
- {
810
- team: z.string().optional().describe("Filter by team \u2014 omit to list all"),
811
- template_type: templateTypeSchema.optional().describe("Filter by template type: mission, document, or checklist")
812
- },
813
- async ({ team, template_type }) => {
814
- const results = await convex.query(api.templates.list, {
815
- team,
816
- template_type
817
- });
818
- return {
819
- content: [{ type: "text", text: JSON.stringify(results, null, 2) }]
820
- };
114
+ "upsert_test_run",
115
+ "upsert_skill_eval_corpus",
116
+ "upsert_runbook",
117
+ "delete_runbook",
118
+ "link_runbook_template",
119
+ "unlink_runbook_template",
120
+ "register_component",
121
+ "update_component",
122
+ "delete_component"
123
+ ]);
124
+ function resolveRole(authHeader) {
125
+ if (!authHeader) return null;
126
+ const prefix = "Bearer ";
127
+ if (!authHeader.startsWith(prefix)) return null;
128
+ const token = authHeader.slice(prefix.length);
129
+ const adminToken = process.env.MCP_BEARER_TOKEN;
130
+ const roToken = process.env.MCP_BEARER_TOKEN_READONLY;
131
+ const enc = new TextEncoder();
132
+ const tokenBuf = enc.encode(token);
133
+ if (adminToken) {
134
+ const adminBuf = enc.encode(adminToken);
135
+ const len = Math.max(tokenBuf.length, adminBuf.length);
136
+ const a = Buffer.alloc(len);
137
+ const b = Buffer.alloc(len);
138
+ Buffer.from(tokenBuf).copy(a);
139
+ Buffer.from(adminBuf).copy(b);
140
+ if (timingSafeEqual(a, b) && token.length === adminToken.length) {
141
+ return "admin";
142
+ }
821
143
  }
822
- );
823
- server.tool(
824
- "get_template",
825
- "Get a single template by its Convex document ID.",
826
- {
827
- id: z.string().describe("Convex document ID for the template")
828
- },
829
- async ({ id }) => {
830
- const result = await convex.query(api.templates.get, { id });
831
- return {
832
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
833
- };
144
+ if (roToken) {
145
+ const roBuf = enc.encode(roToken);
146
+ const len = Math.max(tokenBuf.length, roBuf.length);
147
+ const a = Buffer.alloc(len);
148
+ const b = Buffer.alloc(len);
149
+ Buffer.from(tokenBuf).copy(a);
150
+ Buffer.from(roBuf).copy(b);
151
+ if (timingSafeEqual(a, b) && token.length === roToken.length) {
152
+ return "readonly";
153
+ }
834
154
  }
835
- );
836
- server.tool(
837
- "detect_template_drift",
838
- "Return the VR-side sha256 contentHash + provenance (sourceCommit, sourceRepo) for each template in scope. IMPORTANT: Convex queries have no filesystem access. This tool returns the VR canonical contentHash so the caller can read the source file, compute its SHA256, and compare. name=<slug> filters to a single template; omit name to return every template.",
839
- {
840
- name: z.string().optional().describe("Template slug \u2014 filter to a single template")
841
- },
842
- async ({ name }) => {
843
- const result = await convex.query(api.templates.detectTemplateDrift, {
844
- name
155
+ return null;
156
+ }
157
+ function sendJsonError(res, status, body, extraHeaders) {
158
+ const payload = JSON.stringify(body);
159
+ res.writeHead(status, {
160
+ "Content-Type": "application/json",
161
+ "Content-Length": Buffer.byteLength(payload),
162
+ ...extraHeaders
163
+ });
164
+ res.end(payload);
165
+ }
166
+ async function readBody(req) {
167
+ return new Promise((resolve2, reject) => {
168
+ const chunks = [];
169
+ req.on("data", (c) => chunks.push(c));
170
+ req.on("end", () => resolve2(Buffer.concat(chunks)));
171
+ req.on("error", reject);
172
+ });
173
+ }
174
+ function createHttpApp() {
175
+ return async function handler(req, res) {
176
+ const url = new URL(req.url ?? "/", `http://localhost`);
177
+ if (req.method === "GET" && url.pathname === "/health") {
178
+ sendJsonError(res, 200, {
179
+ status: "ok",
180
+ service: "vantage-registry-mcp",
181
+ transport: "streamable-http"
182
+ });
183
+ return;
184
+ }
185
+ if (url.pathname !== "/mcp") {
186
+ sendJsonError(res, 404, { error: "not_found" });
187
+ return;
188
+ }
189
+ const adminToken = process.env.MCP_BEARER_TOKEN;
190
+ const roToken = process.env.MCP_BEARER_TOKEN_READONLY;
191
+ const bearerEnabled = Boolean(adminToken || roToken);
192
+ let role = "open";
193
+ if (bearerEnabled) {
194
+ const resolved = resolveRole(req.headers["authorization"]);
195
+ if (resolved === null) {
196
+ sendJsonError(
197
+ res,
198
+ 401,
199
+ { error: "Unauthorized", message: "Missing or invalid bearer token" },
200
+ { "WWW-Authenticate": 'Bearer realm="vantage-registry"' }
201
+ );
202
+ return;
203
+ }
204
+ role = resolved;
205
+ }
206
+ const rawBody = await readBody(req);
207
+ let parsedBody = null;
208
+ try {
209
+ parsedBody = JSON.parse(rawBody.toString("utf-8"));
210
+ } catch {
211
+ sendJsonError(res, 400, { error: "invalid_json" });
212
+ return;
213
+ }
214
+ if (role === "readonly") {
215
+ const body = parsedBody;
216
+ if (body && typeof body === "object" && body.method === "tools/call" && body.params && typeof body.params === "object") {
217
+ const toolName = body.params.name;
218
+ if (typeof toolName === "string" && WRITE_TOOLS.has(toolName)) {
219
+ sendJsonError(res, 403, {
220
+ error: "Forbidden",
221
+ message: `Tool '${toolName}' requires admin scope. Read-only token cannot call write tools.`
222
+ });
223
+ return;
224
+ }
225
+ }
226
+ }
227
+ const mcpServer = new McpServer({
228
+ name: "vantage-registry",
229
+ version: "1.7.0"
845
230
  });
846
- return {
847
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
848
- };
849
- }
850
- );
851
- server.tool(
852
- "list_templates_by_category",
853
- "List templates for a specific category using the byCategory index.",
854
- {
855
- category: z.string().describe("Category to filter by \u2014 e.g. 'standards/fleet-shared'")
856
- },
857
- async ({ category }) => {
858
- const result = await convex.query(api.templates.listTemplatesByCategory, {
859
- category
231
+ registerAllTools(mcpServer, role === "admin");
232
+ const transport = new StreamableHTTPServerTransport({
233
+ sessionIdGenerator: void 0,
234
+ // stateless
235
+ enableJsonResponse: true
236
+ // return plain JSON, not SSE
860
237
  });
861
- return {
862
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
863
- };
864
- }
865
- );
866
- server.tool(
867
- "upsert_test_run",
868
- "Insert a skill quality test run and update denormalized quality fields on the parent skill (lastTestedAt, lastReviewerScore, lastEvalDelta, testStatus). reviewerScore format is 'X/Y' e.g. '37/45'. evalDelta is pp delta vs without-skill baseline.",
869
- {
870
- skillId: z.string().describe("Convex document ID of the skill being tested"),
871
- runDate: z.number().describe("Unix timestamp (ms) when the run was executed"),
872
- runByOrchestrator: z.string().describe("Orchestrator that ran the test \u2014 e.g. 'alpha', 'pi'"),
873
- runByMission: z.string().optional().describe("Mission reference \u2014 e.g. 'D70'"),
874
- reviewerScore: z.string().describe("Score as 'X/Y' \u2014 e.g. '37/45'"),
875
- evalDelta: z.number().describe("Percentage-point delta vs without-skill baseline"),
876
- evalAbsoluteWith: z.number().describe("Absolute score with skill (0-100)"),
877
- evalAbsoluteWithout: z.number().describe("Absolute score without skill (0-100)"),
878
- runnerModel: z.string().describe("Model used to run the skill \u2014 e.g. 'claude-sonnet-4-6'"),
879
- judgeModel: z.string().describe("Model used as judge \u2014 e.g. 'claude-haiku-4-5'"),
880
- corpusVersion: z.string().describe("Eval corpus version used \u2014 e.g. '2.0.0'"),
881
- harnessVersion: z.string().describe("Test harness version \u2014 e.g. '0.1.0'"),
882
- skillSha: z.string().describe("Git SHA of the skill file at test time"),
883
- gradingJsonPath: z.string().optional().describe("Path to grading output JSON"),
884
- benchmarkJsonPath: z.string().optional().describe("Path to benchmark output JSON"),
885
- reportPath: z.string().optional().describe("Path to human-readable report"),
886
- timeSpentMinutes: z.number().optional().describe("Time spent running the test in minutes"),
887
- notes: z.string().optional().describe("Free-form notes about this run"),
888
- testStatus: z.enum(["untested", "tested", "needs_retest", "improving", "untestable"]).optional().describe("Override test status \u2014 defaults to 'tested'")
889
- },
890
- async (args) => {
891
- const id = await convex.mutation(api.skillTestRuns.upsertTestRun, {
892
- ...args,
893
- skillId: args.skillId
894
- });
895
- return {
896
- content: [
897
- {
898
- type: "text",
899
- text: JSON.stringify(
900
- { id, skillId: args.skillId, runDate: args.runDate },
901
- null,
902
- 2
903
- )
238
+ await mcpServer.connect(transport);
239
+ await transport.handleRequest(req, res, parsedBody);
240
+ };
241
+ }
242
+ function startHttpServer(port) {
243
+ return new Promise((resolve2, reject) => {
244
+ const handler = createHttpApp();
245
+ const httpServer = createServer(handler);
246
+ httpServer.on("error", reject);
247
+ httpServer.listen(port, "0.0.0.0", () => {
248
+ resolve2({
249
+ close() {
250
+ httpServer.close();
251
+ },
252
+ address() {
253
+ return httpServer.address();
904
254
  }
905
- ]
906
- };
907
- }
908
- );
909
- server.tool(
910
- "get_skill_test_history",
911
- "Return test run history for a skill, ordered newest-first.",
912
- {
913
- skillId: z.string().describe("Convex document ID of the skill"),
914
- limit: z.number().optional().describe("Max number of runs to return \u2014 defaults to 50")
915
- },
916
- async ({ skillId, limit }) => {
917
- const results = await convex.query(api.skillTestRuns.getSkillTestHistory, {
918
- skillId,
919
- limit
255
+ });
920
256
  });
921
- return {
922
- content: [{ type: "text", text: JSON.stringify(results, null, 2) }]
923
- };
924
- }
925
- );
926
- server.tool(
927
- "list_skills_by_freshness",
928
- "Return skills that have never been tested or were last tested more than staleDays ago. Use this to find skills that need a quality test run. Defaults to fields='lite' (bounded output) to stay under MCP token cap. Use fields='full' to retrieve complete skill documents.",
929
- {
930
- staleDays: z.number().describe("Number of days since last test run to consider stale"),
931
- fields: z.enum(["lite", "full"]).optional().default("lite").describe(
932
- "Projection: 'lite' (default, compact \u2014 bounded output) or 'full' (complete document)"
933
- ),
934
- limit: z.number().int().optional().describe("Max results \u2014 defaults to 100, max 500")
935
- },
936
- async ({ staleDays, fields, limit }) => {
937
- const results = await convex.query(
938
- api.skillTestRuns.listSkillsByFreshness,
939
- { staleDays, fields, limit }
940
- );
941
- return {
942
- content: [{ type: "text", text: JSON.stringify(results, null, 2) }]
943
- };
944
- }
945
- );
946
- server.tool(
947
- "list_skills_below_threshold",
948
- "Return skills whose last test run was below a score ratio (scoreMin as 0-1 fraction of 'X/Y') or below a delta threshold (deltaMin in pp). Omit a param to skip that filter. fields='lite' returns compact output. fields='full' returns complete skill documents.",
949
- {
950
- scoreMin: z.number().optional().describe(
951
- "Minimum acceptable score ratio (0-1). Skills below this are returned."
952
- ),
953
- deltaMin: z.number().optional().describe(
954
- "Minimum acceptable eval delta in pp. Skills below this are returned."
955
- ),
956
- fields: z.enum(["lite", "full"]).optional().describe(
957
- "Projection: 'lite' (compact) or 'full' (complete document). Omit for full (backward compat)."
958
- )
959
- },
960
- async ({ scoreMin, deltaMin, fields }) => {
961
- const results = await convex.query(
962
- api.skillTestRuns.listSkillsBelowThreshold,
963
- { scoreMin, deltaMin, fields }
964
- );
965
- return {
966
- content: [{ type: "text", text: JSON.stringify(results, null, 2) }]
967
- };
968
- }
969
- );
970
- server.tool(
971
- "upsert_skill_eval_corpus",
972
- "Insert or update an eval corpus version for a skill. Upserts by (skillId, version).",
973
- {
974
- skillId: z.string().describe("Convex document ID of the skill"),
975
- version: z.string().describe("Corpus version \u2014 e.g. '2.0.0'"),
976
- casesCount: z.number().int().describe("Number of eval cases"),
977
- assertionsCount: z.number().int().describe("Total number of assertions across all cases"),
978
- baselinePrompt: z.string().describe("The baseline prompt used without the skill"),
979
- evalsJsonContent: z.string().describe("Full JSON content of the evals file, or '{}' as placeholder"),
980
- authoredBy: z.string().describe("Orchestrator or person who authored this corpus"),
981
- authoredAt: z.number().describe("Unix timestamp (ms) when the corpus was authored"),
982
- supersedesVersion: z.string().optional().describe("Version this corpus supersedes \u2014 e.g. '1.0.0'")
983
- },
984
- async (args) => {
985
- const id = await convex.mutation(
986
- api.skillEvalCorpus.upsertSkillEvalCorpus,
987
- {
988
- ...args,
989
- skillId: args.skillId
990
- }
991
- );
992
- return {
993
- content: [
994
- {
995
- type: "text",
996
- text: JSON.stringify(
997
- { id, skillId: args.skillId, version: args.version },
998
- null,
999
- 2
1000
- )
1001
- }
1002
- ]
1003
- };
1004
- }
1005
- );
1006
- var phaseSchema = z.object({
1007
- name: z.string().describe("Phase name"),
1008
- description: z.string().describe("What this phase accomplishes"),
1009
- steps: z.array(z.string()).describe("Ordered list of steps in this phase")
1010
- });
1011
- var runbookInputSchema = z.object({
1012
- name: z.string().describe("Input parameter name"),
1013
- type: z.string().describe("Input type \u2014 e.g. 'string', 'boolean'"),
1014
- required: z.boolean().describe("Whether this input is required"),
1015
- description: z.string().describe("What this input controls")
1016
- });
1017
- var runbookOutputSchema = z.object({
1018
- name: z.string().describe("Output artifact name"),
1019
- description: z.string().describe("What this output contains"),
1020
- path_pattern: z.string().optional().describe("File path pattern \u2014 e.g. 'reports/{name}.md'")
1021
- });
1022
- var applicabilitySchema = z.object({
1023
- orchestrators: z.array(z.string()).describe("Orchestrator slugs this runbook applies to"),
1024
- business_units: z.array(z.string()).describe("Business unit slugs this runbook applies to"),
1025
- use_cases: z.array(z.string()).describe("Use case slugs this runbook applies to")
1026
- });
1027
- var linkedTemplateSchema = z.object({
1028
- name: z.string().describe("Template name"),
1029
- template_type: z.string().describe("Template type \u2014 mission, document, or checklist"),
1030
- version: z.string().describe("Template version \u2014 e.g. '1.0.0'"),
1031
- usage_phase: z.string().describe("Phase name where this template is used"),
1032
- required: z.boolean().describe("Whether this template is required for the runbook"),
1033
- description: z.string().describe("How this template is used in this runbook")
1034
- });
1035
- server.tool(
1036
- "upsert_runbook",
1037
- "Create or update a runbook in VantageRegistry. Upserts by name \u2014 if a runbook with the same name exists, it is updated.",
1038
- {
1039
- name: z.string().describe("Runbook slug \u2014 e.g. 'deploy-production', 'onboard-agent'"),
1040
- description: z.string().describe("What this runbook accomplishes"),
1041
- version: z.string().describe("Semantic version \u2014 e.g. '1.0.0'"),
1042
- status: runbookStatusSchema,
1043
- category: z.string().describe("Category \u2014 e.g. 'deployment', 'onboarding', 'quality'"),
1044
- tags: z.array(z.string()).describe("Searchable tags"),
1045
- content: z.string().describe("Full runbook content (markdown)"),
1046
- phases: z.array(phaseSchema).describe("Ordered phases of the runbook"),
1047
- inputs: z.array(runbookInputSchema).describe("Input parameters for the runbook"),
1048
- outputs: z.array(runbookOutputSchema).describe("Output artifacts produced by the runbook"),
1049
- applicability: applicabilitySchema,
1050
- author: z.string().describe("Author slug \u2014 e.g. 'omega'"),
1051
- team: z.string().describe("Owning team slug \u2014 e.g. 'core', 'dev'"),
1052
- related_skills: z.array(z.string()).describe("Skill slugs referenced by this runbook"),
1053
- related_agents: z.array(z.string()).describe("Agent slugs referenced by this runbook"),
1054
- linked_templates: z.array(linkedTemplateSchema).describe("Templates used during this runbook")
1055
- },
1056
- async (args) => {
1057
- try {
1058
- const result = await convex.mutation(api.runbooks.upsertRunbook, args);
257
+ });
258
+ }
259
+ function registerAllTools(s, isAdmin = true) {
260
+ s.tool(
261
+ "upsert_team",
262
+ "Create or update a team in VantageRegistry. Upserts by name \u2014 if a team with the same name exists, it is updated.",
263
+ {
264
+ name: z.string().describe("Team name \u2014 e.g. 'core', 'geo', 'dev'"),
265
+ description: z.string().describe("What this team does"),
266
+ agentCount: z.number().int().describe("Number of agents in this team"),
267
+ skillCount: z.number().int().describe("Number of skills in this team"),
268
+ status: teamStatusSchema,
269
+ project: z.string().optional().describe("Associated project \u2014 e.g. 'vantage-starter'")
270
+ },
271
+ async (args) => {
272
+ const id = await convex.mutation(api.teams.upsert, adminWriteArgs(args, isAdmin));
1059
273
  return {
1060
274
  content: [
1061
- {
1062
- type: "text",
1063
- text: JSON.stringify({ success: true, data: result }, null, 2)
1064
- }
275
+ { type: "text", text: JSON.stringify({ id, ...args }, null, 2) }
1065
276
  ]
1066
277
  };
1067
- } catch (err) {
278
+ }
279
+ );
280
+ s.tool(
281
+ "list_teams",
282
+ "List all teams in VantageRegistry. Optionally filter by status (active, planned, deprecated).",
283
+ {
284
+ status: teamStatusSchema.optional().describe("Filter by status \u2014 omit to list all")
285
+ },
286
+ async ({ status }) => {
287
+ const results = await convex.query(api.teams.list, { status });
288
+ return {
289
+ content: [{ type: "text", text: JSON.stringify(results, null, 2) }]
290
+ };
291
+ }
292
+ );
293
+ s.tool(
294
+ "get_team",
295
+ "Get a single team by its Convex document ID.",
296
+ {
297
+ id: z.string().describe("Convex document ID for the team")
298
+ },
299
+ async ({ id }) => {
300
+ const result = await convex.query(api.teams.get, { id });
301
+ return {
302
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
303
+ };
304
+ }
305
+ );
306
+ s.tool(
307
+ "upsert_agent",
308
+ "Create or update an agent in VantageRegistry. Upserts by name+team \u2014 if an agent with the same name and team exists, it is updated.",
309
+ {
310
+ name: z.string().describe("Agent name \u2014 e.g. 'copywriter', 'strategy-researcher'"),
311
+ team: z.string().describe("Team this agent belongs to"),
312
+ description: z.string().describe("What this agent does"),
313
+ content: z.string().describe("Full agent definition content (markdown)"),
314
+ filePath: z.string().describe("File path relative to project root"),
315
+ lineCount: z.number().int().describe("Number of lines in the agent file"),
316
+ status: agentStatusSchema,
317
+ version: z.string().optional().describe("Semantic version \u2014 e.g. '1.0.0'"),
318
+ source: sourceSchema,
319
+ pricing: z.enum(["free", "paid"]).optional().describe("Pricing tier"),
320
+ price: z.number().optional().describe("Price in EUR (if paid)"),
321
+ license: z.string().optional().describe("License \u2014 e.g. 'MIT', 'proprietary'"),
322
+ publisherId: z.string().optional().describe("Publisher identifier"),
323
+ categories: z.array(z.string()).optional().describe("Categories beyond team name")
324
+ },
325
+ async (args) => {
326
+ const id = await convex.mutation(api.agents.upsert, adminWriteArgs(args, isAdmin));
1068
327
  return {
1069
328
  content: [
1070
329
  {
1071
330
  type: "text",
1072
331
  text: JSON.stringify(
1073
- { success: false, error: String(err) },
332
+ { id, name: args.name, team: args.team },
1074
333
  null,
1075
334
  2
1076
335
  )
@@ -1078,75 +337,95 @@ server.tool(
1078
337
  ]
1079
338
  };
1080
339
  }
1081
- }
1082
- );
1083
- server.tool(
1084
- "get_runbook",
1085
- "Get a single runbook by name. Optionally guard by exact version.",
1086
- {
1087
- name: z.string().describe("Runbook slug \u2014 e.g. 'deploy-production'"),
1088
- version: z.string().optional().describe("If provided, returns null when stored version differs")
1089
- },
1090
- async ({ name, version }) => {
1091
- try {
1092
- const result = await convex.query(api.runbooks.getRunbook, {
1093
- name,
1094
- version
340
+ );
341
+ s.tool(
342
+ "list_agents",
343
+ "List all agents in VantageRegistry. Optionally filter by status, team, or category. fields='lite' returns compact {_id, name, team, status}. fields='full' returns the complete document including content.",
344
+ {
345
+ status: agentStatusSchema.optional().describe("Filter by status \u2014 omit to list all"),
346
+ team: z.string().optional().describe("Filter by team \u2014 e.g. 'dev', 'core'"),
347
+ category: z.string().optional().describe("Filter by category"),
348
+ fields: z.enum(["lite", "full"]).optional().describe(
349
+ "Projection: 'lite' (compact) or 'full' (complete document). Omit to use legacy summary behaviour."
350
+ ),
351
+ summary: z.boolean().optional().describe(
352
+ "Return summary only (no content). Default: true \u2014 legacy, prefer fields='lite'"
353
+ )
354
+ },
355
+ async ({ status, team, category, fields, summary }) => {
356
+ const results = await convex.query(api.agents.list, {
357
+ status,
358
+ team,
359
+ category,
360
+ fields,
361
+ summary
1095
362
  });
1096
363
  return {
1097
- content: [
1098
- {
1099
- type: "text",
1100
- text: JSON.stringify({ success: true, data: result }, null, 2)
1101
- }
1102
- ]
364
+ content: [{ type: "text", text: JSON.stringify(results, null, 2) }]
1103
365
  };
1104
- } catch (err) {
366
+ }
367
+ );
368
+ s.tool(
369
+ "list_agents_by_team",
370
+ "List all agents belonging to a specific team.",
371
+ {
372
+ team: z.string().describe("Team name to filter by"),
373
+ summary: z.boolean().optional().describe("Return summary only (no content). Default: true")
374
+ },
375
+ async ({ team, summary }) => {
376
+ const results = await convex.query(api.agents.listByTeam, {
377
+ team,
378
+ summary
379
+ });
1105
380
  return {
1106
- content: [
1107
- {
1108
- type: "text",
1109
- text: JSON.stringify(
1110
- { success: false, error: String(err) },
1111
- null,
1112
- 2
1113
- )
1114
- }
1115
- ]
381
+ content: [{ type: "text", text: JSON.stringify(results, null, 2) }]
1116
382
  };
1117
383
  }
1118
- }
1119
- );
1120
- server.tool(
1121
- "list_runbooks",
1122
- "List runbooks in VantageRegistry. Defaults to published status. Supports applicability filters.",
1123
- {
1124
- status: runbookStatusSchema.optional().describe("Filter by status \u2014 defaults to 'published'"),
1125
- category: z.string().optional().describe("Filter by category \u2014 e.g. 'deployment'"),
1126
- team: z.string().optional().describe("Filter by owning team"),
1127
- applicability_orchestrator: z.string().optional().describe("Filter by orchestrator slug in applicability"),
1128
- applicability_bu: z.string().optional().describe("Filter by business unit slug in applicability"),
1129
- applicability_use_case: z.string().optional().describe("Filter by use case slug in applicability"),
1130
- limit: z.number().int().optional().describe("Max results to return \u2014 defaults to 50, max 200")
1131
- },
1132
- async (args) => {
1133
- try {
1134
- const result = await convex.query(api.runbooks.listRunbooks, args);
384
+ );
385
+ s.tool(
386
+ "get_agent",
387
+ "Get a single agent by its Convex document ID.",
388
+ {
389
+ id: z.string().describe("Convex document ID for the agent")
390
+ },
391
+ async ({ id }) => {
392
+ const result = await convex.query(api.agents.get, { id });
1135
393
  return {
1136
- content: [
1137
- {
1138
- type: "text",
1139
- text: JSON.stringify({ success: true, data: result }, null, 2)
1140
- }
1141
- ]
394
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
1142
395
  };
1143
- } catch (err) {
396
+ }
397
+ );
398
+ s.tool(
399
+ "upsert_skill",
400
+ "Create or update a skill in VantageRegistry. Upserts by name+team \u2014 if a skill with the same name and team exists, it is updated. Pass vrBody to write the full SKILL.md body (vrContent/vrContentHash/vrContentVersion) in the SAME call \u2014 no separate upsert_skill_content needed (that tool remains for back-compat). Omit vrBody for metadata-only upsert.",
401
+ {
402
+ name: z.string().describe("Skill name \u2014 e.g. 'social-post', 'competitor-watch'"),
403
+ team: z.string().describe("Team this skill belongs to"),
404
+ category: skillCategorySchema,
405
+ description: z.string().describe("What this skill does \u2014 use pushy trigger descriptions"),
406
+ content: z.string().describe("Full SKILL.md content"),
407
+ filePath: z.string().describe("File path relative to project root"),
408
+ lineCount: z.number().int().describe("Number of lines in the skill file"),
409
+ status: skillStatusSchema,
410
+ version: z.string().optional().describe("Semantic version \u2014 e.g. '1.0.0'"),
411
+ source: sourceSchema,
412
+ pricing: z.enum(["free", "paid"]).optional().describe("Pricing tier"),
413
+ price: z.number().optional().describe("Price in EUR (if paid)"),
414
+ license: z.string().optional().describe("License \u2014 e.g. 'MIT', 'proprietary'"),
415
+ publisherId: z.string().optional().describe("Publisher identifier"),
416
+ categories: z.array(z.string()).optional().describe("Categories beyond team name"),
417
+ vrBody: z.string().optional().describe(
418
+ "Full SKILL.md body. When passed, writes vrContent/vrContentHash/vrContentVersion in the same call. Omitted \u2192 metadata-only (unchanged)."
419
+ )
420
+ },
421
+ async (args) => {
422
+ const id = await convex.mutation(api.skills.upsert, adminWriteArgs(args, isAdmin));
1144
423
  return {
1145
424
  content: [
1146
425
  {
1147
426
  type: "text",
1148
427
  text: JSON.stringify(
1149
- { success: false, error: String(err) },
428
+ { id, name: args.name, team: args.team, category: args.category },
1150
429
  null,
1151
430
  2
1152
431
  )
@@ -1154,217 +433,430 @@ server.tool(
1154
433
  ]
1155
434
  };
1156
435
  }
1157
- }
1158
- );
1159
- server.tool(
1160
- "list_runbooks_by_category",
1161
- "List runbooks for a specific category using the byCategory index.",
1162
- {
1163
- category: z.string().describe("Category to filter by \u2014 e.g. 'deployment', 'quality'"),
1164
- status: runbookStatusSchema.optional().describe(
1165
- "Filter by status \u2014 omit to list all statuses in this category"
1166
- )
1167
- },
1168
- async ({ category, status }) => {
1169
- try {
1170
- const result = await convex.query(api.runbooks.listRunbooksByCategory, {
436
+ );
437
+ s.tool(
438
+ "list_skills",
439
+ "List all skills in VantageRegistry. Optionally filter by status, team, or category. fields='lite' returns compact {_id, name, team, category, status}. fields='full' returns the complete document including content.",
440
+ {
441
+ status: skillStatusSchema.optional().describe("Filter by status \u2014 omit to list all"),
442
+ team: z.string().optional().describe("Filter by team \u2014 e.g. 'dev', 'core'"),
443
+ category: skillCategorySchema.optional().describe(
444
+ "Filter by category: capability, composite, playbook, root, or external"
445
+ ),
446
+ fields: z.enum(["lite", "full"]).optional().describe(
447
+ "Projection: 'lite' (compact) or 'full' (complete document). Omit to use legacy summary behaviour."
448
+ ),
449
+ summary: z.boolean().optional().describe(
450
+ "Return summary only (no content). Default: true \u2014 legacy, prefer fields='lite'"
451
+ )
452
+ },
453
+ async ({ status, team, category, fields, summary }) => {
454
+ const results = await convex.query(api.skills.list, {
455
+ status,
456
+ team,
1171
457
  category,
1172
- status
458
+ fields,
459
+ summary
1173
460
  });
1174
461
  return {
1175
- content: [
1176
- {
1177
- type: "text",
1178
- text: JSON.stringify({ success: true, data: result }, null, 2)
1179
- }
1180
- ]
462
+ content: [{ type: "text", text: JSON.stringify(results, null, 2) }]
1181
463
  };
1182
- } catch (err) {
464
+ }
465
+ );
466
+ s.tool(
467
+ "list_skills_by_team",
468
+ "List all skills belonging to a specific team.",
469
+ {
470
+ team: z.string().describe("Team name to filter by"),
471
+ summary: z.boolean().optional().describe("Return summary only (no content). Default: true")
472
+ },
473
+ async ({ team, summary }) => {
474
+ const results = await convex.query(api.skills.listByTeam, {
475
+ team,
476
+ summary
477
+ });
1183
478
  return {
1184
- content: [
1185
- {
1186
- type: "text",
1187
- text: JSON.stringify(
1188
- { success: false, error: String(err) },
1189
- null,
1190
- 2
1191
- )
1192
- }
1193
- ]
479
+ content: [{ type: "text", text: JSON.stringify(results, null, 2) }]
1194
480
  };
1195
481
  }
1196
- }
1197
- );
1198
- server.tool(
1199
- "list_runbooks_by_team",
1200
- "List runbooks for a specific team using the byTeam index.",
1201
- {
1202
- team: z.string().describe("Team slug to filter by \u2014 e.g. 'core', 'dev'"),
1203
- status: runbookStatusSchema.optional().describe("Filter by status \u2014 omit to list all statuses for this team")
1204
- },
1205
- async ({ team, status }) => {
1206
- try {
1207
- const result = await convex.query(api.runbooks.listRunbooksByTeam, {
1208
- team,
1209
- status
482
+ );
483
+ s.tool(
484
+ "list_skills_by_category",
485
+ "List all skills of a specific category (capability, composite, playbook, root, external).",
486
+ {
487
+ category: skillCategorySchema,
488
+ summary: z.boolean().optional().describe("Return summary only (no content). Default: true")
489
+ },
490
+ async ({ category, summary }) => {
491
+ const results = await convex.query(api.skills.listByCategory, {
492
+ category,
493
+ summary
1210
494
  });
1211
495
  return {
1212
- content: [
1213
- {
1214
- type: "text",
1215
- text: JSON.stringify({ success: true, data: result }, null, 2)
1216
- }
1217
- ]
496
+ content: [{ type: "text", text: JSON.stringify(results, null, 2) }]
1218
497
  };
1219
- } catch (err) {
498
+ }
499
+ );
500
+ s.tool(
501
+ "get_skill",
502
+ "Get a single skill by its Convex document ID.",
503
+ {
504
+ id: z.string().describe("Convex document ID for the skill")
505
+ },
506
+ async ({ id }) => {
507
+ const result = await convex.query(api.skills.get, { id });
1220
508
  return {
1221
- content: [
1222
- {
1223
- type: "text",
1224
- text: JSON.stringify(
1225
- { success: false, error: String(err) },
1226
- null,
1227
- 2
1228
- )
1229
- }
1230
- ]
509
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
1231
510
  };
1232
511
  }
1233
- }
1234
- );
1235
- server.tool(
1236
- "delete_runbook",
1237
- "Soft-delete a runbook by name \u2014 sets status to 'deprecated'. Returns {deleted: true} if found, {deleted: false} if not found.",
1238
- {
1239
- name: z.string().describe("Runbook slug to soft-delete")
1240
- },
1241
- async ({ name }) => {
1242
- try {
1243
- const result = await convex.mutation(api.runbooks.deleteRunbook, {
512
+ );
513
+ s.tool(
514
+ "get_skill_content",
515
+ "Fetch the VR-hosted canonical SKILL.md body for a skill. Returns content, sha256 hash, semver version, and last sync timestamp. Returns null fields if the skill has not been hosted in VR yet.",
516
+ {
517
+ name: z.string().describe("Skill slug \u2014 e.g. 'check-messages', 'blog-writer'")
518
+ },
519
+ async ({ name }) => {
520
+ const result = await convex.query(api.skillContentDb.getSkillContent, {
1244
521
  name
1245
522
  });
1246
523
  return {
1247
- content: [
1248
- {
1249
- type: "text",
1250
- text: JSON.stringify({ success: true, data: result }, null, 2)
1251
- }
1252
- ]
524
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
525
+ };
526
+ }
527
+ );
528
+ s.tool(
529
+ "upsert_skill_content",
530
+ "Set or update the canonical SKILL.md body in VantageRegistry. Computes sha256; if content is unchanged from the stored version the call is idempotent (version is not bumped, only contentSyncedAt is updated). If content changed, patch version is bumped (X.Y.Z \u2192 X.Y.Z+1). First write sets version to '1.0.0'. If createIfMissing=true, auto-creates a placeholder skill row if it does not exist yet. Throws if the skill does not exist and createIfMissing is not set.",
531
+ {
532
+ name: z.string().describe("Skill slug \u2014 e.g. 'check-messages'"),
533
+ content: z.string().describe("Full SKILL.md body (frontmatter + markdown)"),
534
+ createIfMissing: z.boolean().optional().default(false).describe(
535
+ "Auto-create a placeholder skill row if it does not exist (default: false)"
536
+ )
537
+ },
538
+ async ({ name, content, createIfMissing }) => {
539
+ const result = await convex.mutation(
540
+ api.skillContentDb.upsertSkillContent,
541
+ adminWriteArgs({ name, content, createIfMissing }, isAdmin)
542
+ );
543
+ return {
544
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
1253
545
  };
1254
- } catch (err) {
546
+ }
547
+ );
548
+ s.tool(
549
+ "detect_skill_drift",
550
+ "Return the VR-side sha256 hash and filePath for each skill in scope. IMPORTANT: Convex queries have no filesystem access. This tool returns the VR canonical hash + stored filePath so the caller can compute disk SHA256 and compare. scope='all' (default) returns every skill. name=<slug> filters to a single skill.",
551
+ {
552
+ name: z.string().optional().describe("Skill slug \u2014 filter to a single skill"),
553
+ scope: z.string().optional().describe("'all' to return every skill (default when name is omitted)")
554
+ },
555
+ async ({ name, scope }) => {
556
+ const result = await convex.query(api.skillContentDb.detectSkillDrift, {
557
+ name,
558
+ scope
559
+ });
1255
560
  return {
1256
- content: [
1257
- {
1258
- type: "text",
1259
- text: JSON.stringify(
1260
- { success: false, error: String(err) },
1261
- null,
1262
- 2
1263
- )
1264
- }
1265
- ]
561
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
562
+ };
563
+ }
564
+ );
565
+ s.tool(
566
+ "get_agent_content",
567
+ "Fetch the VR-hosted canonical agent content body for an agent. Returns content, sha256 hash, semver version, and last sync timestamp. Returns null fields if the agent has not been hosted in VR yet.",
568
+ {
569
+ name: z.string().describe("Agent slug \u2014 e.g. 'dev-convex-expert', 'dev-senior-dev'")
570
+ },
571
+ async ({ name }) => {
572
+ const result = await convex.query(api.agentContentDb.getAgentContent, {
573
+ name
574
+ });
575
+ return {
576
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
577
+ };
578
+ }
579
+ );
580
+ s.tool(
581
+ "upsert_agent_content",
582
+ "Set or update the canonical agent content body in VantageRegistry. Computes sha256; if content is unchanged the call is idempotent (version not bumped). If content changed, patch version is bumped (X.Y.Z \u2192 X.Y.Z+1). First write sets version to '1.0.0'. If createIfMissing=true, auto-creates a placeholder agent row if it does not exist yet. Throws if the agent does not exist and createIfMissing is not set.",
583
+ {
584
+ name: z.string().describe("Agent slug \u2014 e.g. 'dev-convex-expert'"),
585
+ content: z.string().describe("Full agent content body (AGENT.md or similar)"),
586
+ createIfMissing: z.boolean().optional().default(false).describe(
587
+ "Auto-create a placeholder agent row if it does not exist (default: false)"
588
+ )
589
+ },
590
+ async ({ name, content, createIfMissing }) => {
591
+ const result = await convex.mutation(
592
+ api.agentContentDb.upsertAgentContent,
593
+ adminWriteArgs({ name, content, createIfMissing }, isAdmin)
594
+ );
595
+ return {
596
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
597
+ };
598
+ }
599
+ );
600
+ s.tool(
601
+ "detect_agent_drift",
602
+ "Return the VR-side sha256 hash and filePath for each agent in scope. IMPORTANT: Convex queries have no filesystem access. This tool returns the VR canonical hash + stored filePath so the caller can compute disk SHA256 and compare. scope='all' (default) returns every agent. name=<slug> filters to a single agent.",
603
+ {
604
+ name: z.string().optional().describe("Agent slug \u2014 filter to a single agent"),
605
+ scope: z.string().optional().describe("'all' to return every agent (default when name is omitted)")
606
+ },
607
+ async ({ name, scope }) => {
608
+ const result = await convex.query(api.agentContentDb.detectAgentDrift, {
609
+ name,
610
+ scope
611
+ });
612
+ return {
613
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
614
+ };
615
+ }
616
+ );
617
+ s.tool(
618
+ "get_plugin_content",
619
+ "Fetch the VR-hosted canonical plugin content body for a plugin. Returns content, sha256 hash, semver version, and last sync timestamp. Returns null fields if the plugin has not been hosted in VR yet.",
620
+ {
621
+ name: z.string().describe("Plugin slug \u2014 e.g. 'perello-bootstrap', 'perello-identity'")
622
+ },
623
+ async ({ name }) => {
624
+ const result = await convex.query(api.pluginContentDb.getPluginContent, {
625
+ name
626
+ });
627
+ return {
628
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
629
+ };
630
+ }
631
+ );
632
+ s.tool(
633
+ "upsert_plugin_content",
634
+ "Set or update the canonical plugin content body in VantageRegistry. Computes sha256; if content is unchanged the call is idempotent (version not bumped). If content changed, patch version is bumped (X.Y.Z \u2192 X.Y.Z+1). First write sets version to '1.0.0'. If createIfMissing=true, auto-creates a placeholder plugin row if it does not exist yet. Throws if the plugin does not exist and createIfMissing is not set.",
635
+ {
636
+ name: z.string().describe("Plugin slug \u2014 e.g. 'perello-bootstrap'"),
637
+ content: z.string().describe("Full plugin content body"),
638
+ createIfMissing: z.boolean().optional().default(false).describe(
639
+ "Auto-create a placeholder plugin row if it does not exist (default: false)"
640
+ )
641
+ },
642
+ async ({ name, content, createIfMissing }) => {
643
+ const result = await convex.mutation(
644
+ api.pluginContentDb.upsertPluginContent,
645
+ adminWriteArgs({ name, content, createIfMissing }, isAdmin)
646
+ );
647
+ return {
648
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
649
+ };
650
+ }
651
+ );
652
+ s.tool(
653
+ "detect_plugin_drift",
654
+ "Return the VR-side sha256 hash and filePath for each plugin in scope. IMPORTANT: Convex queries have no filesystem access. This tool returns the VR canonical hash + stored filePath so the caller can compute disk SHA256 and compare. scope='all' (default) returns every plugin. name=<slug> filters to a single plugin.",
655
+ {
656
+ name: z.string().optional().describe("Plugin slug \u2014 filter to a single plugin"),
657
+ scope: z.string().optional().describe("'all' to return every plugin (default when name is omitted)")
658
+ },
659
+ async ({ name, scope }) => {
660
+ const result = await convex.query(api.pluginContentDb.detectPluginDrift, {
661
+ name,
662
+ scope
663
+ });
664
+ return {
665
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
666
+ };
667
+ }
668
+ );
669
+ s.tool(
670
+ "get_hook_content",
671
+ "Fetch the VR-hosted canonical hook content body for a hook. Returns content, sha256 hash, semver version, and last sync timestamp. Returns null fields if the hook has not been hosted in VR yet.",
672
+ {
673
+ name: z.string().describe("Hook slug \u2014 e.g. 'enforce-no-task-in-message', 'check-pii'")
674
+ },
675
+ async ({ name }) => {
676
+ const result = await convex.query(api.hookContentDb.getHookContent, {
677
+ name
678
+ });
679
+ return {
680
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
681
+ };
682
+ }
683
+ );
684
+ s.tool(
685
+ "upsert_hook_content",
686
+ "Set or update the canonical hook content body in VantageRegistry. Computes sha256; if content is unchanged the call is idempotent (version not bumped). If content changed, patch version is bumped (X.Y.Z \u2192 X.Y.Z+1). First write sets version to '1.0.0'. If createIfMissing=true, auto-creates a placeholder hook row if it does not exist yet (event defaults to 'PreToolUse' \u2014 update via upsert_hook afterward). Throws if the hook does not exist and createIfMissing is not set.",
687
+ {
688
+ name: z.string().describe("Hook slug \u2014 e.g. 'enforce-no-task-in-message'"),
689
+ content: z.string().describe("Full hook script content"),
690
+ createIfMissing: z.boolean().optional().default(false).describe(
691
+ "Auto-create a placeholder hook row if it does not exist (default: false)"
692
+ )
693
+ },
694
+ async ({ name, content, createIfMissing }) => {
695
+ const result = await convex.mutation(
696
+ api.hookContentDb.upsertHookContent,
697
+ adminWriteArgs({ name, content, createIfMissing }, isAdmin)
698
+ );
699
+ return {
700
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
701
+ };
702
+ }
703
+ );
704
+ s.tool(
705
+ "detect_hook_drift",
706
+ "Return the VR-side sha256 hash and filePath for each hook in scope. IMPORTANT: Convex queries have no filesystem access. This tool returns the VR canonical hash + stored filePath so the caller can compute disk SHA256 and compare. scope='all' (default) returns every hook. name=<slug> filters to a single hook.",
707
+ {
708
+ name: z.string().optional().describe("Hook slug \u2014 filter to a single hook"),
709
+ scope: z.string().optional().describe("'all' to return every hook (default when name is omitted)")
710
+ },
711
+ async ({ name, scope }) => {
712
+ const result = await convex.query(api.hookContentDb.detectHookDrift, {
713
+ name,
714
+ scope
715
+ });
716
+ return {
717
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
718
+ };
719
+ }
720
+ );
721
+ s.tool(
722
+ "get_command_content",
723
+ "Fetch the VR-hosted canonical command content body for a command. Returns content, sha256 hash, semver version, and last sync timestamp. Returns null fields if the command has not been hosted in VR yet.",
724
+ {
725
+ name: z.string().describe("Command slug \u2014 e.g. 'sync-workspace-from-vr', 'deploy-prod'")
726
+ },
727
+ async ({ name }) => {
728
+ const result = await convex.query(api.commandContentDb.getCommandContent, {
729
+ name
730
+ });
731
+ return {
732
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
733
+ };
734
+ }
735
+ );
736
+ s.tool(
737
+ "upsert_command_content",
738
+ "Set or update the canonical command content body in VantageRegistry. Computes sha256; if content is unchanged the call is idempotent (version not bumped). If content changed, patch version is bumped (X.Y.Z \u2192 X.Y.Z+1). First write sets version to '1.0.0'. Auto-creates the command document if it does not exist yet.",
739
+ {
740
+ name: z.string().describe("Command slug \u2014 e.g. 'sync-workspace-from-vr'"),
741
+ content: z.string().describe("Full command script content")
742
+ },
743
+ async ({ name, content }) => {
744
+ const result = await convex.mutation(
745
+ api.commandContentDb.upsertCommandContent,
746
+ adminWriteArgs({ name, content }, isAdmin)
747
+ );
748
+ return {
749
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
750
+ };
751
+ }
752
+ );
753
+ s.tool(
754
+ "detect_command_drift",
755
+ "Return the VR-side sha256 hash and filePath for each command in scope. IMPORTANT: Convex queries have no filesystem access. This tool returns the VR canonical hash + stored filePath so the caller can compute disk SHA256 and compare. scope='all' (default) returns every command. name=<slug> filters to a single command.",
756
+ {
757
+ name: z.string().optional().describe("Command slug \u2014 filter to a single command"),
758
+ scope: z.string().optional().describe("'all' to return every command (default when name is omitted)")
759
+ },
760
+ async ({ name, scope }) => {
761
+ const result = await convex.query(api.commandContentDb.detectCommandDrift, {
762
+ name,
763
+ scope
764
+ });
765
+ return {
766
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
1266
767
  };
1267
768
  }
1268
- }
1269
- );
1270
- server.tool(
1271
- "detect_runbook_drift",
1272
- "Return the VR-side sha256 hash (vrHash) plus category and version for each runbook in scope. IMPORTANT: Convex queries have no filesystem access. This tool computes sha256(runbook.content) server-side and returns it as vrHash so the caller can compute disk SHA256 and compare. name=<slug> filters to a single runbook; omit name to return every runbook (bounded to 500).",
1273
- {
1274
- name: z.string().optional().describe("Runbook slug \u2014 filter to a single runbook")
1275
- },
1276
- async ({ name }) => {
1277
- const result = await convex.query(api.runbooks.detectRunbookDrift, {
1278
- name
1279
- });
1280
- return {
1281
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
1282
- };
1283
- }
1284
- );
1285
- server.tool(
1286
- "link_runbook_template",
1287
- "Create or update a typed link between a runbook and a template in the runbook_template_links junction table. Upserts by (runbookId, templateId) pair \u2014 at-most-one link per pair. linkType controls the relationship semantics: 'uses' = template consumed during execution, 'produces' = output document generated by this runbook, 'references' = informational cross-reference. Returns {linkId, created: true} on insert or {linkId, created: false} on update.",
1288
- {
1289
- runbookId: z.string().describe("Convex document ID for the runbook"),
1290
- templateId: z.string().describe("Convex document ID for the template"),
1291
- linkType: linkTypeSchema,
1292
- usagePhase: z.string().optional().describe(
1293
- "Phase name where this template is used \u2014 kebab-case convention e.g. 'bootstrap', 'audit-phase-2'"
1294
- ),
1295
- order: z.number().optional().describe(
1296
- "Sort order for template lists \u2014 lower numbers appear first (null items sorted last)"
1297
- ),
1298
- createdBy: z.string().optional().describe("Orchestrator or person creating this link \u2014 e.g. 'omega'")
1299
- },
1300
- async (args) => {
1301
- try {
1302
- const result = await convex.mutation(
1303
- api.runbookTemplateLinks.linkRunbookTemplate,
1304
- {
1305
- runbookId: args.runbookId,
1306
- templateId: args.templateId,
1307
- linkType: args.linkType,
1308
- usagePhase: args.usagePhase,
1309
- order: args.order,
1310
- createdBy: args.createdBy
1311
- }
1312
- );
769
+ );
770
+ s.tool(
771
+ "upsert_plugin",
772
+ "Create or update a plugin in VantageRegistry. Upserts by name.",
773
+ {
774
+ name: z.string().describe("Plugin name \u2014 e.g. 'perello-bootstrap', 'perello-identity'"),
775
+ description: z.string().describe("What this plugin provides"),
776
+ agentCount: z.number().int().describe("Number of agents in this plugin"),
777
+ skillCount: z.number().int().describe("Number of skills in this plugin"),
778
+ hookCount: z.number().int().describe("Number of hooks in this plugin"),
779
+ source: pluginSourceSchema,
780
+ status: pluginStatusSchema,
781
+ version: z.string().optional().describe("Semantic version \u2014 e.g. '1.0.0'"),
782
+ filePath: z.string().optional().describe("Plugin directory path"),
783
+ pricing: z.enum(["free", "paid"]).optional().describe("Pricing tier"),
784
+ price: z.number().optional().describe("Price in EUR (if paid)"),
785
+ license: z.string().optional().describe("License \u2014 e.g. 'MIT', 'proprietary'"),
786
+ publisherId: z.string().optional().describe("Publisher identifier"),
787
+ categories: z.array(z.string()).optional().describe("Categories beyond team name")
788
+ },
789
+ async (args) => {
790
+ const id = await convex.mutation(api.plugins.upsert, adminWriteArgs(args, isAdmin));
1313
791
  return {
1314
792
  content: [
1315
793
  {
1316
794
  type: "text",
1317
- text: JSON.stringify({ success: true, data: result }, null, 2)
795
+ text: JSON.stringify({ id, name: args.name }, null, 2)
1318
796
  }
1319
797
  ]
1320
798
  };
1321
- } catch (err) {
799
+ }
800
+ );
801
+ s.tool(
802
+ "list_plugins",
803
+ "List all plugins in VantageRegistry. Optionally filter by status, source, or team. fields='lite' returns compact {_id, name, status, source}. fields='full' returns the complete document.",
804
+ {
805
+ status: pluginStatusSchema.optional().describe("Filter by status \u2014 omit to list all"),
806
+ source: pluginSourceSchema.optional().describe("Filter by source: 'internal' or 'external'"),
807
+ team: z.string().optional().describe("Filter by owning team"),
808
+ fields: z.enum(["lite", "full"]).optional().describe("Projection: 'lite' (compact) or 'full' (complete document)")
809
+ },
810
+ async ({ status, source, team, fields }) => {
811
+ const results = await convex.query(api.plugins.list, {
812
+ status,
813
+ source,
814
+ team,
815
+ fields
816
+ });
1322
817
  return {
1323
- content: [
1324
- {
1325
- type: "text",
1326
- text: JSON.stringify(
1327
- { success: false, error: String(err) },
1328
- null,
1329
- 2
1330
- )
1331
- }
1332
- ]
818
+ content: [{ type: "text", text: JSON.stringify(results, null, 2) }]
1333
819
  };
1334
820
  }
1335
- }
1336
- );
1337
- server.tool(
1338
- "unlink_runbook_template",
1339
- "Hard-delete the typed link between a runbook and a template. Links have no lifecycle state \u2014 they either exist or they don't (no soft-delete). Returns {unlinked: true} if the link was found and deleted, {unlinked: false} if the pair was not linked.",
1340
- {
1341
- runbookId: z.string().describe("Convex document ID for the runbook"),
1342
- templateId: z.string().describe("Convex document ID for the template")
1343
- },
1344
- async (args) => {
1345
- try {
1346
- const result = await convex.mutation(
1347
- api.runbookTemplateLinks.unlinkRunbookTemplate,
1348
- {
1349
- runbookId: args.runbookId,
1350
- templateId: args.templateId
1351
- }
1352
- );
821
+ );
822
+ s.tool(
823
+ "get_plugin",
824
+ "Get a single plugin by its Convex document ID.",
825
+ {
826
+ id: z.string().describe("Convex document ID for the plugin")
827
+ },
828
+ async ({ id }) => {
829
+ const result = await convex.query(api.plugins.get, { id });
1353
830
  return {
1354
- content: [
1355
- {
1356
- type: "text",
1357
- text: JSON.stringify({ success: true, data: result }, null, 2)
1358
- }
1359
- ]
831
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
1360
832
  };
1361
- } catch (err) {
833
+ }
834
+ );
835
+ s.tool(
836
+ "upsert_hook",
837
+ "Create or update a hook in VantageRegistry. Upserts by name.",
838
+ {
839
+ name: z.string().describe("Hook name \u2014 e.g. 'check-pii', 'enforce-skill-references'"),
840
+ event: hookEventSchema,
841
+ matcher: z.string().optional().describe("Tool matcher pattern \u2014 e.g. 'Bash', 'Agent'"),
842
+ description: z.string().describe("What this hook does"),
843
+ content: z.string().describe("Full hook script content"),
844
+ filePath: z.string().describe("File path relative to project root"),
845
+ registered: z.boolean().describe("Whether this hook is registered in settings.json"),
846
+ status: hookStatusSchema,
847
+ version: z.string().optional().describe("Semantic version \u2014 e.g. '1.0.0'"),
848
+ tests: z.array(z.string()).optional().describe(
849
+ "Hook test file paths \u2014 e.g. ['hooks/tests/check-pii.test.py']. Omitted \u2192 not sent (back-compat)."
850
+ )
851
+ },
852
+ async (args) => {
853
+ const id = await convex.mutation(api.hooks.upsert, adminWriteArgs(args, isAdmin));
1362
854
  return {
1363
855
  content: [
1364
856
  {
1365
857
  type: "text",
1366
858
  text: JSON.stringify(
1367
- { success: false, error: String(err) },
859
+ { id, name: args.name, event: args.event },
1368
860
  null,
1369
861
  2
1370
862
  )
@@ -1372,208 +864,215 @@ server.tool(
1372
864
  ]
1373
865
  };
1374
866
  }
1375
- }
1376
- );
1377
- server.tool(
1378
- "list_templates_for_runbook",
1379
- "List all templates linked to a runbook, with enriched template metadata. Returns enriched [{link, template}] objects sorted by link.order ASC then link.createdAt ASC. Optionally filter by linkType ('uses', 'produces', or 'references'). Orphaned links (template deleted) are automatically skipped.",
1380
- {
1381
- runbookId: z.string().describe("Convex document ID for the runbook"),
1382
- linkType: linkTypeSchema.optional().describe(
1383
- "Filter by link type \u2014 omit to return all link types for this runbook"
1384
- )
1385
- },
1386
- async (args) => {
1387
- try {
1388
- const result = await convex.query(
1389
- api.runbookTemplateLinks.listTemplatesForRunbook,
1390
- {
1391
- runbookId: args.runbookId,
1392
- linkType: args.linkType
1393
- }
1394
- );
867
+ );
868
+ s.tool(
869
+ "list_hooks",
870
+ "List all hooks in VantageRegistry. Optionally filter by status or lifecycle event. fields='lite' returns compact {_id, name, status, event, registered}. fields='full' returns the complete document including content.",
871
+ {
872
+ status: hookStatusSchema.optional().describe("Filter by status \u2014 omit to list all"),
873
+ event: hookEventSchema.optional().describe(
874
+ "Filter by lifecycle event \u2014 e.g. 'PreToolUse', 'SessionStart'"
875
+ ),
876
+ fields: z.enum(["lite", "full"]).optional().describe(
877
+ "Projection: 'lite' (compact) or 'full' (complete document with content)"
878
+ )
879
+ },
880
+ async ({ status, event, fields }) => {
881
+ const results = await convex.query(api.hooks.list, {
882
+ status,
883
+ event,
884
+ fields
885
+ });
1395
886
  return {
1396
- content: [
1397
- {
1398
- type: "text",
1399
- text: JSON.stringify({ success: true, data: result }, null, 2)
1400
- }
1401
- ]
887
+ content: [{ type: "text", text: JSON.stringify(results, null, 2) }]
1402
888
  };
1403
- } catch (err) {
889
+ }
890
+ );
891
+ s.tool(
892
+ "get_hook",
893
+ "Get a single hook by its Convex document ID.",
894
+ {
895
+ id: z.string().describe("Convex document ID for the hook")
896
+ },
897
+ async ({ id }) => {
898
+ const result = await convex.query(api.hooks.get, { id });
1404
899
  return {
1405
- content: [
1406
- {
1407
- type: "text",
1408
- text: JSON.stringify(
1409
- { success: false, error: String(err) },
1410
- null,
1411
- 2
1412
- )
1413
- }
1414
- ]
900
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
1415
901
  };
1416
902
  }
1417
- }
1418
- );
1419
- server.tool(
1420
- "list_runbooks_for_template",
1421
- "List all runbooks linked to a template (reverse lookup), with enriched runbook metadata. Symmetric reverse of list_templates_for_runbook \u2014 uses the byTemplate index. Returns enriched [{link, runbook}] objects sorted by link.order ASC then link.createdAt ASC. Optionally filter by linkType ('uses', 'produces', or 'references'). Orphaned links (runbook deleted) are automatically skipped.",
1422
- {
1423
- templateId: z.string().describe("Convex document ID for the template"),
1424
- linkType: linkTypeSchema.optional().describe(
1425
- "Filter by link type \u2014 omit to return all link types for this template"
1426
- )
1427
- },
1428
- async (args) => {
1429
- try {
1430
- const result = await convex.query(
1431
- api.runbookTemplateLinks.listRunbooksForTemplate,
1432
- {
1433
- templateId: args.templateId,
1434
- linkType: args.linkType
1435
- }
1436
- );
903
+ );
904
+ s.tool(
905
+ "upsert_prompt",
906
+ "Create or update a prompt in VantageRegistry. Upserts by name.",
907
+ {
908
+ name: z.string().describe("Prompt name \u2014 e.g. 'system-prompt', 'review-checklist'"),
909
+ team: z.string().optional().describe("Team this prompt belongs to \u2014 defaults to 'global'"),
910
+ purpose: z.string().describe("What this prompt is used for"),
911
+ content: z.string().describe("Full prompt content"),
912
+ version: z.string().optional().describe("Semantic version \u2014 e.g. '1.0.0'")
913
+ },
914
+ async (args) => {
915
+ const id = await convex.mutation(api.prompts.upsert, adminWriteArgs(args, isAdmin));
1437
916
  return {
1438
917
  content: [
1439
918
  {
1440
919
  type: "text",
1441
- text: JSON.stringify({ success: true, data: result }, null, 2)
920
+ text: JSON.stringify({ id, name: args.name }, null, 2)
1442
921
  }
1443
922
  ]
1444
923
  };
1445
- } catch (err) {
924
+ }
925
+ );
926
+ s.tool(
927
+ "list_prompts",
928
+ "List all prompts in VantageRegistry. Optionally filter by team.",
929
+ {
930
+ team: z.string().optional().describe("Filter by team \u2014 omit to list all")
931
+ },
932
+ async ({ team }) => {
933
+ const results = await convex.query(api.prompts.list, { team });
1446
934
  return {
1447
- content: [
1448
- {
1449
- type: "text",
1450
- text: JSON.stringify(
1451
- { success: false, error: String(err) },
1452
- null,
1453
- 2
1454
- )
1455
- }
1456
- ]
935
+ content: [{ type: "text", text: JSON.stringify(results, null, 2) }]
1457
936
  };
1458
937
  }
1459
- }
1460
- );
1461
- var componentKindSchema = z.enum([
1462
- "skill",
1463
- "agent",
1464
- "hook",
1465
- "plugin",
1466
- "prompt",
1467
- "runbook",
1468
- "template",
1469
- "ui-component"
1470
- ]).describe("Component kind");
1471
- var componentStatusSchema = z.enum(["active", "deprecated", "experimental"]).describe("Component status");
1472
- server.tool(
1473
- "register_component",
1474
- "Create or update a component in the VantageRegistry components catalog. Upserts by name+kind \u2014 if a component with the same name and kind exists it is updated. Use this to register any VantageOS primitive: skill, agent, hook, plugin, prompt, runbook, or template.",
1475
- {
1476
- name: z.string().describe(
1477
- "Component name slug \u2014 e.g. 'check-messages', 'dev-convex-expert'"
1478
- ),
1479
- kind: componentKindSchema,
1480
- status: componentStatusSchema,
1481
- ownerTeam: z.string().optional().describe("Team that owns this component \u2014 e.g. 'core', 'dev'"),
1482
- description: z.string().optional().describe("Short description of what this component does"),
1483
- vrEntityId: z.string().optional().describe("ID of the backing VR entity (skill _id, agent _id, etc.)"),
1484
- metadata: z.record(z.string(), z.unknown()).optional().describe("Arbitrary metadata bag \u2014 store extra fields here"),
1485
- tags: z.array(z.string()).optional().describe("Searchable tags \u2014 e.g. ['ai', 'backend']")
1486
- },
1487
- async (args) => {
1488
- try {
1489
- const id = await convex.mutation(api.components.registerComponent, args);
938
+ );
939
+ s.tool(
940
+ "get_prompt",
941
+ "Get a single prompt by its Convex document ID.",
942
+ {
943
+ id: z.string().describe("Convex document ID for the prompt")
944
+ },
945
+ async ({ id }) => {
946
+ const result = await convex.query(api.prompts.get, { id });
1490
947
  return {
1491
- content: [
1492
- {
1493
- type: "text",
1494
- text: JSON.stringify(
1495
- { id, name: args.name, kind: args.kind },
1496
- null,
1497
- 2
1498
- )
1499
- }
1500
- ]
948
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
1501
949
  };
1502
- } catch (err) {
950
+ }
951
+ );
952
+ s.tool(
953
+ "upsert_template",
954
+ "Create or update a template in VantageRegistry. Upserts by name. contentHash is auto-computed server-side (sha256 of content) when omitted.",
955
+ {
956
+ name: z.string().describe("Template name \u2014 e.g. 'agent-brief', 'skill-md'"),
957
+ team: z.string().optional().describe("Team this template belongs to \u2014 defaults to 'global'"),
958
+ purpose: z.string().describe("What this template is used for"),
959
+ content: z.string().describe("Full template content"),
960
+ version: z.string().optional().describe("Semantic version \u2014 e.g. '1.0.0'"),
961
+ template_type: templateTypeSchema.optional().describe(
962
+ "Template type: standard, mission, brief, runbook, document, or checklist"
963
+ ),
964
+ category: z.string().optional().describe("Category \u2014 e.g. 'standards/fleet-shared'"),
965
+ contentHash: z.string().optional().describe("sha256 of content \u2014 auto-computed server-side when omitted"),
966
+ sourceCommit: z.string().optional().describe("Git commit SHA the template content was sourced from"),
967
+ sourceRepo: z.string().optional().describe("Git repo the template content was sourced from")
968
+ },
969
+ async (args) => {
970
+ const id = await convex.mutation(api.templates.upsert, adminWriteArgs(args, isAdmin));
1503
971
  return {
1504
972
  content: [
1505
973
  {
1506
974
  type: "text",
1507
- text: JSON.stringify(
1508
- { success: false, error: String(err) },
1509
- null,
1510
- 2
1511
- )
975
+ text: JSON.stringify({ id, name: args.name }, null, 2)
1512
976
  }
1513
977
  ]
1514
978
  };
1515
979
  }
1516
- }
1517
- );
1518
- server.tool(
1519
- "list_components",
1520
- "List components in the VantageRegistry catalog. Supports filtering by kind, ownerTeam, status, and tags. fields='lite' (default) returns compact {_id, name, kind, status, ownerTeam}. fields='full' returns the complete document including description, tags, metadata, vrEntityId. limit defaults to 100, max 500.",
1521
- {
1522
- kind: componentKindSchema.optional().describe("Filter by kind \u2014 omit to return all kinds"),
1523
- ownerTeam: z.string().optional().describe("Filter by owning team"),
1524
- status: componentStatusSchema.optional().describe("Filter by status \u2014 omit to return all statuses"),
1525
- tags: z.array(z.string()).optional().describe("Filter by tags \u2014 ALL specified tags must match"),
1526
- fields: z.enum(["lite", "full"]).optional().describe("Projection: 'lite' (default) or 'full'"),
1527
- limit: z.number().int().optional().describe("Max results \u2014 defaults to 100, max 500")
1528
- },
1529
- async ({ kind, ownerTeam, status, tags, fields, limit }) => {
1530
- try {
1531
- const filter = kind !== void 0 || ownerTeam !== void 0 || status !== void 0 || tags !== void 0 ? { kind, ownerTeam, status, tags } : void 0;
1532
- const results = await convex.query(api.components.listComponents, {
1533
- filter,
1534
- fields,
1535
- limit
980
+ );
981
+ s.tool(
982
+ "list_templates",
983
+ "List all templates in VantageRegistry. Optionally filter by team and/or template_type.",
984
+ {
985
+ team: z.string().optional().describe("Filter by team \u2014 omit to list all"),
986
+ template_type: templateTypeSchema.optional().describe("Filter by template type: mission, document, or checklist")
987
+ },
988
+ async ({ team, template_type }) => {
989
+ const results = await convex.query(api.templates.list, {
990
+ team,
991
+ template_type
1536
992
  });
1537
993
  return {
1538
994
  content: [{ type: "text", text: JSON.stringify(results, null, 2) }]
1539
995
  };
1540
- } catch (err) {
996
+ }
997
+ );
998
+ s.tool(
999
+ "get_template",
1000
+ "Get a single template by its Convex document ID.",
1001
+ {
1002
+ id: z.string().describe("Convex document ID for the template")
1003
+ },
1004
+ async ({ id }) => {
1005
+ const result = await convex.query(api.templates.get, { id });
1541
1006
  return {
1542
- content: [
1543
- {
1544
- type: "text",
1545
- text: JSON.stringify(
1546
- { success: false, error: String(err) },
1547
- null,
1548
- 2
1549
- )
1550
- }
1551
- ]
1007
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
1552
1008
  };
1553
1009
  }
1554
- }
1555
- );
1556
- server.tool(
1557
- "get_component",
1558
- "Get a single component by its Convex document ID. Returns the full document or null if not found.",
1559
- {
1560
- id: z.string().describe("Convex document ID of the component")
1561
- },
1562
- async ({ id }) => {
1563
- try {
1564
- const result = await convex.query(api.components.getComponent, {
1565
- id
1010
+ );
1011
+ s.tool(
1012
+ "detect_template_drift",
1013
+ "Return the VR-side sha256 contentHash + provenance (sourceCommit, sourceRepo) for each template in scope. IMPORTANT: Convex queries have no filesystem access. This tool returns the VR canonical contentHash so the caller can read the source file, compute its SHA256, and compare. name=<slug> filters to a single template; omit name to return every template.",
1014
+ {
1015
+ name: z.string().optional().describe("Template slug \u2014 filter to a single template")
1016
+ },
1017
+ async ({ name }) => {
1018
+ const result = await convex.query(api.templates.detectTemplateDrift, {
1019
+ name
1020
+ });
1021
+ return {
1022
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
1023
+ };
1024
+ }
1025
+ );
1026
+ s.tool(
1027
+ "list_templates_by_category",
1028
+ "List templates for a specific category using the byCategory index.",
1029
+ {
1030
+ category: z.string().describe("Category to filter by \u2014 e.g. 'standards/fleet-shared'")
1031
+ },
1032
+ async ({ category }) => {
1033
+ const result = await convex.query(api.templates.listTemplatesByCategory, {
1034
+ category
1566
1035
  });
1567
1036
  return {
1568
1037
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
1569
1038
  };
1570
- } catch (err) {
1039
+ }
1040
+ );
1041
+ s.tool(
1042
+ "upsert_test_run",
1043
+ "Insert a skill quality test run and update denormalized quality fields on the parent skill (lastTestedAt, lastReviewerScore, lastEvalDelta, testStatus). reviewerScore format is 'X/Y' e.g. '37/45'. evalDelta is pp delta vs without-skill baseline.",
1044
+ {
1045
+ skillId: z.string().describe("Convex document ID of the skill being tested"),
1046
+ runDate: z.number().describe("Unix timestamp (ms) when the run was executed"),
1047
+ runByOrchestrator: z.string().describe("Orchestrator that ran the test \u2014 e.g. 'alpha', 'pi'"),
1048
+ runByMission: z.string().optional().describe("Mission reference \u2014 e.g. 'D70'"),
1049
+ reviewerScore: z.string().describe("Score as 'X/Y' \u2014 e.g. '37/45'"),
1050
+ evalDelta: z.number().describe("Percentage-point delta vs without-skill baseline"),
1051
+ evalAbsoluteWith: z.number().describe("Absolute score with skill (0-100)"),
1052
+ evalAbsoluteWithout: z.number().describe("Absolute score without skill (0-100)"),
1053
+ runnerModel: z.string().describe("Model used to run the skill \u2014 e.g. 'claude-sonnet-4-6'"),
1054
+ judgeModel: z.string().describe("Model used as judge \u2014 e.g. 'claude-haiku-4-5'"),
1055
+ corpusVersion: z.string().describe("Eval corpus version used \u2014 e.g. '2.0.0'"),
1056
+ harnessVersion: z.string().describe("Test harness version \u2014 e.g. '0.1.0'"),
1057
+ skillSha: z.string().describe("Git SHA of the skill file at test time"),
1058
+ gradingJsonPath: z.string().optional().describe("Path to grading output JSON"),
1059
+ benchmarkJsonPath: z.string().optional().describe("Path to benchmark output JSON"),
1060
+ reportPath: z.string().optional().describe("Path to human-readable report"),
1061
+ timeSpentMinutes: z.number().optional().describe("Time spent running the test in minutes"),
1062
+ notes: z.string().optional().describe("Free-form notes about this run"),
1063
+ testStatus: z.enum(["untested", "tested", "needs_retest", "improving", "untestable"]).optional().describe("Override test status \u2014 defaults to 'tested'")
1064
+ },
1065
+ async (args) => {
1066
+ const id = await convex.mutation(
1067
+ api.skillTestRuns.upsertTestRun,
1068
+ adminWriteArgs({ ...args, skillId: args.skillId }, isAdmin)
1069
+ );
1571
1070
  return {
1572
1071
  content: [
1573
1072
  {
1574
1073
  type: "text",
1575
1074
  text: JSON.stringify(
1576
- { success: false, error: String(err) },
1075
+ { id, skillId: args.skillId, runDate: args.runDate },
1577
1076
  null,
1578
1077
  2
1579
1078
  )
@@ -1581,75 +1080,93 @@ server.tool(
1581
1080
  ]
1582
1081
  };
1583
1082
  }
1584
- }
1585
- );
1586
- server.tool(
1587
- "search_components",
1588
- "Search components by name prefix. Returns up to `limit` lite results whose name starts with the query string. Case-sensitive index range scan. Use list_components with filters for kind/status/tags filtering.",
1589
- {
1590
- query: z.string().describe("Name prefix to search for \u2014 e.g. 'check', 'dev-'"),
1591
- limit: z.number().int().optional().describe("Max results \u2014 defaults to 50, max 200")
1592
- },
1593
- async ({ query, limit }) => {
1594
- try {
1595
- const results = await convex.query(api.components.searchComponents, {
1596
- query,
1083
+ );
1084
+ s.tool(
1085
+ "get_skill_test_history",
1086
+ "Return test run history for a skill, ordered newest-first.",
1087
+ {
1088
+ skillId: z.string().describe("Convex document ID of the skill"),
1089
+ limit: z.number().optional().describe("Max number of runs to return \u2014 defaults to 50")
1090
+ },
1091
+ async ({ skillId, limit }) => {
1092
+ const results = await convex.query(api.skillTestRuns.getSkillTestHistory, {
1093
+ skillId,
1597
1094
  limit
1598
1095
  });
1599
1096
  return {
1600
1097
  content: [{ type: "text", text: JSON.stringify(results, null, 2) }]
1601
1098
  };
1602
- } catch (err) {
1099
+ }
1100
+ );
1101
+ s.tool(
1102
+ "list_skills_by_freshness",
1103
+ "Return skills that have never been tested or were last tested more than staleDays ago. Use this to find skills that need a quality test run. Defaults to fields='lite' (bounded output) to stay under MCP token cap. Use fields='full' to retrieve complete skill documents.",
1104
+ {
1105
+ staleDays: z.number().describe("Number of days since last test run to consider stale"),
1106
+ fields: z.enum(["lite", "full"]).optional().default("lite").describe(
1107
+ "Projection: 'lite' (default, compact \u2014 bounded output) or 'full' (complete document)"
1108
+ ),
1109
+ limit: z.number().int().optional().describe("Max results \u2014 defaults to 100, max 500")
1110
+ },
1111
+ async ({ staleDays, fields, limit }) => {
1112
+ const results = await convex.query(
1113
+ api.skillTestRuns.listSkillsByFreshness,
1114
+ { staleDays, fields, limit }
1115
+ );
1603
1116
  return {
1604
- content: [
1605
- {
1606
- type: "text",
1607
- text: JSON.stringify(
1608
- { success: false, error: String(err) },
1609
- null,
1610
- 2
1611
- )
1612
- }
1613
- ]
1117
+ content: [{ type: "text", text: JSON.stringify(results, null, 2) }]
1614
1118
  };
1615
1119
  }
1616
- }
1617
- );
1618
- server.tool(
1619
- "update_component",
1620
- "Patch individual fields on an existing component. Only provided fields are updated. Throws if not found.",
1621
- {
1622
- id: z.string().describe("Convex document ID of the component to update"),
1623
- name: z.string().optional().describe("New name slug"),
1624
- kind: componentKindSchema.optional().describe("New kind"),
1625
- ownerTeam: z.string().optional().describe("New owning team"),
1626
- description: z.string().optional().describe("New description"),
1627
- status: componentStatusSchema.optional().describe("New status"),
1628
- vrEntityId: z.string().optional().describe("Updated vrEntityId reference"),
1629
- metadata: z.record(z.string(), z.unknown()).optional().describe("Updated metadata bag"),
1630
- tags: z.array(z.string()).optional().describe("Updated tags array")
1631
- },
1632
- async (args) => {
1633
- try {
1634
- await convex.mutation(api.components.updateComponent, {
1635
- ...args,
1636
- id: args.id
1637
- });
1120
+ );
1121
+ s.tool(
1122
+ "list_skills_below_threshold",
1123
+ "Return skills whose last test run was below a score ratio (scoreMin as 0-1 fraction of 'X/Y') or below a delta threshold (deltaMin in pp). Omit a param to skip that filter. fields='lite' returns compact output. fields='full' returns complete skill documents.",
1124
+ {
1125
+ scoreMin: z.number().optional().describe(
1126
+ "Minimum acceptable score ratio (0-1). Skills below this are returned."
1127
+ ),
1128
+ deltaMin: z.number().optional().describe(
1129
+ "Minimum acceptable eval delta in pp. Skills below this are returned."
1130
+ ),
1131
+ fields: z.enum(["lite", "full"]).optional().describe(
1132
+ "Projection: 'lite' (compact) or 'full' (complete document). Omit for full (backward compat)."
1133
+ )
1134
+ },
1135
+ async ({ scoreMin, deltaMin, fields }) => {
1136
+ const results = await convex.query(
1137
+ api.skillTestRuns.listSkillsBelowThreshold,
1138
+ { scoreMin, deltaMin, fields }
1139
+ );
1638
1140
  return {
1639
- content: [
1640
- {
1641
- type: "text",
1642
- text: JSON.stringify({ success: true, id: args.id }, null, 2)
1643
- }
1644
- ]
1141
+ content: [{ type: "text", text: JSON.stringify(results, null, 2) }]
1645
1142
  };
1646
- } catch (err) {
1143
+ }
1144
+ );
1145
+ s.tool(
1146
+ "upsert_skill_eval_corpus",
1147
+ "Insert or update an eval corpus version for a skill. Upserts by (skillId, version).",
1148
+ {
1149
+ skillId: z.string().describe("Convex document ID of the skill"),
1150
+ version: z.string().describe("Corpus version \u2014 e.g. '2.0.0'"),
1151
+ casesCount: z.number().int().describe("Number of eval cases"),
1152
+ assertionsCount: z.number().int().describe("Total number of assertions across all cases"),
1153
+ baselinePrompt: z.string().describe("The baseline prompt used without the skill"),
1154
+ evalsJsonContent: z.string().describe("Full JSON content of the evals file, or '{}' as placeholder"),
1155
+ authoredBy: z.string().describe("Orchestrator or person who authored this corpus"),
1156
+ authoredAt: z.number().describe("Unix timestamp (ms) when the corpus was authored"),
1157
+ supersedesVersion: z.string().optional().describe("Version this corpus supersedes \u2014 e.g. '1.0.0'")
1158
+ },
1159
+ async (args) => {
1160
+ const id = await convex.mutation(
1161
+ api.skillEvalCorpus.upsertSkillEvalCorpus,
1162
+ adminWriteArgs({ ...args, skillId: args.skillId }, isAdmin)
1163
+ );
1647
1164
  return {
1648
1165
  content: [
1649
1166
  {
1650
1167
  type: "text",
1651
1168
  text: JSON.stringify(
1652
- { success: false, error: String(err) },
1169
+ { id, skillId: args.skillId, version: args.version },
1653
1170
  null,
1654
1171
  2
1655
1172
  )
@@ -1657,106 +1174,788 @@ server.tool(
1657
1174
  ]
1658
1175
  };
1659
1176
  }
1660
- }
1661
- );
1662
- server.tool(
1663
- "delete_component",
1664
- "Soft-delete a component by ID \u2014 sets status to 'deprecated'. The document remains in the database and is still retrievable. Returns {deleted: true} if found, {deleted: false} if not found.",
1665
- {
1666
- id: z.string().describe("Convex document ID of the component to soft-delete")
1667
- },
1668
- async ({ id }) => {
1669
- try {
1670
- const result = await convex.mutation(api.components.deleteComponent, {
1671
- id
1177
+ );
1178
+ const phaseSchema = z.object({
1179
+ name: z.string().describe("Phase name"),
1180
+ description: z.string().describe("What this phase accomplishes"),
1181
+ steps: z.array(z.string()).describe("Ordered list of steps in this phase")
1182
+ });
1183
+ const runbookInputSchema = z.object({
1184
+ name: z.string().describe("Input parameter name"),
1185
+ type: z.string().describe("Input type \u2014 e.g. 'string', 'boolean'"),
1186
+ required: z.boolean().describe("Whether this input is required"),
1187
+ description: z.string().describe("What this input controls")
1188
+ });
1189
+ const runbookOutputSchema = z.object({
1190
+ name: z.string().describe("Output artifact name"),
1191
+ description: z.string().describe("What this output contains"),
1192
+ path_pattern: z.string().optional().describe("File path pattern \u2014 e.g. 'reports/{name}.md'")
1193
+ });
1194
+ const applicabilitySchema = z.object({
1195
+ orchestrators: z.array(z.string()).describe("Orchestrator slugs this runbook applies to"),
1196
+ business_units: z.array(z.string()).describe("Business unit slugs this runbook applies to"),
1197
+ use_cases: z.array(z.string()).describe("Use case slugs this runbook applies to")
1198
+ });
1199
+ const linkedTemplateSchema = z.object({
1200
+ name: z.string().describe("Template name"),
1201
+ template_type: z.string().describe("Template type \u2014 mission, document, or checklist"),
1202
+ version: z.string().describe("Template version \u2014 e.g. '1.0.0'"),
1203
+ usage_phase: z.string().describe("Phase name where this template is used"),
1204
+ required: z.boolean().describe("Whether this template is required for the runbook"),
1205
+ description: z.string().describe("How this template is used in this runbook")
1206
+ });
1207
+ s.tool(
1208
+ "upsert_runbook",
1209
+ "Create or update a runbook in VantageRegistry. Upserts by name \u2014 if a runbook with the same name exists, it is updated.",
1210
+ {
1211
+ name: z.string().describe("Runbook slug \u2014 e.g. 'deploy-production', 'onboard-agent'"),
1212
+ description: z.string().describe("What this runbook accomplishes"),
1213
+ version: z.string().describe("Semantic version \u2014 e.g. '1.0.0'"),
1214
+ status: runbookStatusSchema,
1215
+ category: z.string().describe("Category \u2014 e.g. 'deployment', 'onboarding', 'quality'"),
1216
+ tags: z.array(z.string()).describe("Searchable tags"),
1217
+ content: z.string().describe("Full runbook content (markdown)"),
1218
+ phases: z.array(phaseSchema).describe("Ordered phases of the runbook"),
1219
+ inputs: z.array(runbookInputSchema).describe("Input parameters for the runbook"),
1220
+ outputs: z.array(runbookOutputSchema).describe("Output artifacts produced by the runbook"),
1221
+ applicability: applicabilitySchema,
1222
+ author: z.string().describe("Author slug \u2014 e.g. 'omega'"),
1223
+ team: z.string().describe("Owning team slug \u2014 e.g. 'core', 'dev'"),
1224
+ related_skills: z.array(z.string()).describe("Skill slugs referenced by this runbook"),
1225
+ related_agents: z.array(z.string()).describe("Agent slugs referenced by this runbook"),
1226
+ linked_templates: z.array(linkedTemplateSchema).describe("Templates used during this runbook")
1227
+ },
1228
+ async (args) => {
1229
+ try {
1230
+ const result = await convex.mutation(api.runbooks.upsertRunbook, adminWriteArgs(args, isAdmin));
1231
+ return {
1232
+ content: [
1233
+ {
1234
+ type: "text",
1235
+ text: JSON.stringify({ success: true, data: result }, null, 2)
1236
+ }
1237
+ ]
1238
+ };
1239
+ } catch (err) {
1240
+ return {
1241
+ content: [
1242
+ {
1243
+ type: "text",
1244
+ text: JSON.stringify(
1245
+ { success: false, error: String(err) },
1246
+ null,
1247
+ 2
1248
+ )
1249
+ }
1250
+ ]
1251
+ };
1252
+ }
1253
+ }
1254
+ );
1255
+ s.tool(
1256
+ "get_runbook",
1257
+ "Get a single runbook by name. Optionally guard by exact version.",
1258
+ {
1259
+ name: z.string().describe("Runbook slug \u2014 e.g. 'deploy-production'"),
1260
+ version: z.string().optional().describe("If provided, returns null when stored version differs")
1261
+ },
1262
+ async ({ name, version }) => {
1263
+ try {
1264
+ const result = await convex.query(api.runbooks.getRunbook, {
1265
+ name,
1266
+ version
1267
+ });
1268
+ return {
1269
+ content: [
1270
+ {
1271
+ type: "text",
1272
+ text: JSON.stringify({ success: true, data: result }, null, 2)
1273
+ }
1274
+ ]
1275
+ };
1276
+ } catch (err) {
1277
+ return {
1278
+ content: [
1279
+ {
1280
+ type: "text",
1281
+ text: JSON.stringify(
1282
+ { success: false, error: String(err) },
1283
+ null,
1284
+ 2
1285
+ )
1286
+ }
1287
+ ]
1288
+ };
1289
+ }
1290
+ }
1291
+ );
1292
+ s.tool(
1293
+ "list_runbooks",
1294
+ "List runbooks in VantageRegistry. Defaults to published status. Supports applicability filters.",
1295
+ {
1296
+ status: runbookStatusSchema.optional().describe("Filter by status \u2014 defaults to 'published'"),
1297
+ category: z.string().optional().describe("Filter by category \u2014 e.g. 'deployment'"),
1298
+ team: z.string().optional().describe("Filter by owning team"),
1299
+ applicability_orchestrator: z.string().optional().describe("Filter by orchestrator slug in applicability"),
1300
+ applicability_bu: z.string().optional().describe("Filter by business unit slug in applicability"),
1301
+ applicability_use_case: z.string().optional().describe("Filter by use case slug in applicability"),
1302
+ limit: z.number().int().optional().describe("Max results to return \u2014 defaults to 50, max 200")
1303
+ },
1304
+ async (args) => {
1305
+ try {
1306
+ const result = await convex.query(api.runbooks.listRunbooks, args);
1307
+ return {
1308
+ content: [
1309
+ {
1310
+ type: "text",
1311
+ text: JSON.stringify({ success: true, data: result }, null, 2)
1312
+ }
1313
+ ]
1314
+ };
1315
+ } catch (err) {
1316
+ return {
1317
+ content: [
1318
+ {
1319
+ type: "text",
1320
+ text: JSON.stringify(
1321
+ { success: false, error: String(err) },
1322
+ null,
1323
+ 2
1324
+ )
1325
+ }
1326
+ ]
1327
+ };
1328
+ }
1329
+ }
1330
+ );
1331
+ s.tool(
1332
+ "list_runbooks_by_category",
1333
+ "List runbooks for a specific category using the byCategory index.",
1334
+ {
1335
+ category: z.string().describe("Category to filter by \u2014 e.g. 'deployment', 'quality'"),
1336
+ status: runbookStatusSchema.optional().describe(
1337
+ "Filter by status \u2014 omit to list all statuses in this category"
1338
+ )
1339
+ },
1340
+ async ({ category, status }) => {
1341
+ try {
1342
+ const result = await convex.query(api.runbooks.listRunbooksByCategory, {
1343
+ category,
1344
+ status
1345
+ });
1346
+ return {
1347
+ content: [
1348
+ {
1349
+ type: "text",
1350
+ text: JSON.stringify({ success: true, data: result }, null, 2)
1351
+ }
1352
+ ]
1353
+ };
1354
+ } catch (err) {
1355
+ return {
1356
+ content: [
1357
+ {
1358
+ type: "text",
1359
+ text: JSON.stringify(
1360
+ { success: false, error: String(err) },
1361
+ null,
1362
+ 2
1363
+ )
1364
+ }
1365
+ ]
1366
+ };
1367
+ }
1368
+ }
1369
+ );
1370
+ s.tool(
1371
+ "list_runbooks_by_team",
1372
+ "List runbooks for a specific team using the byTeam index.",
1373
+ {
1374
+ team: z.string().describe("Team slug to filter by \u2014 e.g. 'core', 'dev'"),
1375
+ status: runbookStatusSchema.optional().describe("Filter by status \u2014 omit to list all statuses for this team")
1376
+ },
1377
+ async ({ team, status }) => {
1378
+ try {
1379
+ const result = await convex.query(api.runbooks.listRunbooksByTeam, {
1380
+ team,
1381
+ status
1382
+ });
1383
+ return {
1384
+ content: [
1385
+ {
1386
+ type: "text",
1387
+ text: JSON.stringify({ success: true, data: result }, null, 2)
1388
+ }
1389
+ ]
1390
+ };
1391
+ } catch (err) {
1392
+ return {
1393
+ content: [
1394
+ {
1395
+ type: "text",
1396
+ text: JSON.stringify(
1397
+ { success: false, error: String(err) },
1398
+ null,
1399
+ 2
1400
+ )
1401
+ }
1402
+ ]
1403
+ };
1404
+ }
1405
+ }
1406
+ );
1407
+ s.tool(
1408
+ "delete_runbook",
1409
+ "Soft-delete a runbook by name \u2014 sets status to 'deprecated'. Returns {deleted: true} if found, {deleted: false} if not found.",
1410
+ {
1411
+ name: z.string().describe("Runbook slug to soft-delete")
1412
+ },
1413
+ async ({ name }) => {
1414
+ try {
1415
+ const result = await convex.mutation(
1416
+ api.runbooks.deleteRunbook,
1417
+ adminWriteArgs({ name }, isAdmin)
1418
+ );
1419
+ return {
1420
+ content: [
1421
+ {
1422
+ type: "text",
1423
+ text: JSON.stringify({ success: true, data: result }, null, 2)
1424
+ }
1425
+ ]
1426
+ };
1427
+ } catch (err) {
1428
+ return {
1429
+ content: [
1430
+ {
1431
+ type: "text",
1432
+ text: JSON.stringify(
1433
+ { success: false, error: String(err) },
1434
+ null,
1435
+ 2
1436
+ )
1437
+ }
1438
+ ]
1439
+ };
1440
+ }
1441
+ }
1442
+ );
1443
+ s.tool(
1444
+ "detect_runbook_drift",
1445
+ "Return the VR-side sha256 hash (vrHash) plus category and version for each runbook in scope. IMPORTANT: Convex queries have no filesystem access. This tool computes sha256(runbook.content) server-side and returns it as vrHash so the caller can compute disk SHA256 and compare. name=<slug> filters to a single runbook; omit name to return every runbook (bounded to 500).",
1446
+ {
1447
+ name: z.string().optional().describe("Runbook slug \u2014 filter to a single runbook")
1448
+ },
1449
+ async ({ name }) => {
1450
+ const result = await convex.query(api.runbooks.detectRunbookDrift, {
1451
+ name
1672
1452
  });
1673
1453
  return {
1674
- content: [
1454
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
1455
+ };
1456
+ }
1457
+ );
1458
+ s.tool(
1459
+ "link_runbook_template",
1460
+ "Create or update a typed link between a runbook and a template in the runbook_template_links junction table. Upserts by (runbookId, templateId) pair \u2014 at-most-one link per pair. linkType controls the relationship semantics: 'uses' = template consumed during execution, 'produces' = output document generated by this runbook, 'references' = informational cross-reference. Returns {linkId, created: true} on insert or {linkId, created: false} on update.",
1461
+ {
1462
+ runbookId: z.string().describe("Convex document ID for the runbook"),
1463
+ templateId: z.string().describe("Convex document ID for the template"),
1464
+ linkType: linkTypeSchema,
1465
+ usagePhase: z.string().optional().describe(
1466
+ "Phase name where this template is used \u2014 kebab-case convention e.g. 'bootstrap', 'audit-phase-2'"
1467
+ ),
1468
+ order: z.number().optional().describe(
1469
+ "Sort order for template lists \u2014 lower numbers appear first (null items sorted last)"
1470
+ ),
1471
+ createdBy: z.string().optional().describe("Orchestrator or person creating this link \u2014 e.g. 'omega'")
1472
+ },
1473
+ async (args) => {
1474
+ try {
1475
+ const result = await convex.mutation(
1476
+ api.runbookTemplateLinks.linkRunbookTemplate,
1477
+ adminWriteArgs(
1478
+ {
1479
+ runbookId: args.runbookId,
1480
+ templateId: args.templateId,
1481
+ linkType: args.linkType,
1482
+ usagePhase: args.usagePhase,
1483
+ order: args.order,
1484
+ createdBy: args.createdBy
1485
+ },
1486
+ isAdmin
1487
+ )
1488
+ );
1489
+ return {
1490
+ content: [
1491
+ {
1492
+ type: "text",
1493
+ text: JSON.stringify({ success: true, data: result }, null, 2)
1494
+ }
1495
+ ]
1496
+ };
1497
+ } catch (err) {
1498
+ return {
1499
+ content: [
1500
+ {
1501
+ type: "text",
1502
+ text: JSON.stringify(
1503
+ { success: false, error: String(err) },
1504
+ null,
1505
+ 2
1506
+ )
1507
+ }
1508
+ ]
1509
+ };
1510
+ }
1511
+ }
1512
+ );
1513
+ s.tool(
1514
+ "unlink_runbook_template",
1515
+ "Hard-delete the typed link between a runbook and a template. Links have no lifecycle state \u2014 they either exist or they don't (no soft-delete). Returns {unlinked: true} if the link was found and deleted, {unlinked: false} if the pair was not linked.",
1516
+ {
1517
+ runbookId: z.string().describe("Convex document ID for the runbook"),
1518
+ templateId: z.string().describe("Convex document ID for the template")
1519
+ },
1520
+ async (args) => {
1521
+ try {
1522
+ const result = await convex.mutation(
1523
+ api.runbookTemplateLinks.unlinkRunbookTemplate,
1524
+ adminWriteArgs(
1525
+ {
1526
+ runbookId: args.runbookId,
1527
+ templateId: args.templateId
1528
+ },
1529
+ isAdmin
1530
+ )
1531
+ );
1532
+ return {
1533
+ content: [
1534
+ {
1535
+ type: "text",
1536
+ text: JSON.stringify({ success: true, data: result }, null, 2)
1537
+ }
1538
+ ]
1539
+ };
1540
+ } catch (err) {
1541
+ return {
1542
+ content: [
1543
+ {
1544
+ type: "text",
1545
+ text: JSON.stringify(
1546
+ { success: false, error: String(err) },
1547
+ null,
1548
+ 2
1549
+ )
1550
+ }
1551
+ ]
1552
+ };
1553
+ }
1554
+ }
1555
+ );
1556
+ s.tool(
1557
+ "list_templates_for_runbook",
1558
+ "List all templates linked to a runbook, with enriched template metadata. Returns enriched [{link, template}] objects sorted by link.order ASC then link.createdAt ASC. Optionally filter by linkType ('uses', 'produces', or 'references'). Orphaned links (template deleted) are automatically skipped.",
1559
+ {
1560
+ runbookId: z.string().describe("Convex document ID for the runbook"),
1561
+ linkType: linkTypeSchema.optional().describe(
1562
+ "Filter by link type \u2014 omit to return all link types for this runbook"
1563
+ )
1564
+ },
1565
+ async (args) => {
1566
+ try {
1567
+ const result = await convex.query(
1568
+ api.runbookTemplateLinks.listTemplatesForRunbook,
1675
1569
  {
1676
- type: "text",
1677
- text: JSON.stringify({ success: true, ...result }, null, 2)
1570
+ runbookId: args.runbookId,
1571
+ linkType: args.linkType
1678
1572
  }
1679
- ]
1680
- };
1681
- } catch (err) {
1682
- return {
1683
- content: [
1573
+ );
1574
+ return {
1575
+ content: [
1576
+ {
1577
+ type: "text",
1578
+ text: JSON.stringify({ success: true, data: result }, null, 2)
1579
+ }
1580
+ ]
1581
+ };
1582
+ } catch (err) {
1583
+ return {
1584
+ content: [
1585
+ {
1586
+ type: "text",
1587
+ text: JSON.stringify(
1588
+ { success: false, error: String(err) },
1589
+ null,
1590
+ 2
1591
+ )
1592
+ }
1593
+ ]
1594
+ };
1595
+ }
1596
+ }
1597
+ );
1598
+ s.tool(
1599
+ "list_runbooks_for_template",
1600
+ "List all runbooks linked to a template (reverse lookup), with enriched runbook metadata. Symmetric reverse of list_templates_for_runbook \u2014 uses the byTemplate index. Returns enriched [{link, runbook}] objects sorted by link.order ASC then link.createdAt ASC. Optionally filter by linkType ('uses', 'produces', or 'references'). Orphaned links (runbook deleted) are automatically skipped.",
1601
+ {
1602
+ templateId: z.string().describe("Convex document ID for the template"),
1603
+ linkType: linkTypeSchema.optional().describe(
1604
+ "Filter by link type \u2014 omit to return all link types for this template"
1605
+ )
1606
+ },
1607
+ async (args) => {
1608
+ try {
1609
+ const result = await convex.query(
1610
+ api.runbookTemplateLinks.listRunbooksForTemplate,
1684
1611
  {
1685
- type: "text",
1686
- text: JSON.stringify(
1687
- { success: false, error: String(err) },
1688
- null,
1689
- 2
1690
- )
1612
+ templateId: args.templateId,
1613
+ linkType: args.linkType
1691
1614
  }
1692
- ]
1693
- };
1615
+ );
1616
+ return {
1617
+ content: [
1618
+ {
1619
+ type: "text",
1620
+ text: JSON.stringify({ success: true, data: result }, null, 2)
1621
+ }
1622
+ ]
1623
+ };
1624
+ } catch (err) {
1625
+ return {
1626
+ content: [
1627
+ {
1628
+ type: "text",
1629
+ text: JSON.stringify(
1630
+ { success: false, error: String(err) },
1631
+ null,
1632
+ 2
1633
+ )
1634
+ }
1635
+ ]
1636
+ };
1637
+ }
1694
1638
  }
1695
- }
1696
- );
1697
- server.tool(
1698
- "get_stats",
1699
- "Get counts per table in VantageRegistry \u2014 teams, agents, skills, plugins, hooks, and runbooks. Runbooks include breakdown by_status (draft/published/deprecated) and by_category. Use this to get a quick overview of the registry contents.",
1700
- {},
1701
- async () => {
1702
- const [
1703
- teams,
1704
- agents,
1705
- skills,
1706
- plugins,
1707
- hooks,
1708
- rbDraft,
1709
- rbPublished,
1710
- rbDeprecated
1711
- ] = await Promise.all([
1712
- convex.query(api.teams.list, {}),
1713
- convex.query(api.agents.list, {}),
1714
- convex.query(api.skills.list, {}),
1715
- convex.query(api.plugins.list, {}),
1716
- convex.query(api.hooks.list, {}),
1717
- convex.query(api.runbooks.listRunbooks, { status: "draft", limit: 1e3 }),
1718
- convex.query(api.runbooks.listRunbooks, {
1719
- status: "published",
1720
- limit: 1e3
1721
- }),
1722
- convex.query(api.runbooks.listRunbooks, {
1723
- status: "deprecated",
1724
- limit: 1e3
1725
- })
1726
- ]);
1727
- const byCategoryMap = {};
1728
- for (const rb of [...rbDraft, ...rbPublished, ...rbDeprecated]) {
1729
- byCategoryMap[rb.category] = (byCategoryMap[rb.category] ?? 0) + 1;
1730
- }
1731
- const runbooksTotal = rbDraft.length + rbPublished.length + rbDeprecated.length;
1732
- const stats = {
1733
- teams: teams.length,
1734
- agents: agents.length,
1735
- skills: skills.length,
1736
- plugins: plugins.length,
1737
- hooks: hooks.length,
1738
- runbooks_count: {
1739
- total: runbooksTotal,
1740
- by_status: {
1741
- draft: rbDraft.length,
1742
- published: rbPublished.length,
1743
- deprecated: rbDeprecated.length
1639
+ );
1640
+ const componentKindSchema = z.enum([
1641
+ "skill",
1642
+ "agent",
1643
+ "hook",
1644
+ "plugin",
1645
+ "prompt",
1646
+ "runbook",
1647
+ "template",
1648
+ "ui-component"
1649
+ ]).describe("Component kind");
1650
+ const componentStatusSchema = z.enum(["active", "deprecated", "experimental"]).describe("Component status");
1651
+ s.tool(
1652
+ "register_component",
1653
+ "Create or update a component in the VantageRegistry components catalog. Upserts by name+kind \u2014 if a component with the same name and kind exists it is updated. Use this to register any VantageOS primitive: skill, agent, hook, plugin, prompt, runbook, or template.",
1654
+ {
1655
+ name: z.string().describe(
1656
+ "Component name slug \u2014 e.g. 'check-messages', 'dev-convex-expert'"
1657
+ ),
1658
+ kind: componentKindSchema,
1659
+ status: componentStatusSchema,
1660
+ ownerTeam: z.string().optional().describe("Team that owns this component \u2014 e.g. 'core', 'dev'"),
1661
+ description: z.string().optional().describe("Short description of what this component does"),
1662
+ vrEntityId: z.string().optional().describe("ID of the backing VR entity (skill _id, agent _id, etc.)"),
1663
+ metadata: z.record(z.string(), z.unknown()).optional().describe("Arbitrary metadata bag \u2014 store extra fields here"),
1664
+ tags: z.array(z.string()).optional().describe("Searchable tags \u2014 e.g. ['ai', 'backend']")
1665
+ },
1666
+ async (args) => {
1667
+ try {
1668
+ const id = await convex.mutation(api.components.registerComponent, adminWriteArgs(args, isAdmin));
1669
+ return {
1670
+ content: [
1671
+ {
1672
+ type: "text",
1673
+ text: JSON.stringify(
1674
+ { id, name: args.name, kind: args.kind },
1675
+ null,
1676
+ 2
1677
+ )
1678
+ }
1679
+ ]
1680
+ };
1681
+ } catch (err) {
1682
+ return {
1683
+ content: [
1684
+ {
1685
+ type: "text",
1686
+ text: JSON.stringify(
1687
+ { success: false, error: String(err) },
1688
+ null,
1689
+ 2
1690
+ )
1691
+ }
1692
+ ]
1693
+ };
1694
+ }
1695
+ }
1696
+ );
1697
+ s.tool(
1698
+ "list_components",
1699
+ "List components in the VantageRegistry catalog. Supports filtering by kind, ownerTeam, status, and tags. fields='lite' (default) returns compact {_id, name, kind, status, ownerTeam}. fields='full' returns the complete document including description, tags, metadata, vrEntityId. limit defaults to 100, max 500.",
1700
+ {
1701
+ kind: componentKindSchema.optional().describe("Filter by kind \u2014 omit to return all kinds"),
1702
+ ownerTeam: z.string().optional().describe("Filter by owning team"),
1703
+ status: componentStatusSchema.optional().describe("Filter by status \u2014 omit to return all statuses"),
1704
+ tags: z.array(z.string()).optional().describe("Filter by tags \u2014 ALL specified tags must match"),
1705
+ fields: z.enum(["lite", "full"]).optional().describe("Projection: 'lite' (default) or 'full'"),
1706
+ limit: z.number().int().optional().describe("Max results \u2014 defaults to 100, max 500")
1707
+ },
1708
+ async ({ kind, ownerTeam, status, tags, fields, limit }) => {
1709
+ try {
1710
+ const filter = kind !== void 0 || ownerTeam !== void 0 || status !== void 0 || tags !== void 0 ? { kind, ownerTeam, status, tags } : void 0;
1711
+ const results = await convex.query(api.components.listComponents, {
1712
+ filter,
1713
+ fields,
1714
+ limit
1715
+ });
1716
+ return {
1717
+ content: [{ type: "text", text: JSON.stringify(results, null, 2) }]
1718
+ };
1719
+ } catch (err) {
1720
+ return {
1721
+ content: [
1722
+ {
1723
+ type: "text",
1724
+ text: JSON.stringify(
1725
+ { success: false, error: String(err) },
1726
+ null,
1727
+ 2
1728
+ )
1729
+ }
1730
+ ]
1731
+ };
1732
+ }
1733
+ }
1734
+ );
1735
+ s.tool(
1736
+ "get_component",
1737
+ "Get a single component by its Convex document ID. Returns the full document or null if not found.",
1738
+ {
1739
+ id: z.string().describe("Convex document ID of the component")
1740
+ },
1741
+ async ({ id }) => {
1742
+ try {
1743
+ const result = await convex.query(api.components.getComponent, {
1744
+ id
1745
+ });
1746
+ return {
1747
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
1748
+ };
1749
+ } catch (err) {
1750
+ return {
1751
+ content: [
1752
+ {
1753
+ type: "text",
1754
+ text: JSON.stringify(
1755
+ { success: false, error: String(err) },
1756
+ null,
1757
+ 2
1758
+ )
1759
+ }
1760
+ ]
1761
+ };
1762
+ }
1763
+ }
1764
+ );
1765
+ s.tool(
1766
+ "search_components",
1767
+ "Search components by name prefix. Returns up to `limit` lite results whose name starts with the query string. Case-sensitive index range scan. Use list_components with filters for kind/status/tags filtering.",
1768
+ {
1769
+ query: z.string().describe("Name prefix to search for \u2014 e.g. 'check', 'dev-'"),
1770
+ limit: z.number().int().optional().describe("Max results \u2014 defaults to 50, max 200")
1771
+ },
1772
+ async ({ query, limit }) => {
1773
+ try {
1774
+ const results = await convex.query(api.components.searchComponents, {
1775
+ query,
1776
+ limit
1777
+ });
1778
+ return {
1779
+ content: [{ type: "text", text: JSON.stringify(results, null, 2) }]
1780
+ };
1781
+ } catch (err) {
1782
+ return {
1783
+ content: [
1784
+ {
1785
+ type: "text",
1786
+ text: JSON.stringify(
1787
+ { success: false, error: String(err) },
1788
+ null,
1789
+ 2
1790
+ )
1791
+ }
1792
+ ]
1793
+ };
1794
+ }
1795
+ }
1796
+ );
1797
+ s.tool(
1798
+ "update_component",
1799
+ "Patch individual fields on an existing component. Only provided fields are updated. Throws if not found.",
1800
+ {
1801
+ id: z.string().describe("Convex document ID of the component to update"),
1802
+ name: z.string().optional().describe("New name slug"),
1803
+ kind: componentKindSchema.optional().describe("New kind"),
1804
+ ownerTeam: z.string().optional().describe("New owning team"),
1805
+ description: z.string().optional().describe("New description"),
1806
+ status: componentStatusSchema.optional().describe("New status"),
1807
+ vrEntityId: z.string().optional().describe("Updated vrEntityId reference"),
1808
+ metadata: z.record(z.string(), z.unknown()).optional().describe("Updated metadata bag"),
1809
+ tags: z.array(z.string()).optional().describe("Updated tags array")
1810
+ },
1811
+ async (args) => {
1812
+ try {
1813
+ await convex.mutation(
1814
+ api.components.updateComponent,
1815
+ adminWriteArgs({ ...args, id: args.id }, isAdmin)
1816
+ );
1817
+ return {
1818
+ content: [
1819
+ {
1820
+ type: "text",
1821
+ text: JSON.stringify({ success: true, id: args.id }, null, 2)
1822
+ }
1823
+ ]
1824
+ };
1825
+ } catch (err) {
1826
+ return {
1827
+ content: [
1828
+ {
1829
+ type: "text",
1830
+ text: JSON.stringify(
1831
+ { success: false, error: String(err) },
1832
+ null,
1833
+ 2
1834
+ )
1835
+ }
1836
+ ]
1837
+ };
1838
+ }
1839
+ }
1840
+ );
1841
+ s.tool(
1842
+ "delete_component",
1843
+ "Soft-delete a component by ID \u2014 sets status to 'deprecated'. The document remains in the database and is still retrievable. Returns {deleted: true} if found, {deleted: false} if not found.",
1844
+ {
1845
+ id: z.string().describe("Convex document ID of the component to soft-delete")
1846
+ },
1847
+ async ({ id }) => {
1848
+ try {
1849
+ const result = await convex.mutation(
1850
+ api.components.deleteComponent,
1851
+ adminWriteArgs({ id }, isAdmin)
1852
+ );
1853
+ return {
1854
+ content: [
1855
+ {
1856
+ type: "text",
1857
+ text: JSON.stringify({ success: true, ...result }, null, 2)
1858
+ }
1859
+ ]
1860
+ };
1861
+ } catch (err) {
1862
+ return {
1863
+ content: [
1864
+ {
1865
+ type: "text",
1866
+ text: JSON.stringify(
1867
+ { success: false, error: String(err) },
1868
+ null,
1869
+ 2
1870
+ )
1871
+ }
1872
+ ]
1873
+ };
1874
+ }
1875
+ }
1876
+ );
1877
+ s.tool(
1878
+ "get_stats",
1879
+ "Get counts per table in VantageRegistry \u2014 teams, agents, skills, plugins, hooks, and runbooks. Runbooks include breakdown by_status (draft/published/deprecated) and by_category. Use this to get a quick overview of the registry contents.",
1880
+ {},
1881
+ async () => {
1882
+ const [
1883
+ teams,
1884
+ agents,
1885
+ skills,
1886
+ plugins,
1887
+ hooks,
1888
+ rbDraft,
1889
+ rbPublished,
1890
+ rbDeprecated
1891
+ ] = await Promise.all([
1892
+ convex.query(api.teams.list, {}),
1893
+ convex.query(api.agents.list, {}),
1894
+ convex.query(api.skills.list, {}),
1895
+ convex.query(api.plugins.list, {}),
1896
+ convex.query(api.hooks.list, {}),
1897
+ convex.query(api.runbooks.listRunbooks, { status: "draft", limit: 1e3 }),
1898
+ convex.query(api.runbooks.listRunbooks, {
1899
+ status: "published",
1900
+ limit: 1e3
1901
+ }),
1902
+ convex.query(api.runbooks.listRunbooks, {
1903
+ status: "deprecated",
1904
+ limit: 1e3
1905
+ })
1906
+ ]);
1907
+ const byCategoryMap = {};
1908
+ for (const rb of [...rbDraft, ...rbPublished, ...rbDeprecated]) {
1909
+ byCategoryMap[rb.category] = (byCategoryMap[rb.category] ?? 0) + 1;
1910
+ }
1911
+ const runbooksTotal = rbDraft.length + rbPublished.length + rbDeprecated.length;
1912
+ const stats = {
1913
+ teams: teams.length,
1914
+ agents: agents.length,
1915
+ skills: skills.length,
1916
+ plugins: plugins.length,
1917
+ hooks: hooks.length,
1918
+ runbooks_count: {
1919
+ total: runbooksTotal,
1920
+ by_status: {
1921
+ draft: rbDraft.length,
1922
+ published: rbPublished.length,
1923
+ deprecated: rbDeprecated.length
1924
+ },
1925
+ by_category: byCategoryMap
1744
1926
  },
1745
- by_category: byCategoryMap
1746
- },
1747
- total: teams.length + agents.length + skills.length + plugins.length + hooks.length + runbooksTotal
1748
- };
1749
- return {
1750
- content: [{ type: "text", text: JSON.stringify(stats, null, 2) }]
1751
- };
1752
- }
1753
- );
1927
+ total: teams.length + agents.length + skills.length + plugins.length + hooks.length + runbooksTotal
1928
+ };
1929
+ return {
1930
+ content: [{ type: "text", text: JSON.stringify(stats, null, 2) }]
1931
+ };
1932
+ }
1933
+ );
1934
+ }
1935
+ registerAllTools(server, true);
1936
+ var PORT = Number(process.env.PORT ?? 3e3);
1754
1937
  async function main() {
1755
- const transport = new StdioServerTransport();
1756
- await server.connect(transport);
1757
- console.error("VantageRegistry MCP server running on stdio");
1938
+ if (process.env.MCP_TRANSPORT === "http") {
1939
+ const instance = await startHttpServer(PORT);
1940
+ const addr = instance.address();
1941
+ console.error(
1942
+ `VantageRegistry MCP server running on HTTP port ${addr.port}`
1943
+ );
1944
+ console.error(`Health: http://0.0.0.0:${addr.port}/health`);
1945
+ console.error(`MCP: http://0.0.0.0:${addr.port}/mcp`);
1946
+ } else {
1947
+ const transport = new StdioServerTransport();
1948
+ await server.connect(transport);
1949
+ console.error("VantageRegistry MCP server running on stdio");
1950
+ }
1758
1951
  }
1759
1952
  main().catch((err) => {
1760
1953
  console.error("Fatal:", err);
1761
1954
  process.exit(1);
1762
1955
  });
1956
+ export {
1957
+ WRITE_TOOLS,
1958
+ adminWriteArgs,
1959
+ createHttpApp,
1960
+ startHttpServer
1961
+ };