@bodhi-ventures/aiocs 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,720 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ AIOCS_ERROR_CODES,
4
+ AiocsError,
5
+ backfillEmbeddings,
6
+ clearEmbeddings,
7
+ diffSnapshotsForSource,
8
+ exportCatalogBackup,
9
+ fetchSources,
10
+ getDoctorReport,
11
+ getEmbeddingStatus,
12
+ importCatalogBackup,
13
+ initBuiltInSources,
14
+ linkProjectSources,
15
+ listSnapshotsForSource,
16
+ listSources,
17
+ packageDescription,
18
+ packageName,
19
+ packageVersion,
20
+ refreshDueSources,
21
+ runEmbeddingWorker,
22
+ runSourceCanaries,
23
+ searchCatalog,
24
+ showChunk,
25
+ toAiocsError,
26
+ unlinkProjectSources,
27
+ upsertSourceFromSpecFile,
28
+ verifyCoverage
29
+ } from "./chunk-ID3PUSMY.js";
30
+
31
+ // src/mcp-server.ts
32
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
33
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
34
+ import { z } from "zod";
35
+ var doctorCheckSchema = z.object({
36
+ id: z.string(),
37
+ status: z.enum(["pass", "warn", "fail"]),
38
+ summary: z.string(),
39
+ details: z.record(z.string(), z.unknown()).optional()
40
+ });
41
+ var doctorReportSchema = z.object({
42
+ summary: z.object({
43
+ status: z.enum(["healthy", "degraded", "unhealthy"]),
44
+ checkCount: z.number().int().nonnegative(),
45
+ passCount: z.number().int().nonnegative(),
46
+ warnCount: z.number().int().nonnegative(),
47
+ failCount: z.number().int().nonnegative()
48
+ }),
49
+ checks: z.array(doctorCheckSchema)
50
+ });
51
+ var sourceSchema = z.object({
52
+ id: z.string(),
53
+ label: z.string(),
54
+ specPath: z.string().nullable(),
55
+ nextDueAt: z.string(),
56
+ isDue: z.boolean(),
57
+ nextCanaryDueAt: z.string().nullable(),
58
+ isCanaryDue: z.boolean(),
59
+ lastCheckedAt: z.string().nullable(),
60
+ lastSuccessfulSnapshotAt: z.string().nullable(),
61
+ lastSuccessfulSnapshotId: z.string().nullable(),
62
+ lastCanaryCheckedAt: z.string().nullable(),
63
+ lastSuccessfulCanaryAt: z.string().nullable(),
64
+ lastCanaryStatus: z.enum(["pass", "fail"]).nullable()
65
+ });
66
+ var fetchResultSchema = z.object({
67
+ sourceId: z.string(),
68
+ snapshotId: z.string(),
69
+ pageCount: z.number().int().nonnegative(),
70
+ reused: z.boolean()
71
+ });
72
+ var searchResultSchema = z.object({
73
+ chunkId: z.number().int().nonnegative(),
74
+ sourceId: z.string(),
75
+ snapshotId: z.string(),
76
+ pageUrl: z.string(),
77
+ pageTitle: z.string(),
78
+ sectionTitle: z.string(),
79
+ markdown: z.string(),
80
+ score: z.number().optional(),
81
+ signals: z.array(z.enum(["lexical", "vector"])).optional()
82
+ });
83
+ var mcpErrorSchema = z.object({
84
+ code: z.string(),
85
+ message: z.string(),
86
+ details: z.unknown().optional()
87
+ });
88
+ var coverageVerificationSchema = z.object({
89
+ sourceId: z.string(),
90
+ snapshotId: z.string(),
91
+ complete: z.boolean(),
92
+ summary: z.object({
93
+ fileCount: z.number().int().nonnegative(),
94
+ headingCount: z.number().int().nonnegative(),
95
+ matchedHeadingCount: z.number().int().nonnegative(),
96
+ missingHeadingCount: z.number().int().nonnegative(),
97
+ matchCounts: z.object({
98
+ pageTitle: z.number().int().nonnegative(),
99
+ sectionTitle: z.number().int().nonnegative(),
100
+ body: z.number().int().nonnegative()
101
+ })
102
+ }),
103
+ files: z.array(z.object({
104
+ referenceFile: z.string(),
105
+ headingCount: z.number().int().nonnegative(),
106
+ matchedHeadingCount: z.number().int().nonnegative(),
107
+ missingHeadingCount: z.number().int().nonnegative(),
108
+ missingHeadings: z.array(z.string()),
109
+ matchCounts: z.object({
110
+ pageTitle: z.number().int().nonnegative(),
111
+ sectionTitle: z.number().int().nonnegative(),
112
+ body: z.number().int().nonnegative()
113
+ })
114
+ }))
115
+ });
116
+ var canaryResultSchema = z.object({
117
+ sourceId: z.string(),
118
+ status: z.enum(["pass", "fail"]),
119
+ checkedAt: z.string(),
120
+ summary: z.object({
121
+ checkCount: z.number().int().nonnegative(),
122
+ passCount: z.number().int().nonnegative(),
123
+ failCount: z.number().int().nonnegative()
124
+ }),
125
+ checks: z.array(z.object({
126
+ url: z.string(),
127
+ status: z.enum(["pass", "fail"]),
128
+ title: z.string().optional(),
129
+ markdownLength: z.number().int().nonnegative().optional(),
130
+ errorMessage: z.string().optional()
131
+ }))
132
+ });
133
+ var snapshotDiffSchema = z.object({
134
+ sourceId: z.string(),
135
+ fromSnapshotId: z.string(),
136
+ toSnapshotId: z.string(),
137
+ summary: z.object({
138
+ addedPageCount: z.number().int().nonnegative(),
139
+ removedPageCount: z.number().int().nonnegative(),
140
+ changedPageCount: z.number().int().nonnegative(),
141
+ unchangedPageCount: z.number().int().nonnegative()
142
+ }),
143
+ addedPages: z.array(z.object({
144
+ url: z.string(),
145
+ title: z.string()
146
+ })),
147
+ removedPages: z.array(z.object({
148
+ url: z.string(),
149
+ title: z.string()
150
+ })),
151
+ changedPages: z.array(z.object({
152
+ url: z.string(),
153
+ beforeTitle: z.string(),
154
+ afterTitle: z.string(),
155
+ lineSummary: z.object({
156
+ addedLineCount: z.number().int().nonnegative(),
157
+ removedLineCount: z.number().int().nonnegative()
158
+ })
159
+ }))
160
+ });
161
+ var backupManifestSchema = z.object({
162
+ formatVersion: z.literal(1),
163
+ createdAt: z.string(),
164
+ packageVersion: z.string(),
165
+ entries: z.array(z.object({
166
+ relativePath: z.string(),
167
+ type: z.enum(["file", "directory"]),
168
+ size: z.number().int().nonnegative()
169
+ }))
170
+ });
171
+ var embeddingStatusSchema = z.object({
172
+ queue: z.object({
173
+ pendingJobs: z.number().int().nonnegative(),
174
+ runningJobs: z.number().int().nonnegative(),
175
+ failedJobs: z.number().int().nonnegative()
176
+ }),
177
+ sources: z.array(z.object({
178
+ sourceId: z.string(),
179
+ snapshotId: z.string().nullable(),
180
+ totalChunks: z.number().int().nonnegative(),
181
+ indexedChunks: z.number().int().nonnegative(),
182
+ pendingChunks: z.number().int().nonnegative(),
183
+ failedChunks: z.number().int().nonnegative(),
184
+ staleChunks: z.number().int().nonnegative(),
185
+ coverageRatio: z.number()
186
+ }))
187
+ });
188
+ var server = new McpServer({
189
+ name: packageName,
190
+ version: packageVersion,
191
+ title: "aiocs MCP server"
192
+ }, {
193
+ instructions: `${packageDescription} Prefer these tools before live browsing when supported or already-fetched docs may exist locally. Check source_list before assuming a source is missing or stale. Use search mode auto by default, lexical for exact identifiers, and refresh_due for targeted freshness checks before force fetch. Avoid fetch all as a normal answering path, use batch to reduce repeated round trips, and cite sourceId, snapshotId, and pageUrl when returning results.`
194
+ });
195
+ var toolInputSchemas = /* @__PURE__ */ new Map();
196
+ function asToolResult(data) {
197
+ const structuredContent = {
198
+ ok: true,
199
+ data
200
+ };
201
+ return {
202
+ structuredContent,
203
+ content: [
204
+ {
205
+ type: "text",
206
+ text: JSON.stringify(structuredContent, null, 2)
207
+ }
208
+ ]
209
+ };
210
+ }
211
+ function asToolError(error) {
212
+ const normalized = toAiocsError(error);
213
+ const structuredContent = {
214
+ ok: false,
215
+ error: {
216
+ code: normalized.code,
217
+ message: normalized.message,
218
+ ...typeof normalized.details !== "undefined" ? { details: normalized.details } : {}
219
+ }
220
+ };
221
+ return {
222
+ isError: true,
223
+ structuredContent,
224
+ content: [
225
+ {
226
+ type: "text",
227
+ text: JSON.stringify(structuredContent, null, 2)
228
+ }
229
+ ]
230
+ };
231
+ }
232
+ var toolHandlers = {
233
+ version: async () => ({
234
+ name: packageName,
235
+ version: packageVersion
236
+ }),
237
+ doctor: async () => getDoctorReport(),
238
+ init: async (args = {}) => initBuiltInSources({
239
+ ...typeof args.fetch === "boolean" ? { fetch: args.fetch } : {}
240
+ }),
241
+ source_upsert: async (args = {}) => upsertSourceFromSpecFile(args.specFile),
242
+ source_list: async () => listSources(),
243
+ fetch: async (args = {}) => fetchSources(args.sourceIdOrAll),
244
+ canary: async (args = {}) => runSourceCanaries(args.sourceIdOrAll),
245
+ refresh_due: async (args = {}) => refreshDueSources(args.sourceIdOrAll ?? "all"),
246
+ snapshot_list: async (args = {}) => listSnapshotsForSource(args.sourceId),
247
+ diff_snapshots: async (args = {}) => diffSnapshotsForSource({
248
+ sourceId: args.sourceId,
249
+ ...typeof args.fromSnapshotId === "string" ? { fromSnapshotId: args.fromSnapshotId } : {},
250
+ ...typeof args.toSnapshotId === "string" ? { toSnapshotId: args.toSnapshotId } : {}
251
+ }),
252
+ project_link: async (args = {}) => linkProjectSources(args.projectPath, args.sourceIds),
253
+ project_unlink: async (args = {}) => unlinkProjectSources(args.projectPath, args.sourceIds ?? []),
254
+ search: async (args = {}) => searchCatalog(args.query, {
255
+ source: args.sourceIds ?? [],
256
+ ...typeof args.snapshotId === "string" ? { snapshot: args.snapshotId } : {},
257
+ ...typeof args.all === "boolean" ? { all: args.all } : {},
258
+ ...typeof args.project === "string" ? { project: args.project } : {},
259
+ ...typeof args.mode === "string" ? { mode: args.mode } : {},
260
+ ...typeof args.limit === "number" ? { limit: args.limit } : {},
261
+ ...typeof args.offset === "number" ? { offset: args.offset } : {}
262
+ }),
263
+ show: async (args = {}) => showChunk(args.chunkId),
264
+ embeddings_status: async () => getEmbeddingStatus(),
265
+ embeddings_backfill: async (args = {}) => backfillEmbeddings(args.sourceIdOrAll),
266
+ embeddings_clear: async (args = {}) => clearEmbeddings(args.sourceIdOrAll),
267
+ embeddings_run: async () => runEmbeddingWorker(),
268
+ backup_export: async (args = {}) => exportCatalogBackup({
269
+ outputDir: args.outputDir,
270
+ ...typeof args.replaceExisting === "boolean" ? { replaceExisting: args.replaceExisting } : {}
271
+ }),
272
+ backup_import: async (args = {}) => importCatalogBackup({
273
+ inputDir: args.inputDir,
274
+ ...typeof args.replaceExisting === "boolean" ? { replaceExisting: args.replaceExisting } : {}
275
+ }),
276
+ verify_coverage: async (args = {}) => verifyCoverage({
277
+ sourceId: args.sourceId,
278
+ referenceFiles: args.referenceFiles,
279
+ ...typeof args.snapshotId === "string" ? { snapshotId: args.snapshotId } : {}
280
+ })
281
+ };
282
+ var batchableToolNames = Object.keys(toolHandlers);
283
+ async function runToolHandler(name, args) {
284
+ return toolHandlers[name](args);
285
+ }
286
+ function registerAiocsTool(name, config) {
287
+ toolInputSchemas.set(name, config.inputSchema);
288
+ server.registerTool(
289
+ name,
290
+ {
291
+ ...config,
292
+ outputSchema: z.object({
293
+ ok: z.boolean(),
294
+ data: config.outputSchema.optional(),
295
+ error: mcpErrorSchema.optional()
296
+ })
297
+ },
298
+ async (args) => {
299
+ try {
300
+ return asToolResult(await runToolHandler(name, args));
301
+ } catch (error) {
302
+ return asToolError(error);
303
+ }
304
+ }
305
+ );
306
+ }
307
+ function formatZodIssues(error) {
308
+ return error.issues.map((issue) => {
309
+ const path = issue.path.length > 0 ? issue.path.join(".") : "input";
310
+ return `${path}: ${issue.message}`;
311
+ }).join("; ");
312
+ }
313
+ function validateBatchOperationArguments(tool, args) {
314
+ const schema = toolInputSchemas.get(tool);
315
+ if (!schema) {
316
+ return args;
317
+ }
318
+ const parsed = schema.safeParse(args);
319
+ if (!parsed.success) {
320
+ throw new AiocsError(
321
+ AIOCS_ERROR_CODES.invalidArgument,
322
+ `Invalid arguments for tool ${tool}: ${formatZodIssues(parsed.error)}`,
323
+ {
324
+ issues: parsed.error.issues
325
+ }
326
+ );
327
+ }
328
+ return parsed.data;
329
+ }
330
+ registerAiocsTool(
331
+ "version",
332
+ {
333
+ title: "Version",
334
+ description: "Return the current aiocs package name and version.",
335
+ outputSchema: z.object({
336
+ name: z.string(),
337
+ version: z.string()
338
+ })
339
+ }
340
+ );
341
+ registerAiocsTool(
342
+ "doctor",
343
+ {
344
+ title: "Doctor",
345
+ description: "Validate catalog, Playwright, daemon config, source-spec directories, freshness, embeddings, Qdrant, Ollama, and Docker readiness.",
346
+ outputSchema: doctorReportSchema
347
+ }
348
+ );
349
+ registerAiocsTool(
350
+ "init",
351
+ {
352
+ title: "Init",
353
+ description: "Bootstrap the bundled built-in source specs and optionally fetch them.",
354
+ inputSchema: z.object({
355
+ fetch: z.boolean().optional()
356
+ }),
357
+ outputSchema: z.object({
358
+ sourceSpecDir: z.string(),
359
+ userSourceDir: z.string(),
360
+ fetched: z.boolean(),
361
+ initializedSources: z.array(z.object({
362
+ sourceId: z.string(),
363
+ specPath: z.string(),
364
+ configHash: z.string(),
365
+ configChanged: z.boolean()
366
+ })),
367
+ removedSourceIds: z.array(z.string()),
368
+ fetchResults: z.array(fetchResultSchema)
369
+ })
370
+ }
371
+ );
372
+ registerAiocsTool(
373
+ "source_upsert",
374
+ {
375
+ title: "Source upsert",
376
+ description: "Load or update a source spec file in the local catalog.",
377
+ inputSchema: z.object({
378
+ specFile: z.string()
379
+ }),
380
+ outputSchema: z.object({
381
+ sourceId: z.string(),
382
+ configHash: z.string(),
383
+ specPath: z.string()
384
+ })
385
+ }
386
+ );
387
+ registerAiocsTool(
388
+ "source_list",
389
+ {
390
+ title: "Source list",
391
+ description: "List all registered documentation sources.",
392
+ outputSchema: z.object({
393
+ sources: z.array(sourceSchema)
394
+ })
395
+ }
396
+ );
397
+ registerAiocsTool(
398
+ "fetch",
399
+ {
400
+ title: "Fetch",
401
+ description: "Fetch one source or all registered sources and record a snapshot.",
402
+ inputSchema: z.object({
403
+ sourceIdOrAll: z.string()
404
+ }),
405
+ outputSchema: z.object({
406
+ results: z.array(fetchResultSchema)
407
+ })
408
+ }
409
+ );
410
+ registerAiocsTool(
411
+ "canary",
412
+ {
413
+ title: "Canary",
414
+ description: "Run lightweight source extraction canaries without creating snapshots.",
415
+ inputSchema: z.object({
416
+ sourceIdOrAll: z.string()
417
+ }),
418
+ outputSchema: z.object({
419
+ results: z.array(canaryResultSchema)
420
+ })
421
+ }
422
+ );
423
+ registerAiocsTool(
424
+ "refresh_due",
425
+ {
426
+ title: "Refresh due",
427
+ description: "Fetch all due sources, or refresh one specific source only if it is currently due.",
428
+ inputSchema: z.object({
429
+ sourceIdOrAll: z.string().optional()
430
+ }),
431
+ outputSchema: z.object({
432
+ results: z.array(fetchResultSchema)
433
+ })
434
+ }
435
+ );
436
+ registerAiocsTool(
437
+ "snapshot_list",
438
+ {
439
+ title: "Snapshot list",
440
+ description: "List recorded snapshots for a source.",
441
+ inputSchema: z.object({
442
+ sourceId: z.string()
443
+ }),
444
+ outputSchema: z.object({
445
+ sourceId: z.string(),
446
+ snapshots: z.array(z.object({
447
+ snapshotId: z.string(),
448
+ sourceId: z.string(),
449
+ detectedVersion: z.string().nullable(),
450
+ createdAt: z.string(),
451
+ pageCount: z.number().int().nonnegative()
452
+ }))
453
+ })
454
+ }
455
+ );
456
+ registerAiocsTool(
457
+ "diff_snapshots",
458
+ {
459
+ title: "Diff snapshots",
460
+ description: "Compare two source snapshots and report added, removed, and changed pages.",
461
+ inputSchema: z.object({
462
+ sourceId: z.string(),
463
+ fromSnapshotId: z.string().optional(),
464
+ toSnapshotId: z.string().optional()
465
+ }),
466
+ outputSchema: snapshotDiffSchema
467
+ }
468
+ );
469
+ registerAiocsTool(
470
+ "project_link",
471
+ {
472
+ title: "Project link",
473
+ description: "Link one or more sources to a project path for scoped search.",
474
+ inputSchema: z.object({
475
+ projectPath: z.string(),
476
+ sourceIds: z.array(z.string()).min(1)
477
+ }),
478
+ outputSchema: z.object({
479
+ projectPath: z.string(),
480
+ sourceIds: z.array(z.string())
481
+ })
482
+ }
483
+ );
484
+ registerAiocsTool(
485
+ "project_unlink",
486
+ {
487
+ title: "Project unlink",
488
+ description: "Remove one or more source links from a project path.",
489
+ inputSchema: z.object({
490
+ projectPath: z.string(),
491
+ sourceIds: z.array(z.string()).optional()
492
+ }),
493
+ outputSchema: z.object({
494
+ projectPath: z.string(),
495
+ sourceIds: z.array(z.string())
496
+ })
497
+ }
498
+ );
499
+ registerAiocsTool(
500
+ "search",
501
+ {
502
+ title: "Search",
503
+ description: "Search the shared aiocs catalog by query and optional scope filters.",
504
+ inputSchema: z.object({
505
+ query: z.string(),
506
+ sourceIds: z.array(z.string()).optional(),
507
+ snapshotId: z.string().optional(),
508
+ all: z.boolean().optional(),
509
+ project: z.string().optional(),
510
+ mode: z.enum(["auto", "lexical", "hybrid", "semantic"]).optional(),
511
+ limit: z.number().int().positive().optional(),
512
+ offset: z.number().int().nonnegative().optional()
513
+ }),
514
+ outputSchema: z.object({
515
+ query: z.string(),
516
+ total: z.number().int().nonnegative(),
517
+ limit: z.number().int().positive(),
518
+ offset: z.number().int().nonnegative(),
519
+ hasMore: z.boolean(),
520
+ modeRequested: z.enum(["auto", "lexical", "hybrid", "semantic"]),
521
+ modeUsed: z.enum(["lexical", "hybrid", "semantic"]),
522
+ results: z.array(searchResultSchema)
523
+ })
524
+ }
525
+ );
526
+ registerAiocsTool(
527
+ "show",
528
+ {
529
+ title: "Show",
530
+ description: "Return a specific chunk by numeric chunk id.",
531
+ inputSchema: z.object({
532
+ chunkId: z.number().int().nonnegative()
533
+ }),
534
+ outputSchema: z.object({
535
+ chunk: searchResultSchema
536
+ })
537
+ }
538
+ );
539
+ registerAiocsTool(
540
+ "embeddings_status",
541
+ {
542
+ title: "Embeddings status",
543
+ description: "Return embedding backlog and latest-snapshot coverage details.",
544
+ outputSchema: embeddingStatusSchema
545
+ }
546
+ );
547
+ registerAiocsTool(
548
+ "embeddings_backfill",
549
+ {
550
+ title: "Embeddings backfill",
551
+ description: "Queue latest snapshots for embedding rebuild for one source or all sources.",
552
+ inputSchema: z.object({
553
+ sourceIdOrAll: z.string()
554
+ }),
555
+ outputSchema: z.object({
556
+ queuedJobs: z.number().int().nonnegative()
557
+ })
558
+ }
559
+ );
560
+ registerAiocsTool(
561
+ "embeddings_clear",
562
+ {
563
+ title: "Embeddings clear",
564
+ description: "Clear derived embedding state for one source or all sources.",
565
+ inputSchema: z.object({
566
+ sourceIdOrAll: z.string()
567
+ }),
568
+ outputSchema: z.object({
569
+ clearedSources: z.array(z.string())
570
+ })
571
+ }
572
+ );
573
+ registerAiocsTool(
574
+ "embeddings_run",
575
+ {
576
+ title: "Embeddings run",
577
+ description: "Process queued embedding jobs immediately.",
578
+ outputSchema: z.object({
579
+ processedJobs: z.number().int().nonnegative(),
580
+ succeededJobs: z.array(z.object({
581
+ sourceId: z.string(),
582
+ snapshotId: z.string(),
583
+ chunkCount: z.number().int().nonnegative()
584
+ })),
585
+ failedJobs: z.array(z.object({
586
+ sourceId: z.string(),
587
+ snapshotId: z.string(),
588
+ errorMessage: z.string()
589
+ }))
590
+ })
591
+ }
592
+ );
593
+ registerAiocsTool(
594
+ "backup_export",
595
+ {
596
+ title: "Backup export",
597
+ description: "Export the local aiocs data and config into a manifest-backed backup directory.",
598
+ inputSchema: z.object({
599
+ outputDir: z.string(),
600
+ replaceExisting: z.boolean().optional()
601
+ }),
602
+ outputSchema: z.object({
603
+ outputDir: z.string(),
604
+ manifestPath: z.string(),
605
+ manifest: backupManifestSchema
606
+ })
607
+ }
608
+ );
609
+ registerAiocsTool(
610
+ "backup_import",
611
+ {
612
+ title: "Backup import",
613
+ description: "Import a manifest-backed aiocs backup into the local data and config directories.",
614
+ inputSchema: z.object({
615
+ inputDir: z.string(),
616
+ replaceExisting: z.boolean().optional()
617
+ }),
618
+ outputSchema: z.object({
619
+ inputDir: z.string(),
620
+ dataDir: z.string(),
621
+ configDir: z.string().optional(),
622
+ manifest: backupManifestSchema
623
+ })
624
+ }
625
+ );
626
+ registerAiocsTool(
627
+ "verify_coverage",
628
+ {
629
+ title: "Verify coverage",
630
+ description: "Verify a fetched source snapshot against one or more reference markdown files.",
631
+ inputSchema: z.object({
632
+ sourceId: z.string(),
633
+ referenceFiles: z.array(z.string()).min(1),
634
+ snapshotId: z.string().optional()
635
+ }),
636
+ outputSchema: coverageVerificationSchema
637
+ }
638
+ );
639
+ server.registerTool(
640
+ "batch",
641
+ {
642
+ title: "Batch",
643
+ description: "Execute multiple aiocs MCP operations in one call and return per-operation success or error results.",
644
+ inputSchema: z.object({
645
+ operations: z.array(z.object({
646
+ tool: z.string().refine(
647
+ (value) => batchableToolNames.includes(value),
648
+ {
649
+ message: `tool must be one of: ${batchableToolNames.join(", ")}`
650
+ }
651
+ ),
652
+ arguments: z.record(z.string(), z.unknown()).optional()
653
+ })).min(1).max(25)
654
+ }),
655
+ outputSchema: z.object({
656
+ ok: z.boolean(),
657
+ data: z.object({
658
+ results: z.array(z.object({
659
+ index: z.number().int().nonnegative(),
660
+ tool: z.string(),
661
+ ok: z.boolean(),
662
+ data: z.unknown().optional(),
663
+ error: z.object({
664
+ code: z.string(),
665
+ message: z.string(),
666
+ details: z.unknown().optional()
667
+ }).optional()
668
+ }))
669
+ }).optional(),
670
+ error: mcpErrorSchema.optional()
671
+ })
672
+ },
673
+ async ({ operations }) => {
674
+ try {
675
+ const results = [];
676
+ for (const [index, operation] of operations.entries()) {
677
+ try {
678
+ const validatedArgs = validateBatchOperationArguments(
679
+ operation.tool,
680
+ operation.arguments ?? {}
681
+ );
682
+ const data = await runToolHandler(
683
+ operation.tool,
684
+ validatedArgs
685
+ );
686
+ results.push({
687
+ index,
688
+ tool: operation.tool,
689
+ ok: true,
690
+ data
691
+ });
692
+ } catch (error) {
693
+ const normalized = toAiocsError(error);
694
+ results.push({
695
+ index,
696
+ tool: operation.tool,
697
+ ok: false,
698
+ error: {
699
+ code: normalized.code,
700
+ message: normalized.message,
701
+ ...typeof normalized.details !== "undefined" ? { details: normalized.details } : {}
702
+ }
703
+ });
704
+ }
705
+ }
706
+ return asToolResult({ results });
707
+ } catch (error) {
708
+ return asToolError(error);
709
+ }
710
+ }
711
+ );
712
+ async function main() {
713
+ const transport = new StdioServerTransport();
714
+ await server.connect(transport);
715
+ }
716
+ main().catch((error) => {
717
+ process.stderr.write(`${error instanceof Error ? error.stack ?? error.message : String(error)}
718
+ `);
719
+ process.exitCode = 1;
720
+ });