@vantageos/vantage-registry-mcp 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +239 -0
- package/package.json +31 -0
- package/server.ts +1267 -0
package/server.ts
ADDED
|
@@ -0,0 +1,1267 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* VantageRegistry MCP Server
|
|
4
|
+
* Exposes Convex registry functions as Claude Code tools via stdio transport.
|
|
5
|
+
*
|
|
6
|
+
* Tools:
|
|
7
|
+
* upsert_team, list_teams, get_team
|
|
8
|
+
* upsert_agent, list_agents, list_agents_by_team, get_agent
|
|
9
|
+
* upsert_skill, list_skills, list_skills_by_team, list_skills_by_category, get_skill
|
|
10
|
+
* upsert_plugin, list_plugins, get_plugin
|
|
11
|
+
* upsert_hook, list_hooks, get_hook
|
|
12
|
+
* upsert_prompt, list_prompts, get_prompt
|
|
13
|
+
* upsert_template, list_templates, get_template
|
|
14
|
+
* upsert_runbook, get_runbook, list_runbooks, list_runbooks_by_category, list_runbooks_by_team, delete_runbook
|
|
15
|
+
* get_stats
|
|
16
|
+
*
|
|
17
|
+
* Orchestrator: Omega — VantageOS Team | 2026-05-19
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
21
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
22
|
+
import { ConvexHttpClient } from "convex/browser";
|
|
23
|
+
import { readFileSync } from "fs";
|
|
24
|
+
import { resolve } from "path";
|
|
25
|
+
import { z } from "zod";
|
|
26
|
+
import { api } from "../convex/_generated/api.js";
|
|
27
|
+
|
|
28
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
29
|
+
// Bootstrap: resolve CONVEX_URL from env or .env.local
|
|
30
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
function loadConvexUrl(): string {
|
|
33
|
+
if (process.env.CONVEX_URL) {
|
|
34
|
+
return process.env.CONVEX_URL;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const envPath = resolve(import.meta.dirname ?? __dirname, "../.env.local");
|
|
38
|
+
try {
|
|
39
|
+
const raw = readFileSync(envPath, "utf-8");
|
|
40
|
+
for (const line of raw.split("\n")) {
|
|
41
|
+
const trimmed = line.trim();
|
|
42
|
+
if (trimmed.startsWith("CONVEX_URL=")) {
|
|
43
|
+
const value = trimmed.slice("CONVEX_URL=".length).split("#")[0].trim();
|
|
44
|
+
if (value) return value;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
} catch {
|
|
48
|
+
// .env.local not found — fall through to error
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
throw new Error(
|
|
52
|
+
"CONVEX_URL not found. Set it as an environment variable or add it to .env.local",
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
57
|
+
// Shared Zod schemas
|
|
58
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
const teamStatusSchema = z
|
|
61
|
+
.enum(["active", "planned", "deprecated"])
|
|
62
|
+
.describe("Team status");
|
|
63
|
+
|
|
64
|
+
const agentStatusSchema = z
|
|
65
|
+
.enum(["active", "draft", "deprecated"])
|
|
66
|
+
.describe("Agent status");
|
|
67
|
+
|
|
68
|
+
const skillStatusSchema = z
|
|
69
|
+
.enum(["active", "draft", "deprecated"])
|
|
70
|
+
.describe("Skill status");
|
|
71
|
+
|
|
72
|
+
const pluginStatusSchema = z
|
|
73
|
+
.enum(["active", "draft", "deprecated"])
|
|
74
|
+
.describe("Plugin status");
|
|
75
|
+
|
|
76
|
+
const hookStatusSchema = z
|
|
77
|
+
.enum(["active", "dead", "deprecated"])
|
|
78
|
+
.describe("Hook status");
|
|
79
|
+
|
|
80
|
+
const sourceSchema = z
|
|
81
|
+
.enum(["internal", "external", "plugin"])
|
|
82
|
+
.describe("Component source — internal (our code), external (third-party), plugin (from plugin)");
|
|
83
|
+
|
|
84
|
+
const pluginSourceSchema = z
|
|
85
|
+
.enum(["internal", "external"])
|
|
86
|
+
.describe("Plugin source — internal or external");
|
|
87
|
+
|
|
88
|
+
const skillCategorySchema = z
|
|
89
|
+
.enum(["capability", "composite", "playbook", "root", "external"])
|
|
90
|
+
.describe("Skill category type");
|
|
91
|
+
|
|
92
|
+
const hookEventSchema = z
|
|
93
|
+
.enum([
|
|
94
|
+
"PreToolUse",
|
|
95
|
+
"PostToolUse",
|
|
96
|
+
"SessionStart",
|
|
97
|
+
"SessionEnd",
|
|
98
|
+
"Stop",
|
|
99
|
+
"SubagentStop",
|
|
100
|
+
"PreCompact",
|
|
101
|
+
"Notification",
|
|
102
|
+
"UserPromptSubmit",
|
|
103
|
+
])
|
|
104
|
+
.describe("Hook lifecycle event");
|
|
105
|
+
|
|
106
|
+
const runbookStatusSchema = z
|
|
107
|
+
.enum(["draft", "published", "deprecated"])
|
|
108
|
+
.describe("Runbook status");
|
|
109
|
+
|
|
110
|
+
const templateTypeSchema = z
|
|
111
|
+
.enum(["mission", "document", "checklist"])
|
|
112
|
+
.describe("Template type");
|
|
113
|
+
|
|
114
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
115
|
+
// Server setup
|
|
116
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
const convexUrl = loadConvexUrl();
|
|
119
|
+
const convex = new ConvexHttpClient(convexUrl);
|
|
120
|
+
|
|
121
|
+
const server = new McpServer({
|
|
122
|
+
name: "vantage-registry",
|
|
123
|
+
version: "1.1.0",
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
127
|
+
// TEAMS
|
|
128
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
129
|
+
|
|
130
|
+
server.tool(
|
|
131
|
+
"upsert_team",
|
|
132
|
+
"Create or update a team in VantageRegistry. Upserts by name — if a team with the same name exists, it is updated.",
|
|
133
|
+
{
|
|
134
|
+
name: z.string().describe("Team name — e.g. 'core', 'geo', 'dev'"),
|
|
135
|
+
description: z.string().describe("What this team does"),
|
|
136
|
+
agentCount: z.number().int().describe("Number of agents in this team"),
|
|
137
|
+
skillCount: z.number().int().describe("Number of skills in this team"),
|
|
138
|
+
status: teamStatusSchema,
|
|
139
|
+
project: z.string().optional().describe("Associated project — e.g. 'vantage-starter'"),
|
|
140
|
+
},
|
|
141
|
+
async (args) => {
|
|
142
|
+
const id = await convex.mutation(api.teams.upsert, args);
|
|
143
|
+
return {
|
|
144
|
+
content: [{ type: "text", text: JSON.stringify({ id, ...args }, null, 2) }],
|
|
145
|
+
};
|
|
146
|
+
},
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
server.tool(
|
|
150
|
+
"list_teams",
|
|
151
|
+
"List all teams in VantageRegistry. Optionally filter by status (active, planned, deprecated).",
|
|
152
|
+
{
|
|
153
|
+
status: teamStatusSchema.optional().describe("Filter by status — omit to list all"),
|
|
154
|
+
},
|
|
155
|
+
async ({ status }) => {
|
|
156
|
+
const results = await convex.query(api.teams.list, { status });
|
|
157
|
+
return {
|
|
158
|
+
content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
|
|
159
|
+
};
|
|
160
|
+
},
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
server.tool(
|
|
164
|
+
"get_team",
|
|
165
|
+
"Get a single team by its Convex document ID.",
|
|
166
|
+
{
|
|
167
|
+
id: z.string().describe("Convex document ID for the team"),
|
|
168
|
+
},
|
|
169
|
+
async ({ id }) => {
|
|
170
|
+
const result = await convex.query(api.teams.get, { id: id as any });
|
|
171
|
+
return {
|
|
172
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
173
|
+
};
|
|
174
|
+
},
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
178
|
+
// AGENTS
|
|
179
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
180
|
+
|
|
181
|
+
server.tool(
|
|
182
|
+
"upsert_agent",
|
|
183
|
+
"Create or update an agent in VantageRegistry. Upserts by name+team — if an agent with the same name and team exists, it is updated.",
|
|
184
|
+
{
|
|
185
|
+
name: z.string().describe("Agent name — e.g. 'copywriter', 'strategy-researcher'"),
|
|
186
|
+
team: z.string().describe("Team this agent belongs to"),
|
|
187
|
+
description: z.string().describe("What this agent does"),
|
|
188
|
+
content: z.string().describe("Full agent definition content (markdown)"),
|
|
189
|
+
filePath: z.string().describe("File path relative to project root"),
|
|
190
|
+
lineCount: z.number().int().describe("Number of lines in the agent file"),
|
|
191
|
+
status: agentStatusSchema,
|
|
192
|
+
version: z.string().optional().describe("Semantic version — e.g. '1.0.0'"),
|
|
193
|
+
source: sourceSchema,
|
|
194
|
+
pricing: z.enum(["free", "paid"]).optional().describe("Pricing tier"),
|
|
195
|
+
price: z.number().optional().describe("Price in EUR (if paid)"),
|
|
196
|
+
license: z.string().optional().describe("License — e.g. 'MIT', 'proprietary'"),
|
|
197
|
+
publisherId: z.string().optional().describe("Publisher identifier"),
|
|
198
|
+
categories: z.array(z.string()).optional().describe("Categories beyond team name"),
|
|
199
|
+
},
|
|
200
|
+
async (args) => {
|
|
201
|
+
const id = await convex.mutation(api.agents.upsert, args);
|
|
202
|
+
return {
|
|
203
|
+
content: [
|
|
204
|
+
{
|
|
205
|
+
type: "text",
|
|
206
|
+
text: JSON.stringify({ id, name: args.name, team: args.team }, null, 2),
|
|
207
|
+
},
|
|
208
|
+
],
|
|
209
|
+
};
|
|
210
|
+
},
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
server.tool(
|
|
214
|
+
"list_agents",
|
|
215
|
+
"List all agents in VantageRegistry. Optionally filter by status.",
|
|
216
|
+
{
|
|
217
|
+
status: agentStatusSchema.optional().describe("Filter by status — omit to list all"),
|
|
218
|
+
summary: z.boolean().optional().describe("Return summary only (no content). Default: true"),
|
|
219
|
+
},
|
|
220
|
+
async ({ status, summary }) => {
|
|
221
|
+
const results = await convex.query(api.agents.list, { status, summary });
|
|
222
|
+
return {
|
|
223
|
+
content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
|
|
224
|
+
};
|
|
225
|
+
},
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
server.tool(
|
|
229
|
+
"list_agents_by_team",
|
|
230
|
+
"List all agents belonging to a specific team.",
|
|
231
|
+
{
|
|
232
|
+
team: z.string().describe("Team name to filter by"),
|
|
233
|
+
summary: z.boolean().optional().describe("Return summary only (no content). Default: true"),
|
|
234
|
+
},
|
|
235
|
+
async ({ team, summary }) => {
|
|
236
|
+
const results = await convex.query(api.agents.listByTeam, { team, summary });
|
|
237
|
+
return {
|
|
238
|
+
content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
|
|
239
|
+
};
|
|
240
|
+
},
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
server.tool(
|
|
244
|
+
"get_agent",
|
|
245
|
+
"Get a single agent by its Convex document ID.",
|
|
246
|
+
{
|
|
247
|
+
id: z.string().describe("Convex document ID for the agent"),
|
|
248
|
+
},
|
|
249
|
+
async ({ id }) => {
|
|
250
|
+
const result = await convex.query(api.agents.get, { id: id as any });
|
|
251
|
+
return {
|
|
252
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
253
|
+
};
|
|
254
|
+
},
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
258
|
+
// SKILLS
|
|
259
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
260
|
+
|
|
261
|
+
server.tool(
|
|
262
|
+
"upsert_skill",
|
|
263
|
+
"Create or update a skill in VantageRegistry. Upserts by name+team — if a skill with the same name and team exists, it is updated.",
|
|
264
|
+
{
|
|
265
|
+
name: z.string().describe("Skill name — e.g. 'social-post', 'competitor-watch'"),
|
|
266
|
+
team: z.string().describe("Team this skill belongs to"),
|
|
267
|
+
category: skillCategorySchema,
|
|
268
|
+
description: z.string().describe("What this skill does — use pushy trigger descriptions"),
|
|
269
|
+
content: z.string().describe("Full SKILL.md content"),
|
|
270
|
+
filePath: z.string().describe("File path relative to project root"),
|
|
271
|
+
lineCount: z.number().int().describe("Number of lines in the skill file"),
|
|
272
|
+
status: skillStatusSchema,
|
|
273
|
+
version: z.string().optional().describe("Semantic version — e.g. '1.0.0'"),
|
|
274
|
+
source: sourceSchema,
|
|
275
|
+
pricing: z.enum(["free", "paid"]).optional().describe("Pricing tier"),
|
|
276
|
+
price: z.number().optional().describe("Price in EUR (if paid)"),
|
|
277
|
+
license: z.string().optional().describe("License — e.g. 'MIT', 'proprietary'"),
|
|
278
|
+
publisherId: z.string().optional().describe("Publisher identifier"),
|
|
279
|
+
categories: z.array(z.string()).optional().describe("Categories beyond team name"),
|
|
280
|
+
},
|
|
281
|
+
async (args) => {
|
|
282
|
+
const id = await convex.mutation(api.skills.upsert, args);
|
|
283
|
+
return {
|
|
284
|
+
content: [
|
|
285
|
+
{
|
|
286
|
+
type: "text",
|
|
287
|
+
text: JSON.stringify({ id, name: args.name, team: args.team, category: args.category }, null, 2),
|
|
288
|
+
},
|
|
289
|
+
],
|
|
290
|
+
};
|
|
291
|
+
},
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
server.tool(
|
|
295
|
+
"list_skills",
|
|
296
|
+
"List all skills in VantageRegistry. Optionally filter by status.",
|
|
297
|
+
{
|
|
298
|
+
status: skillStatusSchema.optional().describe("Filter by status — omit to list all"),
|
|
299
|
+
summary: z.boolean().optional().describe("Return summary only (no content). Default: true"),
|
|
300
|
+
},
|
|
301
|
+
async ({ status, summary }) => {
|
|
302
|
+
const results = await convex.query(api.skills.list, { status, summary });
|
|
303
|
+
return {
|
|
304
|
+
content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
|
|
305
|
+
};
|
|
306
|
+
},
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
server.tool(
|
|
310
|
+
"list_skills_by_team",
|
|
311
|
+
"List all skills belonging to a specific team.",
|
|
312
|
+
{
|
|
313
|
+
team: z.string().describe("Team name to filter by"),
|
|
314
|
+
summary: z.boolean().optional().describe("Return summary only (no content). Default: true"),
|
|
315
|
+
},
|
|
316
|
+
async ({ team, summary }) => {
|
|
317
|
+
const results = await convex.query(api.skills.listByTeam, { team, summary });
|
|
318
|
+
return {
|
|
319
|
+
content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
|
|
320
|
+
};
|
|
321
|
+
},
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
server.tool(
|
|
325
|
+
"list_skills_by_category",
|
|
326
|
+
"List all skills of a specific category (capability, composite, playbook, root, external).",
|
|
327
|
+
{
|
|
328
|
+
category: skillCategorySchema,
|
|
329
|
+
summary: z.boolean().optional().describe("Return summary only (no content). Default: true"),
|
|
330
|
+
},
|
|
331
|
+
async ({ category, summary }) => {
|
|
332
|
+
const results = await convex.query(api.skills.listByCategory, { category, summary });
|
|
333
|
+
return {
|
|
334
|
+
content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
|
|
335
|
+
};
|
|
336
|
+
},
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
server.tool(
|
|
340
|
+
"get_skill",
|
|
341
|
+
"Get a single skill by its Convex document ID.",
|
|
342
|
+
{
|
|
343
|
+
id: z.string().describe("Convex document ID for the skill"),
|
|
344
|
+
},
|
|
345
|
+
async ({ id }) => {
|
|
346
|
+
const result = await convex.query(api.skills.get, { id: id as any });
|
|
347
|
+
return {
|
|
348
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
349
|
+
};
|
|
350
|
+
},
|
|
351
|
+
);
|
|
352
|
+
|
|
353
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
354
|
+
// SKILL CONTENT — VR Single-Source-of-Truth (D72-VR-SoT)
|
|
355
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
356
|
+
|
|
357
|
+
server.tool(
|
|
358
|
+
"get_skill_content",
|
|
359
|
+
"Fetch the VR-hosted canonical SKILL.md body for a skill. " +
|
|
360
|
+
"Returns content, sha256 hash, semver version, and last sync timestamp. " +
|
|
361
|
+
"Returns null fields if the skill has not been hosted in VR yet.",
|
|
362
|
+
{
|
|
363
|
+
name: z.string().describe("Skill slug — e.g. 'check-messages', 'blog-writer'"),
|
|
364
|
+
},
|
|
365
|
+
async ({ name }) => {
|
|
366
|
+
const result = await convex.query(api.skillContentDb.getSkillContent, { name });
|
|
367
|
+
return {
|
|
368
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
369
|
+
};
|
|
370
|
+
},
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
server.tool(
|
|
374
|
+
"upsert_skill_content",
|
|
375
|
+
"Set or update the canonical SKILL.md body in VantageRegistry. " +
|
|
376
|
+
"Computes sha256; if content is unchanged from the stored version the call is idempotent " +
|
|
377
|
+
"(version is not bumped, only contentSyncedAt is updated). " +
|
|
378
|
+
"If content changed, patch version is bumped (X.Y.Z → X.Y.Z+1). " +
|
|
379
|
+
"First write sets version to '1.0.0'. " +
|
|
380
|
+
"If createIfMissing=true, auto-creates a placeholder skill row if it does not exist yet. " +
|
|
381
|
+
"Throws if the skill does not exist and createIfMissing is not set.",
|
|
382
|
+
{
|
|
383
|
+
name: z.string().describe("Skill slug — e.g. 'check-messages'"),
|
|
384
|
+
content: z.string().describe("Full SKILL.md body (frontmatter + markdown)"),
|
|
385
|
+
createIfMissing: z.boolean().optional().default(false).describe("Auto-create a placeholder skill row if it does not exist (default: false)"),
|
|
386
|
+
},
|
|
387
|
+
async ({ name, content, createIfMissing }) => {
|
|
388
|
+
const result = await convex.mutation(api.skillContentDb.upsertSkillContent, {
|
|
389
|
+
name,
|
|
390
|
+
content,
|
|
391
|
+
createIfMissing,
|
|
392
|
+
});
|
|
393
|
+
return {
|
|
394
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
395
|
+
};
|
|
396
|
+
},
|
|
397
|
+
);
|
|
398
|
+
|
|
399
|
+
server.tool(
|
|
400
|
+
"detect_skill_drift",
|
|
401
|
+
"Return the VR-side sha256 hash and filePath for each skill in scope. " +
|
|
402
|
+
"IMPORTANT: Convex queries have no filesystem access. This tool returns the VR " +
|
|
403
|
+
"canonical hash + stored filePath so the caller can compute disk SHA256 and compare. " +
|
|
404
|
+
"scope='all' (default) returns every skill. name=<slug> filters to a single skill.",
|
|
405
|
+
{
|
|
406
|
+
name: z.string().optional().describe("Skill slug — filter to a single skill"),
|
|
407
|
+
scope: z.string().optional().describe("'all' to return every skill (default when name is omitted)"),
|
|
408
|
+
},
|
|
409
|
+
async ({ name, scope }) => {
|
|
410
|
+
const result = await convex.query(api.skillContentDb.detectSkillDrift, { name, scope });
|
|
411
|
+
return {
|
|
412
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
413
|
+
};
|
|
414
|
+
},
|
|
415
|
+
);
|
|
416
|
+
|
|
417
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
418
|
+
// AGENT CONTENT — VR Single-Source-of-Truth (D72-VR-SoT Phase A)
|
|
419
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
420
|
+
|
|
421
|
+
server.tool(
|
|
422
|
+
"get_agent_content",
|
|
423
|
+
"Fetch the VR-hosted canonical agent content body for an agent. " +
|
|
424
|
+
"Returns content, sha256 hash, semver version, and last sync timestamp. " +
|
|
425
|
+
"Returns null fields if the agent has not been hosted in VR yet.",
|
|
426
|
+
{
|
|
427
|
+
name: z.string().describe("Agent slug — e.g. 'dev-convex-expert', 'dev-senior-dev'"),
|
|
428
|
+
},
|
|
429
|
+
async ({ name }) => {
|
|
430
|
+
const result = await convex.query(api.agentContentDb.getAgentContent, { name });
|
|
431
|
+
return {
|
|
432
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
433
|
+
};
|
|
434
|
+
},
|
|
435
|
+
);
|
|
436
|
+
|
|
437
|
+
server.tool(
|
|
438
|
+
"upsert_agent_content",
|
|
439
|
+
"Set or update the canonical agent content body in VantageRegistry. " +
|
|
440
|
+
"Computes sha256; if content is unchanged the call is idempotent (version not bumped). " +
|
|
441
|
+
"If content changed, patch version is bumped (X.Y.Z → X.Y.Z+1). " +
|
|
442
|
+
"First write sets version to '1.0.0'. " +
|
|
443
|
+
"If createIfMissing=true, auto-creates a placeholder agent row if it does not exist yet. " +
|
|
444
|
+
"Throws if the agent does not exist and createIfMissing is not set.",
|
|
445
|
+
{
|
|
446
|
+
name: z.string().describe("Agent slug — e.g. 'dev-convex-expert'"),
|
|
447
|
+
content: z.string().describe("Full agent content body (AGENT.md or similar)"),
|
|
448
|
+
createIfMissing: z.boolean().optional().default(false).describe("Auto-create a placeholder agent row if it does not exist (default: false)"),
|
|
449
|
+
},
|
|
450
|
+
async ({ name, content, createIfMissing }) => {
|
|
451
|
+
const result = await convex.mutation(api.agentContentDb.upsertAgentContent, {
|
|
452
|
+
name,
|
|
453
|
+
content,
|
|
454
|
+
createIfMissing,
|
|
455
|
+
});
|
|
456
|
+
return {
|
|
457
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
458
|
+
};
|
|
459
|
+
},
|
|
460
|
+
);
|
|
461
|
+
|
|
462
|
+
server.tool(
|
|
463
|
+
"detect_agent_drift",
|
|
464
|
+
"Return the VR-side sha256 hash and filePath for each agent in scope. " +
|
|
465
|
+
"IMPORTANT: Convex queries have no filesystem access. This tool returns the VR " +
|
|
466
|
+
"canonical hash + stored filePath so the caller can compute disk SHA256 and compare. " +
|
|
467
|
+
"scope='all' (default) returns every agent. name=<slug> filters to a single agent.",
|
|
468
|
+
{
|
|
469
|
+
name: z.string().optional().describe("Agent slug — filter to a single agent"),
|
|
470
|
+
scope: z.string().optional().describe("'all' to return every agent (default when name is omitted)"),
|
|
471
|
+
},
|
|
472
|
+
async ({ name, scope }) => {
|
|
473
|
+
const result = await convex.query(api.agentContentDb.detectAgentDrift, { name, scope });
|
|
474
|
+
return {
|
|
475
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
476
|
+
};
|
|
477
|
+
},
|
|
478
|
+
);
|
|
479
|
+
|
|
480
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
481
|
+
// PLUGIN CONTENT — VR Single-Source-of-Truth (D72-VR-SoT Phase A)
|
|
482
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
483
|
+
|
|
484
|
+
server.tool(
|
|
485
|
+
"get_plugin_content",
|
|
486
|
+
"Fetch the VR-hosted canonical plugin content body for a plugin. " +
|
|
487
|
+
"Returns content, sha256 hash, semver version, and last sync timestamp. " +
|
|
488
|
+
"Returns null fields if the plugin has not been hosted in VR yet.",
|
|
489
|
+
{
|
|
490
|
+
name: z.string().describe("Plugin slug — e.g. 'perello-bootstrap', 'perello-identity'"),
|
|
491
|
+
},
|
|
492
|
+
async ({ name }) => {
|
|
493
|
+
const result = await convex.query(api.pluginContentDb.getPluginContent, { name });
|
|
494
|
+
return {
|
|
495
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
496
|
+
};
|
|
497
|
+
},
|
|
498
|
+
);
|
|
499
|
+
|
|
500
|
+
server.tool(
|
|
501
|
+
"upsert_plugin_content",
|
|
502
|
+
"Set or update the canonical plugin content body in VantageRegistry. " +
|
|
503
|
+
"Computes sha256; if content is unchanged the call is idempotent (version not bumped). " +
|
|
504
|
+
"If content changed, patch version is bumped (X.Y.Z → X.Y.Z+1). " +
|
|
505
|
+
"First write sets version to '1.0.0'. " +
|
|
506
|
+
"If createIfMissing=true, auto-creates a placeholder plugin row if it does not exist yet. " +
|
|
507
|
+
"Throws if the plugin does not exist and createIfMissing is not set.",
|
|
508
|
+
{
|
|
509
|
+
name: z.string().describe("Plugin slug — e.g. 'perello-bootstrap'"),
|
|
510
|
+
content: z.string().describe("Full plugin content body"),
|
|
511
|
+
createIfMissing: z.boolean().optional().default(false).describe("Auto-create a placeholder plugin row if it does not exist (default: false)"),
|
|
512
|
+
},
|
|
513
|
+
async ({ name, content, createIfMissing }) => {
|
|
514
|
+
const result = await convex.mutation(api.pluginContentDb.upsertPluginContent, {
|
|
515
|
+
name,
|
|
516
|
+
content,
|
|
517
|
+
createIfMissing,
|
|
518
|
+
});
|
|
519
|
+
return {
|
|
520
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
521
|
+
};
|
|
522
|
+
},
|
|
523
|
+
);
|
|
524
|
+
|
|
525
|
+
server.tool(
|
|
526
|
+
"detect_plugin_drift",
|
|
527
|
+
"Return the VR-side sha256 hash and filePath for each plugin in scope. " +
|
|
528
|
+
"IMPORTANT: Convex queries have no filesystem access. This tool returns the VR " +
|
|
529
|
+
"canonical hash + stored filePath so the caller can compute disk SHA256 and compare. " +
|
|
530
|
+
"scope='all' (default) returns every plugin. name=<slug> filters to a single plugin.",
|
|
531
|
+
{
|
|
532
|
+
name: z.string().optional().describe("Plugin slug — filter to a single plugin"),
|
|
533
|
+
scope: z.string().optional().describe("'all' to return every plugin (default when name is omitted)"),
|
|
534
|
+
},
|
|
535
|
+
async ({ name, scope }) => {
|
|
536
|
+
const result = await convex.query(api.pluginContentDb.detectPluginDrift, { name, scope });
|
|
537
|
+
return {
|
|
538
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
539
|
+
};
|
|
540
|
+
},
|
|
541
|
+
);
|
|
542
|
+
|
|
543
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
544
|
+
// HOOK CONTENT — VR Single-Source-of-Truth (D72-VR-SoT Phase A)
|
|
545
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
546
|
+
|
|
547
|
+
server.tool(
|
|
548
|
+
"get_hook_content",
|
|
549
|
+
"Fetch the VR-hosted canonical hook content body for a hook. " +
|
|
550
|
+
"Returns content, sha256 hash, semver version, and last sync timestamp. " +
|
|
551
|
+
"Returns null fields if the hook has not been hosted in VR yet.",
|
|
552
|
+
{
|
|
553
|
+
name: z.string().describe("Hook slug — e.g. 'enforce-no-task-in-message', 'check-pii'"),
|
|
554
|
+
},
|
|
555
|
+
async ({ name }) => {
|
|
556
|
+
const result = await convex.query(api.hookContentDb.getHookContent, { name });
|
|
557
|
+
return {
|
|
558
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
559
|
+
};
|
|
560
|
+
},
|
|
561
|
+
);
|
|
562
|
+
|
|
563
|
+
server.tool(
|
|
564
|
+
"upsert_hook_content",
|
|
565
|
+
"Set or update the canonical hook content body in VantageRegistry. " +
|
|
566
|
+
"Computes sha256; if content is unchanged the call is idempotent (version not bumped). " +
|
|
567
|
+
"If content changed, patch version is bumped (X.Y.Z → X.Y.Z+1). " +
|
|
568
|
+
"First write sets version to '1.0.0'. " +
|
|
569
|
+
"If createIfMissing=true, auto-creates a placeholder hook row if it does not exist yet " +
|
|
570
|
+
"(event defaults to 'PreToolUse' — update via upsert_hook afterward). " +
|
|
571
|
+
"Throws if the hook does not exist and createIfMissing is not set.",
|
|
572
|
+
{
|
|
573
|
+
name: z.string().describe("Hook slug — e.g. 'enforce-no-task-in-message'"),
|
|
574
|
+
content: z.string().describe("Full hook script content"),
|
|
575
|
+
createIfMissing: z.boolean().optional().default(false).describe("Auto-create a placeholder hook row if it does not exist (default: false)"),
|
|
576
|
+
},
|
|
577
|
+
async ({ name, content, createIfMissing }) => {
|
|
578
|
+
const result = await convex.mutation(api.hookContentDb.upsertHookContent, {
|
|
579
|
+
name,
|
|
580
|
+
content,
|
|
581
|
+
createIfMissing,
|
|
582
|
+
});
|
|
583
|
+
return {
|
|
584
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
585
|
+
};
|
|
586
|
+
},
|
|
587
|
+
);
|
|
588
|
+
|
|
589
|
+
server.tool(
|
|
590
|
+
"detect_hook_drift",
|
|
591
|
+
"Return the VR-side sha256 hash and filePath for each hook in scope. " +
|
|
592
|
+
"IMPORTANT: Convex queries have no filesystem access. This tool returns the VR " +
|
|
593
|
+
"canonical hash + stored filePath so the caller can compute disk SHA256 and compare. " +
|
|
594
|
+
"scope='all' (default) returns every hook. name=<slug> filters to a single hook.",
|
|
595
|
+
{
|
|
596
|
+
name: z.string().optional().describe("Hook slug — filter to a single hook"),
|
|
597
|
+
scope: z.string().optional().describe("'all' to return every hook (default when name is omitted)"),
|
|
598
|
+
},
|
|
599
|
+
async ({ name, scope }) => {
|
|
600
|
+
const result = await convex.query(api.hookContentDb.detectHookDrift, { name, scope });
|
|
601
|
+
return {
|
|
602
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
603
|
+
};
|
|
604
|
+
},
|
|
605
|
+
);
|
|
606
|
+
|
|
607
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
608
|
+
// COMMAND CONTENT — VR Single-Source-of-Truth (D72-VR-SoT Phase A)
|
|
609
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
610
|
+
|
|
611
|
+
server.tool(
|
|
612
|
+
"get_command_content",
|
|
613
|
+
"Fetch the VR-hosted canonical command content body for a command. " +
|
|
614
|
+
"Returns content, sha256 hash, semver version, and last sync timestamp. " +
|
|
615
|
+
"Returns null fields if the command has not been hosted in VR yet.",
|
|
616
|
+
{
|
|
617
|
+
name: z.string().describe("Command slug — e.g. 'sync-workspace-from-vr', 'deploy-prod'"),
|
|
618
|
+
},
|
|
619
|
+
async ({ name }) => {
|
|
620
|
+
const result = await convex.query(api.commandContentDb.getCommandContent, { name });
|
|
621
|
+
return {
|
|
622
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
623
|
+
};
|
|
624
|
+
},
|
|
625
|
+
);
|
|
626
|
+
|
|
627
|
+
server.tool(
|
|
628
|
+
"upsert_command_content",
|
|
629
|
+
"Set or update the canonical command content body in VantageRegistry. " +
|
|
630
|
+
"Computes sha256; if content is unchanged the call is idempotent (version not bumped). " +
|
|
631
|
+
"If content changed, patch version is bumped (X.Y.Z → X.Y.Z+1). " +
|
|
632
|
+
"First write sets version to '1.0.0'. " +
|
|
633
|
+
"Auto-creates the command document if it does not exist yet.",
|
|
634
|
+
{
|
|
635
|
+
name: z.string().describe("Command slug — e.g. 'sync-workspace-from-vr'"),
|
|
636
|
+
content: z.string().describe("Full command script content"),
|
|
637
|
+
},
|
|
638
|
+
async ({ name, content }) => {
|
|
639
|
+
const result = await convex.mutation(api.commandContentDb.upsertCommandContent, {
|
|
640
|
+
name,
|
|
641
|
+
content,
|
|
642
|
+
});
|
|
643
|
+
return {
|
|
644
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
645
|
+
};
|
|
646
|
+
},
|
|
647
|
+
);
|
|
648
|
+
|
|
649
|
+
server.tool(
|
|
650
|
+
"detect_command_drift",
|
|
651
|
+
"Return the VR-side sha256 hash and filePath for each command in scope. " +
|
|
652
|
+
"IMPORTANT: Convex queries have no filesystem access. This tool returns the VR " +
|
|
653
|
+
"canonical hash + stored filePath so the caller can compute disk SHA256 and compare. " +
|
|
654
|
+
"scope='all' (default) returns every command. name=<slug> filters to a single command.",
|
|
655
|
+
{
|
|
656
|
+
name: z.string().optional().describe("Command slug — filter to a single command"),
|
|
657
|
+
scope: z.string().optional().describe("'all' to return every command (default when name is omitted)"),
|
|
658
|
+
},
|
|
659
|
+
async ({ name, scope }) => {
|
|
660
|
+
const result = await convex.query(api.commandContentDb.detectCommandDrift, { name, scope });
|
|
661
|
+
return {
|
|
662
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
663
|
+
};
|
|
664
|
+
},
|
|
665
|
+
);
|
|
666
|
+
|
|
667
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
668
|
+
// PLUGINS
|
|
669
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
670
|
+
|
|
671
|
+
server.tool(
|
|
672
|
+
"upsert_plugin",
|
|
673
|
+
"Create or update a plugin in VantageRegistry. Upserts by name.",
|
|
674
|
+
{
|
|
675
|
+
name: z.string().describe("Plugin name — e.g. 'perello-bootstrap', 'perello-identity'"),
|
|
676
|
+
description: z.string().describe("What this plugin provides"),
|
|
677
|
+
agentCount: z.number().int().describe("Number of agents in this plugin"),
|
|
678
|
+
skillCount: z.number().int().describe("Number of skills in this plugin"),
|
|
679
|
+
hookCount: z.number().int().describe("Number of hooks in this plugin"),
|
|
680
|
+
source: pluginSourceSchema,
|
|
681
|
+
status: pluginStatusSchema,
|
|
682
|
+
version: z.string().optional().describe("Semantic version — e.g. '1.0.0'"),
|
|
683
|
+
filePath: z.string().optional().describe("Plugin directory path"),
|
|
684
|
+
pricing: z.enum(["free", "paid"]).optional().describe("Pricing tier"),
|
|
685
|
+
price: z.number().optional().describe("Price in EUR (if paid)"),
|
|
686
|
+
license: z.string().optional().describe("License — e.g. 'MIT', 'proprietary'"),
|
|
687
|
+
publisherId: z.string().optional().describe("Publisher identifier"),
|
|
688
|
+
categories: z.array(z.string()).optional().describe("Categories beyond team name"),
|
|
689
|
+
},
|
|
690
|
+
async (args) => {
|
|
691
|
+
const id = await convex.mutation(api.plugins.upsert, args);
|
|
692
|
+
return {
|
|
693
|
+
content: [
|
|
694
|
+
{
|
|
695
|
+
type: "text",
|
|
696
|
+
text: JSON.stringify({ id, name: args.name }, null, 2),
|
|
697
|
+
},
|
|
698
|
+
],
|
|
699
|
+
};
|
|
700
|
+
},
|
|
701
|
+
);
|
|
702
|
+
|
|
703
|
+
server.tool(
|
|
704
|
+
"list_plugins",
|
|
705
|
+
"List all plugins in VantageRegistry. Optionally filter by status.",
|
|
706
|
+
{
|
|
707
|
+
status: pluginStatusSchema.optional().describe("Filter by status — omit to list all"),
|
|
708
|
+
},
|
|
709
|
+
async ({ status }) => {
|
|
710
|
+
const results = await convex.query(api.plugins.list, { status });
|
|
711
|
+
return {
|
|
712
|
+
content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
|
|
713
|
+
};
|
|
714
|
+
},
|
|
715
|
+
);
|
|
716
|
+
|
|
717
|
+
server.tool(
|
|
718
|
+
"get_plugin",
|
|
719
|
+
"Get a single plugin by its Convex document ID.",
|
|
720
|
+
{
|
|
721
|
+
id: z.string().describe("Convex document ID for the plugin"),
|
|
722
|
+
},
|
|
723
|
+
async ({ id }) => {
|
|
724
|
+
const result = await convex.query(api.plugins.get, { id: id as any });
|
|
725
|
+
return {
|
|
726
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
727
|
+
};
|
|
728
|
+
},
|
|
729
|
+
);
|
|
730
|
+
|
|
731
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
732
|
+
// HOOKS
|
|
733
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
734
|
+
|
|
735
|
+
server.tool(
|
|
736
|
+
"upsert_hook",
|
|
737
|
+
"Create or update a hook in VantageRegistry. Upserts by name.",
|
|
738
|
+
{
|
|
739
|
+
name: z.string().describe("Hook name — e.g. 'check-pii', 'enforce-skill-references'"),
|
|
740
|
+
event: hookEventSchema,
|
|
741
|
+
matcher: z.string().optional().describe("Tool matcher pattern — e.g. 'Bash', 'Agent'"),
|
|
742
|
+
description: z.string().describe("What this hook does"),
|
|
743
|
+
content: z.string().describe("Full hook script content"),
|
|
744
|
+
filePath: z.string().describe("File path relative to project root"),
|
|
745
|
+
registered: z.boolean().describe("Whether this hook is registered in settings.json"),
|
|
746
|
+
status: hookStatusSchema,
|
|
747
|
+
version: z.string().optional().describe("Semantic version — e.g. '1.0.0'"),
|
|
748
|
+
},
|
|
749
|
+
async (args) => {
|
|
750
|
+
const id = await convex.mutation(api.hooks.upsert, args);
|
|
751
|
+
return {
|
|
752
|
+
content: [
|
|
753
|
+
{
|
|
754
|
+
type: "text",
|
|
755
|
+
text: JSON.stringify({ id, name: args.name, event: args.event }, null, 2),
|
|
756
|
+
},
|
|
757
|
+
],
|
|
758
|
+
};
|
|
759
|
+
},
|
|
760
|
+
);
|
|
761
|
+
|
|
762
|
+
server.tool(
|
|
763
|
+
"list_hooks",
|
|
764
|
+
"List all hooks in VantageRegistry. Optionally filter by status.",
|
|
765
|
+
{
|
|
766
|
+
status: hookStatusSchema.optional().describe("Filter by status — omit to list all"),
|
|
767
|
+
},
|
|
768
|
+
async ({ status }) => {
|
|
769
|
+
const results = await convex.query(api.hooks.list, { status });
|
|
770
|
+
return {
|
|
771
|
+
content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
|
|
772
|
+
};
|
|
773
|
+
},
|
|
774
|
+
);
|
|
775
|
+
|
|
776
|
+
server.tool(
|
|
777
|
+
"get_hook",
|
|
778
|
+
"Get a single hook by its Convex document ID.",
|
|
779
|
+
{
|
|
780
|
+
id: z.string().describe("Convex document ID for the hook"),
|
|
781
|
+
},
|
|
782
|
+
async ({ id }) => {
|
|
783
|
+
const result = await convex.query(api.hooks.get, { id: id as any });
|
|
784
|
+
return {
|
|
785
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
786
|
+
};
|
|
787
|
+
},
|
|
788
|
+
);
|
|
789
|
+
|
|
790
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
791
|
+
// PROMPTS
|
|
792
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
793
|
+
|
|
794
|
+
server.tool(
|
|
795
|
+
"upsert_prompt",
|
|
796
|
+
"Create or update a prompt in VantageRegistry. Upserts by name.",
|
|
797
|
+
{
|
|
798
|
+
name: z.string().describe("Prompt name — e.g. 'system-prompt', 'review-checklist'"),
|
|
799
|
+
team: z.string().optional().describe("Team this prompt belongs to — defaults to 'global'"),
|
|
800
|
+
purpose: z.string().describe("What this prompt is used for"),
|
|
801
|
+
content: z.string().describe("Full prompt content"),
|
|
802
|
+
version: z.string().optional().describe("Semantic version — e.g. '1.0.0'"),
|
|
803
|
+
},
|
|
804
|
+
async (args) => {
|
|
805
|
+
const id = await convex.mutation(api.prompts.upsert, args);
|
|
806
|
+
return {
|
|
807
|
+
content: [
|
|
808
|
+
{
|
|
809
|
+
type: "text",
|
|
810
|
+
text: JSON.stringify({ id, name: args.name }, null, 2),
|
|
811
|
+
},
|
|
812
|
+
],
|
|
813
|
+
};
|
|
814
|
+
},
|
|
815
|
+
);
|
|
816
|
+
|
|
817
|
+
server.tool(
|
|
818
|
+
"list_prompts",
|
|
819
|
+
"List all prompts in VantageRegistry. Optionally filter by team.",
|
|
820
|
+
{
|
|
821
|
+
team: z.string().optional().describe("Filter by team — omit to list all"),
|
|
822
|
+
},
|
|
823
|
+
async ({ team }) => {
|
|
824
|
+
const results = await convex.query(api.prompts.list, { team });
|
|
825
|
+
return {
|
|
826
|
+
content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
|
|
827
|
+
};
|
|
828
|
+
},
|
|
829
|
+
);
|
|
830
|
+
|
|
831
|
+
server.tool(
|
|
832
|
+
"get_prompt",
|
|
833
|
+
"Get a single prompt by its Convex document ID.",
|
|
834
|
+
{
|
|
835
|
+
id: z.string().describe("Convex document ID for the prompt"),
|
|
836
|
+
},
|
|
837
|
+
async ({ id }) => {
|
|
838
|
+
const result = await convex.query(api.prompts.get, { id: id as any });
|
|
839
|
+
return {
|
|
840
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
841
|
+
};
|
|
842
|
+
},
|
|
843
|
+
);
|
|
844
|
+
|
|
845
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
846
|
+
// TEMPLATES
|
|
847
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
848
|
+
|
|
849
|
+
server.tool(
|
|
850
|
+
"upsert_template",
|
|
851
|
+
"Create or update a template in VantageRegistry. Upserts by name.",
|
|
852
|
+
{
|
|
853
|
+
name: z.string().describe("Template name — e.g. 'agent-brief', 'skill-md'"),
|
|
854
|
+
team: z.string().optional().describe("Team this template belongs to — defaults to 'global'"),
|
|
855
|
+
purpose: z.string().describe("What this template is used for"),
|
|
856
|
+
content: z.string().describe("Full template content"),
|
|
857
|
+
version: z.string().optional().describe("Semantic version — e.g. '1.0.0'"),
|
|
858
|
+
},
|
|
859
|
+
async (args) => {
|
|
860
|
+
const id = await convex.mutation(api.templates.upsert, args);
|
|
861
|
+
return {
|
|
862
|
+
content: [
|
|
863
|
+
{
|
|
864
|
+
type: "text",
|
|
865
|
+
text: JSON.stringify({ id, name: args.name }, null, 2),
|
|
866
|
+
},
|
|
867
|
+
],
|
|
868
|
+
};
|
|
869
|
+
},
|
|
870
|
+
);
|
|
871
|
+
|
|
872
|
+
server.tool(
|
|
873
|
+
"list_templates",
|
|
874
|
+
"List all templates in VantageRegistry. Optionally filter by team and/or template_type.",
|
|
875
|
+
{
|
|
876
|
+
team: z.string().optional().describe("Filter by team — omit to list all"),
|
|
877
|
+
template_type: templateTypeSchema.optional().describe("Filter by template type: mission, document, or checklist"),
|
|
878
|
+
},
|
|
879
|
+
async ({ team, template_type }) => {
|
|
880
|
+
const results = await convex.query(api.templates.list, { team, template_type });
|
|
881
|
+
return {
|
|
882
|
+
content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
|
|
883
|
+
};
|
|
884
|
+
},
|
|
885
|
+
);
|
|
886
|
+
|
|
887
|
+
server.tool(
|
|
888
|
+
"get_template",
|
|
889
|
+
"Get a single template by its Convex document ID.",
|
|
890
|
+
{
|
|
891
|
+
id: z.string().describe("Convex document ID for the template"),
|
|
892
|
+
},
|
|
893
|
+
async ({ id }) => {
|
|
894
|
+
const result = await convex.query(api.templates.get, { id: id as any });
|
|
895
|
+
return {
|
|
896
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
897
|
+
};
|
|
898
|
+
},
|
|
899
|
+
);
|
|
900
|
+
|
|
901
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
902
|
+
// SKILL TEST RUNS
|
|
903
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
904
|
+
|
|
905
|
+
server.tool(
|
|
906
|
+
"upsert_test_run",
|
|
907
|
+
"Insert a skill quality test run and update denormalized quality fields on the parent skill " +
|
|
908
|
+
"(lastTestedAt, lastReviewerScore, lastEvalDelta, testStatus). " +
|
|
909
|
+
"reviewerScore format is 'X/Y' e.g. '37/45'. evalDelta is pp delta vs without-skill baseline.",
|
|
910
|
+
{
|
|
911
|
+
skillId: z.string().describe("Convex document ID of the skill being tested"),
|
|
912
|
+
runDate: z.number().describe("Unix timestamp (ms) when the run was executed"),
|
|
913
|
+
runByOrchestrator: z.string().describe("Orchestrator that ran the test — e.g. 'alpha', 'pi'"),
|
|
914
|
+
runByMission: z.string().optional().describe("Mission reference — e.g. 'D70'"),
|
|
915
|
+
reviewerScore: z.string().describe("Score as 'X/Y' — e.g. '37/45'"),
|
|
916
|
+
evalDelta: z.number().describe("Percentage-point delta vs without-skill baseline"),
|
|
917
|
+
evalAbsoluteWith: z.number().describe("Absolute score with skill (0-100)"),
|
|
918
|
+
evalAbsoluteWithout: z.number().describe("Absolute score without skill (0-100)"),
|
|
919
|
+
runnerModel: z.string().describe("Model used to run the skill — e.g. 'claude-sonnet-4-6'"),
|
|
920
|
+
judgeModel: z.string().describe("Model used as judge — e.g. 'claude-haiku-4-5'"),
|
|
921
|
+
corpusVersion: z.string().describe("Eval corpus version used — e.g. '2.0.0'"),
|
|
922
|
+
harnessVersion: z.string().describe("Test harness version — e.g. '0.1.0'"),
|
|
923
|
+
skillSha: z.string().describe("Git SHA of the skill file at test time"),
|
|
924
|
+
gradingJsonPath: z.string().optional().describe("Path to grading output JSON"),
|
|
925
|
+
benchmarkJsonPath: z.string().optional().describe("Path to benchmark output JSON"),
|
|
926
|
+
reportPath: z.string().optional().describe("Path to human-readable report"),
|
|
927
|
+
timeSpentMinutes: z.number().optional().describe("Time spent running the test in minutes"),
|
|
928
|
+
notes: z.string().optional().describe("Free-form notes about this run"),
|
|
929
|
+
testStatus: z.enum(["untested", "tested", "needs_retest", "improving", "untestable"])
|
|
930
|
+
.optional()
|
|
931
|
+
.describe("Override test status — defaults to 'tested'"),
|
|
932
|
+
},
|
|
933
|
+
async (args) => {
|
|
934
|
+
const id = await convex.mutation(api.skillTestRuns.upsertTestRun, {
|
|
935
|
+
...args,
|
|
936
|
+
skillId: args.skillId as any,
|
|
937
|
+
});
|
|
938
|
+
return {
|
|
939
|
+
content: [{ type: "text", text: JSON.stringify({ id, skillId: args.skillId, runDate: args.runDate }, null, 2) }],
|
|
940
|
+
};
|
|
941
|
+
},
|
|
942
|
+
);
|
|
943
|
+
|
|
944
|
+
server.tool(
|
|
945
|
+
"get_skill_test_history",
|
|
946
|
+
"Return test run history for a skill, ordered newest-first.",
|
|
947
|
+
{
|
|
948
|
+
skillId: z.string().describe("Convex document ID of the skill"),
|
|
949
|
+
limit: z.number().optional().describe("Max number of runs to return — defaults to 50"),
|
|
950
|
+
},
|
|
951
|
+
async ({ skillId, limit }) => {
|
|
952
|
+
const results = await convex.query(api.skillTestRuns.getSkillTestHistory, {
|
|
953
|
+
skillId: skillId as any,
|
|
954
|
+
limit,
|
|
955
|
+
});
|
|
956
|
+
return {
|
|
957
|
+
content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
|
|
958
|
+
};
|
|
959
|
+
},
|
|
960
|
+
);
|
|
961
|
+
|
|
962
|
+
server.tool(
|
|
963
|
+
"list_skills_by_freshness",
|
|
964
|
+
"Return skills that have never been tested or were last tested more than staleDays ago. " +
|
|
965
|
+
"Use this to find skills that need a quality test run.",
|
|
966
|
+
{
|
|
967
|
+
staleDays: z.number().describe("Number of days since last test run to consider stale"),
|
|
968
|
+
},
|
|
969
|
+
async ({ staleDays }) => {
|
|
970
|
+
const results = await convex.query(api.skillTestRuns.listSkillsByFreshness, { staleDays });
|
|
971
|
+
return {
|
|
972
|
+
content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
|
|
973
|
+
};
|
|
974
|
+
},
|
|
975
|
+
);
|
|
976
|
+
|
|
977
|
+
server.tool(
|
|
978
|
+
"list_skills_below_threshold",
|
|
979
|
+
"Return skills whose last test run was below a score ratio (scoreMin as 0-1 fraction of 'X/Y') " +
|
|
980
|
+
"or below a delta threshold (deltaMin in pp). Omit a param to skip that filter.",
|
|
981
|
+
{
|
|
982
|
+
scoreMin: z.number().optional().describe("Minimum acceptable score ratio (0-1). Skills below this are returned."),
|
|
983
|
+
deltaMin: z.number().optional().describe("Minimum acceptable eval delta in pp. Skills below this are returned."),
|
|
984
|
+
},
|
|
985
|
+
async ({ scoreMin, deltaMin }) => {
|
|
986
|
+
const results = await convex.query(api.skillTestRuns.listSkillsBelowThreshold, { scoreMin, deltaMin });
|
|
987
|
+
return {
|
|
988
|
+
content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
|
|
989
|
+
};
|
|
990
|
+
},
|
|
991
|
+
);
|
|
992
|
+
|
|
993
|
+
server.tool(
|
|
994
|
+
"upsert_skill_eval_corpus",
|
|
995
|
+
"Insert or update an eval corpus version for a skill. Upserts by (skillId, version).",
|
|
996
|
+
{
|
|
997
|
+
skillId: z.string().describe("Convex document ID of the skill"),
|
|
998
|
+
version: z.string().describe("Corpus version — e.g. '2.0.0'"),
|
|
999
|
+
casesCount: z.number().int().describe("Number of eval cases"),
|
|
1000
|
+
assertionsCount: z.number().int().describe("Total number of assertions across all cases"),
|
|
1001
|
+
baselinePrompt: z.string().describe("The baseline prompt used without the skill"),
|
|
1002
|
+
evalsJsonContent: z.string().describe("Full JSON content of the evals file, or '{}' as placeholder"),
|
|
1003
|
+
authoredBy: z.string().describe("Orchestrator or person who authored this corpus"),
|
|
1004
|
+
authoredAt: z.number().describe("Unix timestamp (ms) when the corpus was authored"),
|
|
1005
|
+
supersedesVersion: z.string().optional().describe("Version this corpus supersedes — e.g. '1.0.0'"),
|
|
1006
|
+
},
|
|
1007
|
+
async (args) => {
|
|
1008
|
+
const id = await convex.mutation(api.skillEvalCorpus.upsertSkillEvalCorpus, {
|
|
1009
|
+
...args,
|
|
1010
|
+
skillId: args.skillId as any,
|
|
1011
|
+
});
|
|
1012
|
+
return {
|
|
1013
|
+
content: [{ type: "text", text: JSON.stringify({ id, skillId: args.skillId, version: args.version }, null, 2) }],
|
|
1014
|
+
};
|
|
1015
|
+
},
|
|
1016
|
+
);
|
|
1017
|
+
|
|
1018
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
1019
|
+
// RUNBOOKS
|
|
1020
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
1021
|
+
|
|
1022
|
+
const phaseSchema = z.object({
|
|
1023
|
+
name: z.string().describe("Phase name"),
|
|
1024
|
+
description: z.string().describe("What this phase accomplishes"),
|
|
1025
|
+
steps: z.array(z.string()).describe("Ordered list of steps in this phase"),
|
|
1026
|
+
});
|
|
1027
|
+
|
|
1028
|
+
const runbookInputSchema = z.object({
|
|
1029
|
+
name: z.string().describe("Input parameter name"),
|
|
1030
|
+
type: z.string().describe("Input type — e.g. 'string', 'boolean'"),
|
|
1031
|
+
required: z.boolean().describe("Whether this input is required"),
|
|
1032
|
+
description: z.string().describe("What this input controls"),
|
|
1033
|
+
});
|
|
1034
|
+
|
|
1035
|
+
const runbookOutputSchema = z.object({
|
|
1036
|
+
name: z.string().describe("Output artifact name"),
|
|
1037
|
+
description: z.string().describe("What this output contains"),
|
|
1038
|
+
path_pattern: z.string().optional().describe("File path pattern — e.g. 'reports/{name}.md'"),
|
|
1039
|
+
});
|
|
1040
|
+
|
|
1041
|
+
const applicabilitySchema = z.object({
|
|
1042
|
+
orchestrators: z.array(z.string()).describe("Orchestrator slugs this runbook applies to"),
|
|
1043
|
+
business_units: z.array(z.string()).describe("Business unit slugs this runbook applies to"),
|
|
1044
|
+
use_cases: z.array(z.string()).describe("Use case slugs this runbook applies to"),
|
|
1045
|
+
});
|
|
1046
|
+
|
|
1047
|
+
const linkedTemplateSchema = z.object({
|
|
1048
|
+
name: z.string().describe("Template name"),
|
|
1049
|
+
template_type: z.string().describe("Template type — mission, document, or checklist"),
|
|
1050
|
+
version: z.string().describe("Template version — e.g. '1.0.0'"),
|
|
1051
|
+
usage_phase: z.string().describe("Phase name where this template is used"),
|
|
1052
|
+
required: z.boolean().describe("Whether this template is required for the runbook"),
|
|
1053
|
+
description: z.string().describe("How this template is used in this runbook"),
|
|
1054
|
+
});
|
|
1055
|
+
|
|
1056
|
+
server.tool(
|
|
1057
|
+
"upsert_runbook",
|
|
1058
|
+
"Create or update a runbook in VantageRegistry. Upserts by name — if a runbook with the same name exists, it is updated.",
|
|
1059
|
+
{
|
|
1060
|
+
name: z.string().describe("Runbook slug — e.g. 'deploy-production', 'onboard-agent'"),
|
|
1061
|
+
description: z.string().describe("What this runbook accomplishes"),
|
|
1062
|
+
version: z.string().describe("Semantic version — e.g. '1.0.0'"),
|
|
1063
|
+
status: runbookStatusSchema,
|
|
1064
|
+
category: z.string().describe("Category — e.g. 'deployment', 'onboarding', 'quality'"),
|
|
1065
|
+
tags: z.array(z.string()).describe("Searchable tags"),
|
|
1066
|
+
content: z.string().describe("Full runbook content (markdown)"),
|
|
1067
|
+
phases: z.array(phaseSchema).describe("Ordered phases of the runbook"),
|
|
1068
|
+
inputs: z.array(runbookInputSchema).describe("Input parameters for the runbook"),
|
|
1069
|
+
outputs: z.array(runbookOutputSchema).describe("Output artifacts produced by the runbook"),
|
|
1070
|
+
applicability: applicabilitySchema,
|
|
1071
|
+
author: z.string().describe("Author slug — e.g. 'omega'"),
|
|
1072
|
+
team: z.string().describe("Owning team slug — e.g. 'core', 'dev'"),
|
|
1073
|
+
related_skills: z.array(z.string()).describe("Skill slugs referenced by this runbook"),
|
|
1074
|
+
related_agents: z.array(z.string()).describe("Agent slugs referenced by this runbook"),
|
|
1075
|
+
linked_templates: z.array(linkedTemplateSchema).describe("Templates used during this runbook"),
|
|
1076
|
+
},
|
|
1077
|
+
async (args) => {
|
|
1078
|
+
try {
|
|
1079
|
+
const result = await convex.mutation(api.runbooks.upsertRunbook, args);
|
|
1080
|
+
return {
|
|
1081
|
+
content: [{ type: "text", text: JSON.stringify({ success: true, data: result }, null, 2) }],
|
|
1082
|
+
};
|
|
1083
|
+
} catch (err) {
|
|
1084
|
+
return {
|
|
1085
|
+
content: [{ type: "text", text: JSON.stringify({ success: false, error: String(err) }, null, 2) }],
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
},
|
|
1089
|
+
);
|
|
1090
|
+
|
|
1091
|
+
server.tool(
|
|
1092
|
+
"get_runbook",
|
|
1093
|
+
"Get a single runbook by name. Optionally guard by exact version.",
|
|
1094
|
+
{
|
|
1095
|
+
name: z.string().describe("Runbook slug — e.g. 'deploy-production'"),
|
|
1096
|
+
version: z.string().optional().describe("If provided, returns null when stored version differs"),
|
|
1097
|
+
},
|
|
1098
|
+
async ({ name, version }) => {
|
|
1099
|
+
try {
|
|
1100
|
+
const result = await convex.query(api.runbooks.getRunbook, { name, version });
|
|
1101
|
+
return {
|
|
1102
|
+
content: [{ type: "text", text: JSON.stringify({ success: true, data: result }, null, 2) }],
|
|
1103
|
+
};
|
|
1104
|
+
} catch (err) {
|
|
1105
|
+
return {
|
|
1106
|
+
content: [{ type: "text", text: JSON.stringify({ success: false, error: String(err) }, null, 2) }],
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
1109
|
+
},
|
|
1110
|
+
);
|
|
1111
|
+
|
|
1112
|
+
server.tool(
|
|
1113
|
+
"list_runbooks",
|
|
1114
|
+
"List runbooks in VantageRegistry. Defaults to published status. Supports applicability filters.",
|
|
1115
|
+
{
|
|
1116
|
+
status: runbookStatusSchema.optional().describe("Filter by status — defaults to 'published'"),
|
|
1117
|
+
category: z.string().optional().describe("Filter by category — e.g. 'deployment'"),
|
|
1118
|
+
team: z.string().optional().describe("Filter by owning team"),
|
|
1119
|
+
applicability_orchestrator: z.string().optional().describe("Filter by orchestrator slug in applicability"),
|
|
1120
|
+
applicability_bu: z.string().optional().describe("Filter by business unit slug in applicability"),
|
|
1121
|
+
applicability_use_case: z.string().optional().describe("Filter by use case slug in applicability"),
|
|
1122
|
+
limit: z.number().int().optional().describe("Max results to return — defaults to 50, max 200"),
|
|
1123
|
+
},
|
|
1124
|
+
async (args) => {
|
|
1125
|
+
try {
|
|
1126
|
+
const result = await convex.query(api.runbooks.listRunbooks, args);
|
|
1127
|
+
return {
|
|
1128
|
+
content: [{ type: "text", text: JSON.stringify({ success: true, data: result }, null, 2) }],
|
|
1129
|
+
};
|
|
1130
|
+
} catch (err) {
|
|
1131
|
+
return {
|
|
1132
|
+
content: [{ type: "text", text: JSON.stringify({ success: false, error: String(err) }, null, 2) }],
|
|
1133
|
+
};
|
|
1134
|
+
}
|
|
1135
|
+
},
|
|
1136
|
+
);
|
|
1137
|
+
|
|
1138
|
+
server.tool(
|
|
1139
|
+
"list_runbooks_by_category",
|
|
1140
|
+
"List runbooks for a specific category using the byCategory index.",
|
|
1141
|
+
{
|
|
1142
|
+
category: z.string().describe("Category to filter by — e.g. 'deployment', 'quality'"),
|
|
1143
|
+
status: runbookStatusSchema.optional().describe("Filter by status — omit to list all statuses in this category"),
|
|
1144
|
+
},
|
|
1145
|
+
async ({ category, status }) => {
|
|
1146
|
+
try {
|
|
1147
|
+
const result = await convex.query(api.runbooks.listRunbooksByCategory, { category, status });
|
|
1148
|
+
return {
|
|
1149
|
+
content: [{ type: "text", text: JSON.stringify({ success: true, data: result }, null, 2) }],
|
|
1150
|
+
};
|
|
1151
|
+
} catch (err) {
|
|
1152
|
+
return {
|
|
1153
|
+
content: [{ type: "text", text: JSON.stringify({ success: false, error: String(err) }, null, 2) }],
|
|
1154
|
+
};
|
|
1155
|
+
}
|
|
1156
|
+
},
|
|
1157
|
+
);
|
|
1158
|
+
|
|
1159
|
+
server.tool(
|
|
1160
|
+
"list_runbooks_by_team",
|
|
1161
|
+
"List runbooks for a specific team using the byTeam index.",
|
|
1162
|
+
{
|
|
1163
|
+
team: z.string().describe("Team slug to filter by — e.g. 'core', 'dev'"),
|
|
1164
|
+
status: runbookStatusSchema.optional().describe("Filter by status — omit to list all statuses for this team"),
|
|
1165
|
+
},
|
|
1166
|
+
async ({ team, status }) => {
|
|
1167
|
+
try {
|
|
1168
|
+
const result = await convex.query(api.runbooks.listRunbooksByTeam, { team, status });
|
|
1169
|
+
return {
|
|
1170
|
+
content: [{ type: "text", text: JSON.stringify({ success: true, data: result }, null, 2) }],
|
|
1171
|
+
};
|
|
1172
|
+
} catch (err) {
|
|
1173
|
+
return {
|
|
1174
|
+
content: [{ type: "text", text: JSON.stringify({ success: false, error: String(err) }, null, 2) }],
|
|
1175
|
+
};
|
|
1176
|
+
}
|
|
1177
|
+
},
|
|
1178
|
+
);
|
|
1179
|
+
|
|
1180
|
+
server.tool(
|
|
1181
|
+
"delete_runbook",
|
|
1182
|
+
"Soft-delete a runbook by name — sets status to 'deprecated'. Returns {deleted: true} if found, {deleted: false} if not found.",
|
|
1183
|
+
{
|
|
1184
|
+
name: z.string().describe("Runbook slug to soft-delete"),
|
|
1185
|
+
},
|
|
1186
|
+
async ({ name }) => {
|
|
1187
|
+
try {
|
|
1188
|
+
const result = await convex.mutation(api.runbooks.deleteRunbook, { name });
|
|
1189
|
+
return {
|
|
1190
|
+
content: [{ type: "text", text: JSON.stringify({ success: true, data: result }, null, 2) }],
|
|
1191
|
+
};
|
|
1192
|
+
} catch (err) {
|
|
1193
|
+
return {
|
|
1194
|
+
content: [{ type: "text", text: JSON.stringify({ success: false, error: String(err) }, null, 2) }],
|
|
1195
|
+
};
|
|
1196
|
+
}
|
|
1197
|
+
},
|
|
1198
|
+
);
|
|
1199
|
+
|
|
1200
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
1201
|
+
// STATS
|
|
1202
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
1203
|
+
|
|
1204
|
+
server.tool(
|
|
1205
|
+
"get_stats",
|
|
1206
|
+
"Get counts per table in VantageRegistry — teams, agents, skills, plugins, hooks, and runbooks. " +
|
|
1207
|
+
"Runbooks include breakdown by_status (draft/published/deprecated) and by_category. " +
|
|
1208
|
+
"Use this to get a quick overview of the registry contents.",
|
|
1209
|
+
{},
|
|
1210
|
+
async () => {
|
|
1211
|
+
const [teams, agents, skills, plugins, hooks, rbDraft, rbPublished, rbDeprecated] = await Promise.all([
|
|
1212
|
+
convex.query(api.teams.list, {}),
|
|
1213
|
+
convex.query(api.agents.list, {}),
|
|
1214
|
+
convex.query(api.skills.list, {}),
|
|
1215
|
+
convex.query(api.plugins.list, {}),
|
|
1216
|
+
convex.query(api.hooks.list, {}),
|
|
1217
|
+
convex.query(api.runbooks.listRunbooks, { status: "draft", limit: 1000 }),
|
|
1218
|
+
convex.query(api.runbooks.listRunbooks, { status: "published", limit: 1000 }),
|
|
1219
|
+
convex.query(api.runbooks.listRunbooks, { status: "deprecated", limit: 1000 }),
|
|
1220
|
+
]);
|
|
1221
|
+
|
|
1222
|
+
// Build by_category map across all statuses
|
|
1223
|
+
const byCategoryMap: Record<string, number> = {};
|
|
1224
|
+
for (const rb of [...rbDraft, ...rbPublished, ...rbDeprecated]) {
|
|
1225
|
+
byCategoryMap[rb.category] = (byCategoryMap[rb.category] ?? 0) + 1;
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
const runbooksTotal = rbDraft.length + rbPublished.length + rbDeprecated.length;
|
|
1229
|
+
|
|
1230
|
+
const stats = {
|
|
1231
|
+
teams: teams.length,
|
|
1232
|
+
agents: agents.length,
|
|
1233
|
+
skills: skills.length,
|
|
1234
|
+
plugins: plugins.length,
|
|
1235
|
+
hooks: hooks.length,
|
|
1236
|
+
runbooks_count: {
|
|
1237
|
+
total: runbooksTotal,
|
|
1238
|
+
by_status: {
|
|
1239
|
+
draft: rbDraft.length,
|
|
1240
|
+
published: rbPublished.length,
|
|
1241
|
+
deprecated: rbDeprecated.length,
|
|
1242
|
+
},
|
|
1243
|
+
by_category: byCategoryMap,
|
|
1244
|
+
},
|
|
1245
|
+
total: teams.length + agents.length + skills.length + plugins.length + hooks.length + runbooksTotal,
|
|
1246
|
+
};
|
|
1247
|
+
|
|
1248
|
+
return {
|
|
1249
|
+
content: [{ type: "text", text: JSON.stringify(stats, null, 2) }],
|
|
1250
|
+
};
|
|
1251
|
+
},
|
|
1252
|
+
);
|
|
1253
|
+
|
|
1254
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1255
|
+
// Start
|
|
1256
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1257
|
+
|
|
1258
|
+
async function main() {
|
|
1259
|
+
const transport = new StdioServerTransport();
|
|
1260
|
+
await server.connect(transport);
|
|
1261
|
+
console.error("VantageRegistry MCP server running on stdio");
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
main().catch((err) => {
|
|
1265
|
+
console.error("Fatal:", err);
|
|
1266
|
+
process.exit(1);
|
|
1267
|
+
});
|