@littlebigbrain/mcp 0.2.5 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @littlebigbrain/mcp
2
2
 
3
- Eleven task-shaped [MCP](https://modelcontextprotocol.io) tools that let Claude, Cursor, Codex, or any MCP client search, query, and write a [Little Big Brain](https://littlebigbrain.com) graph. Ships two ways: a hosted endpoint with OAuth sign-in, and a local stdio server.
3
+ Ten task-shaped [MCP](https://modelcontextprotocol.io) tools that let Claude, Cursor, Codex, or any MCP client search, query, and write a [Little Big Brain](https://littlebigbrain.com) graph. Ships two ways: a hosted endpoint with OAuth sign-in, and a local stdio server.
4
4
 
5
5
  ## Hosted (OAuth) — recommended
6
6
 
@@ -58,17 +58,16 @@ missing.
58
58
 
59
59
  | Tool | Use it for |
60
60
  | --- | --- |
61
- | `lbb_search` | hybrid retrieval, multi-query fusion, optional path following |
62
- | `lbb_ask` | grounded answers with citations |
61
+ | `lbb_search` | hybrid retrieval, multi-query fusion, and semantic graph results |
63
62
  | `lbb_decode` | constrained relation decoding |
64
63
  | `lbb_ground` | vocabulary completion and resolution |
65
- | `lbb_inspect` | ontology, entity, state, history, provenance, traversal |
66
- | `lbb_query` | SPARQL, structured analytics, SHACL, inference |
64
+ | `lbb_inspect` | ontology, schema, entity, state, history, and provenance |
65
+ | `lbb_query` | SPARQL and structured analytics |
67
66
  | `lbb_commit` | facts, properties, and embeddings |
68
67
  | `lbb_observe` | conversation episodes plus reviewed extraction |
69
68
  | `lbb_branch` | isolation branches and validated merge |
70
- | `lbb_configure` | ontology, schema, and inference rules |
71
- | `lbb_index` | BM25, vector, and adjacency refresh |
69
+ | `lbb_models` | shadow evaluation and training datasets |
70
+ | `lbb_configure` | ontology definition and atomic schema publication |
72
71
 
73
72
  Read tools return compact structured envelopes by default — use `detail`, `row_limit`, and returned cursors to page without silently truncating. Write tools derive an idempotency key unless you provide one.
74
73
 
@@ -42,11 +42,6 @@ export const graphScope = {
42
42
  export const jsonObjectSchema = z.record(z.string(), z.unknown());
43
43
  export const jsonObjectArraySchema = z.array(jsonObjectSchema);
44
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
45
  export const entitySelectorSchema = z
51
46
  .object({
52
47
  entity_type: z
@@ -112,54 +107,6 @@ export const searchFeedbackSchema = z
112
107
  .min(1),
113
108
  })
114
109
  .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
110
  export const ontologyFormatSchema = z.enum([
164
111
  "auto",
165
112
  "turtle",
@@ -189,7 +136,7 @@ export const shapeSourceSchema = z
189
136
  format: shapeFormatSchema.optional(),
190
137
  })
191
138
  .strict();
192
- export const schemaModeSchema = z.enum(["warn", "reject"]);
139
+ export const schemaModeSchema = z.enum(["off", "warn", "reject"]);
193
140
  export const ontologyEvolveOpSchema = z.discriminatedUnion("op", [
194
141
  z
195
142
  .object({
@@ -342,19 +289,6 @@ export const inspectInputSchema = z.discriminatedUnion("action", [
342
289
  .object({ action: z.literal("ontology_conformance"), ...readScope })
343
290
  .strict(),
344
291
  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
292
  z
359
293
  .object({
360
294
  action: z.literal("ontology_search"),
@@ -388,52 +322,6 @@ export const inspectInputSchema = z.discriminatedUnion("action", [
388
322
  ...readScope,
389
323
  })
390
324
  .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
325
  z
438
326
  .object({
439
327
  action: z.literal("state"),
@@ -470,18 +358,6 @@ export const inspectInputSchema = z.discriminatedUnion("action", [
470
358
  ...readScope,
471
359
  })
472
360
  .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
361
  z
486
362
  .object({
487
363
  action: z.literal("transitions"),
@@ -549,52 +425,6 @@ export const queryInputSchema = z.discriminatedUnion("mode", [
549
425
  ...readScope,
550
426
  })
551
427
  .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
428
  z
599
429
  .object({
600
430
  mode: z.literal("analyze"),
@@ -623,21 +453,12 @@ export const configureInputSchema = z.discriminatedUnion("action", [
623
453
  merge_default: z.boolean().optional(),
624
454
  })
625
455
  .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
456
  z
635
457
  .object({
636
458
  action: z.literal("publish_schema"),
637
- preview_digest: z.string(),
638
- shapes: shapeSourceSchema,
639
459
  ontology: ontologySourceSchema.optional(),
640
- desired_mode: schemaModeSchema,
460
+ shapes: shapeSourceSchema.optional(),
461
+ desired_mode: schemaModeSchema.optional(),
641
462
  confirm_restrictive: z.boolean().optional(),
642
463
  ...graphScope,
643
464
  })
@@ -662,8 +483,8 @@ export const configureInputSchema = z.discriminatedUnion("action", [
662
483
  * schema is a ZodObject (the SDK reads `.shape` via `normalizeObjectSchema`). A
663
484
  * `z.discriminatedUnion` has `.options`, not `.shape`, so the SDK silently falls
664
485
  * 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
486
+ * clients then stringify every object-valued argument (for example, the
487
+ * structured-query `body`), which the server
667
488
  * rejects as `Expected object, received string`.
668
489
  *
669
490
  * Flatten the union into a single ZodObject purely for advertisement and
@@ -417,55 +417,6 @@ export function toolResult(value) {
417
417
  structuredContent: value,
418
418
  };
419
419
  }
420
- // `lbb_inspect action=entity` returns the full incoming/outgoing/history arrays,
421
- // but the display truncates them to the detail cap (compact shows 5) while
422
- // `counts` still reports the true totals — so a high-degree node reads as "5
423
- // edges" unless the caller already knows to switch to the paged `edges`/`history`
424
- // reads. Make that self-documenting: when a sample is capped, attach the true
425
- // counts plus ready-to-run paged reads so the workaround is discoverable in the
426
- // response instead of tribal knowledge.
427
- export function entityEdgeCapHint(data, detail, identity) {
428
- if (!data || typeof data !== "object")
429
- return {};
430
- const record = data;
431
- const cap = compactLimits(normalizeDetail(detail)).maxItems;
432
- const lengthOf = (field) => {
433
- const value = record[field];
434
- return Array.isArray(value) ? value.length : 0;
435
- };
436
- const capped = {};
437
- const fullReads = [];
438
- const edgeReads = [
439
- { field: "incoming", direction: "in" },
440
- { field: "outgoing", direction: "out" },
441
- ];
442
- for (const { field, direction } of edgeReads) {
443
- const total = lengthOf(field);
444
- if (total > cap) {
445
- capped[field] = total;
446
- fullReads.push({
447
- tool: "lbb_inspect",
448
- arguments: { action: "edges", direction, ...identity },
449
- });
450
- }
451
- }
452
- if (lengthOf("history") > cap) {
453
- capped.history = lengthOf("history");
454
- fullReads.push({
455
- tool: "lbb_inspect",
456
- arguments: { action: "history", ...identity },
457
- });
458
- }
459
- if (Object.keys(capped).length === 0)
460
- return {};
461
- return {
462
- edge_sample: {
463
- note: `This node's edge/history arrays are a display sample capped at ${cap} per field; counts holds the true totals. Read the full set with these paged reads (cursor through them until has_more=false).`,
464
- capped_totals: capped,
465
- full_reads: fullReads,
466
- },
467
- };
468
- }
469
420
  export function errorResult(error) {
470
421
  const payload = error instanceof LbbError
471
422
  ? {
@@ -635,8 +586,6 @@ export function searchBody(p) {
635
586
  lexical: mode === "hybrid" || mode === "lexical",
636
587
  bm25: mode === "hybrid" || mode === "bm25",
637
588
  vector: mode === "hybrid" || mode === "vector",
638
- bm25_source: "persisted",
639
- vector_source: "persisted",
640
589
  consistency: "strong",
641
590
  profile: resolveProfile(p.profile),
642
591
  },
@@ -706,7 +655,7 @@ export function sparqlKeyLabel(value) {
706
655
  }
707
656
  return String(value);
708
657
  }
709
- export function buildPossibilities(relations, entityTypes = []) {
658
+ export function buildPossibilities(relations) {
710
659
  const possibilities = [
711
660
  {
712
661
  name: "Entity composition",
@@ -789,26 +738,6 @@ export function buildPossibilities(relations, entityTypes = []) {
789
738
  },
790
739
  });
791
740
  }
792
- const topType = entityTypes[0]?.name;
793
- if (top && topType) {
794
- possibilities.push({
795
- name: `${topType} nodes that participate in ${top}`,
796
- description: `Select every ${topType} that is the source of at least one ${top} edge.`,
797
- chart: "bar",
798
- run: {
799
- tool: "lbb_query",
800
- args: {
801
- mode: "shacl",
802
- shapes: [
803
- {
804
- targetClass: topType,
805
- property: [{ path: top, min_count: 1, bind: "targets" }],
806
- },
807
- ],
808
- },
809
- },
810
- });
811
- }
812
741
  return possibilities;
813
742
  }
814
743
  export async function guide(scopedClient) {
@@ -825,17 +754,16 @@ export async function guide(scopedClient) {
825
754
  entity_types: entityTypes,
826
755
  relations,
827
756
  capability: {
828
- search: "Use lbb_search for natural-language retrieval, optional multi-query fusion, and optional path following.",
757
+ search: "Use lbb_search for natural-language retrieval and optional multi-query fusion. Semantic graph search may include internally scored paths in its result set.",
829
758
  search_feedback: "After a lbb_search result set is useful or clearly wrong, write relevance labels with lbb_commit mode=search_feedback. Use grade 3 for ideal/good results, grade 1 for partially relevant results, and grade 0 for bad results. Include the original query, search_id when present, target identity, rank, score, and an optional split train/eval/unspecified. These labels are stored in __lbb_feedback/main and later exported as qrels-style training/eval data; they are not customer facts.",
830
- inspect: "Use lbb_inspect for ontology, RDF/SHACL schema, stored rules, metadata, state/history/why, exact traversals, and this guide.",
759
+ inspect: "Use lbb_inspect for ontology, schema metadata, the published conformance report, metadata, state/history/why, and this guide. Use lbb_query with SPARQL property paths for exact path queries.",
831
760
  ontology_decorations: "lbb_inspect action=ontology returns a decoration_status catalog: each ontology decoration is enforced (the engine acts on it — state_reducer, value_type, super_types, properties, supernode_policy; cardinality, which GET /v1/ontology/conformance audits as sh:maxCount; and inverse_name/symmetric, which SPARQL resolves as relation aliases — an inverse name is queryable directly (lowered to ^forward, no stored inverse triple) and a symmetric relation matches both directions), advisory (transitive, temporal_semantics, required), or reserved (stored but unwired — default_weight, resolvable, alias/embedding_fields). You can also always reverse any relation in SPARQL by flipping the triple pattern or using ^forward. Each relation_def also carries edge_count — the number of current edges of that relation in this branch's snapshot — so you can tell at a glance which declared relations are actually populated (edge_count 0 = declared but unused) without a separate summary call.",
832
- query: 'Use lbb_query for structured SPARQL-subset bodies, SPARQL text, SHACL shapes, inference previews, retrieval premises, and canned analysis. A mode=structured body is { patterns: [{ subject, predicate, object }], filters?, group_by?, group_keys?, aggregates?, having? }; a pattern `predicate` is a relation name and is case-insensitive. mode=structured GROUP BY is not limited to entity identity: group_keys can key on a typed scalar property ({ property: { var, field, as } }) or a calendar bucket of a datetime property ({ date_bucket: { var, field, granularity: month|day|…, as } }), with scalar keys returned per group under value_keys[as] — so per-category breakdowns (e.g. by area) and time series (e.g. commits per month) are single server-side queries over typed attributes, not 700 entity fetches bucketed by hand. These typed scalar attributes are set via entity_properties and read back flat under attributes on entity/list reads (there is no nested metadata.attributes blob); discover the queryable field names via lbb_inspect action=ontology (property_defs) or action=schema, and see the lbb_query body field for a copy-paste commits-per-area-per-month example. A FILTER entry has the exact shape { compare: { op: eq|ne|lt|le|gt|ge, left: <term>, right: <term> } } (also and/or/not), where a <term> is { var }, { property: { var, field } }, or { value: { str|i64|f64|bool|date_time|entity } } — e.g. filters: [{ compare: { op: "ge", left: { property: { var: "d", field: "amount" } }, right: { value: { f64: 1000000 } } } }]. In SPARQL text, relations are <https://littlebigbrain.com/r/NAME> (NAME lowercased; reverse with ^) and types <https://littlebigbrain.com/class/NAME> used as `?x a <…/class/NAME>` — the local name is always lowercase, and the tool auto-lowercases /r/, /class/, /p/ IRI local names (noting each rewrite) so a stray uppercase does not silently match nothing; entities are content-addressed, so match a named entity by `?e <http://www.w3.org/2000/01/rdf-schema#label> "Name"` rather than constructing its IRI. Off-graph, a stack also serves the native SPARQL 1.1 Protocol at /sparql for off-the-shelf SPARQL clients.',
761
+ query: "Use lbb_query for structured SPARQL-subset bodies, SPARQL text, and canned analysis. A mode=structured body is { patterns: [{ subject, predicate, object }], filters?, group_by?, group_keys?, aggregates?, having? }; a pattern `predicate` is a relation name and is case-insensitive. mode=structured GROUP BY is not limited to entity identity: group_keys can key on a typed scalar property or calendar bucket. In SPARQL text, relations are <https://littlebigbrain.com/r/NAME> and types <https://littlebigbrain.com/class/NAME>. Each query pins one published watermark.",
833
762
  write: "Use lbb_commit for fact writes and search relevance feedback; omitted idempotency keys are content-derived so retries dedupe. Set typed scalar attributes via entity_properties once the field is registered (add it on a live graph with lbb_configure evolve_ontology add_property). For feedback, use mode=search_feedback rather than fact triplets.",
834
- configure: "Use lbb_configure to define a new ontology, evolve an existing one in place (add_entity_type / add_relation / add_property / widen, rename, narrow/remove), publish a previewed RDF/SHACL schema bundle, or replace stored rules after previewing them with lbb_query mode=infer.",
835
- inference: "Rule body/head terms are { var } or { entity: { entity_type, name } } — a fixed entity lets a rule match or derive a constant value (e.g. a status). A not_exists combinator adds stratified negation for universal conditions. Roll-up example: rule 1 head { var: phase } HAS_INCOMPLETE_DELIVERABLE { var: d }, body phase HAS_DELIVERABLE d, not_exists [ d HAS_DELIVERY_STATUS { entity: DeliveryStatus/Complete } ]; rule 2 head phase HAS_ROLLUP_STATUS { entity: DeliveryStatus/Complete }, body phase HAS_DELIVERABLE any, not_exists [ phase HAS_INCOMPLETE_DELIVERABLE x ] — derives complete only when every deliverable is complete.",
763
+ configure: "Use lbb_configure to define a new ontology, evolve an existing one in place, or atomically publish ontology/SHACL bundle metadata. Conformance validation runs durably after publication.",
836
764
  },
837
- possibilities: buildPossibilities(relations, entityTypes),
838
- how_to: "Ground with lbb_inspect action=guide, retrieve with lbb_search, rate useful/partial/bad retrieval results with lbb_commit mode=search_feedback when you have a judgment, inspect exact entities/schema/rules with lbb_inspect, preview analysis or inference with lbb_query, then write graph facts with lbb_commit or configuration with lbb_configure only when intended.",
765
+ possibilities: buildPossibilities(relations),
766
+ how_to: "Ground with lbb_inspect action=guide, retrieve with lbb_search, rate useful/partial/bad retrieval results when you have a judgment, inspect exact entities/schema with lbb_inspect, run analysis with lbb_query, then write graph facts with lbb_commit or configuration with lbb_configure only when intended.",
839
767
  };
840
768
  }
841
769
  export async function analyze(scopedClient, p) {
@@ -885,8 +813,6 @@ export async function analyze(scopedClient, p) {
885
813
  lexical: true,
886
814
  bm25: true,
887
815
  vector: true,
888
- bm25_source: "persisted",
889
- vector_source: "persisted",
890
816
  consistency: "strong",
891
817
  },
892
818
  facets: [{ field }],
@@ -969,101 +895,6 @@ export function ontologyDefineBody(p) {
969
895
  merge_default: p.merge_default ?? false,
970
896
  };
971
897
  }
972
- export function schemaSourceBody(source) {
973
- if (source === undefined)
974
- return undefined;
975
- return {
976
- source: source.source,
977
- format: source.format ?? "auto",
978
- };
979
- }
980
- export function schemaPreviewBody(p) {
981
- const ontology = schemaSourceBody(p.ontology);
982
- const shapes = schemaSourceBody(p.shapes);
983
- if (ontology === undefined && shapes === undefined) {
984
- throw new Error("schema_preview requires an ontology or shapes source");
985
- }
986
- return {
987
- ontology,
988
- shapes,
989
- base_ontology_version: p.base_ontology_version ?? null,
990
- base_shapes_version: p.base_shapes_version ?? null,
991
- desired_mode: p.desired_mode ?? "warn",
992
- };
993
- }
994
- export function schemaPublishBody(p) {
995
- return {
996
- preview_digest: p.preview_digest,
997
- ontology: schemaSourceBody(p.ontology),
998
- shapes: schemaSourceBody(p.shapes),
999
- desired_mode: p.desired_mode,
1000
- confirm_restrictive: p.confirm_restrictive ?? false,
1001
- };
1002
- }
1003
- export function choosePublishMode(response, requested) {
1004
- const modes = Array.isArray(response.publish_mode_allowed)
1005
- ? response.publish_mode_allowed
1006
- : [];
1007
- if (modes.includes(requested))
1008
- return requested === "reject" ? "reject" : "warn";
1009
- if (modes.includes("warn"))
1010
- return "warn";
1011
- if (modes.includes("reject"))
1012
- return "reject";
1013
- return undefined;
1014
- }
1015
- export function auditSummary(audit) {
1016
- if (!audit || typeof audit !== "object")
1017
- return undefined;
1018
- const a = audit;
1019
- return {
1020
- conforms: a.conforms,
1021
- result_count: a.result_count,
1022
- messages: a.messages,
1023
- sample_results: Array.isArray(a.results)
1024
- ? a.results.slice(0, 5)
1025
- : undefined,
1026
- };
1027
- }
1028
- export async function schemaPreview(target, p) {
1029
- const body = schemaPreviewBody(p);
1030
- const response = (await target.schema.preview(body));
1031
- const desiredMode = typeof response.desired_mode === "string"
1032
- ? response.desired_mode
1033
- : String(body.desired_mode);
1034
- const publishMode = choosePublishMode(response, desiredMode);
1035
- const suggestedPublish = publishMode && body.shapes
1036
- ? {
1037
- tool: "lbb_configure",
1038
- args: {
1039
- action: "publish_schema",
1040
- graph: p.graph,
1041
- branch: p.branch,
1042
- preview_digest: response.preview_digest,
1043
- desired_mode: publishMode,
1044
- confirm_restrictive: response.verdict === "restrictive" && publishMode === "warn"
1045
- ? true
1046
- : undefined,
1047
- ontology: body.ontology,
1048
- shapes: body.shapes,
1049
- },
1050
- }
1051
- : undefined;
1052
- return {
1053
- graph: response.graph,
1054
- verdict: response.verdict,
1055
- can_publish: response.can_publish,
1056
- publish_mode_allowed: response.publish_mode_allowed,
1057
- preview_digest: response.preview_digest,
1058
- desired_mode: response.desired_mode,
1059
- proposed_ontology_version: response.proposed_ontology_version,
1060
- proposed_shapes_version: response.proposed_shapes_version,
1061
- diff: response.diff,
1062
- audit: auditSummary(response.audit),
1063
- messages: response.messages,
1064
- suggested_publish_schema: suggestedPublish,
1065
- };
1066
- }
1067
898
  /**
1068
899
  * Register the hard-break v2 little big brain tool belt on an MCP server. The surface is
1069
900
  * task-oriented for agents; each tool dispatches to the existing @littlebigbrain/client
package/dist/tools.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { z } from "zod";
2
2
  import { IDEMPOTENT_WRITE, MUTATING, READ_ONLY, configureInputSchema, configureWireSchema, detailSchema, graphScope, inspectInputSchema, inspectWireSchema, jsonObjectSchema, queryInputSchema, queryWireSchema, searchFeedbackSchema, } from "./tool-contracts.js";
3
- import { analyze, assertCursorScope, contentHashKey, decodeQueryCursor, effectiveRowLimit, entityEdgeCapHint, enrichError, errorResult, guide, normalizeDetail, normalizeLbbIris, ontologyDefineBody, queryCommitPin, queryEnvelope, requireString, resolveProfile, rowPageFrom, rowPageNext, run, schemaPreview, schemaPublishBody, scoped, searchBody, searchFeedbackHint, stableJson, toolResult, } from "./tool-runtime.js";
3
+ import { analyze, assertCursorScope, contentHashKey, decodeQueryCursor, effectiveRowLimit, enrichError, errorResult, guide, normalizeDetail, normalizeLbbIris, ontologyDefineBody, queryCommitPin, queryEnvelope, requireString, rowPageFrom, rowPageNext, run, scoped, searchBody, searchFeedbackHint, stableJson, toolResult, } from "./tool-runtime.js";
4
4
  export function registerLbbTools(server, client) {
5
5
  server.registerTool("lbb_search", {
6
- description: "Natural-language retrieval over Little Big Brain. Use `query` for one phrasing, `queries` for reciprocal-rank fusion across phrasings, and `follow_paths: true` when you want bounded graph paths from text-resolved seed entities. When you can judge returned results, call lbb_commit mode=search_feedback with good=3, partial=1, bad=0 so Little Big Brain can build customer-specific qrels for embedding training.",
6
+ description: "Natural-language retrieval over Little Big Brain. Use `query` for one phrasing or `queries` for reciprocal-rank fusion across phrasings. Semantic graph search can return internally scored paths alongside entity and assertion results. When you can judge returned results, call lbb_commit mode=search_feedback with good=3, partial=1, bad=0 so Little Big Brain can build customer-specific qrels for embedding training.",
7
7
  inputSchema: {
8
8
  query: z.string().optional().describe("Natural-language query"),
9
9
  queries: z
@@ -12,10 +12,7 @@ export function registerLbbTools(server, client) {
12
12
  .optional()
13
13
  .describe("Multiple phrasings to fuse"),
14
14
  mode: z.enum(["hybrid", "bm25", "vector", "lexical"]).optional(),
15
- follow_paths: z.boolean().optional(),
16
15
  top_k: z.number().int().positive().optional(),
17
- max_hops: z.number().int().positive().max(6).optional(),
18
- direction: z.enum(["out", "in", "both"]).optional(),
19
16
  profile: z
20
17
  .enum(["ndcg_v1", "graph_aware_v1", "baseline", "scored_atom_v1"])
21
18
  .optional(),
@@ -33,7 +30,7 @@ export function registerLbbTools(server, client) {
33
30
  ...graphScope,
34
31
  },
35
32
  annotations: READ_ONLY,
36
- }, ({ query, queries, mode, follow_paths, top_k, max_hops, direction, profile, as_of, as_of_commit_seq, detail, graph, branch, }) => run(client, "lbb_search", detail, () => {
33
+ }, ({ query, queries, mode, top_k, profile, as_of, as_of_commit_seq, detail, graph, branch, }) => run(client, "lbb_search", detail, () => {
37
34
  const target = scoped(client, graph, branch);
38
35
  if (queries?.length) {
39
36
  return target.multiSearch({
@@ -54,26 +51,6 @@ export function registerLbbTools(server, client) {
54
51
  });
55
52
  }
56
53
  const q = requireString(query, "query");
57
- if (follow_paths) {
58
- return target.semanticTraverse({
59
- query: q,
60
- seed_top_k: Math.min(top_k ?? 3, 10),
61
- search: {
62
- lexical: true,
63
- bm25: true,
64
- vector: true,
65
- bm25_source: "persisted",
66
- vector_source: "persisted",
67
- consistency: "strong",
68
- profile: resolveProfile(profile),
69
- },
70
- direction: direction ?? "both",
71
- max_hops: max_hops ?? 2,
72
- max_frontier_entities: 50,
73
- max_paths: top_k ?? 25,
74
- explain: false,
75
- });
76
- }
77
54
  return target.graphSearch(searchBody({
78
55
  query: q,
79
56
  mode,
@@ -85,61 +62,14 @@ export function registerLbbTools(server, client) {
85
62
  }, (data) => ({
86
63
  feedback: searchFeedbackHint(data, { query, queries, graph, branch }),
87
64
  })));
88
- server.registerTool("lbb_ask", {
89
- description: "Ask a natural-language question about the graph and get a grounded answer with citations. The database snaps the question to its real vocabulary (never invented), retrieves against the pinned snapshot, and answers. `mode` is `resident_planner` when a small resident model synthesized the prose, or `grounding_only` when it returns the grounded evidence for your own model to finish. Prefer this over lbb_search when you want a direct, cited answer rather than a ranked list; set `execute: false` to get only the grounding — the real vocabulary the question maps to — without retrieval. The response `explain` block reports how much the database narrowed (vocabulary candidates the question snapped to, plus retrieved entity/assertion counts) and the per-stage latency (ground / retrieve / synth / total ms), so you can see the pipeline that produced the answer.",
90
- inputSchema: {
91
- question: z
92
- .string()
93
- .describe("The natural-language question to answer from the graph"),
94
- top_k: z
95
- .number()
96
- .int()
97
- .positive()
98
- .max(25)
99
- .optional()
100
- .describe("Max citations to return (default 8)"),
101
- execute: z
102
- .boolean()
103
- .optional()
104
- .describe("Run retrieval and answer (default true); false returns only the grounding"),
105
- as_of: z
106
- .string()
107
- .optional()
108
- .describe("Valid-time cursor (RFC 3339): retrieval reflects facts true at this instant"),
109
- as_of_commit_seq: z
110
- .number()
111
- .int()
112
- .nonnegative()
113
- .optional()
114
- .describe("Snapshot pin: retrieval and citations reproduce the graph as of this commit sequence"),
115
- detail: detailSchema,
116
- ...graphScope,
117
- },
118
- annotations: READ_ONLY,
119
- }, ({ question, top_k, execute, as_of, as_of_commit_seq, detail, graph, branch, }) => run(client, "lbb_ask", detail, () => scoped(client, graph, branch).ask({
120
- question,
121
- top_k,
122
- execute,
123
- ...(as_of !== undefined ? { as_of_valid_time: as_of } : {}),
124
- ...(as_of_commit_seq !== undefined ? { as_of_commit_seq } : {}),
125
- })));
126
65
  server.registerTool("lbb_decode", {
127
- description: "Name the relation between two entities. The database narrows the candidates to the relations its type signatures admit for the (source type, target type) pair; if the pair admits exactly one, the database answers alone (`mode: forced`, no model call). Otherwise a small model fine-tuned on this graph's own edges picks from the narrowed set (`mode: model_narrowed`), constrained to real vocabulary so it can only emit a relation that can exist. You can OMIT the types — pass just the names and the database recovers each type by resolving the name to a real entity (echoed in `resolved_source`/`resolved_target`). Use it to fill a missing edge label, verify a relationship, or assemble structured triples. Returns the `relation`, the admissible `candidates`, and `signature_forced`.",
66
+ description: "Name the relation between two entities from the admissible published vocabulary. Types may be omitted and resolved from entity names.",
128
67
  inputSchema: {
129
- source_name: z.string().describe("Source entity display name"),
130
- source_type: z
131
- .string()
132
- .optional()
133
- .describe("Source entity type; omit to have the DB recover it from the name"),
134
- target_name: z.string().describe("Target entity display name"),
135
- target_type: z
136
- .string()
137
- .optional()
138
- .describe("Target entity type; omit to have the DB recover it from the name"),
139
- use_model_when_forced: z
140
- .boolean()
141
- .optional()
142
- .describe("Call the model even when the type pair forces one relation (default false — a forced pair is answered by the DB alone)"),
68
+ source_name: z.string(),
69
+ source_type: z.string().optional(),
70
+ target_name: z.string(),
71
+ target_type: z.string().optional(),
72
+ use_model_when_forced: z.boolean().optional(),
143
73
  detail: detailSchema,
144
74
  ...graphScope,
145
75
  },
@@ -150,19 +80,14 @@ export function registerLbbTools(server, client) {
150
80
  use_model_when_forced,
151
81
  })));
152
82
  server.registerTool("lbb_ground", {
153
- description: "Ground your terms to the graph's real vocabulary before you query or write, so you never guess a type, relation, or property name. Actions: `complete` narrowed autocomplete: completes a prefix against the real vocabulary, optionally narrowed to the relations a (src_type, dst_type) pair actually admits (so you only propose relations that can exist); `resolve` snap free text to the single nearest real vocabulary item by embedding/lexical similarity, never fabricating a name; `audit` — groundability report: signature sparsity, name semantics, sampled narrowing recall, and a narrow / narrow+finetune / lexical recommendation for this graph.",
83
+ description: "Ground terms to the graph's published vocabulary. complete autocompletes a prefix, resolve snaps free text to real vocabulary, and audit reports graph groundability.",
154
84
  inputSchema: {
155
- action: z
156
- .enum(["complete", "resolve", "audit"])
157
- .describe("complete = narrowed vocabulary autocomplete; resolve = snap free text to the nearest real vocabulary; audit = groundability report"),
85
+ action: z.enum(["complete", "resolve", "audit"]),
158
86
  prefix: z
159
87
  .string()
160
88
  .optional()
161
89
  .describe("[complete] Text prefix to complete against the real vocabulary"),
162
- text: z
163
- .string()
164
- .optional()
165
- .describe("[resolve] Free text to snap to the nearest real vocabulary item"),
90
+ text: z.string().optional().describe("[resolve] Free text to resolve"),
166
91
  src_type: z
167
92
  .string()
168
93
  .optional()
@@ -181,20 +106,20 @@ export function registerLbbTools(server, client) {
181
106
  "property",
182
107
  ]))
183
108
  .optional()
184
- .describe("[complete/resolve] Restrict to these vocabulary kinds (default: all)"),
109
+ .describe("Restrict to these vocabulary kinds (default: all)"),
185
110
  top_k: z
186
111
  .number()
187
112
  .int()
188
113
  .positive()
189
114
  .max(50)
190
115
  .optional()
191
- .describe("[complete/resolve] Max results (default 8)"),
116
+ .describe("Max results (default 8)"),
192
117
  sample: z
193
118
  .number()
194
119
  .int()
195
120
  .positive()
196
121
  .optional()
197
- .describe("[audit] Entities to sample for narrowing-recall"),
122
+ .describe("[audit] Entities sampled for narrowing recall"),
198
123
  detail: detailSchema,
199
124
  ...graphScope,
200
125
  },
@@ -209,7 +134,7 @@ export function registerLbbTools(server, client) {
209
134
  });
210
135
  }
211
136
  if (action === "audit") {
212
- return target.groundability(sample != null ? { sample } : {});
137
+ return target.groundability(sample == null ? {} : { sample });
213
138
  }
214
139
  const context = src_type !== undefined || dst_type !== undefined
215
140
  ? { src_type, dst_type }
@@ -222,7 +147,7 @@ export function registerLbbTools(server, client) {
222
147
  });
223
148
  }));
224
149
  server.registerTool("lbb_inspect", {
225
- description: "Read graph context and exact graph facts. Actions: guide, ontology, ontology_conformance, schema, schema_preview, schema_audit, rules, ontology_search, metadata, entity, edges, state, history, transitions, why, traverse. entity returns one node's metadata, scalar attributes, current state, edges, history, and observations — its edge/history arrays are a display sample capped at the detail limit (counts holds the true totals), so on a high-degree node the response carries an `edge_sample` block with the capped totals and ready-to-run paged reads; follow those (or call edges/history directly, paged by row_limit/offset with direction/relation filters and an as_of/as_of_commit_seq pin) to read the full set — total_count tells you when to stop. transitions returns the ordered state-transition log of an entity's relation with dwell time at each value (cycle-time/process analysis). metadata includes temporal_coverage check it before attempting as-of/daily views: as_of_valid_time_degenerate or single_commit_time means every point-in-time query returns the same snapshot. Use ontology_conformance to check the live data against the ontology's own capped-cardinality rules (derived SHACL sh:maxCount, whole-snapshot, never blocks a write) — distinct from schema_audit, which runs the separately-published SHACL shape bundle.",
150
+ description: "Read graph context and exact graph facts. Actions: guide, ontology, ontology_conformance, schema, ontology_search, metadata, entity, state, history, transitions, why. schema reads active ontology/SHACL bundle metadata without running validation. ontology_conformance serves the durable report referenced by the pinned published root. entity returns one node's metadata, scalar attributes, bounded Base-backed edge neighborhood, history, and observations. Use lbb_query with SPARQL property paths for precise path selection.",
226
151
  inputSchema: inspectWireSchema,
227
152
  annotations: READ_ONLY,
228
153
  }, (rawArgs) => {
@@ -243,20 +168,6 @@ export function registerLbbTools(server, client) {
243
168
  return target.ontologyConformance();
244
169
  case "schema":
245
170
  return target.schema.view();
246
- case "schema_preview":
247
- return schemaPreview(target, {
248
- graph: args.graph,
249
- branch: args.branch,
250
- ontology: args.ontology,
251
- shapes: args.shapes,
252
- base_ontology_version: args.base_ontology_version,
253
- base_shapes_version: args.base_shapes_version,
254
- desired_mode: args.desired_mode,
255
- });
256
- case "schema_audit":
257
- return target.schema.audit();
258
- case "rules":
259
- return target.graphRules();
260
171
  case "ontology_search":
261
172
  return target.ontologySearch({
262
173
  query: args.query,
@@ -277,22 +188,6 @@ export function registerLbbTools(server, client) {
277
188
  asOf: args.as_of,
278
189
  asOfCommitSeq: args.as_of_commit_seq,
279
190
  });
280
- case "edges":
281
- // Paged edge listing — the way to read every edge of a high-degree
282
- // node, which `entity` hard-caps. Returns the unified list envelope;
283
- // page by feeding next_cursor back as `cursor` until has_more=false.
284
- return target.graphEdges({
285
- id: args.entity_id,
286
- type: args.entity_type,
287
- name: args.name,
288
- direction: args.direction,
289
- relation: args.relation,
290
- limit: args.row_limit,
291
- cursor: args.cursor,
292
- offset: args.offset,
293
- asOf: args.as_of,
294
- asOfCommitSeq: args.as_of_commit_seq,
295
- });
296
191
  case "state":
297
192
  return target.currentState({
298
193
  entity: {
@@ -323,18 +218,6 @@ export function registerLbbTools(server, client) {
323
218
  name: args.target_name,
324
219
  },
325
220
  });
326
- case "traverse":
327
- return target.traverse({
328
- start: {
329
- entity_type: args.entity_type,
330
- name: args.name,
331
- },
332
- relations: args.relations ?? null,
333
- direction: args.direction ?? "both",
334
- max_hops: args.max_hops ?? 2,
335
- max_frontier_entities: 50,
336
- max_paths: args.top_k ?? 25,
337
- });
338
221
  case "transitions":
339
222
  return target.transitions({
340
223
  entity: {
@@ -346,14 +229,10 @@ export function registerLbbTools(server, client) {
346
229
  as_of_commit_seq: args.as_of_commit_seq ?? null,
347
230
  });
348
231
  }
349
- }, (data) => args.action === "entity"
350
- ? entityEdgeCapHint(data, args.detail, args.entity_id
351
- ? { entity_id: args.entity_id }
352
- : { entity_type: args.entity_type, name: args.name })
353
- : {});
232
+ });
354
233
  });
355
234
  server.registerTool("lbb_query", {
356
- description: "Analytical and expert reads. Modes: structured (SPARQL-subset JSON body), sparql (SPARQL text), shacl, infer, retrieval_premises, analyze. Relations are <https://littlebigbrain.com/r/NAME> and types <https://littlebigbrain.com/class/NAME> (both lowercased); entities are content-addressed, so anchor a named one by its rdfs:label rather than building its IRI (see the sparql/structured field hints, and lbb_inspect action=ontology for exact names). For structured/sparql row paging, MCP owns limit/offset via row_limit/cursor; a cursor reuses the original query/body and pins continuation pages to the original head commit. When a single page's rows are complete server-side (row_page.has_more false) but too large for one MCP result, the envelope reports `rows_shown` (fewer than row_page.returned), keeps `truncated: true`, and hands back a `next` cursor that pages the same result set at a smaller row_limit — so a large fully-returned result is never silently cut without a way to read the rest.",
235
+ description: "Analytical and expert reads. Modes: structured (SPARQL-subset JSON body), sparql (SPARQL text), analyze. Relations are <https://littlebigbrain.com/r/NAME> and types <https://littlebigbrain.com/class/NAME> (both lowercased); entities are content-addressed, so anchor a named one by its rdfs:label rather than building its IRI. Structured and text queries pin one published watermark for the request.",
357
236
  inputSchema: queryWireSchema,
358
237
  annotations: READ_ONLY,
359
238
  }, (rawArgs) => {
@@ -505,53 +384,53 @@ export function registerLbbTools(server, client) {
505
384
  }
506
385
  return run(client, `lbb_query.${args.mode}`, args.detail, async () => {
507
386
  const target = scoped(client, args.graph, args.branch);
508
- switch (args.mode) {
509
- case "shacl":
510
- return target.shacl({
511
- shapes: args.shapes ?? [],
512
- mode: args.shacl_mode ?? "select",
513
- include_derived: args.include_derived ?? false,
514
- rules: args.rules ?? [],
515
- as_of_valid_time: args.as_of ?? null,
516
- top_k: args.top_k ?? 10,
517
- explain: args.explain ?? false,
518
- });
519
- case "infer":
520
- return target.infer({
521
- rules: args.rules ?? [],
522
- max_rounds: args.max_rounds ?? null,
523
- max_derived: args.max_derived ?? null,
524
- max_solutions: args.max_solutions ?? null,
525
- as_of_valid_time: args.as_of ?? null,
526
- as_of_commit_seq: args.as_of_commit_seq ?? null,
527
- });
528
- case "retrieval_premises":
529
- return target.retrievalPremises({
530
- anchor: {
531
- entity_type: args.anchor_type,
532
- name: args.anchor_name,
533
- },
534
- relation: args.relation,
535
- model_id: "lbb-hash-lexical-v1",
536
- target_kind: "entity",
537
- calibration: { a: -1, b: 0 },
538
- threshold: args.threshold ?? 0.5,
539
- max_premises: args.max_premises ?? 50,
540
- query: args.query,
541
- query_top_k: args.query_top_k ?? 50,
542
- });
543
- case "analyze":
544
- return analyze(target, {
545
- metric: args.metric,
546
- chart: args.chart,
547
- top_k: args.top_k,
548
- query: args.query,
549
- field: args.field,
550
- sparql: args.sparql,
551
- });
552
- }
387
+ return analyze(target, {
388
+ metric: args.metric,
389
+ chart: args.chart,
390
+ top_k: args.top_k,
391
+ query: args.query,
392
+ field: args.field,
393
+ sparql: args.sparql,
394
+ });
553
395
  });
554
396
  });
397
+ server.registerTool("lbb_models", {
398
+ description: "Read model-training inputs or compare retrieval configurations over one pinned published snapshot. shadow_eval takes the API ShadowEvalRequest body; dataset actions return bounded training examples at an optional signal split.",
399
+ inputSchema: {
400
+ action: z.enum([
401
+ "shadow_eval",
402
+ "planner_dataset",
403
+ "planner_preference_dataset",
404
+ "suggest_dataset",
405
+ "extractor_dataset",
406
+ ]),
407
+ body: jsonObjectSchema.optional(),
408
+ limit: z.number().int().positive().optional(),
409
+ split_seq: z.number().int().nonnegative().optional(),
410
+ detail: detailSchema,
411
+ ...graphScope,
412
+ },
413
+ annotations: READ_ONLY,
414
+ }, ({ action, body, limit, split_seq, detail, graph, branch }) => run(client, `lbb_models.${action}`, detail, () => {
415
+ const target = scoped(client, graph, branch);
416
+ switch (action) {
417
+ case "shadow_eval":
418
+ if (!body)
419
+ throw new Error("shadow_eval requires body");
420
+ return target.shadowEval(body);
421
+ case "planner_dataset":
422
+ return target.plannerDataset({ limit, splitSeq: split_seq });
423
+ case "planner_preference_dataset":
424
+ return target.plannerPreferenceDataset({
425
+ limit,
426
+ splitSeq: split_seq,
427
+ });
428
+ case "suggest_dataset":
429
+ return target.suggestDataset({ limit, splitSeq: split_seq });
430
+ case "extractor_dataset":
431
+ return target.extractorDataset({ limit, splitSeq: split_seq });
432
+ }
433
+ }));
555
434
  server.registerTool("lbb_commit", {
556
435
  description: "Write graph facts, retract them, or label search results. mode=facts writes triplets/embeddings/properties; mode=retract removes a wrongly-added fact (by edge or by entity) without a full reset; mode=search_feedback labels query/result relevance after lbb_search (Feedback grades: 3=ideal/good, 1=partial, 0=bad; include query, search_id when available, target, rank, score). Explicit idempotency_key wins; when omitted, MCP derives a stable content hash so content-identical retries dedupe. Facts mode defaults edge_idempotency to append; pass skip_unchanged for re-runnable backfills.",
557
436
  inputSchema: {
@@ -661,7 +540,7 @@ export function registerLbbTools(server, client) {
661
540
  });
662
541
  }));
663
542
  server.registerTool("lbb_configure", {
664
- description: "Mutate stored graph configuration. Actions: define_ontology (create a new graph ontology), evolve_ontology (evolve an existing graph's ontology in place by name — add_entity_type / add_relation / add_property (a typed scalar field so entity_properties can write it) / widen_relation, rename and set-inverse/cardinality, and data-gated narrow/remove; bumps the ontology version, preserves every record, no migration), publish_schema (activate a previewed RDF/SHACL shape bundle), and define_rules (replace the branch's stored inference rules body/head terms may be variables or fixed entities, and not_exists combinators add stratified negation for universal conditions).",
543
+ description: "Mutate stored graph configuration. Actions: define_ontology, evolve_ontology, and publish_schema. Schema publication atomically activates metadata and enqueues durable conformance; it never validates the whole graph in the request.",
665
544
  inputSchema: configureWireSchema,
666
545
  annotations: MUTATING,
667
546
  }, (rawArgs) => {
@@ -687,34 +566,17 @@ export function registerLbbTools(server, client) {
687
566
  allow_data_conflicts: args.allow_data_conflicts ?? false,
688
567
  });
689
568
  }
690
- if (args.action === "publish_schema") {
691
- return scoped(client, args.graph, args.branch).schema.publish(schemaPublishBody({
692
- preview_digest: args.preview_digest,
693
- ontology: args.ontology,
694
- shapes: args.shapes,
695
- desired_mode: args.desired_mode,
696
- confirm_restrictive: args.confirm_restrictive,
697
- }));
698
- }
699
- if (args.rules.length === 0 && args.confirm_empty !== true) {
700
- throw new Error("define_rules with an empty rules array requires confirm_empty=true");
569
+ if (args.ontology === undefined && args.shapes === undefined) {
570
+ throw new Error("publish_schema requires an ontology or shapes source");
701
571
  }
702
- return scoped(client, args.graph, args.branch).defineRules({
703
- rules: args.rules,
572
+ return scoped(client, args.graph, args.branch).schema.publish({
573
+ ontology: args.ontology,
574
+ shapes: args.shapes,
575
+ desired_mode: args.desired_mode,
576
+ confirm_restrictive: args.confirm_restrictive,
704
577
  });
705
578
  });
706
579
  });
707
- server.registerTool("lbb_index", {
708
- description: "Build or refresh persisted BM25, vector, and adjacency indexes so recently committed facts become searchable.",
709
- inputSchema: {
710
- background: z
711
- .boolean()
712
- .optional()
713
- .describe("Run detached and poll metadata for completion"),
714
- ...graphScope,
715
- },
716
- annotations: MUTATING,
717
- }, ({ background, graph, branch }) => run(client, "lbb_index", "standard", () => scoped(client, graph, branch).indexRun({ background })));
718
580
  server.registerTool("lbb_branch", {
719
581
  description: "Branch lifecycle. Actions: create (fork a new branch off from_branch — the tool's `branch` argument names the NEW branch) and merge (validate-then-merge: replay from_branch's post-fork commits onto the scoped target branch — its fork parent — as ONE commit with event ids preserved; SHACL-validates the would-be merged state first and refuses with the report on violations; a fact superseded on the target after the fork wins over the branch's version, reported as a supersedure_race conflict; delete_source consumes the merged branch).",
720
582
  inputSchema: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@littlebigbrain/mcp",
3
- "version": "0.2.5",
3
+ "version": "0.3.0",
4
4
  "description": "MCP server for little big brain — graph and hybrid search tools for agents",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -52,7 +52,7 @@
52
52
  "test:coverage": "npm run clean:test && tsc -p tsconfig.test.json && node --test --experimental-test-coverage --test-coverage-include=test-dist/*.js --test-coverage-exclude=test-dist/*.test.js --test-coverage-exclude=test-dist/stdio.js --test-coverage-exclude=test-dist/test-support.js --test-coverage-lines=90 --test-coverage-branches=70 --test-coverage-functions=85 \"test-dist/**/*.test.js\""
53
53
  },
54
54
  "dependencies": {
55
- "@littlebigbrain/client": "^0.6.0",
55
+ "@littlebigbrain/client": "^0.10.0",
56
56
  "@modelcontextprotocol/sdk": "^1",
57
57
  "zod": "^3"
58
58
  },