@littlebigbrain/mcp 0.11.0 → 0.12.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 +4 -0
- package/dist/files-tool.d.ts +43 -0
- package/dist/files-tool.js +183 -0
- package/dist/tool-contracts.js +70 -2
- package/dist/tools.js +193 -3
- package/package.json +2 -2
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
|
+
}
|
package/dist/tool-contracts.js
CHANGED
|
@@ -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
|
-
.
|
|
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) => {
|
|
@@ -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 writes DocEntry and DocMention instances and links each mention to its entry, and infer.llm 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, infer.llm) 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 or infer.llm) 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.");
|
|
@@ -1077,7 +1263,7 @@ export function registerLbbTools(server, client, options = {}) {
|
|
|
1077
1263
|
});
|
|
1078
1264
|
}));
|
|
1079
1265
|
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.",
|
|
1266
|
+
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
1267
|
inputSchema: configureWireSchema,
|
|
1082
1268
|
annotations: MUTATING,
|
|
1083
1269
|
}, (rawArgs) => {
|
|
@@ -1098,10 +1284,14 @@ export function registerLbbTools(server, client, options = {}) {
|
|
|
1098
1284
|
}
|
|
1099
1285
|
if (args.action === "suggest_ontology_change") {
|
|
1100
1286
|
const agent = args.agent ?? "mcp agent";
|
|
1287
|
+
const change = args.change ?? [];
|
|
1288
|
+
if (!change.length && !args.facts)
|
|
1289
|
+
throw new Error("suggest_ontology_change needs change, facts, or both");
|
|
1101
1290
|
return scoped(client, args.graph).ontology.suggestions.create({
|
|
1102
1291
|
title: args.title,
|
|
1103
1292
|
rationale: args.rationale ?? "",
|
|
1104
|
-
change
|
|
1293
|
+
change,
|
|
1294
|
+
...(args.facts ? { facts: args.facts } : {}),
|
|
1105
1295
|
origin: { kind: "agent", id: agent, label: agent },
|
|
1106
1296
|
...(args.anchor ? { anchor: args.anchor } : {}),
|
|
1107
1297
|
...(args.key ? { key: args.key } : {}),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@littlebigbrain/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.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.
|
|
55
|
+
"@littlebigbrain/client": "^0.23.0",
|
|
56
56
|
"@modelcontextprotocol/sdk": "^1",
|
|
57
57
|
"zod": "^3"
|
|
58
58
|
},
|