@littlebigbrain/mcp 0.11.0 → 0.13.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
@@ -155,6 +155,10 @@ its original identifiers; inspect it with SPARQL when choosing query predicates.
155
155
  | `lbb_fit_sources` | Inspect fit from text and preview the text the fit reads. |
156
156
  | `lbb_fit_sources_manage` | Declare, dry-run or refresh a fit source. |
157
157
  | `lbb_fit_sources_delete` | Delete a fit source; its suggestions stay. |
158
+ | `lbb_workflows` | List the starter workflows this server offers and the workflows created from them on a graph (triggered workflows), read one with its status, and preview a new one. |
159
+ | `lbb_workflows_manage` | Create a workflow from a starter workflow on a class (a developer workflow with its workflow type, owned outputs and batch), ask it to run now, or pause and resume a developer workflow. |
160
+ | `lbb_workflows_delete` | Delete a triggered workflow. What it wrote stays. |
161
+ | `lbb_files` | List the files of a graph, and read the parsed pages of one: each line with its id, its box on the page and its text. |
158
162
  | `lbb_commit` | Write or retract JSON facts, or record search feedback. |
159
163
  | `lbb_configure` | Define record types and relationships, list and apply ontology starters (CRM, documents, work), publish validation rules, or keep the graph's notes and worked examples for questions in plain words (`get_rewrite_profile`, `set_rewrite_profile`). |
160
164
  | `lbb_evals` | Label query results and check whether later queries return the expected answers. Read the model checks of a graph, and agree with or correct a check that the user confirmed. |
@@ -0,0 +1,43 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { LbbClient, Schemas } from "@littlebigbrain/client";
3
+ /** Lines one `pages` call returns by default, and at most. */
4
+ export declare const FILE_LINES_DEFAULT = 200;
5
+ export declare const FILE_LINES_MAX = 1000;
6
+ /** Characters of one line's text a call shows; a longer text is cut with "…". */
7
+ export declare const FILE_LINE_CHARS = 400;
8
+ /** Characters of lines one call returns at most, so the result fits an MCP answer. */
9
+ export declare const FILE_LINES_BUDGET = 60000;
10
+ type FilePage = Schemas["FilePage"];
11
+ /** One line as the tool shows it: `<id> [x0,y0,x1,y1] <text>`. */
12
+ export declare function fileLineText(line: Schemas["FileLine"]): string;
13
+ export interface PageLinesWindow {
14
+ /** Lines of the first page to skip (they were shown by the call before). */
15
+ lineOffset: number;
16
+ maxLines: number;
17
+ /** Only lines whose text holds this, ignoring case. */
18
+ contains?: string;
19
+ }
20
+ export interface PageLinesAnswer {
21
+ pages: Array<{
22
+ page: number;
23
+ width: number;
24
+ height: number;
25
+ lines_total: number;
26
+ lines_matching: number;
27
+ lines: string[];
28
+ }>;
29
+ shown: number;
30
+ /** Where the next call starts when lines were left out. */
31
+ next?: {
32
+ from: number;
33
+ line_offset: number;
34
+ };
35
+ }
36
+ /**
37
+ * The lines of parsed pages as short strings, at most `maxLines` and
38
+ * {@link FILE_LINES_BUDGET} characters, from `lineOffset` of the first page.
39
+ * `next` names the page and the offset of the first line left out.
40
+ */
41
+ export declare function pageLines(pages: FilePage[], window: PageLinesWindow): PageLinesAnswer;
42
+ export declare function registerFilesTool(server: McpServer, client: LbbClient): void;
43
+ export {};
@@ -0,0 +1,183 @@
1
+ import { z } from "zod";
2
+ import { READ_ONLY, detailSchema, graphScope } from "./tool-contracts.js";
3
+ import { enrichError, errorResult, run, scoped, toolResult, } from "./tool-runtime.js";
4
+ /** Lines one `pages` call returns by default, and at most. */
5
+ export const FILE_LINES_DEFAULT = 200;
6
+ export const FILE_LINES_MAX = 1_000;
7
+ /** Characters of one line's text a call shows; a longer text is cut with "…". */
8
+ export const FILE_LINE_CHARS = 400;
9
+ /** Characters of lines one call returns at most, so the result fits an MCP answer. */
10
+ export const FILE_LINES_BUDGET = 60_000;
11
+ /** One line as the tool shows it: `<id> [x0,y0,x1,y1] <text>`. */
12
+ export function fileLineText(line) {
13
+ const text = line.text.length > FILE_LINE_CHARS
14
+ ? `${line.text.slice(0, FILE_LINE_CHARS - 1)}…`
15
+ : line.text;
16
+ return `${line.id} [${line.box.join(",")}] ${text}`;
17
+ }
18
+ /**
19
+ * The lines of parsed pages as short strings, at most `maxLines` and
20
+ * {@link FILE_LINES_BUDGET} characters, from `lineOffset` of the first page.
21
+ * `next` names the page and the offset of the first line left out.
22
+ */
23
+ export function pageLines(pages, window) {
24
+ const needle = window.contains?.trim().toLowerCase();
25
+ const answer = { pages: [], shown: 0 };
26
+ let budget = FILE_LINES_BUDGET;
27
+ for (const [index, page] of pages.entries()) {
28
+ const skip = index === 0 ? window.lineOffset : 0;
29
+ const shown = [];
30
+ let matching = 0;
31
+ let stoppedAt;
32
+ for (const [position, line] of page.lines.entries()) {
33
+ if (position < skip)
34
+ continue;
35
+ if (needle && !line.text.toLowerCase().includes(needle))
36
+ continue;
37
+ matching += 1;
38
+ if (stoppedAt !== undefined)
39
+ continue;
40
+ const text = fileLineText(line);
41
+ if (answer.shown >= window.maxLines || text.length > budget) {
42
+ stoppedAt = position;
43
+ continue;
44
+ }
45
+ shown.push(text);
46
+ answer.shown += 1;
47
+ budget -= text.length;
48
+ }
49
+ answer.pages.push({
50
+ page: page.page,
51
+ width: page.width,
52
+ height: page.height,
53
+ lines_total: page.lines.length,
54
+ lines_matching: matching,
55
+ lines: shown,
56
+ });
57
+ if (stoppedAt !== undefined) {
58
+ answer.next = { from: page.page, line_offset: stoppedAt };
59
+ break;
60
+ }
61
+ }
62
+ return answer;
63
+ }
64
+ const SHA256 = /^[0-9a-fA-F]{64}$/;
65
+ /** The SHA-256 of the file a call names, by `sha256` or by its exact name. */
66
+ async function fileSha(target, args) {
67
+ if (args.sha256) {
68
+ if (!SHA256.test(args.sha256.trim()))
69
+ throw new Error("sha256 is 64 hexadecimal characters");
70
+ return { sha256: args.sha256.trim().toLowerCase() };
71
+ }
72
+ if (!args.name)
73
+ throw new Error("pages needs sha256 or name");
74
+ const { files } = await target.files.list();
75
+ const named = files.filter((file) => file.name === args.name);
76
+ if (named.length === 1)
77
+ return { sha256: named[0].sha256, name: named[0].name };
78
+ if (named.length > 1)
79
+ throw new Error(`several files are named ${args.name}; pass sha256: ${named.map((file) => file.sha256).join(", ")}`);
80
+ const names = files.slice(0, 20).map((file) => file.name);
81
+ throw new Error(`no file is named ${args.name}${names.length ? `; the graph has ${names.join(", ")}` : "; the graph has no files"}`);
82
+ }
83
+ export function registerFilesTool(server, client) {
84
+ server.registerTool("lbb_files", {
85
+ description: "Files of the graph, read only. A file is a document the graph holds (its bytes and a Document entity); the documents.parse workflow (lbb_workflows) reads the text layer of each PDF into pages with every line and its box. list shows the files with their sha256, name, size, Document IRI and parse (status parsed, unsupported or failed, page_count). pages reads parsed pages of one file (sha256, or name for an exact file name): from is the first 0-based page index (the page of a region and of a DocEntry or DocMention), to the page after the last one (default from+1). Each line comes as `<line id> [x0,y0,x1,y1] <text>`: the id is p<page number>-l<line number>, both from 1, and the box is normalized to the page (0 to 1, origin top left). contains keeps the lines that hold a text; max_lines caps the lines (default 200). When lines were left out, next names the from and line_offset of the next call. A file not parsed yet answers file_not_parsed.",
86
+ inputSchema: {
87
+ action: z.enum(["list", "pages"]),
88
+ sha256: z
89
+ .string()
90
+ .optional()
91
+ .describe("pages: the file's SHA-256 (64 hex characters)."),
92
+ name: z
93
+ .string()
94
+ .optional()
95
+ .describe("pages: the file's exact name, when you do not have its sha256."),
96
+ from: z
97
+ .number()
98
+ .int()
99
+ .nonnegative()
100
+ .optional()
101
+ .describe("pages: the first page index, 0-based. Default 0."),
102
+ to: z
103
+ .number()
104
+ .int()
105
+ .positive()
106
+ .optional()
107
+ .describe("pages: the page index after the last one. Default from+1; at most 50 pages per call."),
108
+ line_offset: z
109
+ .number()
110
+ .int()
111
+ .nonnegative()
112
+ .optional()
113
+ .describe("pages: lines of the first page to skip, from a previous call's next."),
114
+ max_lines: z
115
+ .number()
116
+ .int()
117
+ .positive()
118
+ .max(FILE_LINES_MAX)
119
+ .optional()
120
+ .describe(`pages: the most lines to return. Default ${FILE_LINES_DEFAULT}.`),
121
+ contains: z
122
+ .string()
123
+ .optional()
124
+ .describe("pages: only the lines whose text holds this, ignoring case."),
125
+ detail: detailSchema,
126
+ ...graphScope,
127
+ },
128
+ annotations: READ_ONLY,
129
+ }, async (args) => {
130
+ const target = scoped(client, args.graph);
131
+ if (args.action === "list") {
132
+ return run(client, "lbb_files.list", args.detail, async () => {
133
+ const { files } = await target.files.list();
134
+ return { files };
135
+ });
136
+ }
137
+ try {
138
+ const file = await fileSha(target, args);
139
+ const from = args.from ?? 0;
140
+ const to = args.to ?? from + 1;
141
+ if (to <= from)
142
+ throw new Error("to must be above from");
143
+ const answer = await target.files.pages(file.sha256, { from, to });
144
+ const lines = pageLines(answer.pages, {
145
+ lineOffset: args.line_offset ?? 0,
146
+ maxLines: args.max_lines ?? FILE_LINES_DEFAULT,
147
+ contains: args.contains,
148
+ });
149
+ const label = file.name ?? answer.sha256.slice(0, 12);
150
+ const range = answer.to - answer.from > 1
151
+ ? `pages ${answer.from} to ${answer.to - 1}`
152
+ : `page ${answer.from}`;
153
+ return toolResult({
154
+ summary: `${label}, ${range} of ${answer.page_count} (0-based): ${lines.shown} lines${lines.next ? ", more after next" : ""}`,
155
+ data: {
156
+ sha256: answer.sha256,
157
+ parser: answer.parser,
158
+ page_count: answer.page_count,
159
+ from: answer.from,
160
+ to: answer.to,
161
+ line_format: "<line id> [x0,y0,x1,y1] <text>",
162
+ pages: lines.pages,
163
+ },
164
+ ...(lines.next
165
+ ? {
166
+ truncated: true,
167
+ next: {
168
+ action: "pages",
169
+ sha256: answer.sha256,
170
+ from: lines.next.from,
171
+ to: answer.to,
172
+ line_offset: lines.next.line_offset,
173
+ ...(args.contains ? { contains: args.contains } : {}),
174
+ },
175
+ }
176
+ : {}),
177
+ });
178
+ }
179
+ catch (error) {
180
+ return errorResult(await enrichError(client, error));
181
+ }
182
+ });
183
+ }
@@ -331,6 +331,73 @@ export const ontologyEvolveOpSchema = z.discriminatedUnion("op", [
331
331
  })
332
332
  .strict(),
333
333
  ]);
334
+ const namedEntityInputSchema = z
335
+ .object({
336
+ type: z.string(),
337
+ name: z.string(),
338
+ key: z
339
+ .string()
340
+ .optional()
341
+ .describe("The record's external key, when it has one"),
342
+ })
343
+ .strict();
344
+ /** Graph facts a review item proposes; accepting commits them. */
345
+ export const suggestedFactsSchema = z
346
+ .object({
347
+ triplets: z
348
+ .array(z
349
+ .object({
350
+ source: namedEntityInputSchema,
351
+ relation: z.string(),
352
+ target: namedEntityInputSchema,
353
+ confidence: z.number().min(0).max(1).optional(),
354
+ evidence: z
355
+ .unknown()
356
+ .optional()
357
+ .describe("Why the link holds: a string, or { text, source_id } as lbb_commit takes it"),
358
+ })
359
+ .passthrough())
360
+ .max(200)
361
+ .optional()
362
+ .describe("Links to commit, as lbb_commit takes triplets"),
363
+ entity_properties: z
364
+ .array(jsonObjectSchema)
365
+ .max(200)
366
+ .optional()
367
+ .describe("Fields to commit, each { type, name, properties: { field: value } } as lbb_commit takes them"),
368
+ replace_owned: z
369
+ .object({
370
+ subjects: z
371
+ .array(namedEntityInputSchema)
372
+ .max(1000)
373
+ .describe("The records this write is for"),
374
+ relations: z
375
+ .array(z.string())
376
+ .max(64)
377
+ .optional()
378
+ .describe("Owned relations: their current out-edges of the subjects that the facts do not write again are removed"),
379
+ properties: z
380
+ .array(z.string())
381
+ .max(64)
382
+ .optional()
383
+ .describe("Owned fields: the ones the facts do not set on a subject are removed"),
384
+ retract_entities: z
385
+ .array(namedEntityInputSchema)
386
+ .max(1000)
387
+ .optional()
388
+ .describe("Records the producer made and no longer makes"),
389
+ })
390
+ .strict()
391
+ .optional()
392
+ .describe("Replace the producer's owned edges and fields on its subjects in the same commit"),
393
+ summary: z
394
+ .string()
395
+ .max(500)
396
+ .optional()
397
+ .describe("One line about the facts for the reviewer"),
398
+ })
399
+ .strict()
400
+ .describe("Graph facts a person confirms: accepting commits them after the change (at most 200 triplets and property rows together). Records graphs only");
334
401
  export const inspectInputSchema = z.discriminatedUnion("action", [
335
402
  z.object({ action: z.literal("guide"), ...readScope }).strict(),
336
403
  z
@@ -773,9 +840,10 @@ export const configureInputSchema = z.discriminatedUnion("action", [
773
840
  .describe("Short imperative text, e.g. 'Add class Contractor'"),
774
841
  change: z
775
842
  .array(ontologyEvolveOpSchema)
776
- .min(1)
777
843
  .max(128)
778
- .describe("The ontology operations a person applies by accepting, in order (same shapes as evolve_ontology ops)"),
844
+ .optional()
845
+ .describe("The ontology operations a person applies by accepting, in order (same shapes as evolve_ontology ops). Up to 128; may be empty or absent when facts is set"),
846
+ facts: suggestedFactsSchema.optional(),
779
847
  rationale: z
780
848
  .string()
781
849
  .max(4000)
package/dist/tools.js CHANGED
@@ -3,13 +3,15 @@ import { metadataPage } from "./metadata-pages.js";
3
3
  import { answerQuestion, } from "./question.js";
4
4
  import { commitAt, compareQuery, describeGraph, findNames, } from "./query-tools.js";
5
5
  import { registerRdfTool } from "./rdf-tool.js";
6
+ import { registerFilesTool } from "./files-tool.js";
6
7
  import { queryTiming } from "./query-observer.js";
7
8
  import { DESTRUCTIVE, IDEMPOTENT_WRITE, MUTATING, READ_ONLY, configureInputSchema, configureWireSchema, detailSchema, graphScope, inspectInputSchema, inspectWireSchema, jsonObjectSchema, queryInputSchema, queryWireSchema, searchFeedbackSchema, yearMonthSchema, } from "./tool-contracts.js";
8
9
  import { analyze, assertCursorScope, contentHashKey, decodeQueryCursor, effectiveRowLimit, enrichError, errorResult, guide, normalizeDetail, normalizeLbbIris, ontologyDefineBody, queryCommitPin, queryEnvelope, queryToolResult, requireString, rowPageFrom, rowPageNext, run, scoped, stableJson, toolResult, } from "./tool-runtime.js";
9
10
  export function registerLbbTools(server, client, options = {}) {
10
11
  registerRdfTool(server, client);
12
+ registerFilesTool(server, client);
11
13
  server.registerTool("lbb_inspect", {
12
- description: "Read graph context and exact graph facts. Actions: guide, graphs, publication, ontology, ontology_conformance, schema, ontology_search, metadata, entity, ontology_suggestions. graphs works before bootstrap; ontology_suggestions lists the ontology change suggestions that wait for review (or were decided). publication reports whether writes are queryable. ontology and schema return complete entries with page_size, section and cursor; follow next until absent. schema reads active native ontology/SHACL metadata without running validation. Query asserted RDF/OWL axioms separately with lbb_query. ontology_conformance serves the durable report referenced by the pinned published root. entity returns one node's attributes and current relationships from the RDF read. For a node's past values, run SPARQL with as_of_commit_seq through lbb_query. Use lbb_query with SPARQL property paths for precise path selection.",
14
+ description: "Read graph context and exact graph facts. Actions: guide, graphs, publication, ontology, ontology_conformance, schema, ontology_search, metadata, entity, ontology_suggestions. graphs works before bootstrap; ontology_suggestions lists the ontology change suggestions that wait for review (or were decided); fact_count is how many graph facts (triplets and entity property rows) accepting one commits, and identity_count how many identities it links. publication reports whether writes are queryable. ontology and schema return complete entries with page_size, section and cursor; follow next until absent. schema reads active native ontology/SHACL metadata without running validation. Query asserted RDF/OWL axioms separately with lbb_query. ontology_conformance serves the durable report referenced by the pinned published root. entity returns one node's attributes and current relationships from the RDF read. For a node's past values, run SPARQL with as_of_commit_seq through lbb_query. Use lbb_query with SPARQL property paths for precise path selection.",
13
15
  inputSchema: inspectWireSchema,
14
16
  annotations: READ_ONLY,
15
17
  }, (rawArgs) => {
@@ -356,7 +358,7 @@ export function registerLbbTools(server, client, options = {}) {
356
358
  });
357
359
  });
358
360
  server.registerTool("lbb_models", {
359
- description: "Read model-training inputs or compare retrieval configurations over one pinned published snapshot, or read what the managed models did. shadow_eval takes the API ShadowEvalRequest body; dataset actions return bounded training examples at an optional signal split. activity reads one month of the stack's model use (all graphs): calls, items, estimated tokens and cost per feature (index, search, fit, judge, training) and model, by day and by graph, the months with activity, and the model each feature uses now.",
361
+ description: "Read model-training inputs or compare retrieval configurations over one pinned published snapshot, or read what the managed models did. shadow_eval takes the API ShadowEvalRequest body; dataset actions return bounded training examples at an optional signal split. activity reads one month of the stack's model use (all graphs): calls, items, estimated tokens and cost per feature (index, search, fit, judge, training, rerank, rewrite, trial, workflows) and model, by day and by graph, the workflows' calls per workflow and job (by_workflow), the months with activity, and the model each feature uses now.",
360
362
  inputSchema: {
361
363
  action: z.enum([
362
364
  "shadow_eval",
@@ -603,6 +605,190 @@ export function registerLbbTools(server, client, options = {}) {
603
605
  }
604
606
  return scoped(client, graph).ontology.fitSources.delete(name);
605
607
  }));
608
+ // Starter workflows and triggered workflows: one API over everything that
609
+ // runs when a class of the graph changes. search.embed and ontology.fit
610
+ // are the embeddings and fit sources above, through the same stored
611
+ // documents.
612
+ const workflowSetup = {
613
+ starter: z
614
+ .string()
615
+ .optional()
616
+ .describe("The starter workflow's catalog id from action=starters, e.g. search.embed (embeddings on a class), ontology.fit (fit from text) or workflow (a developer workflow: your worker's workflow gets the instances). get/preview/use/refresh/pause/resume: required."),
617
+ class: z
618
+ .string()
619
+ .optional()
620
+ .describe("preview/use: the class whose instances the workflow watches (an IRI, or a name the ontology knows). Required unless the starter workflow's watch is fixed."),
621
+ fields: z
622
+ .array(z.string())
623
+ .optional()
624
+ .describe("preview/use: the fields whose change triggers the workflow, as embedding paths (transcript, label, about/label, <https://…>). Omit for the starter workflow's own choice."),
625
+ exclude: z
626
+ .array(z.string())
627
+ .optional()
628
+ .describe("preview/use: fields to drop from the automatic choice."),
629
+ params: jsonObjectSchema
630
+ .optional()
631
+ .describe('preview/use: the starter workflow\'s own settings, as listed in its catalog entry: search.embed takes model, dim and title; ontology.fit takes context (one sentence about the text, e.g. {"context": "interviews with employees"}); workflow takes none.'),
632
+ workflow: z
633
+ .object({
634
+ workflow_type: z
635
+ .string()
636
+ .describe("The workflow type your worker serves, e.g. word-count."),
637
+ version: z.string().describe("Its version, e.g. v1."),
638
+ })
639
+ .strict()
640
+ .optional()
641
+ .describe("preview/use, developer workflow (starter=workflow) only, required there: the workflow of your worker that receives the messages. Creating it creates the instance starter.<name>."),
642
+ owns: z
643
+ .object({
644
+ classes: z.array(z.string()).max(64).optional(),
645
+ relations: z.array(z.string()).max(64).optional(),
646
+ properties: z.array(z.string()).max(64).optional(),
647
+ })
648
+ .strict()
649
+ .optional()
650
+ .describe("preview/use, developer workflow only: what your code writes. classes it creates instances of, relations it writes as out-edges of the watched instances, properties it writes on them. Owned outputs never trigger the workflow itself."),
651
+ batch: z
652
+ .number()
653
+ .int()
654
+ .min(1)
655
+ .max(64)
656
+ .optional()
657
+ .describe("preview/use, developer workflow only: instances per message, 1 to 64 (default 16)."),
658
+ };
659
+ const workflowRequest = (args) => {
660
+ if (!args.starter)
661
+ throw new Error("this action requires starter");
662
+ if (!args.class && (args.fields || args.exclude)) {
663
+ throw new Error("fields and exclude need class");
664
+ }
665
+ return {
666
+ starter: args.starter,
667
+ ...(args.name ? { name: args.name } : {}),
668
+ ...(args.class
669
+ ? {
670
+ watch: {
671
+ class: args.class,
672
+ ...(args.fields ? { fields: args.fields } : {}),
673
+ ...(args.exclude ? { exclude: args.exclude } : {}),
674
+ },
675
+ }
676
+ : {}),
677
+ ...(args.params ? { params: args.params } : {}),
678
+ ...(args.gate !== undefined ? { gate: { below: args.gate } } : {}),
679
+ ...(args.budget_usd_per_month !== undefined
680
+ ? { budget_usd_per_month: args.budget_usd_per_month }
681
+ : {}),
682
+ ...(args.workflow ? { workflow: args.workflow } : {}),
683
+ ...(args.owns ? { owns: args.owns } : {}),
684
+ ...(args.batch !== undefined ? { batch: args.batch } : {}),
685
+ };
686
+ };
687
+ const workflowRef = (args) => {
688
+ if (!args.starter || !args.name) {
689
+ throw new Error("this action requires starter and name");
690
+ }
691
+ return { starter: args.starter, name: args.name };
692
+ };
693
+ server.registerTool("lbb_workflows", {
694
+ description: "Starter workflows and triggered workflows, read only. A starter workflow is a template; a triggered workflow is a workflow created from one on the graph. It watches a class: when a published commit adds or changes instances of the class (or changes the watched fields), the workflow reads them and does its work. search.embed keeps an embedding of the class (the same as lbb_embeddings); ontology.fit reads text fields and files ontology suggestions (the same as lbb_fit_sources); workflow (a developer workflow) sends the new and changed instances to a workflow of the developer's worker as trigger messages, and that code writes the facts the workflow owns. documents.parse reads uploaded PDF files into pages with line boxes, documents.link connects mentions of the same thing across the documents (it writes DocEntry and DocMention instances and links each mention to the entry that defines or describes it), documents.map proposes each link as a FactProposal in the graph's own ontology (fact_subject, fact_predicate, fact_object, fact_confidence, fact_evidence) for a review workflow, and values.extract fills chosen properties from text, each value with a quote; read the files and their pages with lbb_files. starters lists the starter workflows this server offers with their group, settings and whether each can run here. list shows every triggered workflow of the graph with its state (backfilling, ready, paused, failed, unavailable), lag, counters, this month's spend and last error; get shows one (starter and name) with its own status in details (for workflow: the messages sent, the instances waiting, the message in flight and whether it is paused). preview shows what creating one would do (the resolved watch, the instance count and the workflow's own preview; for workflow, the first messages it would send), calls no model and stores nothing. Message workflows of the workflow engine are not listed here. Create, refresh, pause or resume with lbb_workflows_manage; delete with lbb_workflows_delete.",
695
+ inputSchema: {
696
+ action: z.enum(["starters", "list", "get", "preview"]),
697
+ name: z
698
+ .string()
699
+ .optional()
700
+ .describe("get: the triggered workflow's name. preview: the name a new workflow would get."),
701
+ ...workflowSetup,
702
+ sample: z
703
+ .number()
704
+ .int()
705
+ .positive()
706
+ .max(50)
707
+ .optional()
708
+ .describe("preview: how many sample instances to show."),
709
+ iris: z
710
+ .array(z.string())
711
+ .optional()
712
+ .describe("preview: the instances to show instead of a sample."),
713
+ detail: detailSchema,
714
+ ...graphScope,
715
+ },
716
+ annotations: READ_ONLY,
717
+ }, (args) => run(client, `lbb_workflows.${args.action}`, args.detail, () => {
718
+ const workflows = scoped(client, args.graph).workflows;
719
+ switch (args.action) {
720
+ case "starters":
721
+ return workflows.starters.list();
722
+ case "list":
723
+ return workflows.triggered.list();
724
+ case "get":
725
+ return workflows.triggered.get(workflowRef(args));
726
+ case "preview":
727
+ return workflows.triggered.preview({
728
+ ...workflowRequest(args),
729
+ ...(args.sample ? { sample: args.sample } : {}),
730
+ ...(args.iris ? { iris: args.iris } : {}),
731
+ });
732
+ }
733
+ }));
734
+ server.registerTool("lbb_workflows_manage", {
735
+ description: "Create a workflow from a starter workflow, or refresh, pause or resume a triggered workflow. Run lbb_workflows action=preview first and show the user the watch and the instance count. use creates a workflow from a starter workflow of action=starters, under a name (default: the class's local name), with its watch (class, fields) and params; the workflow then reads every instance of the class and follows each published commit. A developer workflow (starter=workflow) also takes workflow (workflow_type and version of the developer's worker), owns and batch; it needs a server with the workflow engine. use again with the same starter and name changes only what you name, and the same request again changes nothing. A workflow that calls models (ontology.fit, documents.link, documents.map, values.extract) spends model budget as it reads. refresh asks the workflow to run now and returns at once with queued; poll lbb_workflows action=get for progress. pause stops a workflow (a developer workflow, documents.parse, documents.link, documents.map or values.extract) from reading and sending; resume lets it catch up. search.embed and ontology.fit cannot be paused.",
736
+ inputSchema: {
737
+ action: z.enum(["use", "refresh", "pause", "resume"]),
738
+ name: z
739
+ .string()
740
+ .optional()
741
+ .describe("The triggered workflow's name, [a-z0-9][a-z0-9-]{0,62} (refresh, pause, resume: required; use: omit for the class's local name)."),
742
+ ...workflowSetup,
743
+ gate: z
744
+ .number()
745
+ .min(0)
746
+ .max(1)
747
+ .optional()
748
+ .describe("use: outputs below this confidence go to review instead of the graph, for a workflow with a review gate."),
749
+ budget_usd_per_month: z
750
+ .number()
751
+ .nonnegative()
752
+ .optional()
753
+ .describe("use: a monthly model budget in USD for this workflow, below the stack's own."),
754
+ detail: detailSchema,
755
+ ...graphScope,
756
+ },
757
+ annotations: MUTATING,
758
+ }, (args) => run(client, `lbb_workflows_manage.${args.action}`, args.detail, () => {
759
+ const workflows = scoped(client, args.graph).workflows;
760
+ switch (args.action) {
761
+ case "use":
762
+ return workflows.starters.use(workflowRequest(args));
763
+ case "refresh":
764
+ return workflows.triggered.refresh(workflowRef(args));
765
+ case "pause":
766
+ return workflows.triggered.pause(workflowRef(args), true);
767
+ case "resume":
768
+ return workflows.triggered.pause(workflowRef(args), false);
769
+ }
770
+ }));
771
+ server.registerTool("lbb_workflows_delete", {
772
+ description: "Delete a triggered workflow: it stops following its class, and what it wrote stays. Deleting search.embed deletes the embedding and its vectors, so its class stops being searchable; the suggestions an ontology.fit filed stay; a developer workflow leaves its workflow instance and the facts its code wrote. Ask the user first; confirm must repeat the name.",
773
+ inputSchema: {
774
+ starter: z
775
+ .string()
776
+ .describe("The starter workflow's catalog id, e.g. search.embed."),
777
+ name: z.string().describe("The triggered workflow's name."),
778
+ confirm: z.string().describe("The same name again, to confirm."),
779
+ detail: detailSchema,
780
+ ...graphScope,
781
+ },
782
+ annotations: DESTRUCTIVE,
783
+ }, ({ starter, name, confirm, detail, graph }) => run(client, "lbb_workflows_delete", detail, () => {
784
+ if (confirm !== name) {
785
+ throw new Error("confirm must repeat the workflow name");
786
+ }
787
+ return scoped(client, graph).workflows.triggered.delete({
788
+ starter,
789
+ name,
790
+ });
791
+ }));
606
792
  const modelUse = z
607
793
  .enum(["ask", "route", "rerank", "fit", "label"])
608
794
  .describe("A use of a model: ask = question answers, route = query routing, rerank = search rerank, fit = ontology fit, label = eval labels.");
@@ -735,7 +921,7 @@ export function registerLbbTools(server, client, options = {}) {
735
921
  }
736
922
  }));
737
923
  server.registerTool("lbb_evals", {
738
- description: "Managed evals: ground truth for the questions an app asks and the searches it runs. A query run with `request` (lbb_query, also mode=question) records a trace: the question, the query that answered it, the type of that query (`query_type`: sparql, hybrid = search by meaning with conditions, search = search by meaning alone) and one item per result (hit or row); trace reads it back with the item ids. label has two forms. On a question's trace, valid alone judges the whole answer: valid=true makes the trace's query the golden query of the question; valid=false with sparql gives the right query, which becomes the golden query; valid=false alone marks the answer wrong. item + valid (or items) judges one result, a citation of the answer: the ground truth of the hits of a search by meaning, and a result marked wrong must not come back for any type. golden freezes a stored query or a search (every result it returns now is relevant). run checks every golden at the current commit: a question is asked again through the query rewriter, then the type of the query it wrote is compared with the expected type, its rows with the rows of the golden query at the same commit (`query_check`), and its results with the judged results; a search is done again; 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. Model checks: a judge model checks a sample of the model calls LBB makes for the graph (rerank, route, ask = the answer of a question, rewrite, fit, propose, label). checks_summary reads a month per job and model (checks, score, right, partly, wrong, reviews) and the judge's agreement with people; checks lists the month's checks, newest first, with the judge's verdict, score and reason and the ground truth (`truth`). review_check records a person's review of one check (call_id): agree=true keeps the judge's verdict; agree=false with verdict (and an optional score, reference and note) corrects it. The review becomes the call's ground truth, so show the check to the user and review only what the user confirmed.",
924
+ description: "Managed evals: ground truth for the questions an app asks and the searches it runs. A query run with `request` (lbb_query, also mode=question) records a trace: the question, the query that answered it, the type of that query (`query_type`: sparql, hybrid = search by meaning with conditions, search = search by meaning alone) and one item per result (hit or row); trace reads it back with the item ids. label has two forms. On a question's trace, valid alone judges the whole answer: valid=true makes the trace's query the golden query of the question; valid=false with sparql gives the right query, which becomes the golden query; valid=false alone marks the answer wrong; route with valid=true (or valid=false with sparql) names the route the question expects, and valid=true alone takes the trace's route. item + valid (or items) judges one result, a citation of the answer: the ground truth of the hits of a search by meaning, and a result marked wrong must not come back for any type. golden freezes a stored query or a search (every result it returns now is relevant). run checks every golden at the current commit: a question is asked again through the query rewriter, then the type of the query it wrote is compared with the expected type, its rows with the rows of the golden query at the same commit (`query_check`), and its results with the judged results, and the router's pick with the golden's route (route_check, which does not change the verdict); a search is done again; new results open a review trace. run_answers checks your own app's answers instead: ask each question golden's request with your own loop, then send answers=[{golden, sparql, route}] (route = the route of lbb_query mode=question with route mode, sparql absent for an unanswerable question, error for a failure); the server runs each query and checks it the same way, and the runs form the app's own series (runner; results with runner reads them). judge lets the platform's judge model (the hosted frontier model) label unlabeled results. summary, traces, goldens, results, and settings read state. Model checks: a judge model checks a sample of the model calls LBB makes for the graph (rerank, route, ask = the answer of a question, rewrite, fit, propose, label). checks_summary reads a month per job and model (checks, score, right, partly, wrong, reviews) and the judge's agreement with people; checks lists the month's checks, newest first, with the judge's verdict, score and reason and the ground truth (`truth`). review_check records a person's review of one check (call_id): agree=true keeps the judge's verdict; agree=false with verdict (and an optional score, reference and note) corrects it. The review becomes the call's ground truth, so show the check to the user and review only what the user confirmed.",
739
925
  inputSchema: {
740
926
  action: z.enum([
741
927
  "summary",
@@ -748,6 +934,7 @@ export function registerLbbTools(server, client, options = {}) {
748
934
  "accept",
749
935
  "delete",
750
936
  "run",
937
+ "run_answers",
751
938
  "results",
752
939
  "settings",
753
940
  "checks_summary",
@@ -788,6 +975,58 @@ export function registerLbbTools(server, client, options = {}) {
788
975
  .string()
789
976
  .optional()
790
977
  .describe("golden: the query to freeze (the search text for surface=search). label: with valid=false and no item, the right query for the question; the server checks and runs it once."),
978
+ route: z
979
+ .enum([
980
+ "lookup",
981
+ "aggregate",
982
+ "search",
983
+ "history",
984
+ "schema",
985
+ "unanswerable",
986
+ ])
987
+ .optional()
988
+ .describe("label: with valid=true, or valid=false with sparql, the route the question expects."),
989
+ answers: z
990
+ .array(z
991
+ .object({
992
+ golden: z.string().min(1),
993
+ sparql: z.string().optional(),
994
+ entailment: z
995
+ .enum(["none", "subclass", "rdfs", "owl"])
996
+ .optional(),
997
+ as_of_commit_seq: z.number().int().nonnegative().optional(),
998
+ route: z
999
+ .object({
1000
+ kind: z.enum([
1001
+ "lookup",
1002
+ "aggregate",
1003
+ "search",
1004
+ "history",
1005
+ "schema",
1006
+ "unanswerable",
1007
+ ]),
1008
+ confidence: z.number(),
1009
+ by: z.enum(["caller", "router", "rewriter"]),
1010
+ probabilities: z.record(z.string(), z.number()).optional(),
1011
+ })
1012
+ .strict()
1013
+ .optional(),
1014
+ reply: z
1015
+ .object({
1016
+ text: z.string(),
1017
+ citations: z.array(z.string()).optional(),
1018
+ })
1019
+ .optional(),
1020
+ error: z.string().optional(),
1021
+ cost_micro_usd: z.number().int().nonnegative().optional(),
1022
+ })
1023
+ .strict())
1024
+ .optional()
1025
+ .describe("run_answers: one answer per question golden your app asked: golden (its id), sparql (the query whose rows hold the answer; leave it out when the question is unanswerable), entailment, as_of_commit_seq, route (as the router returned it), reply, error (your app failed), cost_micro_usd."),
1026
+ runner: z
1027
+ .string()
1028
+ .optional()
1029
+ .describe("run_answers / results: your app's name, lower-case letters, digits and -, at most 40 characters (default app). results without runner reads the server's runs."),
791
1030
  surface: z
792
1031
  .enum(["sparql", "search"])
793
1032
  .optional()
@@ -819,7 +1058,7 @@ export function registerLbbTools(server, client, options = {}) {
819
1058
  consistency: z
820
1059
  .enum(["strong", "eventual"])
821
1060
  .optional()
822
- .describe("run / accept: strong reads the head, eventual the last published commit."),
1061
+ .describe("run / run_answers / accept: strong reads the head, eventual the last published commit."),
823
1062
  month: yearMonthSchema
824
1063
  .optional()
825
1064
  .describe("checks_summary / checks: the month, yyyy-mm (UTC). Defaults to the current month."),
@@ -868,7 +1107,7 @@ export function registerLbbTools(server, client, options = {}) {
868
1107
  ...graphScope,
869
1108
  },
870
1109
  annotations: MUTATING,
871
- }, ({ action, trace_id, item, valid, items, by, note, sparql, surface, embedding, top_k, request, golden_id, limit, unlabeled, consistency, month, job, verdict, reviewed, after, call_id, agree, score, reference, detail, graph, }) => run(client, `lbb_evals.${action}`, detail, () => {
1110
+ }, ({ action, trace_id, item, valid, items, by, note, sparql, route, answers, runner, surface, embedding, top_k, request, golden_id, limit, unlabeled, consistency, month, job, verdict, reviewed, after, call_id, agree, score, reference, detail, graph, }) => run(client, `lbb_evals.${action}`, detail, () => {
872
1111
  const target = scoped(client, graph);
873
1112
  switch (action) {
874
1113
  case "summary":
@@ -889,12 +1128,18 @@ export function registerLbbTools(server, client, options = {}) {
889
1128
  if (sparql !== undefined &&
890
1129
  (item || items?.length || valid !== false))
891
1130
  throw new Error("label takes sparql (the right query) only with valid=false and no item");
1131
+ if (route !== undefined &&
1132
+ (item ||
1133
+ items?.length ||
1134
+ (valid !== true && sparql === undefined)))
1135
+ throw new Error("label takes route only with valid=true, or valid=false with sparql, and no item");
892
1136
  return target.evals.label(trace_id, {
893
1137
  ...(item ? { item, valid } : {}),
894
1138
  ...(items?.length ? { items } : {}),
895
1139
  // The answer of a question as a whole.
896
1140
  ...(!item && !items?.length ? { valid } : {}),
897
1141
  ...(sparql !== undefined ? { sparql } : {}),
1142
+ ...(route !== undefined ? { route } : {}),
898
1143
  by,
899
1144
  note,
900
1145
  });
@@ -924,8 +1169,12 @@ export function registerLbbTools(server, client, options = {}) {
924
1169
  return target.evals.deleteGolden(golden_id);
925
1170
  case "run":
926
1171
  return target.evals.run({ consistency });
1172
+ case "run_answers":
1173
+ if (!answers?.length)
1174
+ throw new Error("run_answers requires answers");
1175
+ return target.evals.runAnswers({ answers, ...(runner ? { runner } : {}) }, { consistency });
927
1176
  case "results":
928
- return target.evals.results({ limit });
1177
+ return target.evals.results({ limit, runner });
929
1178
  case "settings":
930
1179
  return target.evals.settings();
931
1180
  case "checks_summary":
@@ -1077,7 +1326,7 @@ export function registerLbbTools(server, client, options = {}) {
1077
1326
  });
1078
1327
  }));
1079
1328
  server.registerTool("lbb_configure", {
1080
- description: "Manage native schema metadata. Actions: define_ontology (friendly spec with super_types), evolve_ontology (ordered edits including add_super_types), list_starters (the base ontologies crm, documents and work, each with its status on the graph: absent, partial or applied, what applying adds, and conflicts), apply_starter (add what the graph lacks of a starter in one ontology version; a relation the graph has is widened; refused with starter_conflict when the graph holds a term differently; dry_run previews), publish_schema (SHACL activation), suggest_ontology_change (file a change for a person to review instead of applying it; prefer it when the graph's owner reviews ontology changes, and list the result with lbb_inspect action=ontology_suggestions), get_rewrite_profile and set_rewrite_profile (the graph's notes and up to 20 worked question-to-SPARQL examples that lbb_query mode=question reads for every question; pass the version you read as expected_version). define, evolve, publish and set_rewrite_profile support dry_run previews. Definition/import here extracts native metadata; it does NOT store the complete RDF/OWL document as queryable graph facts. Use lbb_rdf import for full OWL and lbb_rdf update for additive INSERT DATA revisions; RDF deletions are unsupported. Publish_schema accepts unchanged ontology plus shapes; use define/evolve for native ontology changes. Publication enqueues durable conformance; a preview does not validate the whole graph.",
1329
+ description: "Manage native schema metadata. Actions: define_ontology (friendly spec with super_types), evolve_ontology (ordered edits including add_super_types), list_starters (the base ontologies crm, documents and work, each with its status on the graph: absent, partial or applied, what applying adds, and conflicts), apply_starter (add what the graph lacks of a starter in one ontology version; a relation the graph has is widened; refused with starter_conflict when the graph holds a term differently; dry_run previews), publish_schema (SHACL activation), suggest_ontology_change (file a change for a person to review instead of applying it; prefer it when the graph's owner reviews ontology changes, and list the result with lbb_inspect action=ontology_suggestions; facts files graph facts for review, which accepting commits after the change, and change may be empty when facts is set), get_rewrite_profile and set_rewrite_profile (the graph's notes and up to 20 worked question-to-SPARQL examples that lbb_query mode=question reads for every question; pass the version you read as expected_version). define, evolve, publish and set_rewrite_profile support dry_run previews. Definition/import here extracts native metadata; it does NOT store the complete RDF/OWL document as queryable graph facts. Use lbb_rdf import for full OWL and lbb_rdf update for additive INSERT DATA revisions; RDF deletions are unsupported. Publish_schema accepts unchanged ontology plus shapes; use define/evolve for native ontology changes. Publication enqueues durable conformance; a preview does not validate the whole graph.",
1081
1330
  inputSchema: configureWireSchema,
1082
1331
  annotations: MUTATING,
1083
1332
  }, (rawArgs) => {
@@ -1098,10 +1347,14 @@ export function registerLbbTools(server, client, options = {}) {
1098
1347
  }
1099
1348
  if (args.action === "suggest_ontology_change") {
1100
1349
  const agent = args.agent ?? "mcp agent";
1350
+ const change = args.change ?? [];
1351
+ if (!change.length && !args.facts)
1352
+ throw new Error("suggest_ontology_change needs change, facts, or both");
1101
1353
  return scoped(client, args.graph).ontology.suggestions.create({
1102
1354
  title: args.title,
1103
1355
  rationale: args.rationale ?? "",
1104
- change: args.change,
1356
+ change,
1357
+ ...(args.facts ? { facts: args.facts } : {}),
1105
1358
  origin: { kind: "agent", id: agent, label: agent },
1106
1359
  ...(args.anchor ? { anchor: args.anchor } : {}),
1107
1360
  ...(args.key ? { key: args.key } : {}),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@littlebigbrain/mcp",
3
- "version": "0.11.0",
3
+ "version": "0.13.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.22.0",
55
+ "@littlebigbrain/client": "^0.25.0",
56
56
  "@modelcontextprotocol/sdk": "^1",
57
57
  "zod": "^3"
58
58
  },