@vantageos/vantage-registry-mcp 1.2.0 → 1.5.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 (3) hide show
  1. package/package.json +5 -3
  2. package/server.js +1732 -0
  3. package/server.ts +1094 -184
package/server.js ADDED
@@ -0,0 +1,1732 @@
1
+ #!/usr/bin/env bun
2
+
3
+ // server.ts
4
+ import { readFileSync } from "node:fs";
5
+ import { resolve } from "node:path";
6
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
7
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
8
+ import { ConvexHttpClient } from "convex/browser";
9
+ import { z } from "zod";
10
+
11
+ // ../convex/_generated/api.js
12
+ import { anyApi, componentsGeneric } from "convex/server";
13
+ var api = anyApi;
14
+ var components = componentsGeneric();
15
+
16
+ // server.ts
17
+ function loadConvexUrl() {
18
+ if (process.env.CONVEX_URL) {
19
+ return process.env.CONVEX_URL;
20
+ }
21
+ const envPath = resolve(import.meta.dirname ?? __dirname, "../.env.local");
22
+ try {
23
+ const raw = readFileSync(envPath, "utf-8");
24
+ for (const line of raw.split("\n")) {
25
+ const trimmed = line.trim();
26
+ if (trimmed.startsWith("CONVEX_URL=")) {
27
+ const value = trimmed.slice("CONVEX_URL=".length).split("#")[0].trim();
28
+ if (value) return value;
29
+ }
30
+ }
31
+ } catch {
32
+ }
33
+ throw new Error(
34
+ "CONVEX_URL not found. Set it as an environment variable or add it to .env.local"
35
+ );
36
+ }
37
+ var teamStatusSchema = z.enum(["active", "planned", "deprecated"]).describe("Team status");
38
+ var agentStatusSchema = z.enum(["active", "draft", "deprecated"]).describe("Agent status");
39
+ var skillStatusSchema = z.enum(["active", "draft", "deprecated"]).describe("Skill status");
40
+ var pluginStatusSchema = z.enum(["active", "draft", "deprecated"]).describe("Plugin status");
41
+ var hookStatusSchema = z.enum(["active", "dead", "deprecated"]).describe("Hook status");
42
+ var sourceSchema = z.enum(["internal", "external", "plugin"]).describe(
43
+ "Component source \u2014 internal (our code), external (third-party), plugin (from plugin)"
44
+ );
45
+ var pluginSourceSchema = z.enum(["internal", "external"]).describe("Plugin source \u2014 internal or external");
46
+ var skillCategorySchema = z.enum(["capability", "composite", "playbook", "root", "external"]).describe("Skill category type");
47
+ var hookEventSchema = z.enum([
48
+ "PreToolUse",
49
+ "PostToolUse",
50
+ "SessionStart",
51
+ "SessionEnd",
52
+ "Stop",
53
+ "SubagentStop",
54
+ "PreCompact",
55
+ "Notification",
56
+ "UserPromptSubmit"
57
+ ]).describe("Hook lifecycle event");
58
+ var runbookStatusSchema = z.enum(["draft", "published", "deprecated"]).describe("Runbook status");
59
+ var templateTypeSchema = z.enum(["standard", "mission", "brief", "runbook", "document", "checklist"]).describe("Template type");
60
+ var linkTypeSchema = z.enum(["uses", "produces", "references"]).describe(
61
+ "Link type \u2014 uses: template consumed during execution, produces: output document generated, references: informational cross-reference"
62
+ );
63
+ var convexUrl = loadConvexUrl();
64
+ var convex = new ConvexHttpClient(convexUrl);
65
+ var server = new McpServer({
66
+ name: "vantage-registry",
67
+ version: "1.5.0"
68
+ });
69
+ server.tool(
70
+ "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
+ "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
+ "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.",
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
+ },
227
+ async (args) => {
228
+ const id = await convex.mutation(api.skills.upsert, args);
229
+ return {
230
+ content: [
231
+ {
232
+ type: "text",
233
+ text: JSON.stringify(
234
+ { id, name: args.name, team: args.team, category: args.category },
235
+ null,
236
+ 2
237
+ )
238
+ }
239
+ ]
240
+ };
241
+ }
242
+ );
243
+ server.tool(
244
+ "list_skills",
245
+ "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.",
246
+ {
247
+ status: skillStatusSchema.optional().describe("Filter by status \u2014 omit to list all"),
248
+ team: z.string().optional().describe("Filter by team \u2014 e.g. 'dev', 'core'"),
249
+ category: skillCategorySchema.optional().describe(
250
+ "Filter by category: capability, composite, playbook, root, or external"
251
+ ),
252
+ fields: z.enum(["lite", "full"]).optional().describe(
253
+ "Projection: 'lite' (compact) or 'full' (complete document). Omit to use legacy summary behaviour."
254
+ ),
255
+ summary: z.boolean().optional().describe(
256
+ "Return summary only (no content). Default: true \u2014 legacy, prefer fields='lite'"
257
+ )
258
+ },
259
+ async ({ status, team, category, fields, summary }) => {
260
+ const results = await convex.query(api.skills.list, {
261
+ status,
262
+ team,
263
+ category,
264
+ fields,
265
+ summary
266
+ });
267
+ return {
268
+ content: [{ type: "text", text: JSON.stringify(results, null, 2) }]
269
+ };
270
+ }
271
+ );
272
+ server.tool(
273
+ "list_skills_by_team",
274
+ "List all skills belonging to a specific team.",
275
+ {
276
+ team: z.string().describe("Team name to filter by"),
277
+ summary: z.boolean().optional().describe("Return summary only (no content). Default: true")
278
+ },
279
+ async ({ team, summary }) => {
280
+ const results = await convex.query(api.skills.listByTeam, {
281
+ team,
282
+ summary
283
+ });
284
+ return {
285
+ content: [{ type: "text", text: JSON.stringify(results, null, 2) }]
286
+ };
287
+ }
288
+ );
289
+ server.tool(
290
+ "list_skills_by_category",
291
+ "List all skills of a specific category (capability, composite, playbook, root, external).",
292
+ {
293
+ category: skillCategorySchema,
294
+ summary: z.boolean().optional().describe("Return summary only (no content). Default: true")
295
+ },
296
+ async ({ category, summary }) => {
297
+ const results = await convex.query(api.skills.listByCategory, {
298
+ category,
299
+ summary
300
+ });
301
+ return {
302
+ content: [{ type: "text", text: JSON.stringify(results, null, 2) }]
303
+ };
304
+ }
305
+ );
306
+ server.tool(
307
+ "get_skill",
308
+ "Get a single skill by its Convex document ID.",
309
+ {
310
+ id: z.string().describe("Convex document ID for the skill")
311
+ },
312
+ async ({ id }) => {
313
+ const result = await convex.query(api.skills.get, { id });
314
+ return {
315
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
316
+ };
317
+ }
318
+ );
319
+ server.tool(
320
+ "get_skill_content",
321
+ "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.",
322
+ {
323
+ name: z.string().describe("Skill slug \u2014 e.g. 'check-messages', 'blog-writer'")
324
+ },
325
+ async ({ name }) => {
326
+ const result = await convex.query(api.skillContentDb.getSkillContent, {
327
+ name
328
+ });
329
+ return {
330
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
331
+ };
332
+ }
333
+ );
334
+ server.tool(
335
+ "upsert_skill_content",
336
+ "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.",
337
+ {
338
+ name: z.string().describe("Skill slug \u2014 e.g. 'check-messages'"),
339
+ content: z.string().describe("Full SKILL.md body (frontmatter + markdown)"),
340
+ createIfMissing: z.boolean().optional().default(false).describe(
341
+ "Auto-create a placeholder skill row if it does not exist (default: false)"
342
+ )
343
+ },
344
+ async ({ name, content, createIfMissing }) => {
345
+ const result = await convex.mutation(
346
+ api.skillContentDb.upsertSkillContent,
347
+ {
348
+ name,
349
+ content,
350
+ createIfMissing
351
+ }
352
+ );
353
+ return {
354
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
355
+ };
356
+ }
357
+ );
358
+ server.tool(
359
+ "detect_skill_drift",
360
+ "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.",
361
+ {
362
+ name: z.string().optional().describe("Skill slug \u2014 filter to a single skill"),
363
+ scope: z.string().optional().describe("'all' to return every skill (default when name is omitted)")
364
+ },
365
+ async ({ name, scope }) => {
366
+ const result = await convex.query(api.skillContentDb.detectSkillDrift, {
367
+ name,
368
+ scope
369
+ });
370
+ return {
371
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
372
+ };
373
+ }
374
+ );
375
+ server.tool(
376
+ "get_agent_content",
377
+ "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.",
378
+ {
379
+ name: z.string().describe("Agent slug \u2014 e.g. 'dev-convex-expert', 'dev-senior-dev'")
380
+ },
381
+ async ({ name }) => {
382
+ const result = await convex.query(api.agentContentDb.getAgentContent, {
383
+ name
384
+ });
385
+ return {
386
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
387
+ };
388
+ }
389
+ );
390
+ server.tool(
391
+ "upsert_agent_content",
392
+ "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.",
393
+ {
394
+ name: z.string().describe("Agent slug \u2014 e.g. 'dev-convex-expert'"),
395
+ content: z.string().describe("Full agent content body (AGENT.md or similar)"),
396
+ createIfMissing: z.boolean().optional().default(false).describe(
397
+ "Auto-create a placeholder agent row if it does not exist (default: false)"
398
+ )
399
+ },
400
+ async ({ name, content, createIfMissing }) => {
401
+ const result = await convex.mutation(
402
+ api.agentContentDb.upsertAgentContent,
403
+ {
404
+ name,
405
+ content,
406
+ createIfMissing
407
+ }
408
+ );
409
+ return {
410
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
411
+ };
412
+ }
413
+ );
414
+ server.tool(
415
+ "detect_agent_drift",
416
+ "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.",
417
+ {
418
+ name: z.string().optional().describe("Agent slug \u2014 filter to a single agent"),
419
+ scope: z.string().optional().describe("'all' to return every agent (default when name is omitted)")
420
+ },
421
+ async ({ name, scope }) => {
422
+ const result = await convex.query(api.agentContentDb.detectAgentDrift, {
423
+ name,
424
+ scope
425
+ });
426
+ return {
427
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
428
+ };
429
+ }
430
+ );
431
+ server.tool(
432
+ "get_plugin_content",
433
+ "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.",
434
+ {
435
+ name: z.string().describe("Plugin slug \u2014 e.g. 'perello-bootstrap', 'perello-identity'")
436
+ },
437
+ async ({ name }) => {
438
+ const result = await convex.query(api.pluginContentDb.getPluginContent, {
439
+ name
440
+ });
441
+ return {
442
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
443
+ };
444
+ }
445
+ );
446
+ server.tool(
447
+ "upsert_plugin_content",
448
+ "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.",
449
+ {
450
+ name: z.string().describe("Plugin slug \u2014 e.g. 'perello-bootstrap'"),
451
+ content: z.string().describe("Full plugin content body"),
452
+ createIfMissing: z.boolean().optional().default(false).describe(
453
+ "Auto-create a placeholder plugin row if it does not exist (default: false)"
454
+ )
455
+ },
456
+ async ({ name, content, createIfMissing }) => {
457
+ const result = await convex.mutation(
458
+ api.pluginContentDb.upsertPluginContent,
459
+ {
460
+ name,
461
+ content,
462
+ createIfMissing
463
+ }
464
+ );
465
+ return {
466
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
467
+ };
468
+ }
469
+ );
470
+ server.tool(
471
+ "detect_plugin_drift",
472
+ "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.",
473
+ {
474
+ name: z.string().optional().describe("Plugin slug \u2014 filter to a single plugin"),
475
+ scope: z.string().optional().describe("'all' to return every plugin (default when name is omitted)")
476
+ },
477
+ async ({ name, scope }) => {
478
+ const result = await convex.query(api.pluginContentDb.detectPluginDrift, {
479
+ name,
480
+ scope
481
+ });
482
+ return {
483
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
484
+ };
485
+ }
486
+ );
487
+ server.tool(
488
+ "get_hook_content",
489
+ "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.",
490
+ {
491
+ name: z.string().describe("Hook slug \u2014 e.g. 'enforce-no-task-in-message', 'check-pii'")
492
+ },
493
+ async ({ name }) => {
494
+ const result = await convex.query(api.hookContentDb.getHookContent, {
495
+ name
496
+ });
497
+ return {
498
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
499
+ };
500
+ }
501
+ );
502
+ server.tool(
503
+ "upsert_hook_content",
504
+ "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.",
505
+ {
506
+ name: z.string().describe("Hook slug \u2014 e.g. 'enforce-no-task-in-message'"),
507
+ content: z.string().describe("Full hook script content"),
508
+ createIfMissing: z.boolean().optional().default(false).describe(
509
+ "Auto-create a placeholder hook row if it does not exist (default: false)"
510
+ )
511
+ },
512
+ async ({ name, content, createIfMissing }) => {
513
+ const result = await convex.mutation(api.hookContentDb.upsertHookContent, {
514
+ name,
515
+ content,
516
+ createIfMissing
517
+ });
518
+ return {
519
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
520
+ };
521
+ }
522
+ );
523
+ server.tool(
524
+ "detect_hook_drift",
525
+ "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.",
526
+ {
527
+ name: z.string().optional().describe("Hook slug \u2014 filter to a single hook"),
528
+ scope: z.string().optional().describe("'all' to return every hook (default when name is omitted)")
529
+ },
530
+ async ({ name, scope }) => {
531
+ const result = await convex.query(api.hookContentDb.detectHookDrift, {
532
+ name,
533
+ scope
534
+ });
535
+ return {
536
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
537
+ };
538
+ }
539
+ );
540
+ server.tool(
541
+ "get_command_content",
542
+ "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.",
543
+ {
544
+ name: z.string().describe("Command slug \u2014 e.g. 'sync-workspace-from-vr', 'deploy-prod'")
545
+ },
546
+ async ({ name }) => {
547
+ const result = await convex.query(api.commandContentDb.getCommandContent, {
548
+ name
549
+ });
550
+ return {
551
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
552
+ };
553
+ }
554
+ );
555
+ server.tool(
556
+ "upsert_command_content",
557
+ "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.",
558
+ {
559
+ name: z.string().describe("Command slug \u2014 e.g. 'sync-workspace-from-vr'"),
560
+ content: z.string().describe("Full command script content")
561
+ },
562
+ async ({ name, content }) => {
563
+ const result = await convex.mutation(
564
+ api.commandContentDb.upsertCommandContent,
565
+ {
566
+ name,
567
+ content
568
+ }
569
+ );
570
+ return {
571
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
572
+ };
573
+ }
574
+ );
575
+ server.tool(
576
+ "detect_command_drift",
577
+ "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.",
578
+ {
579
+ name: z.string().optional().describe("Command slug \u2014 filter to a single command"),
580
+ scope: z.string().optional().describe("'all' to return every command (default when name is omitted)")
581
+ },
582
+ async ({ name, scope }) => {
583
+ const result = await convex.query(api.commandContentDb.detectCommandDrift, {
584
+ name,
585
+ scope
586
+ });
587
+ return {
588
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
589
+ };
590
+ }
591
+ );
592
+ server.tool(
593
+ "upsert_plugin",
594
+ "Create or update a plugin in VantageRegistry. Upserts by name.",
595
+ {
596
+ name: z.string().describe("Plugin name \u2014 e.g. 'perello-bootstrap', 'perello-identity'"),
597
+ description: z.string().describe("What this plugin provides"),
598
+ agentCount: z.number().int().describe("Number of agents in this plugin"),
599
+ skillCount: z.number().int().describe("Number of skills in this plugin"),
600
+ hookCount: z.number().int().describe("Number of hooks in this plugin"),
601
+ source: pluginSourceSchema,
602
+ status: pluginStatusSchema,
603
+ version: z.string().optional().describe("Semantic version \u2014 e.g. '1.0.0'"),
604
+ filePath: z.string().optional().describe("Plugin directory path"),
605
+ pricing: z.enum(["free", "paid"]).optional().describe("Pricing tier"),
606
+ price: z.number().optional().describe("Price in EUR (if paid)"),
607
+ license: z.string().optional().describe("License \u2014 e.g. 'MIT', 'proprietary'"),
608
+ publisherId: z.string().optional().describe("Publisher identifier"),
609
+ categories: z.array(z.string()).optional().describe("Categories beyond team name")
610
+ },
611
+ async (args) => {
612
+ const id = await convex.mutation(api.plugins.upsert, args);
613
+ return {
614
+ content: [
615
+ {
616
+ type: "text",
617
+ text: JSON.stringify({ id, name: args.name }, null, 2)
618
+ }
619
+ ]
620
+ };
621
+ }
622
+ );
623
+ server.tool(
624
+ "list_plugins",
625
+ "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.",
626
+ {
627
+ status: pluginStatusSchema.optional().describe("Filter by status \u2014 omit to list all"),
628
+ source: pluginSourceSchema.optional().describe("Filter by source: 'internal' or 'external'"),
629
+ team: z.string().optional().describe("Filter by owning team"),
630
+ fields: z.enum(["lite", "full"]).optional().describe("Projection: 'lite' (compact) or 'full' (complete document)")
631
+ },
632
+ async ({ status, source, team, fields }) => {
633
+ const results = await convex.query(api.plugins.list, {
634
+ status,
635
+ source,
636
+ team,
637
+ fields
638
+ });
639
+ return {
640
+ content: [{ type: "text", text: JSON.stringify(results, null, 2) }]
641
+ };
642
+ }
643
+ );
644
+ server.tool(
645
+ "get_plugin",
646
+ "Get a single plugin by its Convex document ID.",
647
+ {
648
+ id: z.string().describe("Convex document ID for the plugin")
649
+ },
650
+ async ({ id }) => {
651
+ const result = await convex.query(api.plugins.get, { id });
652
+ return {
653
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
654
+ };
655
+ }
656
+ );
657
+ server.tool(
658
+ "upsert_hook",
659
+ "Create or update a hook in VantageRegistry. Upserts by name.",
660
+ {
661
+ name: z.string().describe("Hook name \u2014 e.g. 'check-pii', 'enforce-skill-references'"),
662
+ event: hookEventSchema,
663
+ matcher: z.string().optional().describe("Tool matcher pattern \u2014 e.g. 'Bash', 'Agent'"),
664
+ description: z.string().describe("What this hook does"),
665
+ content: z.string().describe("Full hook script content"),
666
+ filePath: z.string().describe("File path relative to project root"),
667
+ registered: z.boolean().describe("Whether this hook is registered in settings.json"),
668
+ status: hookStatusSchema,
669
+ version: z.string().optional().describe("Semantic version \u2014 e.g. '1.0.0'")
670
+ },
671
+ async (args) => {
672
+ const id = await convex.mutation(api.hooks.upsert, args);
673
+ return {
674
+ content: [
675
+ {
676
+ type: "text",
677
+ text: JSON.stringify(
678
+ { id, name: args.name, event: args.event },
679
+ null,
680
+ 2
681
+ )
682
+ }
683
+ ]
684
+ };
685
+ }
686
+ );
687
+ server.tool(
688
+ "list_hooks",
689
+ "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.",
690
+ {
691
+ status: hookStatusSchema.optional().describe("Filter by status \u2014 omit to list all"),
692
+ event: hookEventSchema.optional().describe(
693
+ "Filter by lifecycle event \u2014 e.g. 'PreToolUse', 'SessionStart'"
694
+ ),
695
+ fields: z.enum(["lite", "full"]).optional().describe(
696
+ "Projection: 'lite' (compact) or 'full' (complete document with content)"
697
+ )
698
+ },
699
+ async ({ status, event, fields }) => {
700
+ const results = await convex.query(api.hooks.list, {
701
+ status,
702
+ event,
703
+ fields
704
+ });
705
+ return {
706
+ content: [{ type: "text", text: JSON.stringify(results, null, 2) }]
707
+ };
708
+ }
709
+ );
710
+ server.tool(
711
+ "get_hook",
712
+ "Get a single hook by its Convex document ID.",
713
+ {
714
+ id: z.string().describe("Convex document ID for the hook")
715
+ },
716
+ async ({ id }) => {
717
+ const result = await convex.query(api.hooks.get, { id });
718
+ return {
719
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
720
+ };
721
+ }
722
+ );
723
+ server.tool(
724
+ "upsert_prompt",
725
+ "Create or update a prompt in VantageRegistry. Upserts by name.",
726
+ {
727
+ name: z.string().describe("Prompt name \u2014 e.g. 'system-prompt', 'review-checklist'"),
728
+ team: z.string().optional().describe("Team this prompt belongs to \u2014 defaults to 'global'"),
729
+ purpose: z.string().describe("What this prompt is used for"),
730
+ content: z.string().describe("Full prompt content"),
731
+ version: z.string().optional().describe("Semantic version \u2014 e.g. '1.0.0'")
732
+ },
733
+ async (args) => {
734
+ const id = await convex.mutation(api.prompts.upsert, args);
735
+ return {
736
+ content: [
737
+ {
738
+ type: "text",
739
+ text: JSON.stringify({ id, name: args.name }, null, 2)
740
+ }
741
+ ]
742
+ };
743
+ }
744
+ );
745
+ server.tool(
746
+ "list_prompts",
747
+ "List all prompts in VantageRegistry. Optionally filter by team.",
748
+ {
749
+ team: z.string().optional().describe("Filter by team \u2014 omit to list all")
750
+ },
751
+ async ({ team }) => {
752
+ const results = await convex.query(api.prompts.list, { team });
753
+ return {
754
+ content: [{ type: "text", text: JSON.stringify(results, null, 2) }]
755
+ };
756
+ }
757
+ );
758
+ server.tool(
759
+ "get_prompt",
760
+ "Get a single prompt by its Convex document ID.",
761
+ {
762
+ id: z.string().describe("Convex document ID for the prompt")
763
+ },
764
+ async ({ id }) => {
765
+ const result = await convex.query(api.prompts.get, { id });
766
+ return {
767
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
768
+ };
769
+ }
770
+ );
771
+ server.tool(
772
+ "upsert_template",
773
+ "Create or update a template in VantageRegistry. Upserts by name. contentHash is auto-computed server-side (sha256 of content) when omitted.",
774
+ {
775
+ name: z.string().describe("Template name \u2014 e.g. 'agent-brief', 'skill-md'"),
776
+ team: z.string().optional().describe("Team this template belongs to \u2014 defaults to 'global'"),
777
+ purpose: z.string().describe("What this template is used for"),
778
+ content: z.string().describe("Full template content"),
779
+ version: z.string().optional().describe("Semantic version \u2014 e.g. '1.0.0'"),
780
+ template_type: templateTypeSchema.optional().describe(
781
+ "Template type: standard, mission, brief, runbook, document, or checklist"
782
+ ),
783
+ category: z.string().optional().describe("Category \u2014 e.g. 'standards/fleet-shared'"),
784
+ contentHash: z.string().optional().describe("sha256 of content \u2014 auto-computed server-side when omitted"),
785
+ sourceCommit: z.string().optional().describe("Git commit SHA the template content was sourced from"),
786
+ sourceRepo: z.string().optional().describe("Git repo the template content was sourced from")
787
+ },
788
+ async (args) => {
789
+ const id = await convex.mutation(api.templates.upsert, args);
790
+ return {
791
+ content: [
792
+ {
793
+ type: "text",
794
+ text: JSON.stringify({ id, name: args.name }, null, 2)
795
+ }
796
+ ]
797
+ };
798
+ }
799
+ );
800
+ server.tool(
801
+ "list_templates",
802
+ "List all templates in VantageRegistry. Optionally filter by team and/or template_type.",
803
+ {
804
+ team: z.string().optional().describe("Filter by team \u2014 omit to list all"),
805
+ template_type: templateTypeSchema.optional().describe("Filter by template type: mission, document, or checklist")
806
+ },
807
+ async ({ team, template_type }) => {
808
+ const results = await convex.query(api.templates.list, {
809
+ team,
810
+ template_type
811
+ });
812
+ return {
813
+ content: [{ type: "text", text: JSON.stringify(results, null, 2) }]
814
+ };
815
+ }
816
+ );
817
+ server.tool(
818
+ "get_template",
819
+ "Get a single template by its Convex document ID.",
820
+ {
821
+ id: z.string().describe("Convex document ID for the template")
822
+ },
823
+ async ({ id }) => {
824
+ const result = await convex.query(api.templates.get, { id });
825
+ return {
826
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
827
+ };
828
+ }
829
+ );
830
+ server.tool(
831
+ "detect_template_drift",
832
+ "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.",
833
+ {
834
+ name: z.string().optional().describe("Template slug \u2014 filter to a single template")
835
+ },
836
+ async ({ name }) => {
837
+ const result = await convex.query(api.templates.detectTemplateDrift, {
838
+ name
839
+ });
840
+ return {
841
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
842
+ };
843
+ }
844
+ );
845
+ server.tool(
846
+ "list_templates_by_category",
847
+ "List templates for a specific category using the byCategory index.",
848
+ {
849
+ category: z.string().describe("Category to filter by \u2014 e.g. 'standards/fleet-shared'")
850
+ },
851
+ async ({ category }) => {
852
+ const result = await convex.query(api.templates.listTemplatesByCategory, {
853
+ category
854
+ });
855
+ return {
856
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
857
+ };
858
+ }
859
+ );
860
+ server.tool(
861
+ "upsert_test_run",
862
+ "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.",
863
+ {
864
+ skillId: z.string().describe("Convex document ID of the skill being tested"),
865
+ runDate: z.number().describe("Unix timestamp (ms) when the run was executed"),
866
+ runByOrchestrator: z.string().describe("Orchestrator that ran the test \u2014 e.g. 'alpha', 'pi'"),
867
+ runByMission: z.string().optional().describe("Mission reference \u2014 e.g. 'D70'"),
868
+ reviewerScore: z.string().describe("Score as 'X/Y' \u2014 e.g. '37/45'"),
869
+ evalDelta: z.number().describe("Percentage-point delta vs without-skill baseline"),
870
+ evalAbsoluteWith: z.number().describe("Absolute score with skill (0-100)"),
871
+ evalAbsoluteWithout: z.number().describe("Absolute score without skill (0-100)"),
872
+ runnerModel: z.string().describe("Model used to run the skill \u2014 e.g. 'claude-sonnet-4-6'"),
873
+ judgeModel: z.string().describe("Model used as judge \u2014 e.g. 'claude-haiku-4-5'"),
874
+ corpusVersion: z.string().describe("Eval corpus version used \u2014 e.g. '2.0.0'"),
875
+ harnessVersion: z.string().describe("Test harness version \u2014 e.g. '0.1.0'"),
876
+ skillSha: z.string().describe("Git SHA of the skill file at test time"),
877
+ gradingJsonPath: z.string().optional().describe("Path to grading output JSON"),
878
+ benchmarkJsonPath: z.string().optional().describe("Path to benchmark output JSON"),
879
+ reportPath: z.string().optional().describe("Path to human-readable report"),
880
+ timeSpentMinutes: z.number().optional().describe("Time spent running the test in minutes"),
881
+ notes: z.string().optional().describe("Free-form notes about this run"),
882
+ testStatus: z.enum(["untested", "tested", "needs_retest", "improving", "untestable"]).optional().describe("Override test status \u2014 defaults to 'tested'")
883
+ },
884
+ async (args) => {
885
+ const id = await convex.mutation(api.skillTestRuns.upsertTestRun, {
886
+ ...args,
887
+ skillId: args.skillId
888
+ });
889
+ return {
890
+ content: [
891
+ {
892
+ type: "text",
893
+ text: JSON.stringify(
894
+ { id, skillId: args.skillId, runDate: args.runDate },
895
+ null,
896
+ 2
897
+ )
898
+ }
899
+ ]
900
+ };
901
+ }
902
+ );
903
+ server.tool(
904
+ "get_skill_test_history",
905
+ "Return test run history for a skill, ordered newest-first.",
906
+ {
907
+ skillId: z.string().describe("Convex document ID of the skill"),
908
+ limit: z.number().optional().describe("Max number of runs to return \u2014 defaults to 50")
909
+ },
910
+ async ({ skillId, limit }) => {
911
+ const results = await convex.query(api.skillTestRuns.getSkillTestHistory, {
912
+ skillId,
913
+ limit
914
+ });
915
+ return {
916
+ content: [{ type: "text", text: JSON.stringify(results, null, 2) }]
917
+ };
918
+ }
919
+ );
920
+ server.tool(
921
+ "list_skills_by_freshness",
922
+ "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.",
923
+ {
924
+ staleDays: z.number().describe("Number of days since last test run to consider stale"),
925
+ fields: z.enum(["lite", "full"]).optional().default("lite").describe(
926
+ "Projection: 'lite' (default, compact \u2014 bounded output) or 'full' (complete document)"
927
+ ),
928
+ limit: z.number().int().optional().describe("Max results \u2014 defaults to 100, max 500")
929
+ },
930
+ async ({ staleDays, fields, limit }) => {
931
+ const results = await convex.query(
932
+ api.skillTestRuns.listSkillsByFreshness,
933
+ { staleDays, fields, limit }
934
+ );
935
+ return {
936
+ content: [{ type: "text", text: JSON.stringify(results, null, 2) }]
937
+ };
938
+ }
939
+ );
940
+ server.tool(
941
+ "list_skills_below_threshold",
942
+ "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.",
943
+ {
944
+ scoreMin: z.number().optional().describe(
945
+ "Minimum acceptable score ratio (0-1). Skills below this are returned."
946
+ ),
947
+ deltaMin: z.number().optional().describe(
948
+ "Minimum acceptable eval delta in pp. Skills below this are returned."
949
+ ),
950
+ fields: z.enum(["lite", "full"]).optional().describe(
951
+ "Projection: 'lite' (compact) or 'full' (complete document). Omit for full (backward compat)."
952
+ )
953
+ },
954
+ async ({ scoreMin, deltaMin, fields }) => {
955
+ const results = await convex.query(
956
+ api.skillTestRuns.listSkillsBelowThreshold,
957
+ { scoreMin, deltaMin, fields }
958
+ );
959
+ return {
960
+ content: [{ type: "text", text: JSON.stringify(results, null, 2) }]
961
+ };
962
+ }
963
+ );
964
+ server.tool(
965
+ "upsert_skill_eval_corpus",
966
+ "Insert or update an eval corpus version for a skill. Upserts by (skillId, version).",
967
+ {
968
+ skillId: z.string().describe("Convex document ID of the skill"),
969
+ version: z.string().describe("Corpus version \u2014 e.g. '2.0.0'"),
970
+ casesCount: z.number().int().describe("Number of eval cases"),
971
+ assertionsCount: z.number().int().describe("Total number of assertions across all cases"),
972
+ baselinePrompt: z.string().describe("The baseline prompt used without the skill"),
973
+ evalsJsonContent: z.string().describe("Full JSON content of the evals file, or '{}' as placeholder"),
974
+ authoredBy: z.string().describe("Orchestrator or person who authored this corpus"),
975
+ authoredAt: z.number().describe("Unix timestamp (ms) when the corpus was authored"),
976
+ supersedesVersion: z.string().optional().describe("Version this corpus supersedes \u2014 e.g. '1.0.0'")
977
+ },
978
+ async (args) => {
979
+ const id = await convex.mutation(
980
+ api.skillEvalCorpus.upsertSkillEvalCorpus,
981
+ {
982
+ ...args,
983
+ skillId: args.skillId
984
+ }
985
+ );
986
+ return {
987
+ content: [
988
+ {
989
+ type: "text",
990
+ text: JSON.stringify(
991
+ { id, skillId: args.skillId, version: args.version },
992
+ null,
993
+ 2
994
+ )
995
+ }
996
+ ]
997
+ };
998
+ }
999
+ );
1000
+ var phaseSchema = z.object({
1001
+ name: z.string().describe("Phase name"),
1002
+ description: z.string().describe("What this phase accomplishes"),
1003
+ steps: z.array(z.string()).describe("Ordered list of steps in this phase")
1004
+ });
1005
+ var runbookInputSchema = z.object({
1006
+ name: z.string().describe("Input parameter name"),
1007
+ type: z.string().describe("Input type \u2014 e.g. 'string', 'boolean'"),
1008
+ required: z.boolean().describe("Whether this input is required"),
1009
+ description: z.string().describe("What this input controls")
1010
+ });
1011
+ var runbookOutputSchema = z.object({
1012
+ name: z.string().describe("Output artifact name"),
1013
+ description: z.string().describe("What this output contains"),
1014
+ path_pattern: z.string().optional().describe("File path pattern \u2014 e.g. 'reports/{name}.md'")
1015
+ });
1016
+ var applicabilitySchema = z.object({
1017
+ orchestrators: z.array(z.string()).describe("Orchestrator slugs this runbook applies to"),
1018
+ business_units: z.array(z.string()).describe("Business unit slugs this runbook applies to"),
1019
+ use_cases: z.array(z.string()).describe("Use case slugs this runbook applies to")
1020
+ });
1021
+ var linkedTemplateSchema = z.object({
1022
+ name: z.string().describe("Template name"),
1023
+ template_type: z.string().describe("Template type \u2014 mission, document, or checklist"),
1024
+ version: z.string().describe("Template version \u2014 e.g. '1.0.0'"),
1025
+ usage_phase: z.string().describe("Phase name where this template is used"),
1026
+ required: z.boolean().describe("Whether this template is required for the runbook"),
1027
+ description: z.string().describe("How this template is used in this runbook")
1028
+ });
1029
+ server.tool(
1030
+ "upsert_runbook",
1031
+ "Create or update a runbook in VantageRegistry. Upserts by name \u2014 if a runbook with the same name exists, it is updated.",
1032
+ {
1033
+ name: z.string().describe("Runbook slug \u2014 e.g. 'deploy-production', 'onboard-agent'"),
1034
+ description: z.string().describe("What this runbook accomplishes"),
1035
+ version: z.string().describe("Semantic version \u2014 e.g. '1.0.0'"),
1036
+ status: runbookStatusSchema,
1037
+ category: z.string().describe("Category \u2014 e.g. 'deployment', 'onboarding', 'quality'"),
1038
+ tags: z.array(z.string()).describe("Searchable tags"),
1039
+ content: z.string().describe("Full runbook content (markdown)"),
1040
+ phases: z.array(phaseSchema).describe("Ordered phases of the runbook"),
1041
+ inputs: z.array(runbookInputSchema).describe("Input parameters for the runbook"),
1042
+ outputs: z.array(runbookOutputSchema).describe("Output artifacts produced by the runbook"),
1043
+ applicability: applicabilitySchema,
1044
+ author: z.string().describe("Author slug \u2014 e.g. 'omega'"),
1045
+ team: z.string().describe("Owning team slug \u2014 e.g. 'core', 'dev'"),
1046
+ related_skills: z.array(z.string()).describe("Skill slugs referenced by this runbook"),
1047
+ related_agents: z.array(z.string()).describe("Agent slugs referenced by this runbook"),
1048
+ linked_templates: z.array(linkedTemplateSchema).describe("Templates used during this runbook")
1049
+ },
1050
+ async (args) => {
1051
+ try {
1052
+ const result = await convex.mutation(api.runbooks.upsertRunbook, args);
1053
+ return {
1054
+ content: [
1055
+ {
1056
+ type: "text",
1057
+ text: JSON.stringify({ success: true, data: result }, null, 2)
1058
+ }
1059
+ ]
1060
+ };
1061
+ } catch (err) {
1062
+ return {
1063
+ content: [
1064
+ {
1065
+ type: "text",
1066
+ text: JSON.stringify(
1067
+ { success: false, error: String(err) },
1068
+ null,
1069
+ 2
1070
+ )
1071
+ }
1072
+ ]
1073
+ };
1074
+ }
1075
+ }
1076
+ );
1077
+ server.tool(
1078
+ "get_runbook",
1079
+ "Get a single runbook by name. Optionally guard by exact version.",
1080
+ {
1081
+ name: z.string().describe("Runbook slug \u2014 e.g. 'deploy-production'"),
1082
+ version: z.string().optional().describe("If provided, returns null when stored version differs")
1083
+ },
1084
+ async ({ name, version }) => {
1085
+ try {
1086
+ const result = await convex.query(api.runbooks.getRunbook, {
1087
+ name,
1088
+ version
1089
+ });
1090
+ return {
1091
+ content: [
1092
+ {
1093
+ type: "text",
1094
+ text: JSON.stringify({ success: true, data: result }, null, 2)
1095
+ }
1096
+ ]
1097
+ };
1098
+ } catch (err) {
1099
+ return {
1100
+ content: [
1101
+ {
1102
+ type: "text",
1103
+ text: JSON.stringify(
1104
+ { success: false, error: String(err) },
1105
+ null,
1106
+ 2
1107
+ )
1108
+ }
1109
+ ]
1110
+ };
1111
+ }
1112
+ }
1113
+ );
1114
+ server.tool(
1115
+ "list_runbooks",
1116
+ "List runbooks in VantageRegistry. Defaults to published status. Supports applicability filters.",
1117
+ {
1118
+ status: runbookStatusSchema.optional().describe("Filter by status \u2014 defaults to 'published'"),
1119
+ category: z.string().optional().describe("Filter by category \u2014 e.g. 'deployment'"),
1120
+ team: z.string().optional().describe("Filter by owning team"),
1121
+ applicability_orchestrator: z.string().optional().describe("Filter by orchestrator slug in applicability"),
1122
+ applicability_bu: z.string().optional().describe("Filter by business unit slug in applicability"),
1123
+ applicability_use_case: z.string().optional().describe("Filter by use case slug in applicability"),
1124
+ limit: z.number().int().optional().describe("Max results to return \u2014 defaults to 50, max 200")
1125
+ },
1126
+ async (args) => {
1127
+ try {
1128
+ const result = await convex.query(api.runbooks.listRunbooks, args);
1129
+ return {
1130
+ content: [
1131
+ {
1132
+ type: "text",
1133
+ text: JSON.stringify({ success: true, data: result }, null, 2)
1134
+ }
1135
+ ]
1136
+ };
1137
+ } catch (err) {
1138
+ return {
1139
+ content: [
1140
+ {
1141
+ type: "text",
1142
+ text: JSON.stringify(
1143
+ { success: false, error: String(err) },
1144
+ null,
1145
+ 2
1146
+ )
1147
+ }
1148
+ ]
1149
+ };
1150
+ }
1151
+ }
1152
+ );
1153
+ server.tool(
1154
+ "list_runbooks_by_category",
1155
+ "List runbooks for a specific category using the byCategory index.",
1156
+ {
1157
+ category: z.string().describe("Category to filter by \u2014 e.g. 'deployment', 'quality'"),
1158
+ status: runbookStatusSchema.optional().describe(
1159
+ "Filter by status \u2014 omit to list all statuses in this category"
1160
+ )
1161
+ },
1162
+ async ({ category, status }) => {
1163
+ try {
1164
+ const result = await convex.query(api.runbooks.listRunbooksByCategory, {
1165
+ category,
1166
+ status
1167
+ });
1168
+ return {
1169
+ content: [
1170
+ {
1171
+ type: "text",
1172
+ text: JSON.stringify({ success: true, data: result }, null, 2)
1173
+ }
1174
+ ]
1175
+ };
1176
+ } catch (err) {
1177
+ return {
1178
+ content: [
1179
+ {
1180
+ type: "text",
1181
+ text: JSON.stringify(
1182
+ { success: false, error: String(err) },
1183
+ null,
1184
+ 2
1185
+ )
1186
+ }
1187
+ ]
1188
+ };
1189
+ }
1190
+ }
1191
+ );
1192
+ server.tool(
1193
+ "list_runbooks_by_team",
1194
+ "List runbooks for a specific team using the byTeam index.",
1195
+ {
1196
+ team: z.string().describe("Team slug to filter by \u2014 e.g. 'core', 'dev'"),
1197
+ status: runbookStatusSchema.optional().describe("Filter by status \u2014 omit to list all statuses for this team")
1198
+ },
1199
+ async ({ team, status }) => {
1200
+ try {
1201
+ const result = await convex.query(api.runbooks.listRunbooksByTeam, {
1202
+ team,
1203
+ status
1204
+ });
1205
+ return {
1206
+ content: [
1207
+ {
1208
+ type: "text",
1209
+ text: JSON.stringify({ success: true, data: result }, null, 2)
1210
+ }
1211
+ ]
1212
+ };
1213
+ } catch (err) {
1214
+ return {
1215
+ content: [
1216
+ {
1217
+ type: "text",
1218
+ text: JSON.stringify(
1219
+ { success: false, error: String(err) },
1220
+ null,
1221
+ 2
1222
+ )
1223
+ }
1224
+ ]
1225
+ };
1226
+ }
1227
+ }
1228
+ );
1229
+ server.tool(
1230
+ "delete_runbook",
1231
+ "Soft-delete a runbook by name \u2014 sets status to 'deprecated'. Returns {deleted: true} if found, {deleted: false} if not found.",
1232
+ {
1233
+ name: z.string().describe("Runbook slug to soft-delete")
1234
+ },
1235
+ async ({ name }) => {
1236
+ try {
1237
+ const result = await convex.mutation(api.runbooks.deleteRunbook, {
1238
+ name
1239
+ });
1240
+ return {
1241
+ content: [
1242
+ {
1243
+ type: "text",
1244
+ text: JSON.stringify({ success: true, data: result }, null, 2)
1245
+ }
1246
+ ]
1247
+ };
1248
+ } catch (err) {
1249
+ return {
1250
+ content: [
1251
+ {
1252
+ type: "text",
1253
+ text: JSON.stringify(
1254
+ { success: false, error: String(err) },
1255
+ null,
1256
+ 2
1257
+ )
1258
+ }
1259
+ ]
1260
+ };
1261
+ }
1262
+ }
1263
+ );
1264
+ server.tool(
1265
+ "link_runbook_template",
1266
+ "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.",
1267
+ {
1268
+ runbookId: z.string().describe("Convex document ID for the runbook"),
1269
+ templateId: z.string().describe("Convex document ID for the template"),
1270
+ linkType: linkTypeSchema,
1271
+ usagePhase: z.string().optional().describe(
1272
+ "Phase name where this template is used \u2014 kebab-case convention e.g. 'bootstrap', 'audit-phase-2'"
1273
+ ),
1274
+ order: z.number().optional().describe(
1275
+ "Sort order for template lists \u2014 lower numbers appear first (null items sorted last)"
1276
+ ),
1277
+ createdBy: z.string().optional().describe("Orchestrator or person creating this link \u2014 e.g. 'omega'")
1278
+ },
1279
+ async (args) => {
1280
+ try {
1281
+ const result = await convex.mutation(
1282
+ api.runbookTemplateLinks.linkRunbookTemplate,
1283
+ {
1284
+ runbookId: args.runbookId,
1285
+ templateId: args.templateId,
1286
+ linkType: args.linkType,
1287
+ usagePhase: args.usagePhase,
1288
+ order: args.order,
1289
+ createdBy: args.createdBy
1290
+ }
1291
+ );
1292
+ return {
1293
+ content: [
1294
+ {
1295
+ type: "text",
1296
+ text: JSON.stringify({ success: true, data: result }, null, 2)
1297
+ }
1298
+ ]
1299
+ };
1300
+ } catch (err) {
1301
+ return {
1302
+ content: [
1303
+ {
1304
+ type: "text",
1305
+ text: JSON.stringify(
1306
+ { success: false, error: String(err) },
1307
+ null,
1308
+ 2
1309
+ )
1310
+ }
1311
+ ]
1312
+ };
1313
+ }
1314
+ }
1315
+ );
1316
+ server.tool(
1317
+ "unlink_runbook_template",
1318
+ "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.",
1319
+ {
1320
+ runbookId: z.string().describe("Convex document ID for the runbook"),
1321
+ templateId: z.string().describe("Convex document ID for the template")
1322
+ },
1323
+ async (args) => {
1324
+ try {
1325
+ const result = await convex.mutation(
1326
+ api.runbookTemplateLinks.unlinkRunbookTemplate,
1327
+ {
1328
+ runbookId: args.runbookId,
1329
+ templateId: args.templateId
1330
+ }
1331
+ );
1332
+ return {
1333
+ content: [
1334
+ {
1335
+ type: "text",
1336
+ text: JSON.stringify({ success: true, data: result }, null, 2)
1337
+ }
1338
+ ]
1339
+ };
1340
+ } catch (err) {
1341
+ return {
1342
+ content: [
1343
+ {
1344
+ type: "text",
1345
+ text: JSON.stringify(
1346
+ { success: false, error: String(err) },
1347
+ null,
1348
+ 2
1349
+ )
1350
+ }
1351
+ ]
1352
+ };
1353
+ }
1354
+ }
1355
+ );
1356
+ server.tool(
1357
+ "list_templates_for_runbook",
1358
+ "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.",
1359
+ {
1360
+ runbookId: z.string().describe("Convex document ID for the runbook"),
1361
+ linkType: linkTypeSchema.optional().describe(
1362
+ "Filter by link type \u2014 omit to return all link types for this runbook"
1363
+ )
1364
+ },
1365
+ async (args) => {
1366
+ try {
1367
+ const result = await convex.query(
1368
+ api.runbookTemplateLinks.listTemplatesForRunbook,
1369
+ {
1370
+ runbookId: args.runbookId,
1371
+ linkType: args.linkType
1372
+ }
1373
+ );
1374
+ return {
1375
+ content: [
1376
+ {
1377
+ type: "text",
1378
+ text: JSON.stringify({ success: true, data: result }, null, 2)
1379
+ }
1380
+ ]
1381
+ };
1382
+ } catch (err) {
1383
+ return {
1384
+ content: [
1385
+ {
1386
+ type: "text",
1387
+ text: JSON.stringify(
1388
+ { success: false, error: String(err) },
1389
+ null,
1390
+ 2
1391
+ )
1392
+ }
1393
+ ]
1394
+ };
1395
+ }
1396
+ }
1397
+ );
1398
+ server.tool(
1399
+ "list_runbooks_for_template",
1400
+ "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.",
1401
+ {
1402
+ templateId: z.string().describe("Convex document ID for the template"),
1403
+ linkType: linkTypeSchema.optional().describe(
1404
+ "Filter by link type \u2014 omit to return all link types for this template"
1405
+ )
1406
+ },
1407
+ async (args) => {
1408
+ try {
1409
+ const result = await convex.query(
1410
+ api.runbookTemplateLinks.listRunbooksForTemplate,
1411
+ {
1412
+ templateId: args.templateId,
1413
+ linkType: args.linkType
1414
+ }
1415
+ );
1416
+ return {
1417
+ content: [
1418
+ {
1419
+ type: "text",
1420
+ text: JSON.stringify({ success: true, data: result }, null, 2)
1421
+ }
1422
+ ]
1423
+ };
1424
+ } catch (err) {
1425
+ return {
1426
+ content: [
1427
+ {
1428
+ type: "text",
1429
+ text: JSON.stringify(
1430
+ { success: false, error: String(err) },
1431
+ null,
1432
+ 2
1433
+ )
1434
+ }
1435
+ ]
1436
+ };
1437
+ }
1438
+ }
1439
+ );
1440
+ var componentKindSchema = z.enum(["skill", "agent", "hook", "plugin", "prompt", "runbook", "template"]).describe("Component kind");
1441
+ var componentStatusSchema = z.enum(["active", "deprecated", "experimental"]).describe("Component status");
1442
+ server.tool(
1443
+ "register_component",
1444
+ "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.",
1445
+ {
1446
+ name: z.string().describe(
1447
+ "Component name slug \u2014 e.g. 'check-messages', 'dev-convex-expert'"
1448
+ ),
1449
+ kind: componentKindSchema,
1450
+ status: componentStatusSchema,
1451
+ ownerTeam: z.string().optional().describe("Team that owns this component \u2014 e.g. 'core', 'dev'"),
1452
+ description: z.string().optional().describe("Short description of what this component does"),
1453
+ vrEntityId: z.string().optional().describe("ID of the backing VR entity (skill _id, agent _id, etc.)"),
1454
+ metadata: z.record(z.string(), z.unknown()).optional().describe("Arbitrary metadata bag \u2014 store extra fields here"),
1455
+ tags: z.array(z.string()).optional().describe("Searchable tags \u2014 e.g. ['ai', 'backend']")
1456
+ },
1457
+ async (args) => {
1458
+ try {
1459
+ const id = await convex.mutation(api.components.registerComponent, args);
1460
+ return {
1461
+ content: [
1462
+ {
1463
+ type: "text",
1464
+ text: JSON.stringify(
1465
+ { id, name: args.name, kind: args.kind },
1466
+ null,
1467
+ 2
1468
+ )
1469
+ }
1470
+ ]
1471
+ };
1472
+ } catch (err) {
1473
+ return {
1474
+ content: [
1475
+ {
1476
+ type: "text",
1477
+ text: JSON.stringify(
1478
+ { success: false, error: String(err) },
1479
+ null,
1480
+ 2
1481
+ )
1482
+ }
1483
+ ]
1484
+ };
1485
+ }
1486
+ }
1487
+ );
1488
+ server.tool(
1489
+ "list_components",
1490
+ "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.",
1491
+ {
1492
+ kind: componentKindSchema.optional().describe("Filter by kind \u2014 omit to return all kinds"),
1493
+ ownerTeam: z.string().optional().describe("Filter by owning team"),
1494
+ status: componentStatusSchema.optional().describe("Filter by status \u2014 omit to return all statuses"),
1495
+ tags: z.array(z.string()).optional().describe("Filter by tags \u2014 ALL specified tags must match"),
1496
+ fields: z.enum(["lite", "full"]).optional().describe("Projection: 'lite' (default) or 'full'"),
1497
+ limit: z.number().int().optional().describe("Max results \u2014 defaults to 100, max 500")
1498
+ },
1499
+ async ({ kind, ownerTeam, status, tags, fields, limit }) => {
1500
+ try {
1501
+ const filter = kind !== void 0 || ownerTeam !== void 0 || status !== void 0 || tags !== void 0 ? { kind, ownerTeam, status, tags } : void 0;
1502
+ const results = await convex.query(api.components.listComponents, {
1503
+ filter,
1504
+ fields,
1505
+ limit
1506
+ });
1507
+ return {
1508
+ content: [{ type: "text", text: JSON.stringify(results, null, 2) }]
1509
+ };
1510
+ } catch (err) {
1511
+ return {
1512
+ content: [
1513
+ {
1514
+ type: "text",
1515
+ text: JSON.stringify(
1516
+ { success: false, error: String(err) },
1517
+ null,
1518
+ 2
1519
+ )
1520
+ }
1521
+ ]
1522
+ };
1523
+ }
1524
+ }
1525
+ );
1526
+ server.tool(
1527
+ "get_component",
1528
+ "Get a single component by its Convex document ID. Returns the full document or null if not found.",
1529
+ {
1530
+ id: z.string().describe("Convex document ID of the component")
1531
+ },
1532
+ async ({ id }) => {
1533
+ try {
1534
+ const result = await convex.query(api.components.getComponent, {
1535
+ id
1536
+ });
1537
+ return {
1538
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
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
+ server.tool(
1557
+ "search_components",
1558
+ "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.",
1559
+ {
1560
+ query: z.string().describe("Name prefix to search for \u2014 e.g. 'check', 'dev-'"),
1561
+ limit: z.number().int().optional().describe("Max results \u2014 defaults to 50, max 200")
1562
+ },
1563
+ async ({ query, limit }) => {
1564
+ try {
1565
+ const results = await convex.query(api.components.searchComponents, {
1566
+ query,
1567
+ limit
1568
+ });
1569
+ return {
1570
+ content: [{ type: "text", text: JSON.stringify(results, null, 2) }]
1571
+ };
1572
+ } catch (err) {
1573
+ return {
1574
+ content: [
1575
+ {
1576
+ type: "text",
1577
+ text: JSON.stringify(
1578
+ { success: false, error: String(err) },
1579
+ null,
1580
+ 2
1581
+ )
1582
+ }
1583
+ ]
1584
+ };
1585
+ }
1586
+ }
1587
+ );
1588
+ server.tool(
1589
+ "update_component",
1590
+ "Patch individual fields on an existing component. Only provided fields are updated. Throws if not found.",
1591
+ {
1592
+ id: z.string().describe("Convex document ID of the component to update"),
1593
+ name: z.string().optional().describe("New name slug"),
1594
+ kind: componentKindSchema.optional().describe("New kind"),
1595
+ ownerTeam: z.string().optional().describe("New owning team"),
1596
+ description: z.string().optional().describe("New description"),
1597
+ status: componentStatusSchema.optional().describe("New status"),
1598
+ vrEntityId: z.string().optional().describe("Updated vrEntityId reference"),
1599
+ metadata: z.record(z.string(), z.unknown()).optional().describe("Updated metadata bag"),
1600
+ tags: z.array(z.string()).optional().describe("Updated tags array")
1601
+ },
1602
+ async (args) => {
1603
+ try {
1604
+ await convex.mutation(api.components.updateComponent, {
1605
+ ...args,
1606
+ id: args.id
1607
+ });
1608
+ return {
1609
+ content: [
1610
+ {
1611
+ type: "text",
1612
+ text: JSON.stringify({ success: true, id: args.id }, null, 2)
1613
+ }
1614
+ ]
1615
+ };
1616
+ } catch (err) {
1617
+ return {
1618
+ content: [
1619
+ {
1620
+ type: "text",
1621
+ text: JSON.stringify(
1622
+ { success: false, error: String(err) },
1623
+ null,
1624
+ 2
1625
+ )
1626
+ }
1627
+ ]
1628
+ };
1629
+ }
1630
+ }
1631
+ );
1632
+ server.tool(
1633
+ "delete_component",
1634
+ "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.",
1635
+ {
1636
+ id: z.string().describe("Convex document ID of the component to soft-delete")
1637
+ },
1638
+ async ({ id }) => {
1639
+ try {
1640
+ const result = await convex.mutation(api.components.deleteComponent, {
1641
+ id
1642
+ });
1643
+ return {
1644
+ content: [
1645
+ {
1646
+ type: "text",
1647
+ text: JSON.stringify({ success: true, ...result }, null, 2)
1648
+ }
1649
+ ]
1650
+ };
1651
+ } catch (err) {
1652
+ return {
1653
+ content: [
1654
+ {
1655
+ type: "text",
1656
+ text: JSON.stringify(
1657
+ { success: false, error: String(err) },
1658
+ null,
1659
+ 2
1660
+ )
1661
+ }
1662
+ ]
1663
+ };
1664
+ }
1665
+ }
1666
+ );
1667
+ server.tool(
1668
+ "get_stats",
1669
+ "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.",
1670
+ {},
1671
+ async () => {
1672
+ const [
1673
+ teams,
1674
+ agents,
1675
+ skills,
1676
+ plugins,
1677
+ hooks,
1678
+ rbDraft,
1679
+ rbPublished,
1680
+ rbDeprecated
1681
+ ] = await Promise.all([
1682
+ convex.query(api.teams.list, {}),
1683
+ convex.query(api.agents.list, {}),
1684
+ convex.query(api.skills.list, {}),
1685
+ convex.query(api.plugins.list, {}),
1686
+ convex.query(api.hooks.list, {}),
1687
+ convex.query(api.runbooks.listRunbooks, { status: "draft", limit: 1e3 }),
1688
+ convex.query(api.runbooks.listRunbooks, {
1689
+ status: "published",
1690
+ limit: 1e3
1691
+ }),
1692
+ convex.query(api.runbooks.listRunbooks, {
1693
+ status: "deprecated",
1694
+ limit: 1e3
1695
+ })
1696
+ ]);
1697
+ const byCategoryMap = {};
1698
+ for (const rb of [...rbDraft, ...rbPublished, ...rbDeprecated]) {
1699
+ byCategoryMap[rb.category] = (byCategoryMap[rb.category] ?? 0) + 1;
1700
+ }
1701
+ const runbooksTotal = rbDraft.length + rbPublished.length + rbDeprecated.length;
1702
+ const stats = {
1703
+ teams: teams.length,
1704
+ agents: agents.length,
1705
+ skills: skills.length,
1706
+ plugins: plugins.length,
1707
+ hooks: hooks.length,
1708
+ runbooks_count: {
1709
+ total: runbooksTotal,
1710
+ by_status: {
1711
+ draft: rbDraft.length,
1712
+ published: rbPublished.length,
1713
+ deprecated: rbDeprecated.length
1714
+ },
1715
+ by_category: byCategoryMap
1716
+ },
1717
+ total: teams.length + agents.length + skills.length + plugins.length + hooks.length + runbooksTotal
1718
+ };
1719
+ return {
1720
+ content: [{ type: "text", text: JSON.stringify(stats, null, 2) }]
1721
+ };
1722
+ }
1723
+ );
1724
+ async function main() {
1725
+ const transport = new StdioServerTransport();
1726
+ await server.connect(transport);
1727
+ console.error("VantageRegistry MCP server running on stdio");
1728
+ }
1729
+ main().catch((err) => {
1730
+ console.error("Fatal:", err);
1731
+ process.exit(1);
1732
+ });