@littlebigbrain/mcp 0.2.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,702 @@
1
+ import { z } from "zod";
2
+ export const DEFAULT_DETAIL = "compact";
3
+ export const HARD_OUTPUT_CHARS = 80_000;
4
+ export const MAX_QUERY_ROW_LIMIT = 5_000;
5
+ export const READ_ONLY = { readOnlyHint: true };
6
+ export const IDEMPOTENT_WRITE = {
7
+ readOnlyHint: false,
8
+ destructiveHint: false,
9
+ idempotentHint: true,
10
+ openWorldHint: false,
11
+ };
12
+ export const MUTATING = {
13
+ readOnlyHint: false,
14
+ destructiveHint: false,
15
+ openWorldHint: false,
16
+ };
17
+ export const detailSchema = z
18
+ .enum(["compact", "standard", "full"])
19
+ .optional()
20
+ .describe("Response detail level. Defaults to compact.");
21
+ export const rowLimitSchema = z
22
+ .number()
23
+ .int()
24
+ .positive()
25
+ .max(MAX_QUERY_ROW_LIMIT)
26
+ .optional()
27
+ .describe("Maximum query rows to return in this page. Defaults by detail: compact=20, standard=100, full=1000.");
28
+ export const cursorSchema = z
29
+ .string()
30
+ .optional()
31
+ .describe("Opaque cursor from a previous lbb_query row page; reruns the original query at the next offset.");
32
+ export const graphScope = {
33
+ graph: z
34
+ .string()
35
+ .optional()
36
+ .describe("Graph to target; defaults to the connection's graph"),
37
+ branch: z
38
+ .string()
39
+ .optional()
40
+ .describe("Branch to target; defaults to the connection's branch"),
41
+ };
42
+ export const jsonObjectSchema = z.record(z.string(), z.unknown());
43
+ export const jsonObjectArraySchema = z.array(jsonObjectSchema);
44
+ export const readScope = { detail: detailSchema, ...graphScope };
45
+ // Typed shape for inference rules (define_rules / infer / shacl include_derived).
46
+ // Advertising the structure is what makes a rule's full power discoverable: a
47
+ // term can be a fixed entity (not just a variable), so a rule can match or derive
48
+ // a constant value (e.g. a status); and `not_exists` adds stratified negation, so
49
+ // a rule can express a universal condition like "all children complete".
50
+ export const entitySelectorSchema = z
51
+ .object({
52
+ entity_type: z
53
+ .string()
54
+ .optional()
55
+ .describe("Entity type name (with `name`, names a fixed entity)"),
56
+ name: z
57
+ .string()
58
+ .optional()
59
+ .describe("Entity name (paired with `entity_type`)"),
60
+ entity_id: z
61
+ .string()
62
+ .optional()
63
+ .describe("Entity id (hex), as an alternative to type+name"),
64
+ })
65
+ .passthrough();
66
+ export const searchFeedbackTargetSchema = z.discriminatedUnion("kind", [
67
+ z
68
+ .object({
69
+ kind: z.literal("entity"),
70
+ entity: entitySelectorSchema,
71
+ })
72
+ .passthrough(),
73
+ z
74
+ .object({
75
+ kind: z.literal("assertion"),
76
+ edge_event_id: z.string(),
77
+ })
78
+ .passthrough(),
79
+ z
80
+ .object({
81
+ kind: z.literal("observation"),
82
+ observation_id: z.string(),
83
+ })
84
+ .passthrough(),
85
+ z
86
+ .object({
87
+ kind: z.literal("concept"),
88
+ concept_id: z.string().optional(),
89
+ name: z.string().optional(),
90
+ })
91
+ .passthrough(),
92
+ ]);
93
+ export const searchFeedbackSchema = z
94
+ .object({
95
+ query: z.string().min(1),
96
+ search_id: z.string().optional(),
97
+ snapshot: z.record(z.string(), z.unknown()).optional(),
98
+ profile: z.string().optional(),
99
+ model_id: z.string().optional(),
100
+ labeler_id: z.string().optional(),
101
+ labels: z
102
+ .array(z
103
+ .object({
104
+ target: searchFeedbackTargetSchema,
105
+ rank: z.number().int().positive().optional(),
106
+ score: z.number().optional(),
107
+ grade: z.number().int().min(0).max(3),
108
+ reason: z.string().optional(),
109
+ split: z.enum(["train", "eval", "unspecified"]).optional(),
110
+ })
111
+ .passthrough())
112
+ .min(1),
113
+ })
114
+ .passthrough();
115
+ export const ruleTermSchema = z
116
+ .union([
117
+ z
118
+ .object({
119
+ var: z.string().describe("A variable, joined across patterns by name"),
120
+ })
121
+ .passthrough(),
122
+ z.object({ entity: entitySelectorSchema }).passthrough(),
123
+ ])
124
+ .describe('A rule term: { "var": "x" } (a variable) or { "entity": { "entity_type": "DeliveryStatus", "name": "Complete" } } (a fixed entity used as a constant in the body or head)');
125
+ export const ruleTriplePatternSchema = z
126
+ .object({
127
+ subject: ruleTermSchema.optional(),
128
+ predicate: z
129
+ .string()
130
+ .optional()
131
+ .describe('Relation name. In a rule body, the reserved "rdf:type" makes a type-membership constraint: the object names a class ({ entity: { entity_type: "Contact" } }, no name) and matches every entity of that class and its subtypes (rdfs:subClassOf closure), so one rule keyed on a supertype fires for all subtypes. Not allowed in a head or an exists/not_exists filter.'),
132
+ object: ruleTermSchema.optional(),
133
+ })
134
+ .passthrough();
135
+ export const ruleCombinatorSchema = z
136
+ .union([
137
+ z.object({ exists: z.array(ruleTriplePatternSchema) }).passthrough(),
138
+ z.object({ not_exists: z.array(ruleTriplePatternSchema) }).passthrough(),
139
+ ])
140
+ .describe("An existence filter over the body solutions: { exists: [...] } (semijoin — keep rows with a compatible match) or { not_exists: [...] } (negation/antijoin — keep rows with none)");
141
+ export const inferenceRuleSchema = z
142
+ .object({
143
+ name: z.string(),
144
+ order: z
145
+ .number()
146
+ .int()
147
+ .optional()
148
+ .describe("Run order, low to high (a determinism hint)"),
149
+ body: z
150
+ .array(ruleTriplePatternSchema)
151
+ .optional()
152
+ .describe("The condition: a basic graph pattern over current edges (asserted + already-derived), joined on shared variables. Terms may be variables or fixed entities."),
153
+ combinators: z
154
+ .array(ruleCombinatorSchema)
155
+ .optional()
156
+ .describe('exists/not_exists filters folded over the body. not_exists is stratified negation — it lets a rule express a universal condition, e.g. derive "phase complete" only when not_exists an incomplete deliverable. A negation cycle is rejected.'),
157
+ head: ruleTriplePatternSchema
158
+ .optional()
159
+ .describe("The triple derived once per body solution. Every head variable must be bound by the body; a fixed-entity object derives a constant (e.g. set the rolled-up status to the Complete entity)."),
160
+ })
161
+ .passthrough();
162
+ export const inferenceRuleArraySchema = z.array(inferenceRuleSchema);
163
+ export const ontologyFormatSchema = z.enum([
164
+ "auto",
165
+ "turtle",
166
+ "json_ld",
167
+ "rdf_xml",
168
+ "csv",
169
+ "tsv",
170
+ "lbb_json",
171
+ "spec",
172
+ ]);
173
+ export const shapeFormatSchema = z.enum([
174
+ "auto",
175
+ "turtle",
176
+ "n_triples",
177
+ "n_quads",
178
+ "trig",
179
+ ]);
180
+ export const ontologySourceSchema = z
181
+ .object({
182
+ source: z.string().describe("Ontology source text"),
183
+ format: ontologyFormatSchema.optional(),
184
+ })
185
+ .strict();
186
+ export const shapeSourceSchema = z
187
+ .object({
188
+ source: z.string().describe("SHACL/RDF shape source text"),
189
+ format: shapeFormatSchema.optional(),
190
+ })
191
+ .strict();
192
+ export const schemaModeSchema = z.enum(["warn", "reject"]);
193
+ export const ontologyEvolveOpSchema = z.discriminatedUnion("op", [
194
+ z
195
+ .object({
196
+ op: z.literal("widen_relation"),
197
+ relation: z
198
+ .string()
199
+ .describe("Relation to widen, by name (case-insensitive)"),
200
+ add_domain: z
201
+ .array(z.string())
202
+ .optional()
203
+ .describe("Entity-type names to add to the relation's domain (source types)"),
204
+ add_range: z
205
+ .array(z.string())
206
+ .optional()
207
+ .describe("Entity-type names to add to the relation's range (target types)"),
208
+ })
209
+ .strict(),
210
+ z
211
+ .object({
212
+ op: z.literal("add_entity_type"),
213
+ name: z
214
+ .string()
215
+ .describe("Display name of the new entity type (idempotent if it exists)"),
216
+ })
217
+ .strict(),
218
+ z
219
+ .object({
220
+ op: z.literal("add_relation"),
221
+ name: z
222
+ .string()
223
+ .describe("Display name of the new relation, e.g. HAS_PHASE"),
224
+ domain: z
225
+ .array(z.string())
226
+ .optional()
227
+ .describe("Entity-type names allowed as the source (domain); must already exist"),
228
+ range: z
229
+ .array(z.string())
230
+ .optional()
231
+ .describe("Entity-type names allowed as the target (range); must already exist"),
232
+ cardinality: z
233
+ .enum(["one_to_one", "one_to_many", "many_to_one", "many_to_many"])
234
+ .optional()
235
+ .describe("Defaults to many_to_many"),
236
+ temporal_semantics: z
237
+ .enum(["atemporal", "valid_time", "commit_time", "bitemporal"])
238
+ .optional()
239
+ .describe("Defaults to bitemporal"),
240
+ reducer: z
241
+ .string()
242
+ .optional()
243
+ .describe("State-reducer token, e.g. append_only (default), latest_wins"),
244
+ inverse_name: z
245
+ .string()
246
+ .optional()
247
+ .describe("Optional inverse-relation display name, e.g. PHASE_OF (enables one-hop reverse traversal)"),
248
+ transitive: z.boolean().optional(),
249
+ symmetric: z.boolean().optional(),
250
+ })
251
+ .strict(),
252
+ z
253
+ .object({
254
+ op: z.literal("add_property"),
255
+ name: z
256
+ .string()
257
+ .describe("Display name of the new scalar property field, e.g. status (idempotent if it exists)"),
258
+ value_type: z
259
+ .enum(["bool", "i64", "f64", "date_time", "keyword", "text", "bytes"])
260
+ .optional()
261
+ .describe("Scalar type; defaults to text. Lets a later lbb_commit set entity_properties[].field"),
262
+ required: z
263
+ .boolean()
264
+ .optional()
265
+ .describe("Advisory required flag (not enforced on commit)"),
266
+ })
267
+ .strict(),
268
+ z
269
+ .object({
270
+ op: z.literal("rename_entity_type"),
271
+ from: z.string().describe("Current entity-type name"),
272
+ to: z
273
+ .string()
274
+ .describe("New display name (stable id stays frozen; records keep resolving)"),
275
+ })
276
+ .strict(),
277
+ z
278
+ .object({
279
+ op: z.literal("rename_relation"),
280
+ from: z.string().describe("Current relation name"),
281
+ to: z
282
+ .string()
283
+ .describe("New display name (stable id stays frozen; edges keep resolving)"),
284
+ })
285
+ .strict(),
286
+ z
287
+ .object({
288
+ op: z.literal("set_relation_inverse"),
289
+ relation: z.string().describe("Relation to set the inverse on, by name"),
290
+ inverse_name: z
291
+ .string()
292
+ .describe("Inverse-relation display name, e.g. PHASE_OF (enables one-hop reverse traversal)"),
293
+ })
294
+ .strict(),
295
+ z
296
+ .object({
297
+ op: z.literal("set_relation_cardinality"),
298
+ relation: z.string().describe("Relation to change, by name"),
299
+ cardinality: z.enum([
300
+ "one_to_one",
301
+ "one_to_many",
302
+ "many_to_one",
303
+ "many_to_many",
304
+ ]),
305
+ })
306
+ .strict(),
307
+ z
308
+ .object({
309
+ op: z.literal("narrow_relation"),
310
+ relation: z.string().describe("Relation to narrow, by name"),
311
+ remove_domain: z
312
+ .array(z.string())
313
+ .optional()
314
+ .describe("Entity-type names to remove from the relation's domain (subtractive)"),
315
+ remove_range: z
316
+ .array(z.string())
317
+ .optional()
318
+ .describe("Entity-type names to remove from the relation's range (subtractive)"),
319
+ })
320
+ .strict(),
321
+ z
322
+ .object({
323
+ op: z.literal("remove_entity_type"),
324
+ name: z
325
+ .string()
326
+ .describe("Entity type to tombstone — kept readable for old records, rejected for new commits"),
327
+ })
328
+ .strict(),
329
+ z
330
+ .object({
331
+ op: z.literal("remove_relation"),
332
+ name: z
333
+ .string()
334
+ .describe("Relation to tombstone — old edges stay readable, rejected for new commits"),
335
+ })
336
+ .strict(),
337
+ ]);
338
+ export const inspectInputSchema = z.discriminatedUnion("action", [
339
+ z.object({ action: z.literal("guide"), ...readScope }).strict(),
340
+ z.object({ action: z.literal("ontology"), ...readScope }).strict(),
341
+ z
342
+ .object({ action: z.literal("ontology_conformance"), ...readScope })
343
+ .strict(),
344
+ z.object({ action: z.literal("schema"), ...readScope }).strict(),
345
+ z.object({ action: z.literal("schema_audit"), ...readScope }).strict(),
346
+ z.object({ action: z.literal("rules"), ...readScope }).strict(),
347
+ z
348
+ .object({
349
+ action: z.literal("schema_preview"),
350
+ ontology: ontologySourceSchema.optional(),
351
+ shapes: shapeSourceSchema.optional(),
352
+ base_ontology_version: z.number().int().nonnegative().optional(),
353
+ base_shapes_version: z.number().int().nonnegative().optional(),
354
+ desired_mode: schemaModeSchema.optional(),
355
+ ...readScope,
356
+ })
357
+ .strict(),
358
+ z
359
+ .object({
360
+ action: z.literal("ontology_search"),
361
+ query: z
362
+ .string()
363
+ .describe("Ontology concept, term, or relation to search"),
364
+ top_k: z.number().int().positive().optional(),
365
+ ...readScope,
366
+ })
367
+ .strict(),
368
+ z.object({ action: z.literal("metadata"), ...readScope }).strict(),
369
+ z
370
+ .object({
371
+ action: z.literal("entity"),
372
+ entity_id: z
373
+ .string()
374
+ .optional()
375
+ .describe("Entity id (hex); alternative to entity_type+name"),
376
+ entity_type: z.string().optional(),
377
+ name: z.string().optional(),
378
+ as_of: z
379
+ .string()
380
+ .optional()
381
+ .describe("Valid-time snapshot pin (RFC3339): reproduce the node as of this instant."),
382
+ as_of_commit_seq: z
383
+ .number()
384
+ .int()
385
+ .nonnegative()
386
+ .optional()
387
+ .describe("Snapshot pin: reproduce the node (state, edges, history) as of this commit_seq."),
388
+ ...readScope,
389
+ })
390
+ .strict(),
391
+ z
392
+ .object({
393
+ action: z.literal("edges"),
394
+ entity_id: z
395
+ .string()
396
+ .optional()
397
+ .describe("Entity id (hex); alternative to entity_type+name"),
398
+ entity_type: z.string().optional(),
399
+ name: z.string().optional(),
400
+ direction: z
401
+ .enum(["out", "in", "both"])
402
+ .optional()
403
+ .describe("Edges out of / into / touching the entity (default both)."),
404
+ relation: z
405
+ .string()
406
+ .optional()
407
+ .describe("Filter to a single relation name."),
408
+ row_limit: z
409
+ .number()
410
+ .int()
411
+ .positive()
412
+ .optional()
413
+ .describe("Max edges returned this page (server caps at 1000; default 150)."),
414
+ cursor: z
415
+ .string()
416
+ .optional()
417
+ .describe("Opaque cursor from the previous page's `next_cursor`. The response is the unified list envelope { object:'list', data, has_more, next_cursor, total_count }; page a high-degree node (entity detail hard-caps its edge sample) by feeding next_cursor back here until has_more is false. Each row carries valid_time for a per-edge timeline."),
418
+ offset: z
419
+ .number()
420
+ .int()
421
+ .nonnegative()
422
+ .optional()
423
+ .describe("Legacy alias for `cursor` (still accepted); prefer paging with `cursor`."),
424
+ as_of: z
425
+ .string()
426
+ .optional()
427
+ .describe("Valid-time snapshot pin (RFC3339)."),
428
+ as_of_commit_seq: z
429
+ .number()
430
+ .int()
431
+ .nonnegative()
432
+ .optional()
433
+ .describe("Snapshot pin: list edges as of this commit_seq."),
434
+ ...readScope,
435
+ })
436
+ .strict(),
437
+ z
438
+ .object({
439
+ action: z.literal("state"),
440
+ entity_type: z.string(),
441
+ name: z.string(),
442
+ relation: z.string().optional(),
443
+ as_of: z.string().optional(),
444
+ as_of_commit_seq: z
445
+ .number()
446
+ .int()
447
+ .nonnegative()
448
+ .optional()
449
+ .describe("Snapshot pin: reproduce the state as of this commit_seq, hiding later commits. Errors if past head."),
450
+ ...readScope,
451
+ })
452
+ .strict(),
453
+ z
454
+ .object({
455
+ action: z.literal("history"),
456
+ entity_type: z.string(),
457
+ name: z.string(),
458
+ relation: z.string().optional(),
459
+ ...readScope,
460
+ })
461
+ .strict(),
462
+ z
463
+ .object({
464
+ action: z.literal("why"),
465
+ source_type: z.string(),
466
+ source_name: z.string(),
467
+ relation: z.string(),
468
+ target_type: z.string(),
469
+ target_name: z.string(),
470
+ ...readScope,
471
+ })
472
+ .strict(),
473
+ z
474
+ .object({
475
+ action: z.literal("traverse"),
476
+ entity_type: z.string(),
477
+ name: z.string(),
478
+ relations: z.array(z.string()).optional(),
479
+ direction: z.enum(["out", "in", "both"]).optional(),
480
+ max_hops: z.number().int().positive().max(6).optional(),
481
+ top_k: z.number().int().positive().optional(),
482
+ ...readScope,
483
+ })
484
+ .strict(),
485
+ z
486
+ .object({
487
+ action: z.literal("transitions"),
488
+ entity_type: z.string(),
489
+ name: z.string(),
490
+ relation: z
491
+ .string()
492
+ .describe("State/status relation to trace, e.g. IN_STAGE or HAS_STATUS"),
493
+ as_of: z.string().optional(),
494
+ as_of_commit_seq: z.number().int().nonnegative().optional(),
495
+ ...readScope,
496
+ })
497
+ .strict(),
498
+ ]);
499
+ // The graph's RDF projection uses a fixed IRI scheme; teaching it here lets an
500
+ // agent write a valid query on the first attempt instead of round-tripping
501
+ // through the ontology to reverse-engineer term IRIs.
502
+ export const SPARQL_IRI_GUIDE = 'IRI scheme: relations are <https://littlebigbrain.com/r/NAME> (NAME lowercased, e.g. writes_to; reverse a relation with the ^ path operator, no stored inverse triple). Types are <https://littlebigbrain.com/class/NAME> (lowercased), matched as `?x a <…/class/NAME>` with rdfs:subClassOf closure on by default. Property fields are <https://littlebigbrain.com/p/NAME> (lowercased). The local name is ALWAYS lowercase — an uppercase one (e.g. <…/r/FOR_CLIENT>) is a different, non-existent IRI that silently matches nothing; this tool auto-lowercases the local name of /r/, /class/, and /p/ IRIs for you and adds a `notes` entry when it does, so a stray uppercase still resolves. (Structured mode\'s `predicate` is case-insensitive on its own.) Entities are content-addressed <https://littlebigbrain.com/e/HASH> — never build an entity IRI from a name; anchor a named entity by its label instead: `?e <http://www.w3.org/2000/01/rdf-schema#label> "Acme"`. Discover the exact relation and type names with lbb_inspect action=ontology. SELECT and ASK only (CONSTRUCT/DESCRIBE are rejected).';
503
+ export const queryInputSchema = z.discriminatedUnion("mode", [
504
+ z
505
+ .object({
506
+ mode: z.literal("structured"),
507
+ body: jsonObjectSchema
508
+ .optional()
509
+ .describe('Structured SPARQL-subset or analytics request body. Shape: { patterns: [{ subject, predicate, object }], filters?, group_by?, group_keys?, aggregates?, having?, order_by?, select?, limit?, distinct? }. Each pattern term is { var: "x" } or a fixed { entity: { entity_type, name } }; `predicate` is a relation name and is case-insensitive here (FOR_CLIENT and for_client both resolve — unlike SPARQL text, which needs the lowercased IRI local name). ' +
510
+ "FILTER — `filters` is a list of conditions, each of exact shape " +
511
+ '{ "compare": { "op": <op>, "left": <term>, "right": <term> } } (or { "and": [<filter>…] }, { "or": [<filter>…] }, { "not": <filter> }). ' +
512
+ "`op` is one of eq | ne | lt | le | gt | ge (NOT the symbols =,<,>). Each <term> is exactly one of " +
513
+ '{ "var": "x" }, { "property": { "var": "x", "field": "amount" } } (a typed scalar attribute), or ' +
514
+ '{ "value": <typed> } — and <typed> is exactly one wrapper: { "str": "…" }, { "i64": 5 }, { "f64": 0.9 }, { "bool": true }, { "date_time": "2026-01-01" } (RFC3339), or { "entity": { "entity_type": "T", "name": "N" } }. ' +
515
+ 'Complete runnable example — deals whose amount ≥ 1000000: { "patterns": [{ "subject": { "var": "d" }, "predicate": "for_client", "object": { "var": "c" } }], "filters": [{ "compare": { "op": "ge", "left": { "property": { "var": "d", "field": "amount" } }, "right": { "value": { "f64": 1000000 } } } }] }. ' +
516
+ "Comparisons use the field's real declared type (numbers as numbers, datetimes as instants), so they run server-side. " +
517
+ 'GROUP BY supports both entity-identity keys (group_by: ["s"]) and typed scalar keys via group_keys: a property value ({ property: { var, field, as } }) or a calendar bucket of a datetime property ({ date_bucket: { var, field, granularity: year|month|week|day|hour, as } }). Scalar keys come back per group under value_keys[as] — so a per-area breakdown or a commits-per-month time series is one server-side query, no client-side bucketing. Worked example -- commits per area per month in one query: { "patterns": [{ "subject": { "var": "c" }, "predicate": "committed_to", "object": { "var": "repo" } }], "group_keys": [{ "date_bucket": { "var": "c", "field": "committed_at", "granularity": "month", "as": "m" } }, { "property": { "var": "c", "field": "area", "as": "area" } }], "aggregates": [{ "func": "count", "as": "n" }], "order_by": [{ "var": "m" }] } -- area and committed_at are typed entity attributes (set via entity_properties; readable flat under attributes, never a nested metadata blob), and each group returns value_keys.m + value_keys.area + aggregates.n. `having: [...]` takes the same filter shape over the aggregated groups (e.g. { "compare": { "op": "gt", "left": { "var": "n" }, "right": { "value": { "i64": 10 } } } }); it is evaluated only on this grouped path, NOT alongside `combinators` (UNION/OPTIONAL/MINUS), which route to the analytics engine. Cheap aggregate count: pair an equality having (e.g. { "compare": { "op": "eq", "left": { "var": "n" }, "right": { "value": { "i64": 4 } } } }) with row_limit: 1 -- the response row_page.total reports how many groups match without materializing them all, so you read the count off row_page.total instead of paging every matching row. For snapshot pinning prefer the top-level `as_of` / `as_of_commit_seq` arguments below; a bare `as_of` key inside the body is rejected (the body\'s valid-time field is `as_of_valid_time`).'),
518
+ as_of: z
519
+ .string()
520
+ .optional()
521
+ .describe("Snapshot pin (valid-time, RFC3339): evaluate the body as of this instant. Folded into the request's `as_of_valid_time`. Top-level here is the supported spelling — a bare `as_of` inside the body is rejected, since the server silently ignores it."),
522
+ as_of_commit_seq: z
523
+ .number()
524
+ .int()
525
+ .nonnegative()
526
+ .optional()
527
+ .describe("Snapshot pin: evaluate the body as of this commit_seq, hiding later commits. Errors if past head. Top-level alias for the body's `as_of_commit_seq` (either works for this one)."),
528
+ row_limit: rowLimitSchema,
529
+ cursor: cursorSchema,
530
+ ...readScope,
531
+ })
532
+ .strict(),
533
+ z
534
+ .object({
535
+ mode: z.literal("sparql"),
536
+ query: z
537
+ .string()
538
+ .optional()
539
+ .describe(`SPARQL 1.1 query text (SELECT or ASK). ${SPARQL_IRI_GUIDE} Example: SELECT ?service ?db WHERE { ?service <https://littlebigbrain.com/r/writes_to> ?db } LIMIT 10`),
540
+ as_of: z.string().optional(),
541
+ as_of_commit_seq: z
542
+ .number()
543
+ .int()
544
+ .nonnegative()
545
+ .optional()
546
+ .describe("Snapshot pin: run the query as of this commit_seq. Errors if past head."),
547
+ row_limit: rowLimitSchema,
548
+ cursor: cursorSchema,
549
+ ...readScope,
550
+ })
551
+ .strict(),
552
+ z
553
+ .object({
554
+ mode: z.literal("shacl"),
555
+ shacl_mode: z
556
+ .enum(["select", "validate"])
557
+ .optional()
558
+ .describe("select returns focus nodes; validate returns a report"),
559
+ shapes: jsonObjectArraySchema.optional(),
560
+ rules: inferenceRuleArraySchema.optional(),
561
+ include_derived: z.boolean().optional(),
562
+ as_of: z.string().optional(),
563
+ explain: z.boolean().optional(),
564
+ top_k: z.number().int().positive().optional(),
565
+ ...readScope,
566
+ })
567
+ .strict(),
568
+ z
569
+ .object({
570
+ mode: z.literal("infer"),
571
+ rules: inferenceRuleArraySchema.optional(),
572
+ as_of: z.string().optional(),
573
+ as_of_commit_seq: z
574
+ .number()
575
+ .int()
576
+ .nonnegative()
577
+ .optional()
578
+ .describe("Snapshot pin: run inference as of this commit_seq. Errors if past head."),
579
+ max_rounds: z.number().int().positive().optional(),
580
+ max_derived: z.number().int().positive().optional(),
581
+ max_solutions: z.number().int().positive().optional(),
582
+ ...readScope,
583
+ })
584
+ .strict(),
585
+ z
586
+ .object({
587
+ mode: z.literal("retrieval_premises"),
588
+ anchor_type: z.string(),
589
+ anchor_name: z.string(),
590
+ relation: z.string(),
591
+ query: z.string(),
592
+ threshold: z.number().min(0).max(1).optional(),
593
+ max_premises: z.number().int().positive().optional(),
594
+ query_top_k: z.number().int().positive().optional(),
595
+ ...readScope,
596
+ })
597
+ .strict(),
598
+ z
599
+ .object({
600
+ mode: z.literal("analyze"),
601
+ metric: z
602
+ .enum(["entity_types", "relations", "overview", "facets", "sparql"])
603
+ .optional(),
604
+ chart: z.enum(["bar", "pie"]).optional(),
605
+ top_k: z.number().int().positive().optional(),
606
+ query: z.string().optional(),
607
+ field: z.string().optional(),
608
+ sparql: jsonObjectSchema.optional(),
609
+ ...readScope,
610
+ })
611
+ .strict(),
612
+ ]);
613
+ export const configureInputSchema = z.discriminatedUnion("action", [
614
+ z
615
+ .object({
616
+ action: z.literal("define_ontology"),
617
+ graph: z.string().describe("Graph to create or redefine"),
618
+ branch: graphScope.branch,
619
+ entity_types: jsonObjectArraySchema.optional(),
620
+ relations: jsonObjectArraySchema.optional(),
621
+ source: z.string().optional(),
622
+ format: ontologyFormatSchema.optional(),
623
+ merge_default: z.boolean().optional(),
624
+ })
625
+ .strict(),
626
+ z
627
+ .object({
628
+ action: z.literal("define_rules"),
629
+ rules: inferenceRuleArraySchema,
630
+ confirm_empty: z.boolean().optional(),
631
+ ...graphScope,
632
+ })
633
+ .strict(),
634
+ z
635
+ .object({
636
+ action: z.literal("publish_schema"),
637
+ preview_digest: z.string(),
638
+ shapes: shapeSourceSchema,
639
+ ontology: ontologySourceSchema.optional(),
640
+ desired_mode: schemaModeSchema,
641
+ confirm_restrictive: z.boolean().optional(),
642
+ ...graphScope,
643
+ })
644
+ .strict(),
645
+ z
646
+ .object({
647
+ action: z.literal("evolve_ontology"),
648
+ ops: z
649
+ .array(ontologyEvolveOpSchema)
650
+ .min(1)
651
+ .describe("Ontology changes to apply in order (additive, in-place edits, or subtractive)"),
652
+ allow_data_conflicts: z
653
+ .boolean()
654
+ .optional()
655
+ .describe("Apply subtractive ops (narrow/remove) even when current data conflicts; affected records are kept and begin to warn. Default false rejects a conflicting subtractive request and reports the conflicts."),
656
+ ...graphScope,
657
+ })
658
+ .strict(),
659
+ ]);
660
+ /**
661
+ * MCP's `registerTool` advertises a JSON Schema for an input only when the
662
+ * schema is a ZodObject (the SDK reads `.shape` via `normalizeObjectSchema`). A
663
+ * `z.discriminatedUnion` has `.options`, not `.shape`, so the SDK silently falls
664
+ * back to an empty `{ type: "object", properties: {} }` advertisement — and
665
+ * clients then stringify every object-valued argument (the `schema_preview`
666
+ * `ontology`/`shapes` sources, the structured-query `body`, …), which the server
667
+ * rejects as `Expected object, received string`.
668
+ *
669
+ * Flatten the union into a single ZodObject purely for advertisement and
670
+ * transport: the discriminant becomes an enum, every branch field is merged in
671
+ * as optional, and unknown keys pass through. Each handler still `safeParse`s the
672
+ * raw args against the original strict union before dispatching, so per-variant
673
+ * required/forbidden fields are enforced exactly as before.
674
+ */
675
+ export function advertiseUnion(discriminator, union) {
676
+ const merged = {};
677
+ const variants = [];
678
+ for (const option of union.options) {
679
+ for (const [key, field] of Object.entries(option.shape)) {
680
+ const schema = field;
681
+ if (key === discriminator) {
682
+ const value = schema.value;
683
+ if (!variants.includes(value))
684
+ variants.push(value);
685
+ continue;
686
+ }
687
+ if (!(key in merged))
688
+ merged[key] = schema.isOptional() ? schema : schema.optional();
689
+ }
690
+ }
691
+ return z
692
+ .object({
693
+ [discriminator]: z
694
+ .enum(variants)
695
+ .describe(`Selects the variant (one of: ${variants.join(", ")}).`),
696
+ ...merged,
697
+ })
698
+ .passthrough();
699
+ }
700
+ export const inspectWireSchema = advertiseUnion("action", inspectInputSchema);
701
+ export const queryWireSchema = advertiseUnion("mode", queryInputSchema);
702
+ export const configureWireSchema = advertiseUnion("action", configureInputSchema);