@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 +6 -7
- package/dist/tool-contracts.js +5 -184
- package/dist/tool-runtime.js +7 -176
- package/dist/tools.js +72 -210
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @littlebigbrain/mcp
|
|
2
2
|
|
|
3
|
-
|
|
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,
|
|
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
|
|
66
|
-
| `lbb_query` | SPARQL
|
|
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
|
-
| `
|
|
71
|
-
| `
|
|
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
|
|
package/dist/tool-contracts.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
666
|
-
*
|
|
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
|
package/dist/tool-runtime.js
CHANGED
|
@@ -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
|
|
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
|
|
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,
|
|
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:
|
|
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
|
|
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
|
|
838
|
-
how_to: "Ground with lbb_inspect action=guide, retrieve with lbb_search, rate useful/partial/bad retrieval results
|
|
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,
|
|
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
|
|
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,
|
|
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
|
|
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()
|
|
130
|
-
source_type: z
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
|
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("
|
|
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("
|
|
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
|
|
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
|
|
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,
|
|
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
|
-
}
|
|
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),
|
|
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
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
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
|
|
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.
|
|
691
|
-
|
|
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).
|
|
703
|
-
|
|
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.
|
|
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.
|
|
55
|
+
"@littlebigbrain/client": "^0.10.0",
|
|
56
56
|
"@modelcontextprotocol/sdk": "^1",
|
|
57
57
|
"zod": "^3"
|
|
58
58
|
},
|