@littlebigbrain/mcp 0.5.2 → 0.6.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
@@ -64,8 +64,8 @@ Requires Node.js 18+. Copy the complete endpoint and a stack API key from
64
64
  ```
65
65
 
66
66
  The client starts the server and communicates through standard input and output
67
- (stdio). Keep the key in your local configuration. Set `LBB_GRAPH` or
68
- `LBB_BRANCH` to change the default scope from `main`.
67
+ (stdio). Keep the key in your local configuration. Set `LBB_GRAPH` to change
68
+ the default graph from `main`.
69
69
 
70
70
  ## Run a first query
71
71
 
@@ -120,8 +120,6 @@ its original identifiers; inspect it with SPARQL when choosing query predicates.
120
120
  | `lbb_embeddings_delete` | Delete an embedding and its stored vectors. |
121
121
  | `lbb_commit` | Write or retract JSON facts, or record search feedback. |
122
122
  | `lbb_configure` | Define record types and relationships, or publish validation rules. |
123
- | `lbb_branch` | Create branches and merge reviewed changes. |
124
- | `lbb_observe` | Store conversation episodes and reviewed facts extracted from them. |
125
123
  | `lbb_evals` | Label query results and check whether later queries return the expected answers. |
126
124
  | `lbb_models` | Compare retrieval settings and read model training datasets. |
127
125
 
@@ -130,8 +128,8 @@ for embedding setup. To define constraints with the Shapes Constraint Language
130
128
  (SHACL), see [query and validation](https://docs.littlebigbrain.com/guides/sparql-and-shacl/).
131
129
 
132
130
  RDF imports and JSON facts use different write workflows. A graph first written
133
- through `lbb_rdf` does not accept JSON fact writes through `lbb_commit`, or branch
134
- forks and merges. Choose the workflow when creating the graph. RDF updates
131
+ through `lbb_rdf` does not accept JSON fact writes through `lbb_commit`. Choose
132
+ the workflow when creating the graph. RDF updates
135
133
  support additive `INSERT DATA`;
136
134
  see the [RDF guide](https://docs.littlebigbrain.com/guides/load-rdf/) for format and
137
135
  update limits.
@@ -22,7 +22,6 @@ export interface McpHttpServerOptions {
22
22
  baseUrl: string;
23
23
  apiKey: string;
24
24
  graph?: string;
25
- branch?: string;
26
25
  }) => LbbClient;
27
26
  }
28
27
  export declare function bearer(req: IncomingMessage): string | undefined;
@@ -150,7 +150,6 @@ export function createMcpHttpServer(options) {
150
150
  baseUrl,
151
151
  apiKey,
152
152
  graph: url.searchParams.get("graph") ?? undefined,
153
- branch: url.searchParams.get("branch") ?? undefined,
154
153
  };
155
154
  const client = options.clientFactory
156
155
  ? options.clientFactory(clientOptions)
@@ -4,7 +4,6 @@ import { metadataPageSchema } from "./tool-contracts.js";
4
4
  type Request = z.infer<z.ZodObject<typeof metadataPageSchema>> & {
5
5
  action: "ontology" | "schema";
6
6
  graph?: string;
7
- branch?: string;
8
7
  };
9
8
  /** Stateless pagination of complete entries, including their full nested values.
10
9
  * A digest rejects mixed-version reads instead of skipping/repeating definitions
@@ -24,7 +23,6 @@ export declare function metadataPage(client: LbbClient, args: Request): Promise<
24
23
  next: {
25
24
  action: "ontology" | "schema";
26
25
  graph: string | undefined;
27
- branch: string | undefined;
28
26
  section: string | undefined;
29
27
  page_size: number;
30
28
  cursor: string;
@@ -7,7 +7,6 @@ const cursorSchema = z
7
7
  v: z.literal(1),
8
8
  action: z.enum(["ontology", "schema"]),
9
9
  graph: z.string().optional(),
10
- branch: z.string().optional(),
11
10
  section: z.string().optional(),
12
11
  page_size: z.number().int().min(1).max(500),
13
12
  offset: z.number().int().nonnegative().safe(),
@@ -27,23 +26,16 @@ export async function metadataPage(client, args) {
27
26
  catch {
28
27
  throw new Error("invalid lbb_inspect cursor; restart without cursor");
29
28
  }
30
- for (const key of [
31
- "action",
32
- "graph",
33
- "branch",
34
- "section",
35
- "page_size",
36
- ]) {
29
+ for (const key of ["action", "graph", "section", "page_size"]) {
37
30
  if (args[key] !== undefined && args[key] !== cursor[key]) {
38
31
  throw new Error(`cursor ${key} does not match the supplied ${key}`);
39
32
  }
40
33
  }
41
34
  }
42
35
  const graph = cursor?.graph ?? args.graph;
43
- const branch = cursor?.branch ?? args.branch;
44
36
  const section = cursor?.section ?? args.section;
45
37
  const pageSize = cursor?.page_size ?? args.page_size ?? 50;
46
- const target = client.withScope({ graph, branch });
38
+ const target = client.withScope({ graph });
47
39
  const value = (args.action === "ontology"
48
40
  ? await target.ontologyView({ counts: true })
49
41
  : await target.schema.view());
@@ -62,7 +54,6 @@ export async function metadataPage(client, args) {
62
54
  v: 1,
63
55
  action: args.action,
64
56
  graph,
65
- branch,
66
57
  section,
67
58
  page_size: pageSize,
68
59
  fingerprint,
@@ -94,7 +85,6 @@ export async function metadataPage(client, args) {
94
85
  ? {
95
86
  action: args.action,
96
87
  graph,
97
- branch,
98
88
  section,
99
89
  page_size: pageSize,
100
90
  cursor: Buffer.from(JSON.stringify({
package/dist/rdf-tool.js CHANGED
@@ -49,13 +49,12 @@ export function registerRdfTool(server, client) {
49
49
  return errorResult(parsed.error);
50
50
  const args = parsed.data;
51
51
  return run(client, `lbb_rdf.${args.action}`, args.detail, async () => {
52
- const target = scoped(client, args.graph, args.branch);
52
+ const target = scoped(client, args.graph);
53
53
  const { idempotency_key } = args;
54
54
  const operation = { ...args };
55
55
  delete operation.detail;
56
56
  delete operation.idempotency_key;
57
- const key = idempotency_key ??
58
- contentHashKey({ graph: args.graph, branch: args.branch }, operation);
57
+ const key = idempotency_key ?? contentHashKey({ graph: args.graph }, operation);
59
58
  if (args.action === "import") {
60
59
  return target.importRdf(args.source, {
61
60
  format: args.format ?? "turtle",
package/dist/stdio.js CHANGED
@@ -12,7 +12,7 @@ const BASE_URL_HELP = "LBB_BASE_URL is required. Copy endpoint_url from your Lit
12
12
  *
13
13
  * LBB_BASE_URL (required for tool calls; copy endpoint_url from the stack Connect page)
14
14
  * LBB_API_KEY stack API key (lbb_sk_live_…) or single-mode token
15
- * LBB_GRAPH / LBB_BRANCH (optional; server defaults to main/main)
15
+ * LBB_GRAPH (optional; server defaults to main)
16
16
  *
17
17
  * The base URL is required lazily, at the first tool invocation, not at
18
18
  * process start: MCP hosts launch servers to complete the initialize
@@ -44,7 +44,6 @@ async function main() {
44
44
  baseUrl,
45
45
  apiKey: process.env.LBB_API_KEY,
46
46
  graph: process.env.LBB_GRAPH,
47
- branch: process.env.LBB_BRANCH,
48
47
  })
49
48
  : unconfiguredClient();
50
49
  const server = buildLbbServer(client);
@@ -40,10 +40,6 @@ export const graphScope = {
40
40
  .string()
41
41
  .optional()
42
42
  .describe("Graph to target; defaults to the connection's graph"),
43
- branch: z
44
- .string()
45
- .optional()
46
- .describe("Branch to target; defaults to the connection's branch"),
47
43
  };
48
44
  export const jsonObjectSchema = z.record(z.string(), z.unknown());
49
45
  export const jsonObjectArraySchema = z.array(jsonObjectSchema);
@@ -576,7 +572,6 @@ export const configureInputSchema = z.discriminatedUnion("action", [
576
572
  .optional()
577
573
  .describe("Preview the exact definition without creating a graph or writing metadata."),
578
574
  graph: z.string().describe("Graph to create or redefine"),
579
- branch: graphScope.branch,
580
575
  entity_types: z.array(z.union([z.string(), jsonObjectSchema])).optional(),
581
576
  relations: z.array(z.union([z.string(), jsonObjectSchema])).optional(),
582
577
  source: z.string().optional(),
@@ -627,7 +622,7 @@ export const configureInputSchema = z.discriminatedUnion("action", [
627
622
  * rejects as `Expected object, received string`.
628
623
  *
629
624
  * Flatten the union into a single ZodObject purely for advertisement and
630
- * transport: the discriminant becomes an enum, every branch field is merged in
625
+ * transport: the discriminant becomes an enum, every variant field is merged in
631
626
  * as optional, and unknown keys pass through. Each handler still `safeParse`s the
632
627
  * raw args against the original strict union before dispatching, so per-variant
633
628
  * required/forbidden fields are enforced exactly as before.
@@ -2,9 +2,7 @@ import { Buffer } from "node:buffer";
2
2
  import { createHash } from "node:crypto";
3
3
  import { LbbError } from "@littlebigbrain/client";
4
4
  import { DEFAULT_DETAIL, HARD_OUTPUT_CHARS, MAX_QUERY_ROW_LIMIT, } from "./tool-contracts.js";
5
- export const scoped = (client, graph, branch) => graph !== undefined || branch !== undefined
6
- ? client.withScope({ graph, branch })
7
- : client;
5
+ export const scoped = (client, graph) => graph !== undefined ? client.withScope({ graph }) : client;
8
6
  export function normalizeDetail(detail) {
9
7
  return detail === "standard" || detail === "full" ? detail : DEFAULT_DETAIL;
10
8
  }
@@ -50,9 +48,6 @@ export function assertCursorScope(args, cursor) {
50
48
  if (args.graph !== undefined && args.graph !== cursor.graph) {
51
49
  throw new Error("cursor graph does not match the supplied graph argument");
52
50
  }
53
- if (args.branch !== undefined && args.branch !== cursor.branch) {
54
- throw new Error("cursor branch does not match the supplied branch argument");
55
- }
56
51
  }
57
52
  export function rowPageFrom(value) {
58
53
  if (!value || typeof value !== "object")
@@ -190,7 +185,7 @@ export function normalizeLbbIris(query) {
190
185
  export function contentHashKey(scope, payload) {
191
186
  const digest = createHash("sha256")
192
187
  .update(stableJson({
193
- scope: { graph: scope.graph ?? null, branch: scope.branch ?? null },
188
+ scope: { graph: scope.graph ?? null },
194
189
  payload,
195
190
  }))
196
191
  .digest("hex");
@@ -440,60 +435,38 @@ export function errorResult(error) {
440
435
  };
441
436
  }
442
437
  /**
443
- * A graph-scope 404 surfaces as a raw object-storage key
444
- * (`not found: tenants/<t>/graphs/<g>/branches/<b>/heads/current.json`), which
445
- * only means something if you already know the graph's real name. Rewrite it
446
- * into an actionable message: name the graph/branch this request targeted, and —
447
- * via the tenant-scoped `GET /v1/graphs`, which resolves even when the scoped
448
- * graph is absent — list the graphs (or branches) that do exist and tell the
449
- * caller to pass `graph=`/`branch=`. Every other error passes through untouched.
438
+ * A graph-scope 404 surfaces as a raw object-storage key ending in
439
+ * `heads/current.json`, which only means something if you already know the
440
+ * graph's real name. Rewrite it into an actionable message: name the graph this
441
+ * request targeted and — via the tenant-scoped `GET /v1/graphs`, which resolves
442
+ * even when the scoped graph is absent — list the graphs that do exist and tell
443
+ * the caller to pass `graph=`. Every other error passes through untouched,
444
+ * including a head 404 on a graph the listing still reports.
450
445
  */
451
446
  export async function enrichError(client, error) {
452
447
  if (!(error instanceof LbbError) || error.status !== 404)
453
448
  return error;
454
- const match = /graphs\/([^/]+)\/branches\/([^/]+)\/heads\/current\.json/.exec(error.message);
449
+ const match = /graphs\/([^/\s]+)\/(?:[^/\s]+\/)*heads\/current\.json/.exec(error.message);
455
450
  if (!match)
456
451
  return error;
457
- const [, graph, branch] = match;
458
- let graphs = [];
452
+ const [, graph] = match;
453
+ let names = [];
459
454
  try {
460
455
  const listed = (await client.listGraphs());
461
- graphs = (listed.data ?? []).flatMap((g) => typeof g.graph_id === "string"
462
- ? [
463
- {
464
- graph_id: g.graph_id,
465
- branches: Array.isArray(g.branches)
466
- ? g.branches
467
- : undefined,
468
- },
469
- ]
470
- : []);
456
+ names = (listed.data ?? []).flatMap((g) => typeof g.graph_id === "string" ? [g.graph_id] : []);
471
457
  }
472
458
  catch {
473
459
  // Listing failed too (auth, transport); fall back to the generic-but-actionable hint.
474
460
  }
475
- const existing = graphs.find((g) => g.graph_id === graph);
476
- let message;
477
- if (existing) {
478
- const branches = existing.branches ?? [];
479
- const list = branches.length > 0
480
- ? ` Existing branches: ${branches.slice(0, 50).join(", ")}.`
481
- : "";
482
- message =
483
- `branch "${branch}" was not found on graph "${graph}" in this tenant.${list} ` +
484
- "Pass an existing branch as the `branch` argument.";
485
- }
486
- else {
487
- const names = graphs.map((g) => g.graph_id);
488
- const list = names.length > 0
489
- ? ` Available graphs in this tenant: ${names.slice(0, 50).join(", ")}.`
490
- : "";
491
- const example = names.length > 0 ? ` (e.g. graph="${names[0]}")` : "";
492
- message =
493
- `graph "${graph}" was not found in this tenant — this request targeted graph "${graph}", branch "${branch}" ` +
494
- `(either you passed it or it is the connection default).${list} ` +
495
- `Pass an existing graph as the \`graph\` argument${example}.`;
496
- }
461
+ if (names.includes(graph))
462
+ return error;
463
+ const list = names.length > 0
464
+ ? ` Available graphs in this tenant: ${names.slice(0, 50).join(", ")}.`
465
+ : "";
466
+ const example = names.length > 0 ? ` (e.g. graph="${names[0]}")` : "";
467
+ const message = `graph "${graph}" was not found in this tenant — this request targeted graph "${graph}" ` +
468
+ `(either you passed it or it is the connection default).${list} ` +
469
+ `Pass an existing graph as the \`graph\` argument${example}.`;
497
470
  return new LbbError(error.status, error.body, {
498
471
  type: error.type,
499
472
  code: error.code,
@@ -684,7 +657,7 @@ export async function guide(scopedClient) {
684
657
  capability: {
685
658
  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.",
686
659
  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.",
687
- 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.",
660
+ 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 graph'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.",
688
661
  search: "Use lbb_query mode=search to find instances by meaning. It covers every searchable class; filter narrows it by class and by relationship, and the graph checks every hit. To plan a search: (1) lbb_embeddings action=list gives the searchable classes, the facts each text is made from, and the graph's model. (2) lbb_query mode=sparql shows the relationships of a class on a sample: PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#> SELECT ?p (COUNT(DISTINCT ?s) AS ?n) (SAMPLE(?l) AS ?example) WHERE { { SELECT ?s WHERE { ?s a <CLASS> } LIMIT 500 } ?s ?p ?o . FILTER(isIRI(?o) && ?p != rdf:type) OPTIONAL { ?o rdfs:label ?l } } GROUP BY ?p ORDER BY DESC(?n); write ?o ?p ?s for the links into the class, and SELECT ?c WHERE { ?c rdfs:subClassOf* <CLASS> } (same prefixes) for its subclasses. (3) lbb_query mode=search with filter [{class}, {via, to}] and explain=true returns the plan: the resolved relationship and entities, the allowed count, and the mode, with no model call. Check it, then run the search without explain. A name in to resolves against labels (a small typo too); an unknown relationship or name answers with the options.",
689
662
  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.",
690
663
  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.",
package/dist/tools.js CHANGED
@@ -21,7 +21,7 @@ export function registerLbbTools(server, client, options = {}) {
21
21
  .catch(async (error) => errorResult(await enrichError(client, error)));
22
22
  }
23
23
  return run(client, `lbb_inspect.${args.action}`, args.detail, () => {
24
- const target = scoped(client, args.graph, args.branch);
24
+ const target = scoped(client, args.graph);
25
25
  switch (args.action) {
26
26
  case "guide":
27
27
  return guide(target);
@@ -110,7 +110,7 @@ export function registerLbbTools(server, client, options = {}) {
110
110
  if (cursor && cursor.mode !== args.mode) {
111
111
  throw new Error(`cursor is for ${cursor.mode}, not ${args.mode}`);
112
112
  }
113
- assertCursorScope({ graph: args.graph, branch: args.branch }, cursor);
113
+ assertCursorScope({ graph: args.graph }, cursor);
114
114
  if (cursor &&
115
115
  args.row_limit !== undefined &&
116
116
  args.row_limit !== cursor.row_limit) {
@@ -119,9 +119,8 @@ export function registerLbbTools(server, client, options = {}) {
119
119
  const detail = normalizeDetail(args.detail ?? cursor?.detail);
120
120
  const rowLimit = effectiveRowLimit(detail, args.row_limit ?? cursor?.row_limit);
121
121
  const graph = cursor?.graph ?? args.graph;
122
- const branch = cursor?.branch ?? args.branch;
123
122
  const offset = cursor?.offset ?? 0;
124
- const target = scoped(client, graph, branch);
123
+ const target = scoped(client, graph);
125
124
  const timing = queryTiming(options, {
126
125
  mode: args.mode,
127
126
  continuation: cursor !== undefined,
@@ -230,7 +229,6 @@ export function registerLbbTools(server, client, options = {}) {
230
229
  v: 1,
231
230
  mode: "structured",
232
231
  graph,
233
- branch,
234
232
  detail,
235
233
  row_limit: rowLimit,
236
234
  body,
@@ -284,7 +282,6 @@ export function registerLbbTools(server, client, options = {}) {
284
282
  v: 1,
285
283
  mode: "sparql",
286
284
  graph,
287
- branch,
288
285
  detail,
289
286
  row_limit: rowLimit,
290
287
  query,
@@ -305,7 +302,7 @@ export function registerLbbTools(server, client, options = {}) {
305
302
  if (args.mode === "search") {
306
303
  const searchArgs = args;
307
304
  return run(client, "lbb_query.search", undefined, async () => {
308
- const target = scoped(client, searchArgs.graph, searchArgs.branch);
305
+ const target = scoped(client, searchArgs.graph);
309
306
  return target.embeddings.search({
310
307
  embedding: searchArgs.embedding,
311
308
  text: searchArgs.text,
@@ -319,7 +316,7 @@ export function registerLbbTools(server, client, options = {}) {
319
316
  });
320
317
  }
321
318
  return run(client, `lbb_query.${args.mode}`, args.detail, async () => {
322
- const target = scoped(client, args.graph, args.branch);
319
+ const target = scoped(client, args.graph);
323
320
  return analyze(target, {
324
321
  metric: args.metric,
325
322
  chart: args.chart,
@@ -333,13 +330,7 @@ export function registerLbbTools(server, client, options = {}) {
333
330
  server.registerTool("lbb_models", {
334
331
  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.",
335
332
  inputSchema: {
336
- action: z.enum([
337
- "shadow_eval",
338
- "planner_dataset",
339
- "planner_preference_dataset",
340
- "suggest_dataset",
341
- "extractor_dataset",
342
- ]),
333
+ action: z.enum(["shadow_eval", "suggest_dataset", "extractor_dataset"]),
343
334
  body: jsonObjectSchema.optional(),
344
335
  limit: z.number().int().positive().optional(),
345
336
  split_seq: z.number().int().nonnegative().optional(),
@@ -347,20 +338,13 @@ export function registerLbbTools(server, client, options = {}) {
347
338
  ...graphScope,
348
339
  },
349
340
  annotations: READ_ONLY,
350
- }, ({ action, body, limit, split_seq, detail, graph, branch }) => run(client, `lbb_models.${action}`, detail, () => {
351
- const target = scoped(client, graph, branch);
341
+ }, ({ action, body, limit, split_seq, detail, graph }) => run(client, `lbb_models.${action}`, detail, () => {
342
+ const target = scoped(client, graph);
352
343
  switch (action) {
353
344
  case "shadow_eval":
354
345
  if (!body)
355
346
  throw new Error("shadow_eval requires body");
356
347
  return target.shadowEval(body);
357
- case "planner_dataset":
358
- return target.plannerDataset({ limit, splitSeq: split_seq });
359
- case "planner_preference_dataset":
360
- return target.plannerPreferenceDataset({
361
- limit,
362
- splitSeq: split_seq,
363
- });
364
348
  case "suggest_dataset":
365
349
  return target.suggestDataset({ limit, splitSeq: split_seq });
366
350
  case "extractor_dataset":
@@ -416,7 +400,7 @@ export function registerLbbTools(server, client, options = {}) {
416
400
  },
417
401
  annotations: READ_ONLY,
418
402
  }, (args) => run(client, `lbb_embeddings.${args.action}`, args.detail, () => {
419
- const target = scoped(client, args.graph, args.branch);
403
+ const target = scoped(client, args.graph);
420
404
  switch (args.action) {
421
405
  case "list":
422
406
  return target.embeddings.list();
@@ -446,7 +430,7 @@ export function registerLbbTools(server, client, options = {}) {
446
430
  },
447
431
  annotations: MUTATING,
448
432
  }, (args) => run(client, `lbb_embeddings_manage.${args.action}`, args.detail, () => {
449
- const target = scoped(client, args.graph, args.branch);
433
+ const target = scoped(client, args.graph);
450
434
  switch (args.action) {
451
435
  case "declare":
452
436
  return target.embeddings.declare(recipeOf(args));
@@ -472,11 +456,11 @@ export function registerLbbTools(server, client, options = {}) {
472
456
  ...graphScope,
473
457
  },
474
458
  annotations: DESTRUCTIVE,
475
- }, ({ name, confirm, detail, graph, branch }) => run(client, "lbb_embeddings_delete", detail, () => {
459
+ }, ({ name, confirm, detail, graph }) => run(client, "lbb_embeddings_delete", detail, () => {
476
460
  if (confirm !== name) {
477
461
  throw new Error("confirm must repeat the embedding name");
478
462
  }
479
- return scoped(client, graph, branch).embeddings.delete(name);
463
+ return scoped(client, graph).embeddings.delete(name);
480
464
  }));
481
465
  server.registerTool("lbb_evals", {
482
466
  description: "Managed evals: the thumbs up / thumbs down of the graph, per result. A query run with `request` (lbb_query) records a trace with one item per result (hit or row); trace reads it back with the item ids. label marks one result (item + valid) or several (items) relevant or not; the labels become the golden's ground truth. golden freezes a query (every result it returns now is relevant). run replays every golden at the current commit: pass when every relevant result is back and no wrong one is; new results open a review trace. judge lets the platform's judge model (the hosted frontier model) label unlabeled results. summary, traces, goldens, results, and settings read state.",
@@ -562,8 +546,8 @@ export function registerLbbTools(server, client, options = {}) {
562
546
  ...graphScope,
563
547
  },
564
548
  annotations: MUTATING,
565
- }, ({ action, trace_id, item, valid, items, by, note, sparql, surface, embedding, top_k, request, golden_id, limit, unlabeled, consistency, detail, graph, branch, }) => run(client, `lbb_evals.${action}`, detail, () => {
566
- const target = scoped(client, graph, branch);
549
+ }, ({ action, trace_id, item, valid, items, by, note, sparql, surface, embedding, top_k, request, golden_id, limit, unlabeled, consistency, detail, graph, }) => run(client, `lbb_evals.${action}`, detail, () => {
550
+ const target = scoped(client, graph);
567
551
  switch (action) {
568
552
  case "summary":
569
553
  return target.evals.summary();
@@ -684,7 +668,7 @@ export function registerLbbTools(server, client, options = {}) {
684
668
  ...graphScope,
685
669
  },
686
670
  annotations: IDEMPOTENT_WRITE,
687
- }, ({ idempotency_key, mode, triplets, entity_embeddings, entity_properties, search_feedback, dry_run, observed_at, edge_idempotency, retract_edges, retract_entities, graph, branch, }) => run(client, "lbb_commit", "standard", () => {
671
+ }, ({ idempotency_key, mode, triplets, entity_embeddings, entity_properties, search_feedback, dry_run, observed_at, edge_idempotency, retract_edges, retract_entities, graph, }) => run(client, "lbb_commit", "standard", () => {
688
672
  const commitMode = mode ??
689
673
  (search_feedback
690
674
  ? "search_feedback"
@@ -700,8 +684,8 @@ export function registerLbbTools(server, client, options = {}) {
700
684
  throw new Error("lbb_commit mode=retract requires retract_edges or retract_entities");
701
685
  }
702
686
  const key = idempotency_key ??
703
- contentHashKey({ graph, branch }, { mode: "retract", edges, entities });
704
- return scoped(client, graph, branch).retract({ edges, entities }, {
687
+ contentHashKey({ graph }, { mode: "retract", edges, entities });
688
+ return scoped(client, graph).retract({ edges, entities }, {
705
689
  idempotencyKey: key,
706
690
  });
707
691
  }
@@ -709,8 +693,8 @@ export function registerLbbTools(server, client, options = {}) {
709
693
  if (!search_feedback)
710
694
  throw new Error("lbb_commit mode=search_feedback requires search_feedback");
711
695
  const key = idempotency_key ??
712
- contentHashKey({ graph, branch }, { mode: "search_feedback", search_feedback });
713
- return scoped(client, graph, branch).searchFeedback(search_feedback, { idempotencyKey: key });
696
+ contentHashKey({ graph }, { mode: "search_feedback", search_feedback });
697
+ return scoped(client, graph).searchFeedback(search_feedback, { idempotencyKey: key });
714
698
  }
715
699
  if (search_feedback) {
716
700
  throw new Error("lbb_commit facts mode cannot include search_feedback");
@@ -727,10 +711,10 @@ export function registerLbbTools(server, client, options = {}) {
727
711
  payload.entity_properties.length === 0) {
728
712
  throw new Error("lbb_commit requires at least one triplet, entity embedding, or entity property");
729
713
  }
730
- const key = idempotency_key ?? contentHashKey({ graph, branch }, payload);
714
+ const key = idempotency_key ?? contentHashKey({ graph }, payload);
731
715
  if (dry_run)
732
- return scoped(client, graph, branch).commitDryRun(payload);
733
- return scoped(client, graph, branch).commit(payload, {
716
+ return scoped(client, graph).commitDryRun(payload);
717
+ return scoped(client, graph).commit(payload, {
734
718
  idempotencyKey: key,
735
719
  });
736
720
  }));
@@ -745,9 +729,7 @@ export function registerLbbTools(server, client, options = {}) {
745
729
  const args = parsed.data;
746
730
  return run(client, `lbb_configure.${args.action}`, "standard", () => {
747
731
  if (args.action === "define_ontology") {
748
- return client
749
- .withScope({ graph: args.graph, branch: args.branch })
750
- .ontologyDefine(ontologyDefineBody({
732
+ return client.withScope({ graph: args.graph }).ontologyDefine(ontologyDefineBody({
751
733
  entity_types: args.entity_types,
752
734
  relations: args.relations,
753
735
  source: args.source,
@@ -757,7 +739,7 @@ export function registerLbbTools(server, client, options = {}) {
757
739
  }));
758
740
  }
759
741
  if (args.action === "evolve_ontology") {
760
- return scoped(client, args.graph, args.branch).ontology.evolve({
742
+ return scoped(client, args.graph).ontology.evolve({
761
743
  ops: args.ops,
762
744
  allow_data_conflicts: args.allow_data_conflicts ?? false,
763
745
  }, { dryRun: args.dry_run });
@@ -765,7 +747,7 @@ export function registerLbbTools(server, client, options = {}) {
765
747
  if (args.shapes === undefined) {
766
748
  throw new Error("publish_schema requires a SHACL shapes source; use define_ontology or evolve_ontology for native metadata changes");
767
749
  }
768
- return scoped(client, args.graph, args.branch).schema.publish({
750
+ return scoped(client, args.graph).schema.publish({
769
751
  ontology: args.ontology,
770
752
  shapes: args.shapes,
771
753
  desired_mode: args.desired_mode,
@@ -773,85 +755,4 @@ export function registerLbbTools(server, client, options = {}) {
773
755
  }, { dryRun: args.dry_run });
774
756
  });
775
757
  });
776
- server.registerTool("lbb_branch", {
777
- 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).",
778
- inputSchema: {
779
- action: z
780
- .enum(["create", "merge"])
781
- .describe("create = fork a new branch; merge = replay a child branch onto its fork parent"),
782
- from_branch: z
783
- .string()
784
- .describe("create: the branch to fork from; merge: the child branch whose commits are replayed"),
785
- validate: z
786
- .boolean()
787
- .optional()
788
- .describe("merge only: refuse on SHACL violations of the would-be merged state (default true)"),
789
- delete_source: z
790
- .boolean()
791
- .optional()
792
- .describe("merge only: delete every object under the merged branch after success"),
793
- ...graphScope,
794
- },
795
- annotations: MUTATING,
796
- }, ({ action, from_branch, validate, delete_source, graph, branch }) => run(client, `lbb_branch.${action}`, "standard", () => {
797
- const target = scoped(client, graph, branch);
798
- if (action === "create")
799
- return target.createBranch({ from_branch });
800
- return target.mergeBranch({
801
- from_branch,
802
- validate: validate ?? true,
803
- delete_source: delete_source ?? false,
804
- });
805
- }));
806
- server.registerTool("lbb_observe", {
807
- description: "Remember a conversation: store the turns verbatim as an EPISODE evidence entity, then anchor + gate the supplied facts on an observe branch (LLM extraction cannot poison the main graph). Facts with both endpoints already in the graph are anchored; unanchored facts need confidence >= 0.8 to mint new entities, else they come back needs_review. auto_merge merges the branch onto the scoped branch when SHACL validation is clean (the validate-then-merge). Server flag-gated (--enable-observe). This build takes caller-extracted facts (each with a structured triplet); bare statements come back needs_review.",
808
- inputSchema: {
809
- session_id: z
810
- .string()
811
- .describe("Caller's conversation id (drives the default observe branch name)"),
812
- turns: z
813
- .array(z.object({
814
- role: z.string().describe("user | assistant | tool"),
815
- content: z.string(),
816
- name: z.string().optional(),
817
- ts: z.string().optional().describe("RFC 3339 timestamp"),
818
- }))
819
- .min(1)
820
- .describe("The conversation slice to remember (stored verbatim)"),
821
- source: z
822
- .string()
823
- .optional()
824
- .describe("Source label, e.g. support-bot"),
825
- facts: z
826
- .array(z.object({
827
- fact: z.string().describe("Natural-language statement"),
828
- confidence: z.number().optional().describe("0..1 (default 0.9)"),
829
- triplet: jsonObjectSchema
830
- .optional()
831
- .describe("Structured form {source:{type,name}, relation, target:{type,name}} — required for the fact to commit"),
832
- }))
833
- .optional()
834
- .describe("Caller-extracted candidate facts; omit with extract:false to store the episode only"),
835
- extract: z
836
- .boolean()
837
- .optional()
838
- .describe("false = store the episode only (default true)"),
839
- observe_branch: z
840
- .string()
841
- .optional()
842
- .describe("Branch for the facts (default observe-<hash12(session_id)>)"),
843
- auto_merge: z
844
- .boolean()
845
- .optional()
846
- .describe("Merge onto the scoped branch when validation is clean"),
847
- ...graphScope,
848
- },
849
- annotations: MUTATING,
850
- }, ({ session_id, turns, source, facts, extract, observe_branch, auto_merge, graph, branch, }) => run(client, "lbb_observe", "standard", () => scoped(client, graph, branch).observe({
851
- episode: { turns, session_id, source },
852
- extract: extract ?? true,
853
- extraction: { byo_completion: (facts ?? []) },
854
- branch: observe_branch,
855
- auto_merge: auto_merge ?? false,
856
- })));
857
758
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@littlebigbrain/mcp",
3
- "version": "0.5.2",
3
+ "version": "0.6.0",
4
4
  "description": "MCP server for little big brain: search, graph queries, and data import",
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.13.2",
55
+ "@littlebigbrain/client": "^0.14.0",
56
56
  "@modelcontextprotocol/sdk": "^1",
57
57
  "zod": "^3"
58
58
  },