@littlebigbrain/mcp 0.8.0 → 0.9.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 +19 -3
- package/dist/question.d.ts +24 -0
- package/dist/question.js +87 -0
- package/dist/tool-contracts.js +51 -3
- package/dist/tool-runtime.js +4 -2
- package/dist/tools.js +115 -12
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -104,6 +104,22 @@ The result contains one row on a fresh graph:
|
|
|
104
104
|
The query follows a stored relationship to its database label. Strong consistency
|
|
105
105
|
lets it read the import without waiting for a background index job.
|
|
106
106
|
|
|
107
|
+
An agent without a SPARQL query can pass the question in plain words:
|
|
108
|
+
|
|
109
|
+
```json
|
|
110
|
+
{
|
|
111
|
+
"mode": "question",
|
|
112
|
+
"graph": "quickstart",
|
|
113
|
+
"question": "Which database does Auth Service write to?"
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
The server selects the kind of query, writes the SPARQL query from a
|
|
118
|
+
description of the graph, checks it, and runs it. The result holds the route,
|
|
119
|
+
the rationale, the query, and its rows. `next` continues the same query with
|
|
120
|
+
`mode: "sparql"`. Each question uses model tokens and counts toward a daily
|
|
121
|
+
limit of the stack, so the tool does not retry a failed call.
|
|
122
|
+
|
|
107
123
|
For your own data, start with `lbb_inspect` using `action: "guide"` or
|
|
108
124
|
`action: "ontology"`. Imported Resource Description Framework (RDF) data keeps
|
|
109
125
|
its original identifiers; inspect it with SPARQL when choosing query predicates.
|
|
@@ -113,15 +129,15 @@ its original identifiers; inspect it with SPARQL when choosing query predicates.
|
|
|
113
129
|
| Tool | Purpose |
|
|
114
130
|
| --- | --- |
|
|
115
131
|
| `lbb_inspect` | Read the schema, graph status, and entities. |
|
|
116
|
-
| `lbb_query` |
|
|
132
|
+
| `lbb_query` | Ask a question in plain words, run SPARQL queries, search by meaning (also inside a SPARQL query with `search:similarTo`), or request summary statistics. |
|
|
117
133
|
| `lbb_rdf` | Import RDF documents or add facts with SPARQL `INSERT DATA`. |
|
|
118
134
|
| `lbb_embeddings` | Inspect search setup and preview the text to embed. |
|
|
119
135
|
| `lbb_embeddings_manage` | Set up or refresh embeddings, or change the embedding model. |
|
|
120
136
|
| `lbb_embeddings_delete` | Delete an embedding and its stored vectors. |
|
|
121
137
|
| `lbb_commit` | Write or retract JSON facts, or record search feedback. |
|
|
122
138
|
| `lbb_configure` | Define record types and relationships, list and apply ontology starters (CRM, documents, work), or publish validation rules. |
|
|
123
|
-
| `lbb_evals` | Label query results and check whether later queries return the expected answers. |
|
|
124
|
-
| `lbb_models` | Compare retrieval settings
|
|
139
|
+
| `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. |
|
|
140
|
+
| `lbb_models` | Compare retrieval settings, read model training datasets, or read a month of managed-model activity. |
|
|
125
141
|
|
|
126
142
|
See [search by meaning](https://docs.littlebigbrain.com/guides/search-by-meaning/)
|
|
127
143
|
for embedding setup. To define constraints with the Shapes Constraint Language
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { LbbClient } from "@littlebigbrain/client";
|
|
2
|
+
type QuestionRoute = "lookup" | "aggregate" | "search" | "history" | "schema" | "unanswerable";
|
|
3
|
+
export interface QuestionArgs {
|
|
4
|
+
question: string;
|
|
5
|
+
context?: string;
|
|
6
|
+
route?: QuestionRoute;
|
|
7
|
+
limit?: number;
|
|
8
|
+
run?: boolean;
|
|
9
|
+
detail?: string;
|
|
10
|
+
graph?: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* `lbb_query mode=question`: the server turns the question into a SPARQL
|
|
14
|
+
* query and, by default, runs it. The rows are bounded as `mode=sparql`
|
|
15
|
+
* bounds them, and `next` continues the same query with `mode=sparql`.
|
|
16
|
+
*/
|
|
17
|
+
export declare function answerQuestion(client: LbbClient, args: QuestionArgs, textFormat?: "compact" | "pretty"): Promise<{
|
|
18
|
+
content: {
|
|
19
|
+
type: "text";
|
|
20
|
+
text: string;
|
|
21
|
+
}[];
|
|
22
|
+
structuredContent: Record<string, unknown>;
|
|
23
|
+
}>;
|
|
24
|
+
export {};
|
package/dist/question.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { QUESTION_MAX_ROWS } from "./tool-contracts.js";
|
|
2
|
+
import { defaultRowLimit, normalizeDetail, queryEnvelope, queryToolResult, rowPageFrom, rowPageNext, scoped, } from "./tool-runtime.js";
|
|
3
|
+
/**
|
|
4
|
+
* `lbb_query mode=question`: the server turns the question into a SPARQL
|
|
5
|
+
* query and, by default, runs it. The rows are bounded as `mode=sparql`
|
|
6
|
+
* bounds them, and `next` continues the same query with `mode=sparql`.
|
|
7
|
+
*/
|
|
8
|
+
export async function answerQuestion(client, args, textFormat) {
|
|
9
|
+
const detail = normalizeDetail(args.detail);
|
|
10
|
+
const run = args.run ?? true;
|
|
11
|
+
const rowLimit = Math.min(args.limit ?? defaultRowLimit(detail), QUESTION_MAX_ROWS);
|
|
12
|
+
const target = scoped(client, args.graph);
|
|
13
|
+
const body = {
|
|
14
|
+
question: args.question,
|
|
15
|
+
...(args.context !== undefined ? { context: args.context } : {}),
|
|
16
|
+
...(args.route !== undefined ? { route: args.route } : {}),
|
|
17
|
+
run,
|
|
18
|
+
...(run ? { limit: rowLimit } : {}),
|
|
19
|
+
};
|
|
20
|
+
// Each call uses model tokens, and the server corrects a failing query
|
|
21
|
+
// once, so a failed call is not retried.
|
|
22
|
+
const response = await target.request("POST", "/v1/query/rewrite", {
|
|
23
|
+
body,
|
|
24
|
+
retry: false,
|
|
25
|
+
query: { consistency: target.defaultConsistency },
|
|
26
|
+
});
|
|
27
|
+
const { route, query, result } = response;
|
|
28
|
+
const traceId = result?.trace_id ?? null;
|
|
29
|
+
const answer = {
|
|
30
|
+
route: { kind: route.kind, confidence: route.confidence, by: route.by },
|
|
31
|
+
rationale: response.rationale,
|
|
32
|
+
sparql: query?.sparql ?? null,
|
|
33
|
+
entailment: query?.entailment ?? null,
|
|
34
|
+
...(typeof query?.as_of_commit_seq === "number"
|
|
35
|
+
? { as_of_commit_seq: query.as_of_commit_seq }
|
|
36
|
+
: {}),
|
|
37
|
+
...(response.history ? { history: response.history } : {}),
|
|
38
|
+
error: response.error ?? null,
|
|
39
|
+
trace_id: traceId,
|
|
40
|
+
attempts: response.attempts,
|
|
41
|
+
};
|
|
42
|
+
const notes = [];
|
|
43
|
+
if (traceId) {
|
|
44
|
+
notes.push(`eval trace ${traceId}: to label rows, read their item ids with lbb_evals action=trace trace_id=${traceId}, then call lbb_evals action=label trace_id=${traceId} item=<id> valid=true|false (or items=[…]).`);
|
|
45
|
+
}
|
|
46
|
+
if (response.history?.as_of_date) {
|
|
47
|
+
notes.push(`The question names the date ${response.history.as_of_date}. Find the commit of that date, then run the query with lbb_query mode=sparql as_of_commit_seq=<commit>.`);
|
|
48
|
+
}
|
|
49
|
+
if (response.error && query) {
|
|
50
|
+
notes.push("The last query failed (see error). Correct it and run it with lbb_query mode=sparql, or ask again with more context.");
|
|
51
|
+
}
|
|
52
|
+
const label = `lbb_query.question: route ${route.kind} (${route.by}, confidence ${route.confidence.toFixed(2)})`;
|
|
53
|
+
const data = result
|
|
54
|
+
? JSON.parse(result.results)
|
|
55
|
+
: undefined;
|
|
56
|
+
const rowPage = result ? rowPageFrom(result) : undefined;
|
|
57
|
+
const bindings = data?.results
|
|
58
|
+
?.bindings;
|
|
59
|
+
if (data && rowPage && query && Array.isArray(bindings)) {
|
|
60
|
+
// The same query continues with mode=sparql at the commit the run read.
|
|
61
|
+
const pin = query.as_of_commit_seq ?? result?.snapshot?.served_at_seq;
|
|
62
|
+
const cursorBase = {
|
|
63
|
+
v: 1,
|
|
64
|
+
mode: "sparql",
|
|
65
|
+
graph: args.graph,
|
|
66
|
+
detail,
|
|
67
|
+
row_limit: rowLimit,
|
|
68
|
+
query: query.sparql,
|
|
69
|
+
entailment: query.entailment,
|
|
70
|
+
...(typeof pin === "number" ? { as_of_commit_seq: pin } : {}),
|
|
71
|
+
};
|
|
72
|
+
return queryToolResult(queryEnvelope(label, { ...answer, ...data }, detail, rowPage, rowPageNext(cursorBase, rowPage), cursorBase, { textFormat, notes }), textFormat);
|
|
73
|
+
}
|
|
74
|
+
// No rows to page: the query only, an ASK answer, or no query at all. The
|
|
75
|
+
// query text is returned whole.
|
|
76
|
+
const summary = data
|
|
77
|
+
? `${label}: ran the query`
|
|
78
|
+
: query
|
|
79
|
+
? `${label}: ${response.error ? "the query failed" : "the query, not run"}`
|
|
80
|
+
: `${label}: no query`;
|
|
81
|
+
return queryToolResult({
|
|
82
|
+
summary,
|
|
83
|
+
data: { ...answer, ...(data ?? {}) },
|
|
84
|
+
...(rowPage ? { row_page: rowPage } : {}),
|
|
85
|
+
...(notes.length ? { notes } : {}),
|
|
86
|
+
}, textFormat);
|
|
87
|
+
}
|
package/dist/tool-contracts.js
CHANGED
|
@@ -2,6 +2,8 @@ import { z } from "zod";
|
|
|
2
2
|
export const DEFAULT_DETAIL = "compact";
|
|
3
3
|
export const HARD_OUTPUT_CHARS = 80_000;
|
|
4
4
|
export const MAX_QUERY_ROW_LIMIT = 5_000;
|
|
5
|
+
/** The most rows `POST /v1/query/rewrite` returns from a run. */
|
|
6
|
+
export const QUESTION_MAX_ROWS = 1_000;
|
|
5
7
|
export const READ_ONLY = { readOnlyHint: true };
|
|
6
8
|
export const IDEMPOTENT_WRITE = {
|
|
7
9
|
readOnlyHint: false,
|
|
@@ -43,6 +45,8 @@ export const graphScope = {
|
|
|
43
45
|
};
|
|
44
46
|
export const jsonObjectSchema = z.record(z.string(), z.unknown());
|
|
45
47
|
export const jsonObjectArraySchema = z.array(jsonObjectSchema);
|
|
48
|
+
/** A month, `yyyy-mm` (UTC), as the model activity and model checks read it. */
|
|
49
|
+
export const yearMonthSchema = z.string().regex(/^\d{4}-(0[1-9]|1[0-2])$/);
|
|
46
50
|
export const readScope = { detail: detailSchema, ...graphScope };
|
|
47
51
|
export const metadataPageSchema = {
|
|
48
52
|
page_size: z
|
|
@@ -373,10 +377,11 @@ export const inspectInputSchema = z.discriminatedUnion("action", [
|
|
|
373
377
|
.describe("Entity id (hex); alternative to entity_type+name"),
|
|
374
378
|
entity_type: z.string().optional(),
|
|
375
379
|
name: z.string().optional(),
|
|
380
|
+
// Kept for an actionable error when an older connector sends this field.
|
|
376
381
|
as_of: z
|
|
377
382
|
.string()
|
|
378
383
|
.optional()
|
|
379
|
-
.describe("
|
|
384
|
+
.describe("Unsupported for entity; use as_of_commit_seq for a retained commit snapshot."),
|
|
380
385
|
as_of_commit_seq: z
|
|
381
386
|
.number()
|
|
382
387
|
.int()
|
|
@@ -391,6 +396,9 @@ export const inspectInputSchema = z.discriminatedUnion("action", [
|
|
|
391
396
|
// agent write a valid query on the first attempt instead of round-tripping
|
|
392
397
|
// through the ontology to reverse-engineer term IRIs.
|
|
393
398
|
export const SPARQL_IRI_GUIDE = 'IRI scheme: relations are <https://littlebigbrain.com/r/NAME> (NAME lowercased, e.g. writes_to; reverse a relation with the ^ path operator, no stored inverse triple). Types are <https://littlebigbrain.com/class/NAME> (lowercased), matched as `?x a <…/class/NAME>` with explicit entailment=subclass, rdfs, or owl for inference (default none). Property fields are <https://littlebigbrain.com/p/NAME> (lowercased). The local name is ALWAYS lowercase — an uppercase one (e.g. <…/r/FOR_CLIENT>) is a different, non-existent IRI that silently matches nothing; this tool auto-lowercases the local name of /r/, /class/, and /p/ IRIs for you and adds a `notes` entry when it does, so a stray uppercase still resolves. (Structured mode\'s `predicate` is case-insensitive on its own.) Entities are content-addressed <https://littlebigbrain.com/e/HASH> — never build an entity IRI from a name; anchor a named entity by its label instead: `?e <http://www.w3.org/2000/01/rdf-schema#label> "Acme"`. Discover the exact relation and type names with lbb_inspect action=ontology. SELECT and ASK only (CONSTRUCT/DESCRIBE are rejected).';
|
|
399
|
+
// Search by meaning runs inside SPARQL text as a pattern, so an agent can
|
|
400
|
+
// filter and join the hits in one query instead of a search then a lookup.
|
|
401
|
+
export const SPARQL_SEARCH_GUIDE = 'Search by meaning inside the query (the graph needs an embedding; see lbb_embeddings action=list): PREFIX search: <https://littlebigbrain.com/search#> SELECT ?x ?score WHERE { ?x search:similarTo "card payments" ; search:score ?score . ?x <https://littlebigbrain.com/r/calls> ?y } ORDER BY DESC(?score) LIMIT 5. The other patterns filter and join the hits. One search:similarTo per query, with a variable subject, in the main group (not inside OPTIONAL, UNION, MINUS or a subquery). The object is text, an entity IRI (records like that one) or a vector literal. search:top N sets the hits (1 to 1000; default the query LIMIT, then 10) and search:embedding "name" searches one embedding. FILTER(?score > 0.8) keeps close hits. The result\'s search field reports the plan (nearest, filter_first or search_first), the hits and complete.';
|
|
394
402
|
export const queryInputSchema = z.discriminatedUnion("mode", [
|
|
395
403
|
z
|
|
396
404
|
.object({
|
|
@@ -433,7 +441,7 @@ export const queryInputSchema = z.discriminatedUnion("mode", [
|
|
|
433
441
|
query: z
|
|
434
442
|
.string()
|
|
435
443
|
.optional()
|
|
436
|
-
.describe(`SPARQL 1.1 query text (SELECT or ASK). Valid-time as_of is unsupported; use as_of_commit_seq for a retained commit snapshot. ${SPARQL_IRI_GUIDE} Example: SELECT ?service ?db WHERE { ?service <https://littlebigbrain.com/r/writes_to> ?db } LIMIT 10`),
|
|
444
|
+
.describe(`SPARQL 1.1 query text (SELECT or ASK). Valid-time as_of is unsupported; use as_of_commit_seq for a retained commit snapshot. ${SPARQL_IRI_GUIDE} Example: SELECT ?service ?db WHERE { ?service <https://littlebigbrain.com/r/writes_to> ?db } LIMIT 10. ${SPARQL_SEARCH_GUIDE}`),
|
|
437
445
|
// Kept for an actionable error when an older connector sends this field.
|
|
438
446
|
as_of: z
|
|
439
447
|
.string()
|
|
@@ -454,6 +462,42 @@ export const queryInputSchema = z.discriminatedUnion("mode", [
|
|
|
454
462
|
...readScope,
|
|
455
463
|
})
|
|
456
464
|
.strict(),
|
|
465
|
+
z
|
|
466
|
+
.object({
|
|
467
|
+
mode: z.literal("question"),
|
|
468
|
+
question: z
|
|
469
|
+
.string()
|
|
470
|
+
.min(1)
|
|
471
|
+
.describe("question: the question in plain words (required), 1 to 4,000 characters. The server writes the SPARQL query from a description of this graph, checks it, and runs it."),
|
|
472
|
+
context: z
|
|
473
|
+
.string()
|
|
474
|
+
.optional()
|
|
475
|
+
.describe("question: notes for the model, at most 8,000 characters: what the data means, units, names to prefer."),
|
|
476
|
+
route: z
|
|
477
|
+
.enum([
|
|
478
|
+
"lookup",
|
|
479
|
+
"aggregate",
|
|
480
|
+
"search",
|
|
481
|
+
"history",
|
|
482
|
+
"schema",
|
|
483
|
+
"unanswerable",
|
|
484
|
+
])
|
|
485
|
+
.optional()
|
|
486
|
+
.describe("question: the kind of query, when you know it. Omit it and the router model selects it."),
|
|
487
|
+
limit: z
|
|
488
|
+
.number()
|
|
489
|
+
.int()
|
|
490
|
+
.min(1)
|
|
491
|
+
.max(QUESTION_MAX_ROWS)
|
|
492
|
+
.optional()
|
|
493
|
+
.describe("question: rows the run returns, 1 to 1,000. Defaults by detail: compact=20, standard=100, full=1000."),
|
|
494
|
+
run: z
|
|
495
|
+
.boolean()
|
|
496
|
+
.optional()
|
|
497
|
+
.describe("question: run the query and return its rows. Defaults to true; false returns the query only."),
|
|
498
|
+
...readScope,
|
|
499
|
+
})
|
|
500
|
+
.strict(),
|
|
457
501
|
z
|
|
458
502
|
.object({
|
|
459
503
|
mode: z.literal("search"),
|
|
@@ -503,6 +547,10 @@ export const queryInputSchema = z.discriminatedUnion("mode", [
|
|
|
503
547
|
.boolean()
|
|
504
548
|
.optional()
|
|
505
549
|
.describe("Plan without running: the scope, the resolved filter, and the allowed count. No model call."),
|
|
550
|
+
rerank: z
|
|
551
|
+
.boolean()
|
|
552
|
+
.optional()
|
|
553
|
+
.describe("true: the managed rerank model (Jev) orders the best hits by how well each answers the text, and each hit gets its relevance (0 to 1); adds about 0.3 s. false: the similarity order. Omit it to follow the graph's search setting."),
|
|
506
554
|
request: z
|
|
507
555
|
.string()
|
|
508
556
|
.optional()
|
|
@@ -610,7 +658,7 @@ export const configureInputSchema = z.discriminatedUnion("action", [
|
|
|
610
658
|
change: z
|
|
611
659
|
.array(ontologyEvolveOpSchema)
|
|
612
660
|
.min(1)
|
|
613
|
-
.max(
|
|
661
|
+
.max(128)
|
|
614
662
|
.describe("The ontology operations a person applies by accepting, in order (same shapes as evolve_ontology ops)"),
|
|
615
663
|
rationale: z
|
|
616
664
|
.string()
|
package/dist/tool-runtime.js
CHANGED
|
@@ -324,6 +324,7 @@ export function queryEnvelope(label, value, detailArg, rowPage, next, repage, op
|
|
|
324
324
|
return {
|
|
325
325
|
...envelope(label, value, detailArg, next),
|
|
326
326
|
row_page: rowPage,
|
|
327
|
+
...(options.extra ?? {}),
|
|
327
328
|
...(options.notes?.length ? { notes: options.notes } : {}),
|
|
328
329
|
};
|
|
329
330
|
}
|
|
@@ -353,6 +354,7 @@ export function queryEnvelope(label, value, detailArg, rowPage, next, repage, op
|
|
|
353
354
|
next: hasMore
|
|
354
355
|
? continuationNext(repage, page.offset + count, repage.row_limit)
|
|
355
356
|
: undefined,
|
|
357
|
+
...(options.extra ?? {}),
|
|
356
358
|
...(options.notes?.length ? { notes: options.notes } : {}),
|
|
357
359
|
};
|
|
358
360
|
};
|
|
@@ -658,8 +660,8 @@ export async function guide(scopedClient) {
|
|
|
658
660
|
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.",
|
|
659
661
|
inspect: "Use lbb_inspect for ontology, schema metadata, the published conformance report, metadata, entities, and this guide. Use lbb_query with SPARQL property paths for exact path queries.",
|
|
660
662
|
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.",
|
|
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.",
|
|
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.",
|
|
663
|
+
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. To search and filter in one query, use SPARQL text: PREFIX search: <https://littlebigbrain.com/search#> SELECT ?x ?score WHERE { ?x search:similarTo \"words\" ; search:score ?score . ?x a <CLASS> } ORDER BY DESC(?score) LIMIT 10. Any other pattern on ?x filters the hits; the result's search field reports the plan and complete. (4) For a question in words, rerank=true lets the rerank model (Jev) put first the hits that answer it; each hit then carries relevance (0 to 1), and the search takes about 0.3 s more. Without rerank the graph's search setting decides.",
|
|
664
|
+
query: "Use lbb_query for questions in plain words (mode=question), structured SPARQL-subset bodies, SPARQL text, and canned analysis. SPARQL is the only query surface; mode=question also returns the SPARQL query the server wrote for the question. 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.",
|
|
663
665
|
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.",
|
|
664
666
|
configure: "Use lbb_configure dry_run=true to preview definitions, ordered evolution (including add_super_types), or SHACL publication. Definition extracts native metadata; use lbb_rdf import to store full RDF/OWL and lbb_rdf update for additive INSERT DATA; RDF deletion/replacement is unsupported. Follow every lbb_inspect ontology/schema next cursor; fields are complete and metadata changes invalidate the cursor. Publication is asynchronous: inspect action=publication, then verify asserted axioms AND expected answers with lbb_query entailment=owl. SHACL conformance alone does not prove ontology completeness. First identify the questions the model must answer, reusable vocabularies, dates, evidence, and distinctions such as hypothesis versus confirmed fact.",
|
|
665
667
|
},
|
package/dist/tools.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { metadataPage } from "./metadata-pages.js";
|
|
3
|
+
import { answerQuestion } from "./question.js";
|
|
3
4
|
import { registerRdfTool } from "./rdf-tool.js";
|
|
4
5
|
import { queryTiming } from "./query-observer.js";
|
|
5
|
-
import { DESTRUCTIVE, IDEMPOTENT_WRITE, MUTATING, READ_ONLY, configureInputSchema, configureWireSchema, detailSchema, graphScope, inspectInputSchema, inspectWireSchema, jsonObjectSchema, queryInputSchema, queryWireSchema, searchFeedbackSchema, } from "./tool-contracts.js";
|
|
6
|
+
import { DESTRUCTIVE, IDEMPOTENT_WRITE, MUTATING, READ_ONLY, configureInputSchema, configureWireSchema, detailSchema, graphScope, inspectInputSchema, inspectWireSchema, jsonObjectSchema, queryInputSchema, queryWireSchema, searchFeedbackSchema, yearMonthSchema, } from "./tool-contracts.js";
|
|
6
7
|
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";
|
|
7
8
|
export function registerLbbTools(server, client, options = {}) {
|
|
8
9
|
registerRdfTool(server, client);
|
|
@@ -46,6 +47,9 @@ export function registerLbbTools(server, client, options = {}) {
|
|
|
46
47
|
limit: args.limit ?? 50,
|
|
47
48
|
});
|
|
48
49
|
case "entity":
|
|
50
|
+
if (args.as_of !== undefined) {
|
|
51
|
+
throw new Error("entity valid-time as_of is not supported; use as_of_commit_seq for a retained commit snapshot");
|
|
52
|
+
}
|
|
49
53
|
return target.entityDetail({
|
|
50
54
|
...(args.entity_id
|
|
51
55
|
? { id: args.entity_id }
|
|
@@ -53,14 +57,13 @@ export function registerLbbTools(server, client, options = {}) {
|
|
|
53
57
|
type: requireString(args.entity_type, "entity_type"),
|
|
54
58
|
name: requireString(args.name, "name"),
|
|
55
59
|
}),
|
|
56
|
-
asOf: args.as_of,
|
|
57
60
|
asOfCommitSeq: args.as_of_commit_seq,
|
|
58
61
|
});
|
|
59
62
|
}
|
|
60
63
|
});
|
|
61
64
|
});
|
|
62
65
|
server.registerTool("lbb_query", {
|
|
63
|
-
description: "Analytical and expert reads. Modes: structured (SPARQL-subset JSON body), sparql (SPARQL text), search (instances by meaning over every searchable class; filter narrows by class and relationship; every hit checked against the graph), analyze. SPARQL is the query language; search finds what the words describe. To plan a search: lbb_embeddings action=list, then SPARQL for a class's relationships on a sample, then mode=search with explain=true to check the resolved filter before the real search (lbb_inspect action=guide has the queries). Relations are <https://littlebigbrain.com/r/NAME> and types <https://littlebigbrain.com/class/NAME> (both lowercased); entities are content-addressed, so anchor a named one by its rdfs:label rather than building its IRI. Structured and text queries pin one published watermark for the request.",
|
|
66
|
+
description: "Analytical and expert reads. Modes: question (a question in plain words), structured (SPARQL-subset JSON body), sparql (SPARQL text), search (instances by meaning over every searchable class; filter narrows by class and relationship; every hit checked against the graph), analyze. Use mode=question when you have a question in plain words and no SPARQL query: the server selects the kind of query (route), writes the SPARQL query from a description of the graph, checks it, runs it, and returns the route, the rationale, the query and its rows. Continue or correct that query with mode=sparql. Each question uses model tokens and counts toward a daily limit of the stack. SPARQL is the query language; search finds what the words describe. SPARQL text can also search by meaning inside the query with ?x <https://littlebigbrain.com/search#similarTo> \"words\", so the other patterns filter and join the hits in one query; the result's search field reports the plan. To plan a search: lbb_embeddings action=list, then SPARQL for a class's relationships on a sample, then mode=search with explain=true to check the resolved filter before the real search (lbb_inspect action=guide has the queries). Relations are <https://littlebigbrain.com/r/NAME> and types <https://littlebigbrain.com/class/NAME> (both lowercased); entities are content-addressed, so anchor a named one by its rdfs:label rather than building its IRI. Structured and text queries pin one published watermark for the request.",
|
|
64
67
|
inputSchema: queryWireSchema,
|
|
65
68
|
annotations: READ_ONLY,
|
|
66
69
|
}, (rawArgs) => {
|
|
@@ -95,9 +98,9 @@ export function registerLbbTools(server, client, options = {}) {
|
|
|
95
98
|
const pin = (requested) => !cursor && requested === undefined
|
|
96
99
|
? timing.measureAsync("query_pin_metadata", () => queryCommitPin(target, requested, cursor))
|
|
97
100
|
: queryCommitPin(target, requested, cursor);
|
|
98
|
-
const buildEnvelope = (value, rowPage, next, cursorBase, notes) => timing.measure("query_envelope", () => {
|
|
101
|
+
const buildEnvelope = (value, rowPage, next, cursorBase, notes, extra) => timing.measure("query_envelope", () => {
|
|
99
102
|
timing.counts({ received_rows: rowPage?.returned });
|
|
100
|
-
const result = queryEnvelope(`lbb_query.${args.mode}`, value, detail, rowPage, next, cursorBase, { textFormat: options.queryTextFormat, notes });
|
|
103
|
+
const result = queryEnvelope(`lbb_query.${args.mode}`, value, detail, rowPage, next, cursorBase, { textFormat: options.queryTextFormat, notes, extra });
|
|
101
104
|
if (options.timing) {
|
|
102
105
|
const nextCursor = result.next?.cursor;
|
|
103
106
|
timing.counts({
|
|
@@ -241,6 +244,12 @@ export function registerLbbTools(server, client, options = {}) {
|
|
|
241
244
|
if (response.trace_id) {
|
|
242
245
|
notes.push(`eval trace ${response.trace_id}: to label rows, read their item ids with lbb_evals action=trace trace_id=${response.trace_id}, then call lbb_evals action=label trace_id=${response.trace_id} item=<id> valid=true|false (or items=[…]).`);
|
|
243
246
|
}
|
|
247
|
+
// A query with a search:similarTo pattern reports how the search
|
|
248
|
+
// ran; keep that report whole next to the rows.
|
|
249
|
+
const search = response.search ?? undefined;
|
|
250
|
+
if (search && !search.complete) {
|
|
251
|
+
notes.push(`search bound ${search.hits} of the ${search.top} hits asked for (complete: false): fewer entities satisfy the rest of the query among the candidates the search may score. When at most 20,000 entities match the other patterns, the search scores every match.`);
|
|
252
|
+
}
|
|
244
253
|
const data = timing.measure("query_results_parse", () => JSON.parse(response.results));
|
|
245
254
|
const rowPage = rowPageFrom(response);
|
|
246
255
|
const cursorBase = {
|
|
@@ -256,7 +265,7 @@ export function registerLbbTools(server, client, options = {}) {
|
|
|
256
265
|
as_of_commit_seq: asOfCommitSeq,
|
|
257
266
|
};
|
|
258
267
|
const next = rowPageNext(cursorBase, rowPage);
|
|
259
|
-
const sparqlEnvelope = buildEnvelope(data, rowPage, next, cursorBase, notes);
|
|
268
|
+
const sparqlEnvelope = buildEnvelope(data, rowPage, next, cursorBase, notes, search ? { search } : undefined);
|
|
260
269
|
return render(sparqlEnvelope);
|
|
261
270
|
}
|
|
262
271
|
catch (error) {
|
|
@@ -264,6 +273,17 @@ export function registerLbbTools(server, client, options = {}) {
|
|
|
264
273
|
}
|
|
265
274
|
})();
|
|
266
275
|
}
|
|
276
|
+
if (args.mode === "question") {
|
|
277
|
+
const questionArgs = args;
|
|
278
|
+
return (async () => {
|
|
279
|
+
try {
|
|
280
|
+
return await answerQuestion(client, questionArgs, options.queryTextFormat);
|
|
281
|
+
}
|
|
282
|
+
catch (error) {
|
|
283
|
+
return errorResult(await enrichError(client, error));
|
|
284
|
+
}
|
|
285
|
+
})();
|
|
286
|
+
}
|
|
267
287
|
if (args.mode === "search") {
|
|
268
288
|
const searchArgs = args;
|
|
269
289
|
// The caller's detail shapes the hits like every other mode: compact
|
|
@@ -279,6 +299,7 @@ export function registerLbbTools(server, client, options = {}) {
|
|
|
279
299
|
request: searchArgs.request,
|
|
280
300
|
filter: searchArgs.filter,
|
|
281
301
|
explain: searchArgs.explain,
|
|
302
|
+
rerank: searchArgs.rerank,
|
|
282
303
|
});
|
|
283
304
|
});
|
|
284
305
|
}
|
|
@@ -295,17 +316,25 @@ export function registerLbbTools(server, client, options = {}) {
|
|
|
295
316
|
});
|
|
296
317
|
});
|
|
297
318
|
server.registerTool("lbb_models", {
|
|
298
|
-
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.",
|
|
319
|
+
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.",
|
|
299
320
|
inputSchema: {
|
|
300
|
-
action: z.enum([
|
|
321
|
+
action: z.enum([
|
|
322
|
+
"shadow_eval",
|
|
323
|
+
"suggest_dataset",
|
|
324
|
+
"extractor_dataset",
|
|
325
|
+
"activity",
|
|
326
|
+
]),
|
|
301
327
|
body: jsonObjectSchema.optional(),
|
|
302
328
|
limit: z.number().int().positive().optional(),
|
|
303
329
|
split_seq: z.number().int().nonnegative().optional(),
|
|
330
|
+
month: yearMonthSchema
|
|
331
|
+
.optional()
|
|
332
|
+
.describe("activity: the month, yyyy-mm (UTC). Defaults to the current month."),
|
|
304
333
|
detail: detailSchema,
|
|
305
334
|
...graphScope,
|
|
306
335
|
},
|
|
307
336
|
annotations: READ_ONLY,
|
|
308
|
-
}, ({ action, body, limit, split_seq, detail, graph }) => run(client, `lbb_models.${action}`, detail, () => {
|
|
337
|
+
}, ({ action, body, limit, split_seq, month, detail, graph }) => run(client, `lbb_models.${action}`, detail, () => {
|
|
309
338
|
const target = scoped(client, graph);
|
|
310
339
|
switch (action) {
|
|
311
340
|
case "shadow_eval":
|
|
@@ -316,6 +345,8 @@ export function registerLbbTools(server, client, options = {}) {
|
|
|
316
345
|
return target.suggestDataset({ limit, splitSeq: split_seq });
|
|
317
346
|
case "extractor_dataset":
|
|
318
347
|
return target.extractorDataset({ limit, splitSeq: split_seq });
|
|
348
|
+
case "activity":
|
|
349
|
+
return target.modelActivity({ month });
|
|
319
350
|
}
|
|
320
351
|
}));
|
|
321
352
|
const embeddingSetup = {
|
|
@@ -430,7 +461,7 @@ export function registerLbbTools(server, client, options = {}) {
|
|
|
430
461
|
return scoped(client, graph).embeddings.delete(name);
|
|
431
462
|
}));
|
|
432
463
|
server.registerTool("lbb_evals", {
|
|
433
|
-
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.",
|
|
464
|
+
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. Model checks: a judge model checks a sample of the model calls LBB makes for the graph (rerank, route, 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.",
|
|
434
465
|
inputSchema: {
|
|
435
466
|
action: z.enum([
|
|
436
467
|
"summary",
|
|
@@ -445,6 +476,9 @@ export function registerLbbTools(server, client, options = {}) {
|
|
|
445
476
|
"run",
|
|
446
477
|
"results",
|
|
447
478
|
"settings",
|
|
479
|
+
"checks_summary",
|
|
480
|
+
"checks",
|
|
481
|
+
"review_check",
|
|
448
482
|
]),
|
|
449
483
|
trace_id: z
|
|
450
484
|
.string()
|
|
@@ -472,7 +506,10 @@ export function registerLbbTools(server, client, options = {}) {
|
|
|
472
506
|
.string()
|
|
473
507
|
.optional()
|
|
474
508
|
.describe("label: who labels (an agent name)."),
|
|
475
|
-
note: z
|
|
509
|
+
note: z
|
|
510
|
+
.string()
|
|
511
|
+
.optional()
|
|
512
|
+
.describe("label / review_check: why. review_check: at most 2,000 characters."),
|
|
476
513
|
sparql: z
|
|
477
514
|
.string()
|
|
478
515
|
.optional()
|
|
@@ -509,11 +546,47 @@ export function registerLbbTools(server, client, options = {}) {
|
|
|
509
546
|
.enum(["strong", "eventual"])
|
|
510
547
|
.optional()
|
|
511
548
|
.describe("run / accept: strong reads the head, eventual the last published commit."),
|
|
549
|
+
month: yearMonthSchema
|
|
550
|
+
.optional()
|
|
551
|
+
.describe("checks_summary / checks: the month, yyyy-mm (UTC). Defaults to the current month."),
|
|
552
|
+
job: z
|
|
553
|
+
.enum(["rerank", "route", "rewrite", "fit", "propose", "label"])
|
|
554
|
+
.optional()
|
|
555
|
+
.describe("checks: only the checks of one job."),
|
|
556
|
+
verdict: z
|
|
557
|
+
.enum(["right", "partly", "wrong"])
|
|
558
|
+
.optional()
|
|
559
|
+
.describe("checks: only the checks whose ground truth has this verdict. review_check with agree=false: the right verdict."),
|
|
560
|
+
reviewed: z
|
|
561
|
+
.boolean()
|
|
562
|
+
.optional()
|
|
563
|
+
.describe("checks: true = only the checks a person reviewed, false = only the others."),
|
|
564
|
+
after: z
|
|
565
|
+
.string()
|
|
566
|
+
.optional()
|
|
567
|
+
.describe("checks: the next_after of the previous page."),
|
|
568
|
+
call_id: z
|
|
569
|
+
.string()
|
|
570
|
+
.optional()
|
|
571
|
+
.describe("review_check: the check's `call` id, from checks."),
|
|
572
|
+
agree: z
|
|
573
|
+
.boolean()
|
|
574
|
+
.optional()
|
|
575
|
+
.describe("review_check: true = the judge is right, false = correct it with verdict."),
|
|
576
|
+
score: z
|
|
577
|
+
.number()
|
|
578
|
+
.min(0)
|
|
579
|
+
.max(1)
|
|
580
|
+
.optional()
|
|
581
|
+
.describe("review_check with agree=false: the right score, 0 to 1. Omit for the judge's score of that verdict."),
|
|
582
|
+
reference: jsonObjectSchema
|
|
583
|
+
.optional()
|
|
584
|
+
.describe('review_check with agree=false: the right answer. A rerank or label check: {"grades": {"<hit id>": 0..3}}; a route or fit check: {"picks": {"<question id>": "<option>"}}.'),
|
|
512
585
|
detail: detailSchema,
|
|
513
586
|
...graphScope,
|
|
514
587
|
},
|
|
515
588
|
annotations: MUTATING,
|
|
516
|
-
}, ({ 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, () => {
|
|
589
|
+
}, ({ 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, () => {
|
|
517
590
|
const target = scoped(client, graph);
|
|
518
591
|
switch (action) {
|
|
519
592
|
case "summary":
|
|
@@ -567,6 +640,36 @@ export function registerLbbTools(server, client, options = {}) {
|
|
|
567
640
|
return target.evals.results({ limit });
|
|
568
641
|
case "settings":
|
|
569
642
|
return target.evals.settings();
|
|
643
|
+
case "checks_summary":
|
|
644
|
+
return target.checks.summary({ month });
|
|
645
|
+
case "checks":
|
|
646
|
+
return target.checks.list({
|
|
647
|
+
job,
|
|
648
|
+
month,
|
|
649
|
+
verdict,
|
|
650
|
+
reviewed,
|
|
651
|
+
after,
|
|
652
|
+
limit,
|
|
653
|
+
});
|
|
654
|
+
case "review_check":
|
|
655
|
+
if (!call_id)
|
|
656
|
+
throw new Error("review_check requires call_id");
|
|
657
|
+
if (agree === undefined)
|
|
658
|
+
throw new Error("review_check requires agree");
|
|
659
|
+
if (agree &&
|
|
660
|
+
(verdict !== undefined ||
|
|
661
|
+
score !== undefined ||
|
|
662
|
+
reference !== undefined))
|
|
663
|
+
throw new Error("review_check with agree=true takes no verdict, score or reference");
|
|
664
|
+
if (!agree && !verdict)
|
|
665
|
+
throw new Error("review_check with agree=false requires verdict");
|
|
666
|
+
return target.checks.review(call_id, {
|
|
667
|
+
agree,
|
|
668
|
+
...(verdict ? { verdict } : {}),
|
|
669
|
+
...(score !== undefined ? { score } : {}),
|
|
670
|
+
...(reference ? { reference } : {}),
|
|
671
|
+
...(note ? { note } : {}),
|
|
672
|
+
});
|
|
570
673
|
}
|
|
571
674
|
}));
|
|
572
675
|
server.registerTool("lbb_commit", {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@littlebigbrain/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.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.19.0",
|
|
56
56
|
"@modelcontextprotocol/sdk": "^1",
|
|
57
57
|
"zod": "^3"
|
|
58
58
|
},
|