@vantageos/vantage-registry-mcp 1.6.0 → 1.7.0

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