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