@littlebigbrain/mcp 0.3.0 → 0.4.1
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 +1 -4
- package/dist/tool-contracts.js +3 -3
- package/dist/tool-runtime.js +5 -106
- package/dist/tools.js +11 -159
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -58,11 +58,8 @@ missing.
|
|
|
58
58
|
|
|
59
59
|
| Tool | Use it for |
|
|
60
60
|
| --- | --- |
|
|
61
|
-
| `lbb_search` | hybrid retrieval, multi-query fusion, and semantic graph results |
|
|
62
|
-
| `lbb_decode` | constrained relation decoding |
|
|
63
|
-
| `lbb_ground` | vocabulary completion and resolution |
|
|
64
61
|
| `lbb_inspect` | ontology, schema, entity, state, history, and provenance |
|
|
65
|
-
| `lbb_query` | SPARQL
|
|
62
|
+
| `lbb_query` | SPARQL text, structured SPARQL bodies, and canned analysis |
|
|
66
63
|
| `lbb_commit` | facts, properties, and embeddings |
|
|
67
64
|
| `lbb_observe` | conversation episodes plus reviewed extraction |
|
|
68
65
|
| `lbb_branch` | isolation branches and validated merge |
|
package/dist/tool-contracts.js
CHANGED
|
@@ -382,7 +382,7 @@ export const queryInputSchema = z.discriminatedUnion("mode", [
|
|
|
382
382
|
mode: z.literal("structured"),
|
|
383
383
|
body: jsonObjectSchema
|
|
384
384
|
.optional()
|
|
385
|
-
.describe('Structured SPARQL-subset
|
|
385
|
+
.describe('Structured SPARQL-subset request body. Shape: { patterns: [{ subject, predicate, object }], filters?, group_by?, group_keys?, aggregates?, having?, order_by?, select?, limit?, distinct? }. Each pattern term is { var: "x" } or a fixed { entity: { entity_type, name } }; `predicate` is a relation name and is case-insensitive here (FOR_CLIENT and for_client both resolve — unlike SPARQL text, which needs the lowercased IRI local name). ' +
|
|
386
386
|
"FILTER — `filters` is a list of conditions, each of exact shape " +
|
|
387
387
|
'{ "compare": { "op": <op>, "left": <term>, "right": <term> } } (or { "and": [<filter>…] }, { "or": [<filter>…] }, { "not": <filter> }). ' +
|
|
388
388
|
"`op` is one of eq | ne | lt | le | gt | ge (NOT the symbols =,<,>). Each <term> is exactly one of " +
|
|
@@ -390,7 +390,7 @@ export const queryInputSchema = z.discriminatedUnion("mode", [
|
|
|
390
390
|
'{ "value": <typed> } — and <typed> is exactly one wrapper: { "str": "…" }, { "i64": 5 }, { "f64": 0.9 }, { "bool": true }, { "date_time": "2026-01-01" } (RFC3339), or { "entity": { "entity_type": "T", "name": "N" } }. ' +
|
|
391
391
|
'Complete runnable example — deals whose amount ≥ 1000000: { "patterns": [{ "subject": { "var": "d" }, "predicate": "for_client", "object": { "var": "c" } }], "filters": [{ "compare": { "op": "ge", "left": { "property": { "var": "d", "field": "amount" } }, "right": { "value": { "f64": 1000000 } } } }] }. ' +
|
|
392
392
|
"Comparisons use the field's real declared type (numbers as numbers, datetimes as instants), so they run server-side. " +
|
|
393
|
-
'GROUP BY supports both entity-identity keys (group_by: ["s"]) and typed scalar keys via group_keys: a property value ({ property: { var, field, as } }) or a calendar bucket of a datetime property ({ date_bucket: { var, field, granularity: year|month|week|day|hour, as } }). Scalar keys come back per group under value_keys[as] — so a per-area breakdown or a commits-per-month time series is one server-side query, no client-side bucketing. Worked example -- commits per area per month in one query: { "patterns": [{ "subject": { "var": "c" }, "predicate": "committed_to", "object": { "var": "repo" } }], "group_keys": [{ "date_bucket": { "var": "c", "field": "committed_at", "granularity": "month", "as": "m" } }, { "property": { "var": "c", "field": "area", "as": "area" } }], "aggregates": [{ "func": "count", "as": "n" }], "order_by": [{ "var": "m" }] } -- area and committed_at are typed entity attributes (set via entity_properties; readable flat under attributes, never a nested metadata blob), and each group returns value_keys.m + value_keys.area + aggregates.n. `having: [...]` takes the same filter shape over the aggregated groups (e.g. { "compare": { "op": "gt", "left": { "var": "n" }, "right": { "value": { "i64": 10 } } } })
|
|
393
|
+
'GROUP BY supports both entity-identity keys (group_by: ["s"]) and typed scalar keys via group_keys: a property value ({ property: { var, field, as } }) or a calendar bucket of a datetime property ({ date_bucket: { var, field, granularity: year|month|week|day|hour, as } }). Scalar keys come back per group under value_keys[as] — so a per-area breakdown or a commits-per-month time series is one server-side query, no client-side bucketing. Worked example -- commits per area per month in one query: { "patterns": [{ "subject": { "var": "c" }, "predicate": "committed_to", "object": { "var": "repo" } }], "group_keys": [{ "date_bucket": { "var": "c", "field": "committed_at", "granularity": "month", "as": "m" } }, { "property": { "var": "c", "field": "area", "as": "area" } }], "aggregates": [{ "func": "count", "as": "n" }], "order_by": [{ "var": "m" }] } -- area and committed_at are typed entity attributes (set via entity_properties; readable flat under attributes, never a nested metadata blob), and each group returns value_keys.m + value_keys.area + aggregates.n. `having: [...]` takes the same filter shape over the aggregated groups (e.g. { "compare": { "op": "gt", "left": { "var": "n" }, "right": { "value": { "i64": 10 } } } }). A `combinators` key (UNION/OPTIONAL/MINUS/EXISTS) is rejected here; express those with SPARQL text under mode=sparql. Cheap aggregate count: pair an equality having (e.g. { "compare": { "op": "eq", "left": { "var": "n" }, "right": { "value": { "i64": 4 } } } }) with row_limit: 1 -- the response row_page.total reports how many groups match without materializing them all, so you read the count off row_page.total instead of paging every matching row. For snapshot pinning prefer the top-level `as_of` / `as_of_commit_seq` arguments below; a bare `as_of` key inside the body is rejected (the body\'s valid-time field is `as_of_valid_time`).'),
|
|
394
394
|
as_of: z
|
|
395
395
|
.string()
|
|
396
396
|
.optional()
|
|
@@ -429,7 +429,7 @@ export const queryInputSchema = z.discriminatedUnion("mode", [
|
|
|
429
429
|
.object({
|
|
430
430
|
mode: z.literal("analyze"),
|
|
431
431
|
metric: z
|
|
432
|
-
.enum(["entity_types", "relations", "overview", "
|
|
432
|
+
.enum(["entity_types", "relations", "overview", "sparql"])
|
|
433
433
|
.optional(),
|
|
434
434
|
chart: z.enum(["bar", "pie"]).optional(),
|
|
435
435
|
top_k: z.number().int().positive().optional(),
|
package/dist/tool-runtime.js
CHANGED
|
@@ -523,81 +523,6 @@ export async function run(client, label, detail, fn, augment) {
|
|
|
523
523
|
return errorResult(await enrichError(client, error));
|
|
524
524
|
}
|
|
525
525
|
}
|
|
526
|
-
/**
|
|
527
|
-
* Point-of-use feedback affordance attached to every `lbb_search` result: an
|
|
528
|
-
* agent looking at results it can judge gets a ready-to-run
|
|
529
|
-
* `lbb_commit mode=search_feedback` template (the standout "guide ships runnable
|
|
530
|
-
* possibilities" pattern, applied to the moment of judgment). The `search_id` is
|
|
531
|
-
* pre-filled from the response when present so the label set ties back to this
|
|
532
|
-
* exact ranked run.
|
|
533
|
-
*/
|
|
534
|
-
export function searchFeedbackHint(data, ctx) {
|
|
535
|
-
const searchId = data &&
|
|
536
|
-
typeof data === "object" &&
|
|
537
|
-
typeof data.search_id === "string"
|
|
538
|
-
? data.search_id
|
|
539
|
-
: undefined;
|
|
540
|
-
return {
|
|
541
|
-
how: "If you can judge these results, rate them with lbb_commit mode=search_feedback. Little Big Brain stores the labels as customer-specific qrels (in __lbb_feedback) and exports them as training/eval data for embedding fine-tuning — they improve retrieval, and are kept separate from customer facts. Skip it when you have no basis to judge.",
|
|
542
|
-
grades: { ideal_or_good: 3, partially_relevant: 1, bad: 0 },
|
|
543
|
-
example: {
|
|
544
|
-
tool: "lbb_commit",
|
|
545
|
-
args: {
|
|
546
|
-
mode: "search_feedback",
|
|
547
|
-
...(ctx.graph !== undefined ? { graph: ctx.graph } : {}),
|
|
548
|
-
...(ctx.branch !== undefined ? { branch: ctx.branch } : {}),
|
|
549
|
-
search_feedback: {
|
|
550
|
-
query: ctx.query ?? ctx.queries?.[0] ?? "<the query you ran>",
|
|
551
|
-
...(searchId !== undefined ? { search_id: searchId } : {}),
|
|
552
|
-
labels: [
|
|
553
|
-
{
|
|
554
|
-
target: {
|
|
555
|
-
kind: "entity",
|
|
556
|
-
entity: { entity_type: "<type>", name: "<name>" },
|
|
557
|
-
},
|
|
558
|
-
rank: 1,
|
|
559
|
-
score: 0.0,
|
|
560
|
-
grade: 3,
|
|
561
|
-
},
|
|
562
|
-
],
|
|
563
|
-
split: "unspecified",
|
|
564
|
-
},
|
|
565
|
-
},
|
|
566
|
-
},
|
|
567
|
-
};
|
|
568
|
-
}
|
|
569
|
-
export function resolveProfile(profile) {
|
|
570
|
-
switch (profile) {
|
|
571
|
-
case "ndcg_v1":
|
|
572
|
-
case "graph_aware_v1":
|
|
573
|
-
case "scored_atom_v1":
|
|
574
|
-
case "baseline":
|
|
575
|
-
return profile;
|
|
576
|
-
default:
|
|
577
|
-
return undefined;
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
export function searchBody(p) {
|
|
581
|
-
const mode = p.mode ?? "hybrid";
|
|
582
|
-
return {
|
|
583
|
-
query: p.query,
|
|
584
|
-
targets: ["concepts", "entities", "assertions", "paths", "observations"],
|
|
585
|
-
search: {
|
|
586
|
-
lexical: mode === "hybrid" || mode === "lexical",
|
|
587
|
-
bm25: mode === "hybrid" || mode === "bm25",
|
|
588
|
-
vector: mode === "hybrid" || mode === "vector",
|
|
589
|
-
consistency: "strong",
|
|
590
|
-
profile: resolveProfile(p.profile),
|
|
591
|
-
},
|
|
592
|
-
max_hops: 2,
|
|
593
|
-
top_k: p.top_k ?? 10,
|
|
594
|
-
...(p.as_of !== undefined ? { as_of_valid_time: p.as_of } : {}),
|
|
595
|
-
...(p.as_of_commit_seq !== undefined
|
|
596
|
-
? { as_of_commit_seq: p.as_of_commit_seq }
|
|
597
|
-
: {}),
|
|
598
|
-
explain: false,
|
|
599
|
-
};
|
|
600
|
-
}
|
|
601
526
|
export function vegaChart(kind, title, points, categoryTitle, valueTitle) {
|
|
602
527
|
const base = {
|
|
603
528
|
$schema: "https://vega.github.io/schema/vega-lite/v5.json",
|
|
@@ -754,16 +679,15 @@ export async function guide(scopedClient) {
|
|
|
754
679
|
entity_types: entityTypes,
|
|
755
680
|
relations,
|
|
756
681
|
capability: {
|
|
757
|
-
|
|
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.",
|
|
682
|
+
search_feedback: "When a 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.",
|
|
759
683
|
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.",
|
|
760
684
|
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.",
|
|
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.",
|
|
762
|
-
write: "Use lbb_commit for fact writes and
|
|
685
|
+
query: "Use lbb_query for structured SPARQL-subset bodies, SPARQL text, and canned analysis. SPARQL is the only query surface. 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.",
|
|
686
|
+
write: "Use lbb_commit for fact writes and 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.",
|
|
763
687
|
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.",
|
|
764
688
|
},
|
|
765
689
|
possibilities: buildPossibilities(relations),
|
|
766
|
-
how_to: "Ground with lbb_inspect action=guide,
|
|
690
|
+
how_to: "Ground with lbb_inspect action=guide, query with lbb_query (structured bodies or SPARQL text), rate useful/partial/bad result sets when you have a judgment, inspect exact entities/schema with lbb_inspect, then write graph facts with lbb_commit or configuration with lbb_configure only when intended.",
|
|
767
691
|
};
|
|
768
692
|
}
|
|
769
693
|
export async function analyze(scopedClient, p) {
|
|
@@ -804,31 +728,6 @@ export async function analyze(scopedClient, p) {
|
|
|
804
728
|
];
|
|
805
729
|
}
|
|
806
730
|
}
|
|
807
|
-
else if (metric === "facets") {
|
|
808
|
-
const field = requireString(p.field, "field");
|
|
809
|
-
const res = (await scopedClient.graphSearch({
|
|
810
|
-
query: p.query ?? "",
|
|
811
|
-
targets: ["entities", "assertions", "observations"],
|
|
812
|
-
search: {
|
|
813
|
-
lexical: true,
|
|
814
|
-
bm25: true,
|
|
815
|
-
vector: true,
|
|
816
|
-
consistency: "strong",
|
|
817
|
-
},
|
|
818
|
-
facets: [{ field }],
|
|
819
|
-
max_hops: 1,
|
|
820
|
-
top_k: 50,
|
|
821
|
-
explain: false,
|
|
822
|
-
}));
|
|
823
|
-
const facet = (res.facets ?? []).find((f) => f.field === field) ??
|
|
824
|
-
(res.facets ?? [])[0];
|
|
825
|
-
title = `${p.query ? `"${p.query}"` : "All"} by ${field}`;
|
|
826
|
-
categoryTitle = field;
|
|
827
|
-
points = (facet?.buckets ?? []).map((b) => ({
|
|
828
|
-
label: b.value,
|
|
829
|
-
value: b.count,
|
|
830
|
-
}));
|
|
831
|
-
}
|
|
832
731
|
else if (metric === "sparql") {
|
|
833
732
|
const body = requireObject(p.sparql, "sparql");
|
|
834
733
|
const res = (await scopedClient.sparql(body));
|
|
@@ -855,7 +754,7 @@ export async function analyze(scopedClient, p) {
|
|
|
855
754
|
}));
|
|
856
755
|
}
|
|
857
756
|
else {
|
|
858
|
-
throw new Error("metric must be one of entity_types, relations, overview,
|
|
757
|
+
throw new Error("metric must be one of entity_types, relations, overview, sparql");
|
|
859
758
|
}
|
|
860
759
|
points.sort((a, b) => b.value - a.value);
|
|
861
760
|
if (typeof p.top_k === "number" && p.top_k > 0)
|
package/dist/tools.js
CHANGED
|
@@ -1,151 +1,7 @@
|
|
|
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, enrichError, errorResult, guide, normalizeDetail, normalizeLbbIris, ontologyDefineBody, queryCommitPin, queryEnvelope, requireString, rowPageFrom, rowPageNext, run, scoped,
|
|
3
|
+
import { analyze, assertCursorScope, contentHashKey, decodeQueryCursor, effectiveRowLimit, enrichError, errorResult, guide, normalizeDetail, normalizeLbbIris, ontologyDefineBody, queryCommitPin, queryEnvelope, requireString, rowPageFrom, rowPageNext, run, scoped, stableJson, toolResult, } from "./tool-runtime.js";
|
|
4
4
|
export function registerLbbTools(server, client) {
|
|
5
|
-
server.registerTool("lbb_search", {
|
|
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
|
-
inputSchema: {
|
|
8
|
-
query: z.string().optional().describe("Natural-language query"),
|
|
9
|
-
queries: z
|
|
10
|
-
.array(z.string())
|
|
11
|
-
.min(1)
|
|
12
|
-
.optional()
|
|
13
|
-
.describe("Multiple phrasings to fuse"),
|
|
14
|
-
mode: z.enum(["hybrid", "bm25", "vector", "lexical"]).optional(),
|
|
15
|
-
top_k: z.number().int().positive().optional(),
|
|
16
|
-
profile: z
|
|
17
|
-
.enum(["ndcg_v1", "graph_aware_v1", "baseline", "scored_atom_v1"])
|
|
18
|
-
.optional(),
|
|
19
|
-
as_of: z
|
|
20
|
-
.string()
|
|
21
|
-
.optional()
|
|
22
|
-
.describe("Valid-time cursor (RFC 3339): results reflect facts true at this instant"),
|
|
23
|
-
as_of_commit_seq: z
|
|
24
|
-
.number()
|
|
25
|
-
.int()
|
|
26
|
-
.nonnegative()
|
|
27
|
-
.optional()
|
|
28
|
-
.describe("Snapshot pin: results reproduce the graph as of this commit sequence (echoed back in snapshot.as_of_commit_seq); a pin past head is an error"),
|
|
29
|
-
detail: detailSchema,
|
|
30
|
-
...graphScope,
|
|
31
|
-
},
|
|
32
|
-
annotations: READ_ONLY,
|
|
33
|
-
}, ({ query, queries, mode, top_k, profile, as_of, as_of_commit_seq, detail, graph, branch, }) => run(client, "lbb_search", detail, () => {
|
|
34
|
-
const target = scoped(client, graph, branch);
|
|
35
|
-
if (queries?.length) {
|
|
36
|
-
return target.multiSearch({
|
|
37
|
-
subqueries: queries.map((q, index) => ({
|
|
38
|
-
id: `q${index}`,
|
|
39
|
-
weight: 1.0,
|
|
40
|
-
request: searchBody({
|
|
41
|
-
query: q,
|
|
42
|
-
mode,
|
|
43
|
-
top_k,
|
|
44
|
-
profile,
|
|
45
|
-
as_of,
|
|
46
|
-
as_of_commit_seq,
|
|
47
|
-
}),
|
|
48
|
-
})),
|
|
49
|
-
top_k: top_k ?? 10,
|
|
50
|
-
explain: false,
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
const q = requireString(query, "query");
|
|
54
|
-
return target.graphSearch(searchBody({
|
|
55
|
-
query: q,
|
|
56
|
-
mode,
|
|
57
|
-
top_k,
|
|
58
|
-
profile,
|
|
59
|
-
as_of,
|
|
60
|
-
as_of_commit_seq,
|
|
61
|
-
}));
|
|
62
|
-
}, (data) => ({
|
|
63
|
-
feedback: searchFeedbackHint(data, { query, queries, graph, branch }),
|
|
64
|
-
})));
|
|
65
|
-
server.registerTool("lbb_decode", {
|
|
66
|
-
description: "Name the relation between two entities from the admissible published vocabulary. Types may be omitted and resolved from entity names.",
|
|
67
|
-
inputSchema: {
|
|
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(),
|
|
73
|
-
detail: detailSchema,
|
|
74
|
-
...graphScope,
|
|
75
|
-
},
|
|
76
|
-
annotations: READ_ONLY,
|
|
77
|
-
}, ({ source_name, source_type, target_name, target_type, use_model_when_forced, detail, graph, branch, }) => run(client, "lbb_decode", detail, () => scoped(client, graph, branch).decode({
|
|
78
|
-
source: { name: source_name, type: source_type },
|
|
79
|
-
target: { name: target_name, type: target_type },
|
|
80
|
-
use_model_when_forced,
|
|
81
|
-
})));
|
|
82
|
-
server.registerTool("lbb_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.",
|
|
84
|
-
inputSchema: {
|
|
85
|
-
action: z.enum(["complete", "resolve", "audit"]),
|
|
86
|
-
prefix: z
|
|
87
|
-
.string()
|
|
88
|
-
.optional()
|
|
89
|
-
.describe("[complete] Text prefix to complete against the real vocabulary"),
|
|
90
|
-
text: z.string().optional().describe("[resolve] Free text to resolve"),
|
|
91
|
-
src_type: z
|
|
92
|
-
.string()
|
|
93
|
-
.optional()
|
|
94
|
-
.describe("[complete] Narrow relation completions to those admitted FROM this source type"),
|
|
95
|
-
dst_type: z
|
|
96
|
-
.string()
|
|
97
|
-
.optional()
|
|
98
|
-
.describe("[complete] Narrow relation completions to those admitted INTO this target type"),
|
|
99
|
-
kinds: z
|
|
100
|
-
.array(z.enum([
|
|
101
|
-
"term",
|
|
102
|
-
"attribute_value",
|
|
103
|
-
"attribute_field",
|
|
104
|
-
"class",
|
|
105
|
-
"relation",
|
|
106
|
-
"property",
|
|
107
|
-
]))
|
|
108
|
-
.optional()
|
|
109
|
-
.describe("Restrict to these vocabulary kinds (default: all)"),
|
|
110
|
-
top_k: z
|
|
111
|
-
.number()
|
|
112
|
-
.int()
|
|
113
|
-
.positive()
|
|
114
|
-
.max(50)
|
|
115
|
-
.optional()
|
|
116
|
-
.describe("Max results (default 8)"),
|
|
117
|
-
sample: z
|
|
118
|
-
.number()
|
|
119
|
-
.int()
|
|
120
|
-
.positive()
|
|
121
|
-
.optional()
|
|
122
|
-
.describe("[audit] Entities sampled for narrowing recall"),
|
|
123
|
-
detail: detailSchema,
|
|
124
|
-
...graphScope,
|
|
125
|
-
},
|
|
126
|
-
annotations: READ_ONLY,
|
|
127
|
-
}, ({ action, prefix, text, src_type, dst_type, kinds, top_k, sample, detail, graph, branch, }) => run(client, "lbb_ground", detail, () => {
|
|
128
|
-
const target = scoped(client, graph, branch);
|
|
129
|
-
if (action === "resolve") {
|
|
130
|
-
return target.resolveTerm({
|
|
131
|
-
text: requireString(text, "text"),
|
|
132
|
-
kinds: (kinds ?? ["class", "relation", "property"]),
|
|
133
|
-
top_k,
|
|
134
|
-
});
|
|
135
|
-
}
|
|
136
|
-
if (action === "audit") {
|
|
137
|
-
return target.groundability(sample == null ? {} : { sample });
|
|
138
|
-
}
|
|
139
|
-
const context = src_type !== undefined || dst_type !== undefined
|
|
140
|
-
? { src_type, dst_type }
|
|
141
|
-
: undefined;
|
|
142
|
-
return target.suggest({
|
|
143
|
-
prefix: requireString(prefix, "prefix"),
|
|
144
|
-
kinds: kinds,
|
|
145
|
-
context,
|
|
146
|
-
limit: top_k,
|
|
147
|
-
});
|
|
148
|
-
}));
|
|
149
5
|
server.registerTool("lbb_inspect", {
|
|
150
6
|
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.",
|
|
151
7
|
inputSchema: inspectWireSchema,
|
|
@@ -232,7 +88,7 @@ export function registerLbbTools(server, client) {
|
|
|
232
88
|
});
|
|
233
89
|
});
|
|
234
90
|
server.registerTool("lbb_query", {
|
|
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.",
|
|
91
|
+
description: "Analytical and expert reads. Modes: structured (SPARQL-subset JSON body), sparql (SPARQL text), analyze. SPARQL is the only query surface. 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.",
|
|
236
92
|
inputSchema: queryWireSchema,
|
|
237
93
|
annotations: READ_ONLY,
|
|
238
94
|
}, (rawArgs) => {
|
|
@@ -301,19 +157,15 @@ export function registerLbbTools(server, client) {
|
|
|
301
157
|
as_of_commit_seq: asOfCommitSeq,
|
|
302
158
|
as_of_valid_time: asOfValidTime ?? null,
|
|
303
159
|
};
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
//
|
|
307
|
-
//
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
if (hasCombinators && hasHaving) {
|
|
312
|
-
throw new Error("HAVING is not evaluated alongside UNION/OPTIONAL/MINUS combinators — remove the combinators to use HAVING (grouped aggregation), or apply the threshold client-side.");
|
|
160
|
+
// The analytics route is gone; structured bodies run only on the
|
|
161
|
+
// SPARQL-select path, which rejects unknown fields. Name the
|
|
162
|
+
// removal here so a `combinators` body fails with an actionable
|
|
163
|
+
// message instead of an opaque schema rejection.
|
|
164
|
+
if (Array.isArray(request.combinators) &&
|
|
165
|
+
request.combinators.length > 0) {
|
|
166
|
+
throw new Error("`combinators` (UNION/OPTIONAL/MINUS/EXISTS) is no longer accepted by structured mode; the analytics route was removed. Express the same query as SPARQL text with mode=sparql.");
|
|
313
167
|
}
|
|
314
|
-
const response =
|
|
315
|
-
? await target.analytics(request)
|
|
316
|
-
: await target.sparql(request);
|
|
168
|
+
const response = await target.sparql(request);
|
|
317
169
|
const rowPage = rowPageFrom(response);
|
|
318
170
|
const cursorBase = {
|
|
319
171
|
v: 1,
|
|
@@ -432,7 +284,7 @@ export function registerLbbTools(server, client) {
|
|
|
432
284
|
}
|
|
433
285
|
}));
|
|
434
286
|
server.registerTool("lbb_commit", {
|
|
435
|
-
description: "Write graph facts, retract them, or label
|
|
287
|
+
description: "Write graph facts, retract them, or label ranked 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 stores query/result relevance labels (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.",
|
|
436
288
|
inputSchema: {
|
|
437
289
|
idempotency_key: z.string().optional(),
|
|
438
290
|
mode: z.enum(["facts", "retract", "search_feedback"]).optional(),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@littlebigbrain/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
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.11.0",
|
|
56
56
|
"@modelcontextprotocol/sdk": "^1",
|
|
57
57
|
"zod": "^3"
|
|
58
58
|
},
|