@littlebigbrain/mcp 0.5.0 → 0.5.2
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 +121 -119
- package/dist/index.d.ts +1 -0
- package/dist/query-observer.d.ts +33 -0
- package/dist/query-observer.js +95 -0
- package/dist/server.d.ts +2 -1
- package/dist/server.js +2 -2
- package/dist/tool-contracts.js +66 -0
- package/dist/tool-runtime.js +84 -93
- package/dist/tools.d.ts +2 -1
- package/dist/tools.js +321 -14
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,38 +1,52 @@
|
|
|
1
1
|
# @littlebigbrain/mcp
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[Model Context Protocol (MCP)](https://modelcontextprotocol.io) server for
|
|
4
|
+
[little big brain](https://littlebigbrain.com), a search platform for AI
|
|
5
|
+
applications such as chatbots, search tools, and agents.
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
Connect an MCP client to search your data, follow relationships, and read the
|
|
8
|
+
facts behind an answer. The tools also let you load data and define validation
|
|
9
|
+
rules.
|
|
6
10
|
|
|
7
|
-
|
|
11
|
+
[Documentation](https://docs.littlebigbrain.com/sdks/mcp/) ·
|
|
12
|
+
[Quickstart](https://docs.littlebigbrain.com/start/quickstart/) ·
|
|
13
|
+
[Issues](https://github.com/littlebigbrains/lbb-mcp/issues)
|
|
8
14
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
## Connect
|
|
16
|
+
|
|
17
|
+
Create a stack in the [console](https://cloud.littlebigbrain.com), then choose a
|
|
18
|
+
hosted connection or a local process.
|
|
19
|
+
|
|
20
|
+
### Hosted connection
|
|
21
|
+
|
|
22
|
+
For clients that support remote MCP with OAuth, use this URL and sign in with
|
|
23
|
+
your little big brain account. Replace `<stack-slug>` with your stack's slug:
|
|
24
|
+
|
|
25
|
+
```text
|
|
26
|
+
https://mcp.littlebigbrain.com/mcp/<stack-slug>
|
|
17
27
|
```
|
|
18
28
|
|
|
19
|
-
|
|
29
|
+
For clients that use an `mcpServers` JSON configuration:
|
|
20
30
|
|
|
21
31
|
```json
|
|
22
32
|
{
|
|
23
33
|
"mcpServers": {
|
|
24
34
|
"lbb": {
|
|
25
|
-
"
|
|
26
|
-
"url": "https://mcp.littlebigbrain.com",
|
|
27
|
-
"headers": { "X-LBB-Stack": "<stack-slug>" }
|
|
35
|
+
"url": "https://mcp.littlebigbrain.com/mcp/<stack-slug>"
|
|
28
36
|
}
|
|
29
37
|
}
|
|
30
38
|
}
|
|
31
39
|
```
|
|
32
40
|
|
|
33
|
-
|
|
41
|
+
This connection uses account sign-in and does not require a stack API key.
|
|
42
|
+
The [connection guide](https://docs.littlebigbrain.com/sdks/mcp/#hosted-streamable-http-oauth)
|
|
43
|
+
includes client-specific setup, including the origin URL and `X-LBB-Stack` header
|
|
44
|
+
for Codex.
|
|
45
|
+
|
|
46
|
+
### Local process
|
|
34
47
|
|
|
35
|
-
|
|
48
|
+
Requires Node.js 18+. Copy the complete endpoint and a stack API key from
|
|
49
|
+
**Connect** in the console, then add them to your client's MCP configuration:
|
|
36
50
|
|
|
37
51
|
```json
|
|
38
52
|
{
|
|
@@ -41,140 +55,128 @@ Run against any data-plane endpoint with a stack API key:
|
|
|
41
55
|
"command": "npx",
|
|
42
56
|
"args": ["-y", "@littlebigbrain/mcp"],
|
|
43
57
|
"env": {
|
|
44
|
-
"LBB_BASE_URL": "https
|
|
45
|
-
"LBB_API_KEY": "
|
|
58
|
+
"LBB_BASE_URL": "https://<your-complete-stack-host>",
|
|
59
|
+
"LBB_API_KEY": "<your-stack-api-key>"
|
|
46
60
|
}
|
|
47
61
|
}
|
|
48
62
|
}
|
|
49
63
|
}
|
|
50
64
|
```
|
|
51
65
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
missing.
|
|
66
|
+
The client starts the server and communicates through standard input and output
|
|
67
|
+
(stdio). Keep the key in your local configuration. Set `LBB_GRAPH` or
|
|
68
|
+
`LBB_BRANCH` to change the default scope from `main`.
|
|
56
69
|
|
|
57
|
-
##
|
|
70
|
+
## Run a first query
|
|
58
71
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
| `lbb_query` | SPARQL text, structured SPARQL bodies, and canned analysis |
|
|
64
|
-
| `lbb_commit` | facts, properties, and embeddings |
|
|
65
|
-
| `lbb_observe` | conversation episodes plus reviewed extraction |
|
|
66
|
-
| `lbb_branch` | isolation branches and validated merge |
|
|
67
|
-
| `lbb_models` | shadow evaluation and training datasets |
|
|
68
|
-
| `lbb_configure` | native ontology definition/evolution and SHACL preview/publication |
|
|
69
|
-
|
|
70
|
-
Read tools return compact structured envelopes by default — use `detail`, `row_limit`, and returned cursors to page without silently truncating. Write tools derive an idempotency key unless you provide one.
|
|
71
|
-
|
|
72
|
-
Both `lbb_query` SPARQL modes (`sparql` and `structured`) support retained commit reads
|
|
73
|
-
through `as_of_commit_seq`. When omitted, the connector pins the current head
|
|
74
|
-
commit and reuses it for cursor pages. Valid-time `as_of` is unsupported and is
|
|
75
|
-
rejected before an API call, including when carried in an old cursor. Start a
|
|
76
|
-
new query without that selector or choose a retained commit sequence.
|
|
77
|
-
|
|
78
|
-
## Create and evolve an ontology through MCP
|
|
79
|
-
|
|
80
|
-
Start with `lbb_inspect action=guide`; `action=graphs` helps select an existing
|
|
81
|
-
scope, and a missing graph returns bootstrap guidance. Decide what questions
|
|
82
|
-
the graph must answer before choosing classes and relations. Distinguish
|
|
83
|
-
source-backed facts from hypotheses, and preserve evidence and dates.
|
|
84
|
-
|
|
85
|
-
Native metadata and stored RDF axioms are separate:
|
|
86
|
-
|
|
87
|
-
- `lbb_configure action=define_ontology` accepts a friendly `spec`, including
|
|
88
|
-
class `super_types`. Unknown spec fields fail explicitly. `lbb_json` expects
|
|
89
|
-
an internal serialized ontology, not a friendly spec. Raw OWL supplied to
|
|
90
|
-
configure is reduced to native metadata; it is not stored as a full document.
|
|
91
|
-
- `lbb_rdf action=import` stores the complete Turtle, N-Triples, N-Quads, or TriG
|
|
92
|
-
document as queryable graph facts, including RDF lists, annotations, and OWL
|
|
93
|
-
axioms. Pass `source`; the published RDF tier supports only the default RDF
|
|
94
|
-
graph. Dataset formats must contain only default-graph quads. The first RDF
|
|
95
|
-
data write selects RDF-native storage, which refuses
|
|
96
|
-
later property-graph commits; choose the write workflow before bootstrap.
|
|
97
|
-
- `lbb_configure action=evolve_ontology` supports explicit native changes,
|
|
98
|
-
including `add_super_types`. `dry_run: true` previews define/evolve/publish;
|
|
99
|
-
the same flag previews `lbb_commit mode=facts` without writing.
|
|
100
|
-
- `lbb_rdf action=update` submits SPARQL Update unchanged; currently only
|
|
101
|
-
`INSERT DATA` is supported. DELETE, WHERE, and graph replacement are refused.
|
|
102
|
-
Re-importing is
|
|
103
|
-
additive and does not remove obsolete axioms. Content-based retry keys are
|
|
104
|
-
automatic; use a new explicit key for an intentional repeat after other edits.
|
|
105
|
-
|
|
106
|
-
For example, add a superclass without a browser or RDF conversion:
|
|
72
|
+
This example stores three facts in a graph named `quickstart`: a service writes
|
|
73
|
+
to a database, and each has a label. The graph is created on its first write.
|
|
74
|
+
|
|
75
|
+
Ask your client to call `lbb_rdf` with these arguments:
|
|
107
76
|
|
|
108
77
|
```json
|
|
109
78
|
{
|
|
110
|
-
"action": "
|
|
111
|
-
"
|
|
79
|
+
"action": "import",
|
|
80
|
+
"graph": "quickstart",
|
|
81
|
+
"format": "ntriples",
|
|
82
|
+
"source": "<https://example.org/auth-service> <https://example.org/writesTo> <https://example.org/user-db> .\n<https://example.org/auth-service> <http://www.w3.org/2000/01/rdf-schema#label> \"Auth Service\" .\n<https://example.org/user-db> <http://www.w3.org/2000/01/rdf-schema#label> \"User Database\" .",
|
|
83
|
+
"idempotency_key": "mcp-quickstart-v1"
|
|
112
84
|
}
|
|
113
85
|
```
|
|
114
86
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
After applying, inspect `action=publication`, then verify both asserted axioms
|
|
126
|
-
and expected inferred answers. `lbb_query mode=sparql` accepts explicit
|
|
127
|
-
`entailment: "none" | "subclass" | "rdfs" | "owl"` (default `none`),
|
|
128
|
-
`consistency: "eventual" | "strong"`, and `min_indexed_seq`. Cursors retain
|
|
129
|
-
these controls. OWL is the server's supported inference profile, not arbitrary
|
|
130
|
-
OWL DL. An upload acknowledgement is not proof of successful reasoning.
|
|
131
|
-
|
|
132
|
-
To read asserted axioms in the default graph, query with `entailment: "none"`
|
|
133
|
-
and follow all returned row cursors:
|
|
134
|
-
|
|
135
|
-
```sparql
|
|
136
|
-
SELECT ?s ?p ?o WHERE {
|
|
137
|
-
?s ?p ?o
|
|
138
|
-
} ORDER BY ?s ?p ?o
|
|
87
|
+
Now ask **which database does Auth Service write to?** The `lbb_query` call is:
|
|
88
|
+
|
|
89
|
+
```json
|
|
90
|
+
{
|
|
91
|
+
"mode": "sparql",
|
|
92
|
+
"graph": "quickstart",
|
|
93
|
+
"consistency": "strong",
|
|
94
|
+
"query": "SELECT ?database WHERE { <https://example.org/auth-service> <https://example.org/writesTo> ?db . ?db <http://www.w3.org/2000/01/rdf-schema#label> ?database } ORDER BY ?database LIMIT 10"
|
|
95
|
+
}
|
|
139
96
|
```
|
|
140
97
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
98
|
+
The result contains one row on a fresh graph:
|
|
99
|
+
|
|
100
|
+
| database |
|
|
101
|
+
| --- |
|
|
102
|
+
| User Database |
|
|
103
|
+
|
|
104
|
+
The query follows a stored relationship to its database label. Strong consistency
|
|
105
|
+
lets it read the import without waiting for a background index job.
|
|
106
|
+
|
|
107
|
+
For your own data, start with `lbb_inspect` using `action: "guide"` or
|
|
108
|
+
`action: "ontology"`. Imported Resource Description Framework (RDF) data keeps
|
|
109
|
+
its original identifiers; inspect it with SPARQL when choosing query predicates.
|
|
110
|
+
|
|
111
|
+
## Tools
|
|
112
|
+
|
|
113
|
+
| Tool | Purpose |
|
|
114
|
+
| --- | --- |
|
|
115
|
+
| `lbb_inspect` | Read the schema, graph status, entities, history, and evidence. |
|
|
116
|
+
| `lbb_query` | Run SPARQL queries, search by meaning, or request summary statistics. |
|
|
117
|
+
| `lbb_rdf` | Import RDF documents or add facts with SPARQL `INSERT DATA`. |
|
|
118
|
+
| `lbb_embeddings` | Inspect search setup and preview the text to embed. |
|
|
119
|
+
| `lbb_embeddings_manage` | Set up or refresh embeddings, or change the embedding model. |
|
|
120
|
+
| `lbb_embeddings_delete` | Delete an embedding and its stored vectors. |
|
|
121
|
+
| `lbb_commit` | Write or retract JSON facts, or record search feedback. |
|
|
122
|
+
| `lbb_configure` | Define record types and relationships, or publish validation rules. |
|
|
123
|
+
| `lbb_branch` | Create branches and merge reviewed changes. |
|
|
124
|
+
| `lbb_observe` | Store conversation episodes and reviewed facts extracted from them. |
|
|
125
|
+
| `lbb_evals` | Label query results and check whether later queries return the expected answers. |
|
|
126
|
+
| `lbb_models` | Compare retrieval settings and read model training datasets. |
|
|
127
|
+
|
|
128
|
+
See [search by meaning](https://docs.littlebigbrain.com/guides/search-by-meaning/)
|
|
129
|
+
for embedding setup. To define constraints with the Shapes Constraint Language
|
|
130
|
+
(SHACL), see [query and validation](https://docs.littlebigbrain.com/guides/sparql-and-shacl/).
|
|
131
|
+
|
|
132
|
+
RDF imports and JSON facts use different write workflows. A graph first written
|
|
133
|
+
through `lbb_rdf` does not accept JSON fact writes through `lbb_commit`, or branch
|
|
134
|
+
forks and merges. Choose the workflow when creating the graph. RDF updates
|
|
135
|
+
support additive `INSERT DATA`;
|
|
136
|
+
see the [RDF guide](https://docs.littlebigbrain.com/guides/load-rdf/) for format and
|
|
137
|
+
update limits.
|
|
138
|
+
|
|
139
|
+
## Pages and saved queries
|
|
140
|
+
|
|
141
|
+
When a result includes `next`, pass those arguments to the same tool to read the
|
|
142
|
+
next page. Continue until `next` is absent. `row_limit` sets a maximum; large
|
|
143
|
+
values can make a page shorter.
|
|
144
|
+
|
|
145
|
+
SPARQL queries keep the same commit across pages. Save the query and its returned
|
|
146
|
+
commit sequence to check an answer later, then use `as_of_commit_seq` to select
|
|
147
|
+
that version. See [history and replay](https://docs.littlebigbrain.com/guides/time-travel-audit/)
|
|
148
|
+
for retention and evidence handling.
|
|
149
149
|
|
|
150
150
|
## Embed the server
|
|
151
151
|
|
|
152
|
-
|
|
152
|
+
To serve the tools from your own Node.js process:
|
|
153
153
|
|
|
154
154
|
```ts
|
|
155
155
|
import { createMcpHttpServer } from "@littlebigbrain/mcp";
|
|
156
156
|
|
|
157
157
|
createMcpHttpServer({
|
|
158
|
-
baseUrl:
|
|
158
|
+
baseUrl: process.env.LBB_BASE_URL!,
|
|
159
159
|
mcpPath: "/mcp",
|
|
160
160
|
allowedHosts: ["127.0.0.1", "localhost", "::1"],
|
|
161
161
|
}).listen(8080, "127.0.0.1");
|
|
162
162
|
```
|
|
163
163
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
164
|
+
Clients connect to `http://127.0.0.1:8080/mcp` and send a stack API key in
|
|
165
|
+
`Authorization: Bearer <key>`. See the
|
|
166
|
+
[Node.js integration guide](https://docs.littlebigbrain.com/sdks/mcp/#embed-in-a-node-process)
|
|
167
|
+
for HTTP options and integration with an existing MCP server.
|
|
167
168
|
|
|
168
|
-
##
|
|
169
|
+
## Development
|
|
169
170
|
|
|
170
|
-
From
|
|
171
|
-
real-server MCP test (it creates and removes its own temporary data root):
|
|
171
|
+
From a clone of this repository:
|
|
172
172
|
|
|
173
173
|
```sh
|
|
174
|
-
|
|
175
|
-
npm run build
|
|
176
|
-
|
|
174
|
+
npm ci
|
|
175
|
+
npm run build
|
|
176
|
+
npm run typecheck
|
|
177
|
+
npm test
|
|
177
178
|
```
|
|
178
179
|
|
|
179
|
-
|
|
180
|
-
|
|
180
|
+
## License
|
|
181
|
+
|
|
182
|
+
[Apache-2.0](LICENSE).
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export { registerLbbTools } from "./tools.js";
|
|
2
2
|
export { buildLbbServer } from "./server.js";
|
|
3
|
+
export type { LbbServerOptions, McpQueryStage, McpQueryStageEvent, } from "./query-observer.js";
|
|
3
4
|
export { createMcpHttpServer, type McpHttpServerOptions, } from "./http-server.js";
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** Local opt-in query measurements. No event contains query or response data. */
|
|
2
|
+
export type McpQueryStage = "query_pin_metadata" | "query_http_total" | "query_results_parse" | "query_envelope" | "query_render";
|
|
3
|
+
export interface McpQueryStageEvent {
|
|
4
|
+
stage: McpQueryStage;
|
|
5
|
+
durationMs: number;
|
|
6
|
+
outcome: "success" | "error";
|
|
7
|
+
mode: "sparql" | "structured";
|
|
8
|
+
continuation: boolean;
|
|
9
|
+
row_limit: number;
|
|
10
|
+
offset: number;
|
|
11
|
+
received_rows?: number;
|
|
12
|
+
returned_rows?: number;
|
|
13
|
+
cursor_bytes?: number;
|
|
14
|
+
text_bytes?: number;
|
|
15
|
+
}
|
|
16
|
+
export interface LbbServerOptions {
|
|
17
|
+
/** Query text only; fitting and rendering always use this same format. */
|
|
18
|
+
queryTextFormat?: "compact" | "pretty";
|
|
19
|
+
timing?: {
|
|
20
|
+
/** Monotonic milliseconds; defaults to performance.now(). */
|
|
21
|
+
now?: () => number;
|
|
22
|
+
observe: (event: Readonly<McpQueryStageEvent>) => void;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
type QueryCounts = Pick<McpQueryStageEvent, "received_rows" | "returned_rows" | "cursor_bytes" | "text_bytes">;
|
|
26
|
+
type QueryIdentity = Pick<McpQueryStageEvent, "mode" | "continuation" | "row_limit" | "offset">;
|
|
27
|
+
/** Each tool call owns its small counter record; servers may serve concurrently. */
|
|
28
|
+
export declare function queryTiming(options: LbbServerOptions, identity: QueryIdentity): {
|
|
29
|
+
counts(update: QueryCounts): void;
|
|
30
|
+
measure<T>(stage: McpQueryStage, operation: () => T): T;
|
|
31
|
+
measureAsync<T>(stage: McpQueryStage, operation: () => Promise<T>): Promise<T>;
|
|
32
|
+
};
|
|
33
|
+
export {};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
function count(value) {
|
|
2
|
+
return Number.isFinite(value)
|
|
3
|
+
? Math.min(Number.MAX_SAFE_INTEGER, Math.max(0, Math.floor(value)))
|
|
4
|
+
: 0;
|
|
5
|
+
}
|
|
6
|
+
/** Each tool call owns its small counter record; servers may serve concurrently. */
|
|
7
|
+
export function queryTiming(options, identity) {
|
|
8
|
+
const timing = options.timing;
|
|
9
|
+
const counts = {};
|
|
10
|
+
const now = () => {
|
|
11
|
+
try {
|
|
12
|
+
const value = timing?.now?.() ?? performance.now();
|
|
13
|
+
return Number.isFinite(value) ? value : performance.now();
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return performance.now();
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
const emit = (stage, start, outcome) => {
|
|
20
|
+
if (!timing)
|
|
21
|
+
return;
|
|
22
|
+
const elapsed = now() - start;
|
|
23
|
+
const event = {
|
|
24
|
+
stage,
|
|
25
|
+
durationMs: Number.isFinite(elapsed)
|
|
26
|
+
? Math.min(Number.MAX_SAFE_INTEGER, Math.max(0, elapsed))
|
|
27
|
+
: 0,
|
|
28
|
+
outcome,
|
|
29
|
+
mode: identity.mode,
|
|
30
|
+
continuation: identity.continuation,
|
|
31
|
+
row_limit: count(identity.row_limit),
|
|
32
|
+
offset: count(identity.offset),
|
|
33
|
+
...counts,
|
|
34
|
+
};
|
|
35
|
+
try {
|
|
36
|
+
// Async callbacks also remain isolated even though observation is not
|
|
37
|
+
// awaited and the public callback contract does not require a Promise.
|
|
38
|
+
const pending = timing.observe(event);
|
|
39
|
+
if (pending &&
|
|
40
|
+
typeof pending.then === "function") {
|
|
41
|
+
void Promise.resolve(pending).catch(() => { });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// A local observer cannot turn a successful query into a failure.
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
return {
|
|
49
|
+
counts(update) {
|
|
50
|
+
if (!timing)
|
|
51
|
+
return;
|
|
52
|
+
// Explicit keys prevent caller-supplied objects from adding fields.
|
|
53
|
+
for (const key of [
|
|
54
|
+
"received_rows",
|
|
55
|
+
"returned_rows",
|
|
56
|
+
"cursor_bytes",
|
|
57
|
+
"text_bytes",
|
|
58
|
+
]) {
|
|
59
|
+
const value = update[key];
|
|
60
|
+
if (value !== undefined)
|
|
61
|
+
counts[key] = count(value);
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
measure(stage, operation) {
|
|
65
|
+
if (!timing)
|
|
66
|
+
return operation();
|
|
67
|
+
const start = now();
|
|
68
|
+
let outcome = "error";
|
|
69
|
+
try {
|
|
70
|
+
const result = operation();
|
|
71
|
+
outcome = "success";
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
emit(stage, start, outcome);
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
measureAsync(stage, operation) {
|
|
79
|
+
if (!timing)
|
|
80
|
+
return operation();
|
|
81
|
+
const start = now();
|
|
82
|
+
return (async () => {
|
|
83
|
+
let outcome = "error";
|
|
84
|
+
try {
|
|
85
|
+
const result = await operation();
|
|
86
|
+
outcome = "success";
|
|
87
|
+
return result;
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
emit(stage, start, outcome);
|
|
91
|
+
}
|
|
92
|
+
})();
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
}
|
package/dist/server.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import type { LbbClient } from "@littlebigbrain/client";
|
|
3
|
+
import type { LbbServerOptions } from "./query-observer.js";
|
|
3
4
|
/** Build an MCP server exposing the little big brain tool belt, bound to one client. */
|
|
4
|
-
export declare function buildLbbServer(client: LbbClient): McpServer;
|
|
5
|
+
export declare function buildLbbServer(client: LbbClient, options?: LbbServerOptions): McpServer;
|
package/dist/server.js
CHANGED
|
@@ -3,8 +3,8 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
3
3
|
import { registerLbbTools } from "./tools.js";
|
|
4
4
|
const { version } = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
5
5
|
/** Build an MCP server exposing the little big brain tool belt, bound to one client. */
|
|
6
|
-
export function buildLbbServer(client) {
|
|
6
|
+
export function buildLbbServer(client, options = {}) {
|
|
7
7
|
const server = new McpServer({ name: "lbb", version });
|
|
8
|
-
registerLbbTools(server, client);
|
|
8
|
+
registerLbbTools(server, client, options);
|
|
9
9
|
return server;
|
|
10
10
|
}
|
package/dist/tool-contracts.js
CHANGED
|
@@ -14,6 +14,12 @@ export const MUTATING = {
|
|
|
14
14
|
destructiveHint: false,
|
|
15
15
|
openWorldHint: false,
|
|
16
16
|
};
|
|
17
|
+
export const DESTRUCTIVE = {
|
|
18
|
+
readOnlyHint: false,
|
|
19
|
+
destructiveHint: true,
|
|
20
|
+
idempotentHint: true,
|
|
21
|
+
openWorldHint: false,
|
|
22
|
+
};
|
|
17
23
|
export const detailSchema = z
|
|
18
24
|
.enum(["compact", "standard", "full"])
|
|
19
25
|
.optional()
|
|
@@ -481,11 +487,71 @@ export const queryInputSchema = z.discriminatedUnion("mode", [
|
|
|
481
487
|
.nonnegative()
|
|
482
488
|
.optional()
|
|
483
489
|
.describe("Snapshot pin: run the query as of this commit_seq. Errors if past head."),
|
|
490
|
+
request: z
|
|
491
|
+
.string()
|
|
492
|
+
.optional()
|
|
493
|
+
.describe("The user's own words behind this query (managed evals). When present the server records an eval trace and the result carries its trace_id; label the rows valid or not with lbb_evals action=label. Omit on continuation pages."),
|
|
484
494
|
row_limit: rowLimitSchema,
|
|
485
495
|
cursor: cursorSchema,
|
|
486
496
|
...readScope,
|
|
487
497
|
})
|
|
488
498
|
.strict(),
|
|
499
|
+
z
|
|
500
|
+
.object({
|
|
501
|
+
mode: z.literal("search"),
|
|
502
|
+
embedding: z
|
|
503
|
+
.string()
|
|
504
|
+
.optional()
|
|
505
|
+
.describe("One embedding by name (see lbb_embeddings action=list). Omit it to search every searchable class of the graph; each hit names its class."),
|
|
506
|
+
text: z
|
|
507
|
+
.string()
|
|
508
|
+
.optional()
|
|
509
|
+
.describe("The query text (required); embedded with the graph's model."),
|
|
510
|
+
top_k: z.number().int().positive().max(200).optional(),
|
|
511
|
+
probe: z
|
|
512
|
+
.number()
|
|
513
|
+
.int()
|
|
514
|
+
.positive()
|
|
515
|
+
.optional()
|
|
516
|
+
.describe("Clusters to read (default 4·√clusters, at least 8). More reads cost latency and find more."),
|
|
517
|
+
include: z
|
|
518
|
+
.array(z.enum(["text"]))
|
|
519
|
+
.optional()
|
|
520
|
+
.describe("text: return the embedded text of each hit."),
|
|
521
|
+
filter: z
|
|
522
|
+
.array(z
|
|
523
|
+
.object({
|
|
524
|
+
class: z
|
|
525
|
+
.union([z.string(), z.array(z.string())])
|
|
526
|
+
.optional()
|
|
527
|
+
.describe("A class condition: one class IRI or a list (any of); subclasses too. One class condition per search."),
|
|
528
|
+
via: z
|
|
529
|
+
.string()
|
|
530
|
+
.optional()
|
|
531
|
+
.describe("A relationship condition: the relationship, a local name (calls), prefix:name, or <iri>."),
|
|
532
|
+
to: z
|
|
533
|
+
.union([z.string(), z.array(z.string())])
|
|
534
|
+
.optional()
|
|
535
|
+
.describe("With via: the entity, an IRI or a name (its label; a spelling slip resolves when one entity is clearly closest), or a list (any of)."),
|
|
536
|
+
direction: z
|
|
537
|
+
.enum(["out", "in"])
|
|
538
|
+
.optional()
|
|
539
|
+
.describe("With via: out (default), the hit links to the entity; in, the entity links to the hit."),
|
|
540
|
+
})
|
|
541
|
+
.strict())
|
|
542
|
+
.optional()
|
|
543
|
+
.describe('Conditions every hit must meet: {"class": …} or {"via": …, "to": …}. The response\'s filter shows how each condition resolved; an unknown relationship or name answers with the options.'),
|
|
544
|
+
explain: z
|
|
545
|
+
.boolean()
|
|
546
|
+
.optional()
|
|
547
|
+
.describe("Plan without running: the scope, the resolved filter, and the allowed count. No model call."),
|
|
548
|
+
request: z
|
|
549
|
+
.string()
|
|
550
|
+
.optional()
|
|
551
|
+
.describe("The user's own words (managed evals): records an eval trace; label it with lbb_evals action=label."),
|
|
552
|
+
...readScope,
|
|
553
|
+
})
|
|
554
|
+
.strict(),
|
|
489
555
|
z
|
|
490
556
|
.object({
|
|
491
557
|
mode: z.literal("analyze"),
|
package/dist/tool-runtime.js
CHANGED
|
@@ -311,104 +311,94 @@ export function envelope(label, value, detailArg, next) {
|
|
|
311
311
|
next: { ...(next ?? {}), detail: "full" },
|
|
312
312
|
};
|
|
313
313
|
}
|
|
314
|
-
export function queryEnvelope(label, value, detailArg, rowPage, next, repage) {
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
const
|
|
319
|
-
const
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
const rowText = rowPage
|
|
327
|
-
? rowPage.returned < rowPage.total
|
|
328
|
-
? `returned ${rowPage.returned} of ${rowPage.total} rows`
|
|
329
|
-
: `returned ${rowPage.returned} rows`
|
|
330
|
-
: undefined;
|
|
331
|
-
const serverFlags = serverTruncationFlags(value);
|
|
332
|
-
const serverTruncated = serverFlags.length > 0;
|
|
333
|
-
const serverText = serverTruncated
|
|
334
|
-
? ` [server-truncated: ${serverFlags.join(", ")}]`
|
|
335
|
-
: "";
|
|
336
|
-
let result = {
|
|
337
|
-
summary: rowText
|
|
338
|
-
? `${label}: ${rowText}${serverText}${state.truncated ? " [truncated output]" : ""}`
|
|
339
|
-
: defaultSummary(label, value, state.truncated),
|
|
340
|
-
data,
|
|
341
|
-
counts: countsFor(value),
|
|
342
|
-
row_page: rowPage,
|
|
343
|
-
truncated: state.truncated || partialRows || serverTruncated || undefined,
|
|
344
|
-
next: partialRows && next
|
|
345
|
-
? next
|
|
346
|
-
: state.truncated && nextDetail(detail)
|
|
347
|
-
? { detail: nextDetail(detail) }
|
|
348
|
-
: next,
|
|
349
|
-
};
|
|
350
|
-
let text = JSON.stringify(result, null, 2);
|
|
351
|
-
if (text.length <= HARD_OUTPUT_CHARS)
|
|
352
|
-
return result;
|
|
353
|
-
// The full result overflows one MCP tool result, so the displayed rows are
|
|
354
|
-
// capped to fit. `row_page`/`counts` still describe the *server* page, so
|
|
355
|
-
// reporting only those reads as "every row delivered" even when the display
|
|
356
|
-
// was cut — the recurring MCP false-positive. So: state shown-vs-returned
|
|
357
|
-
// explicitly (`rows_shown`), and give advice that matches reality —
|
|
358
|
-
// * server itself withheld rows (partialRows): page with the existing cursor;
|
|
359
|
-
// * server returned the complete set but it is too big: page the same set at
|
|
360
|
-
// a smaller row_limit via a fresh cursor (offered here as `next`) or narrow
|
|
361
|
-
// with HAVING.
|
|
362
|
-
// "page with the cursor" is never suggested unless a cursor is actually given.
|
|
363
|
-
const remedy = partialRows
|
|
364
|
-
? " page with the cursor for the remaining rows"
|
|
365
|
-
: repage
|
|
366
|
-
? " re-run with the returned cursor to page the full set at a smaller row_limit, or add a HAVING filter to narrow the groups"
|
|
367
|
-
: " re-run with a lower row_limit to page the full set, or add a HAVING filter to narrow the groups";
|
|
368
|
-
const capNote = (shown) => returned !== undefined && shown < returned
|
|
369
|
-
? ` [MCP showed ${shown} of ${returned} rows — over the ${HARD_OUTPUT_CHARS}-char output budget]`
|
|
370
|
-
: " [hard-capped for MCP output]";
|
|
371
|
-
for (const cap of [200, 100, 50, 25, 10, 5, 3]) {
|
|
372
|
-
const hardState = { truncated: true };
|
|
373
|
-
data = truncateValue(value, { maxItems: cap, maxString: 160 }, hardState);
|
|
374
|
-
const shown = returned !== undefined ? Math.min(cap, returned) : cap;
|
|
375
|
-
const hardNext = partialRows
|
|
376
|
-
? next
|
|
377
|
-
: repage
|
|
378
|
-
? continuationNext(repage, shown, Math.max(1, shown))
|
|
314
|
+
export function queryEnvelope(label, value, detailArg, rowPage, next, repage, options = {}) {
|
|
315
|
+
// A query page is data, not a preview: keep entire terms/values and page
|
|
316
|
+
// the rows themselves to fit the wire budget. The cursor must advance by
|
|
317
|
+
// what the caller actually received, even inside a partial server page.
|
|
318
|
+
const source = value;
|
|
319
|
+
const results = source?.results;
|
|
320
|
+
const key = Array.isArray(results?.bindings)
|
|
321
|
+
? "bindings"
|
|
322
|
+
: Array.isArray(source?.groups) && source.groups.length > 0
|
|
323
|
+
? "groups"
|
|
324
|
+
: Array.isArray(source?.solutions)
|
|
325
|
+
? "solutions"
|
|
379
326
|
: undefined;
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
data,
|
|
385
|
-
counts: countsFor(value),
|
|
327
|
+
const rows = key === "bindings" ? results?.bindings : key ? source[key] : undefined;
|
|
328
|
+
if (!rowPage || !repage || !Array.isArray(rows)) {
|
|
329
|
+
return {
|
|
330
|
+
...envelope(label, value, detailArg, next),
|
|
386
331
|
row_page: rowPage,
|
|
387
|
-
|
|
388
|
-
truncated: true,
|
|
389
|
-
next: hardNext,
|
|
332
|
+
...(options.notes?.length ? { notes: options.notes } : {}),
|
|
390
333
|
};
|
|
391
|
-
text = JSON.stringify(result, null, 2);
|
|
392
|
-
if (text.length <= HARD_OUTPUT_CHARS)
|
|
393
|
-
return result;
|
|
394
334
|
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
:
|
|
410
|
-
|
|
335
|
+
if (rows.length !== rowPage.returned) {
|
|
336
|
+
throw new Error("Query row count does not match its page; refusing a cursor that could skip rows.");
|
|
337
|
+
}
|
|
338
|
+
const flags = serverTruncationFlags(value);
|
|
339
|
+
const build = (count) => {
|
|
340
|
+
const hasMore = count < rows.length || rowPage.has_more;
|
|
341
|
+
const page = {
|
|
342
|
+
...rowPage,
|
|
343
|
+
returned: count,
|
|
344
|
+
has_more: hasMore,
|
|
345
|
+
next_offset: hasMore ? rowPage.offset + count : undefined,
|
|
346
|
+
};
|
|
347
|
+
const selected = rows.slice(0, count);
|
|
348
|
+
const data = key === "bindings"
|
|
349
|
+
? { ...source, results: { ...results, bindings: selected } }
|
|
350
|
+
: { ...source, [key]: selected, row_page: page };
|
|
351
|
+
return {
|
|
352
|
+
summary: `${label}: returned ${count}${count < page.total ? ` of ${page.total}` : ""} rows${flags.length ? ` [server-truncated: ${flags.join(", ")}]` : ""}${count < rows.length ? " [byte-bounded page; continue with the cursor]" : ""}`,
|
|
353
|
+
data,
|
|
354
|
+
counts: countsFor(data),
|
|
355
|
+
row_page: page,
|
|
356
|
+
rows_shown: count < rows.length ? count : undefined,
|
|
357
|
+
truncated: hasMore || flags.length > 0 || undefined,
|
|
358
|
+
next: hasMore
|
|
359
|
+
? continuationNext(repage, page.offset + count, repage.row_limit)
|
|
411
360
|
: undefined,
|
|
361
|
+
...(options.notes?.length ? { notes: options.notes } : {}),
|
|
362
|
+
};
|
|
363
|
+
};
|
|
364
|
+
// Retain the existing reserve, but include actual normalization notes in
|
|
365
|
+
// every fit so even a large note list cannot push the final text over cap.
|
|
366
|
+
// Count UTF-8 bytes, including multibyte literals, not JavaScript code units.
|
|
367
|
+
const fits = (page) => Buffer.byteLength(serializeQueryEnvelope(page, options.textFormat), "utf8") <=
|
|
368
|
+
HARD_OUTPUT_CHARS - 2048;
|
|
369
|
+
const full = build(rows.length);
|
|
370
|
+
if (fits(full))
|
|
371
|
+
return full;
|
|
372
|
+
let lo = 0;
|
|
373
|
+
let hi = rows.length;
|
|
374
|
+
while (lo < hi) {
|
|
375
|
+
const mid = Math.ceil((lo + hi) / 2);
|
|
376
|
+
if (fits(build(mid)))
|
|
377
|
+
lo = mid;
|
|
378
|
+
else
|
|
379
|
+
hi = mid - 1;
|
|
380
|
+
}
|
|
381
|
+
if (lo === 0) {
|
|
382
|
+
throw new Error(rows.length === 0
|
|
383
|
+
? "Query page metadata or normalization notes exceed the MCP output budget. Narrow the query or use the SPARQL HTTP API; no rows were skipped."
|
|
384
|
+
: "One query row exceeds the MCP output budget. Project fewer fields or use the SPARQL HTTP API for this value; no rows were skipped.");
|
|
385
|
+
}
|
|
386
|
+
return build(lo);
|
|
387
|
+
}
|
|
388
|
+
/** One formatter for query page fitting and the text returned to MCP clients. */
|
|
389
|
+
export function serializeQueryEnvelope(value, format = "compact") {
|
|
390
|
+
return format === "pretty"
|
|
391
|
+
? JSON.stringify(value, null, 2)
|
|
392
|
+
: JSON.stringify(value);
|
|
393
|
+
}
|
|
394
|
+
export function queryToolResult(value, format) {
|
|
395
|
+
const text = serializeQueryEnvelope(value, format);
|
|
396
|
+
if (Buffer.byteLength(text, "utf8") > HARD_OUTPUT_CHARS) {
|
|
397
|
+
throw new Error("Query response exceeds the MCP output budget. Narrow the query or use the SPARQL HTTP API; no rows were skipped.");
|
|
398
|
+
}
|
|
399
|
+
return {
|
|
400
|
+
content: [{ type: "text", text }],
|
|
401
|
+
structuredContent: value,
|
|
412
402
|
};
|
|
413
403
|
}
|
|
414
404
|
export function toolResult(value) {
|
|
@@ -695,6 +685,7 @@ export async function guide(scopedClient) {
|
|
|
695
685
|
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.",
|
|
696
686
|
inspect: "Use lbb_inspect for ontology, schema metadata, the published conformance report, metadata, state/history/why, and this guide. Use lbb_query with SPARQL property paths for exact path queries.",
|
|
697
687
|
ontology_decorations: "lbb_inspect action=ontology returns a decoration_status catalog: each ontology decoration is enforced (the engine acts on it — state_reducer, value_type, super_types, properties, supernode_policy; cardinality, which GET /v1/ontology/conformance audits as sh:maxCount; and inverse_name/symmetric, which SPARQL resolves as relation aliases — an inverse name is queryable directly (lowered to ^forward, no stored inverse triple) and a symmetric relation matches both directions), advisory (transitive, temporal_semantics, required), or reserved (stored but unwired — default_weight, resolvable, alias/embedding_fields). You can also always reverse any relation in SPARQL by flipping the triple pattern or using ^forward. Each relation_def also carries edge_count — the number of current edges of that relation in this branch's snapshot — so you can tell at a glance which declared relations are actually populated (edge_count 0 = declared but unused) without a separate summary call.",
|
|
688
|
+
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.",
|
|
698
689
|
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.",
|
|
699
690
|
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.",
|
|
700
691
|
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.",
|
package/dist/tools.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import { LbbClient } from "@littlebigbrain/client";
|
|
3
|
-
|
|
3
|
+
import { type LbbServerOptions } from "./query-observer.js";
|
|
4
|
+
export declare function registerLbbTools(server: McpServer, client: LbbClient, options?: LbbServerOptions): void;
|
package/dist/tools.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { metadataPage } from "./metadata-pages.js";
|
|
3
3
|
import { registerRdfTool } from "./rdf-tool.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
4
|
+
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 { 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
|
+
export function registerLbbTools(server, client, options = {}) {
|
|
7
8
|
registerRdfTool(server, client);
|
|
8
9
|
server.registerTool("lbb_inspect", {
|
|
9
10
|
description: "Read graph context and exact graph facts. Actions: guide, graphs, publication, ontology, ontology_conformance, schema, ontology_search, metadata, entity, state, history, transitions, why. graphs works before bootstrap; 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 metadata, scalar attributes, bounded Base-backed edge neighborhood, history, and observations. Use lbb_query with SPARQL property paths for precise path selection.",
|
|
@@ -94,7 +95,7 @@ export function registerLbbTools(server, client) {
|
|
|
94
95
|
});
|
|
95
96
|
});
|
|
96
97
|
server.registerTool("lbb_query", {
|
|
97
|
-
description: "Analytical and expert reads. Modes: structured (SPARQL-subset JSON body), sparql (SPARQL text), analyze. SPARQL is the
|
|
98
|
+
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.",
|
|
98
99
|
inputSchema: queryWireSchema,
|
|
99
100
|
annotations: READ_ONLY,
|
|
100
101
|
}, (rawArgs) => {
|
|
@@ -121,6 +122,37 @@ export function registerLbbTools(server, client) {
|
|
|
121
122
|
const branch = cursor?.branch ?? args.branch;
|
|
122
123
|
const offset = cursor?.offset ?? 0;
|
|
123
124
|
const target = scoped(client, graph, branch);
|
|
125
|
+
const timing = queryTiming(options, {
|
|
126
|
+
mode: args.mode,
|
|
127
|
+
continuation: cursor !== undefined,
|
|
128
|
+
row_limit: rowLimit,
|
|
129
|
+
offset,
|
|
130
|
+
});
|
|
131
|
+
const pin = (requested) => !cursor && requested === undefined
|
|
132
|
+
? timing.measureAsync("query_pin_metadata", () => queryCommitPin(target, requested, cursor))
|
|
133
|
+
: queryCommitPin(target, requested, cursor);
|
|
134
|
+
const buildEnvelope = (value, rowPage, next, cursorBase, notes) => timing.measure("query_envelope", () => {
|
|
135
|
+
timing.counts({ received_rows: rowPage?.returned });
|
|
136
|
+
const result = queryEnvelope(`lbb_query.${args.mode}`, value, detail, rowPage, next, cursorBase, { textFormat: options.queryTextFormat, notes });
|
|
137
|
+
if (options.timing) {
|
|
138
|
+
const nextCursor = result.next?.cursor;
|
|
139
|
+
timing.counts({
|
|
140
|
+
returned_rows: rowPageFrom(result)?.returned,
|
|
141
|
+
cursor_bytes: typeof nextCursor === "string"
|
|
142
|
+
? Buffer.byteLength(nextCursor, "utf8")
|
|
143
|
+
: 0,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
return result;
|
|
147
|
+
});
|
|
148
|
+
const render = (value) => timing.measure("query_render", () => {
|
|
149
|
+
const result = queryToolResult(value, options.queryTextFormat);
|
|
150
|
+
if (options.timing)
|
|
151
|
+
timing.counts({
|
|
152
|
+
text_bytes: Buffer.byteLength(result.content[0].text, "utf8"),
|
|
153
|
+
});
|
|
154
|
+
return result;
|
|
155
|
+
});
|
|
124
156
|
for (const key of [
|
|
125
157
|
"entailment",
|
|
126
158
|
"consistency",
|
|
@@ -174,7 +206,7 @@ export function registerLbbTools(server, client) {
|
|
|
174
206
|
args.as_of_commit_seq !== cursor.as_of_commit_seq) {
|
|
175
207
|
throw new Error("cursor as_of_commit_seq does not match the supplied as_of_commit_seq argument");
|
|
176
208
|
}
|
|
177
|
-
const asOfCommitSeq = await
|
|
209
|
+
const asOfCommitSeq = await pin(requestedCommitSeq);
|
|
178
210
|
const request = {
|
|
179
211
|
...body,
|
|
180
212
|
limit: rowLimit,
|
|
@@ -189,10 +221,10 @@ export function registerLbbTools(server, client) {
|
|
|
189
221
|
request.combinators.length > 0) {
|
|
190
222
|
throw new Error("`combinators` (UNION/OPTIONAL/MINUS/EXISTS) is no longer accepted by structured mode; the analytics route was removed. Express the same query as SPARQL text with mode=sparql.");
|
|
191
223
|
}
|
|
192
|
-
const response = await target.sparql(request, {
|
|
224
|
+
const response = await timing.measureAsync("query_http_total", () => target.sparql(request, {
|
|
193
225
|
consistency,
|
|
194
226
|
minIndexedSeq,
|
|
195
|
-
});
|
|
227
|
+
}));
|
|
196
228
|
const rowPage = rowPageFrom(response);
|
|
197
229
|
const cursorBase = {
|
|
198
230
|
v: 1,
|
|
@@ -207,7 +239,7 @@ export function registerLbbTools(server, client) {
|
|
|
207
239
|
as_of_commit_seq: asOfCommitSeq,
|
|
208
240
|
};
|
|
209
241
|
const next = rowPageNext(cursorBase, rowPage);
|
|
210
|
-
return
|
|
242
|
+
return render(buildEnvelope(response, rowPage, next, cursorBase));
|
|
211
243
|
}
|
|
212
244
|
// Canonicalize little big brain relation/class/property IRI local-name case up
|
|
213
245
|
// front, then use the normalized text everywhere (mismatch check,
|
|
@@ -229,16 +261,24 @@ export function registerLbbTools(server, client) {
|
|
|
229
261
|
args.as_of_commit_seq !== cursor.as_of_commit_seq) {
|
|
230
262
|
throw new Error("cursor as_of_commit_seq does not match the supplied as_of_commit_seq argument");
|
|
231
263
|
}
|
|
232
|
-
const asOfCommitSeq = await
|
|
264
|
+
const asOfCommitSeq = await pin(args.as_of_commit_seq);
|
|
233
265
|
const entailment = cursor?.entailment ?? args.entailment ?? "none";
|
|
234
|
-
const response = await target.sparqlText({
|
|
266
|
+
const response = await timing.measureAsync("query_http_total", () => target.sparqlText({
|
|
235
267
|
query,
|
|
236
268
|
entailment,
|
|
237
269
|
as_of_commit_seq: asOfCommitSeq ?? null,
|
|
238
270
|
limit: rowLimit,
|
|
239
271
|
offset,
|
|
240
|
-
|
|
241
|
-
|
|
272
|
+
// Managed evals: the first page records a trace; a
|
|
273
|
+
// continuation page re-reads the same rows and must not.
|
|
274
|
+
...(cursor === undefined && args.request
|
|
275
|
+
? { request: args.request }
|
|
276
|
+
: {}),
|
|
277
|
+
}, { consistency, minIndexedSeq }));
|
|
278
|
+
if (response.trace_id) {
|
|
279
|
+
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=[…]).`);
|
|
280
|
+
}
|
|
281
|
+
const data = timing.measure("query_results_parse", () => JSON.parse(response.results));
|
|
242
282
|
const rowPage = rowPageFrom(response);
|
|
243
283
|
const cursorBase = {
|
|
244
284
|
v: 1,
|
|
@@ -254,14 +294,30 @@ export function registerLbbTools(server, client) {
|
|
|
254
294
|
as_of_commit_seq: asOfCommitSeq,
|
|
255
295
|
};
|
|
256
296
|
const next = rowPageNext(cursorBase, rowPage);
|
|
257
|
-
const sparqlEnvelope =
|
|
258
|
-
return
|
|
297
|
+
const sparqlEnvelope = buildEnvelope(data, rowPage, next, cursorBase, notes);
|
|
298
|
+
return render(sparqlEnvelope);
|
|
259
299
|
}
|
|
260
300
|
catch (error) {
|
|
261
301
|
return errorResult(await enrichError(client, error));
|
|
262
302
|
}
|
|
263
303
|
})();
|
|
264
304
|
}
|
|
305
|
+
if (args.mode === "search") {
|
|
306
|
+
const searchArgs = args;
|
|
307
|
+
return run(client, "lbb_query.search", undefined, async () => {
|
|
308
|
+
const target = scoped(client, searchArgs.graph, searchArgs.branch);
|
|
309
|
+
return target.embeddings.search({
|
|
310
|
+
embedding: searchArgs.embedding,
|
|
311
|
+
text: searchArgs.text,
|
|
312
|
+
top_k: searchArgs.top_k,
|
|
313
|
+
probe: searchArgs.probe,
|
|
314
|
+
include: searchArgs.include,
|
|
315
|
+
request: searchArgs.request,
|
|
316
|
+
filter: searchArgs.filter,
|
|
317
|
+
explain: searchArgs.explain,
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
}
|
|
265
321
|
return run(client, `lbb_query.${args.mode}`, args.detail, async () => {
|
|
266
322
|
const target = scoped(client, args.graph, args.branch);
|
|
267
323
|
return analyze(target, {
|
|
@@ -311,6 +367,257 @@ export function registerLbbTools(server, client) {
|
|
|
311
367
|
return target.extractorDataset({ limit, splitSeq: split_seq });
|
|
312
368
|
}
|
|
313
369
|
}));
|
|
370
|
+
const embeddingSetup = {
|
|
371
|
+
class: z.string().optional().describe("preview/declare: the class IRI."),
|
|
372
|
+
from: z
|
|
373
|
+
.array(z.string())
|
|
374
|
+
.optional()
|
|
375
|
+
.describe('preview/declare: the fields, e.g. ["label", "description", "calls/label"]. Omit for the automatic choice; on an existing embedding, omit to keep its fields.'),
|
|
376
|
+
exclude: z
|
|
377
|
+
.array(z.string())
|
|
378
|
+
.optional()
|
|
379
|
+
.describe("preview/declare: fields to drop from the automatic choice."),
|
|
380
|
+
title: z
|
|
381
|
+
.string()
|
|
382
|
+
.optional()
|
|
383
|
+
.describe('preview/declare: the field that names each hit, e.g. "display_name". Omit for the ontology\'s name property (skos:prefLabel, a property declared rdfs:subPropertyOf rdfs:label, else rdfs:label); on an existing embedding, omit to keep its name. preview reports title_source and, when the labels read as keys, title_suggestion.'),
|
|
384
|
+
model: z
|
|
385
|
+
.string()
|
|
386
|
+
.optional()
|
|
387
|
+
.describe("model: the graph's new model. preview/declare: omit it; a graph has one model (the graph's, or the platform's default for the first embedding)."),
|
|
388
|
+
dim: z.number().int().positive().optional(),
|
|
389
|
+
};
|
|
390
|
+
const recipeOf = (args) => {
|
|
391
|
+
if (!args.class)
|
|
392
|
+
throw new Error("this action requires class");
|
|
393
|
+
return {
|
|
394
|
+
class: args.class,
|
|
395
|
+
...(args.name ? { name: args.name } : {}),
|
|
396
|
+
...(args.from ? { from: args.from } : {}),
|
|
397
|
+
...(args.exclude ? { exclude: args.exclude } : {}),
|
|
398
|
+
...(args.title ? { title: args.title } : {}),
|
|
399
|
+
...(args.model ? { model: args.model } : {}),
|
|
400
|
+
...(args.dim ? { dim: args.dim } : {}),
|
|
401
|
+
};
|
|
402
|
+
};
|
|
403
|
+
server.registerTool("lbb_embeddings", {
|
|
404
|
+
description: "Search setup, read only: embeddings declared on classes (an embedding on a class covers its subclasses; one model per graph). list shows each embedding's status (serving and building version, backfill progress, lag) and the graph's model; get shows one; preview shows every candidate fact of a class with its coverage and examples, and the exact text of sample instances, and stores nothing. Declare, refresh, or change the model with lbb_embeddings_manage; search with lbb_query mode=search. list is the first step of planning a search (lbb_inspect action=guide has the recipe).",
|
|
405
|
+
inputSchema: {
|
|
406
|
+
action: z.enum(["list", "get", "preview"]),
|
|
407
|
+
name: z.string().optional().describe("get: the embedding name."),
|
|
408
|
+
...embeddingSetup,
|
|
409
|
+
sample: z.number().int().positive().max(50).optional(),
|
|
410
|
+
iris: z
|
|
411
|
+
.array(z.string())
|
|
412
|
+
.optional()
|
|
413
|
+
.describe("preview: the instances to show instead of a sample."),
|
|
414
|
+
detail: detailSchema,
|
|
415
|
+
...graphScope,
|
|
416
|
+
},
|
|
417
|
+
annotations: READ_ONLY,
|
|
418
|
+
}, (args) => run(client, `lbb_embeddings.${args.action}`, args.detail, () => {
|
|
419
|
+
const target = scoped(client, args.graph, args.branch);
|
|
420
|
+
switch (args.action) {
|
|
421
|
+
case "list":
|
|
422
|
+
return target.embeddings.list();
|
|
423
|
+
case "get":
|
|
424
|
+
if (!args.name)
|
|
425
|
+
throw new Error("get requires name");
|
|
426
|
+
return target.embeddings.get(args.name);
|
|
427
|
+
case "preview":
|
|
428
|
+
return target.embeddings.preview({
|
|
429
|
+
...recipeOf(args),
|
|
430
|
+
...(args.sample ? { sample: args.sample } : {}),
|
|
431
|
+
...(args.iris ? { iris: args.iris } : {}),
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
}));
|
|
435
|
+
server.registerTool("lbb_embeddings_manage", {
|
|
436
|
+
description: "Declare or refresh an embedding, or change the graph's model. Run lbb_embeddings action=preview first and show the user the text it will embed. declare sets the class, the fields (one-hop property paths such as label, description, calls/label; omit for an automatic choice); a new embedding takes the graph's model, and another model is refused. On an existing embedding it changes only what you name, and a changed recipe builds a new version while the old one serves. model moves every embedding of the graph to `model`: each builds a new version, and the graph switches when all are ready (a search never mixes two models). refresh runs one step of the embed job now (it also runs by itself after each published commit).",
|
|
437
|
+
inputSchema: {
|
|
438
|
+
action: z.enum(["declare", "refresh", "model"]),
|
|
439
|
+
name: z
|
|
440
|
+
.string()
|
|
441
|
+
.optional()
|
|
442
|
+
.describe("The embedding name (refresh: required)."),
|
|
443
|
+
...embeddingSetup,
|
|
444
|
+
detail: detailSchema,
|
|
445
|
+
...graphScope,
|
|
446
|
+
},
|
|
447
|
+
annotations: MUTATING,
|
|
448
|
+
}, (args) => run(client, `lbb_embeddings_manage.${args.action}`, args.detail, () => {
|
|
449
|
+
const target = scoped(client, args.graph, args.branch);
|
|
450
|
+
switch (args.action) {
|
|
451
|
+
case "declare":
|
|
452
|
+
return target.embeddings.declare(recipeOf(args));
|
|
453
|
+
case "refresh":
|
|
454
|
+
if (!args.name)
|
|
455
|
+
throw new Error("refresh requires name");
|
|
456
|
+
return target.embeddings.refresh(args.name);
|
|
457
|
+
case "model":
|
|
458
|
+
if (!args.model)
|
|
459
|
+
throw new Error("model requires model");
|
|
460
|
+
return target.embeddings.setModel({
|
|
461
|
+
model: args.model,
|
|
462
|
+
...(args.dim ? { dim: args.dim } : {}),
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
}));
|
|
466
|
+
server.registerTool("lbb_embeddings_delete", {
|
|
467
|
+
description: "Delete an embedding: its name and its vectors (index-gc removes the stored runs). Its class stops being searchable until it is declared and built again. Ask the user first; confirm must repeat the name.",
|
|
468
|
+
inputSchema: {
|
|
469
|
+
name: z.string().describe("The embedding name."),
|
|
470
|
+
confirm: z.string().describe("The same name again, to confirm."),
|
|
471
|
+
detail: detailSchema,
|
|
472
|
+
...graphScope,
|
|
473
|
+
},
|
|
474
|
+
annotations: DESTRUCTIVE,
|
|
475
|
+
}, ({ name, confirm, detail, graph, branch }) => run(client, "lbb_embeddings_delete", detail, () => {
|
|
476
|
+
if (confirm !== name) {
|
|
477
|
+
throw new Error("confirm must repeat the embedding name");
|
|
478
|
+
}
|
|
479
|
+
return scoped(client, graph, branch).embeddings.delete(name);
|
|
480
|
+
}));
|
|
481
|
+
server.registerTool("lbb_evals", {
|
|
482
|
+
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.",
|
|
483
|
+
inputSchema: {
|
|
484
|
+
action: z.enum([
|
|
485
|
+
"summary",
|
|
486
|
+
"traces",
|
|
487
|
+
"trace",
|
|
488
|
+
"label",
|
|
489
|
+
"judge",
|
|
490
|
+
"goldens",
|
|
491
|
+
"golden",
|
|
492
|
+
"accept",
|
|
493
|
+
"delete",
|
|
494
|
+
"run",
|
|
495
|
+
"results",
|
|
496
|
+
"settings",
|
|
497
|
+
]),
|
|
498
|
+
trace_id: z
|
|
499
|
+
.string()
|
|
500
|
+
.optional()
|
|
501
|
+
.describe("trace / label / judge: the trace id from lbb_query."),
|
|
502
|
+
item: z
|
|
503
|
+
.string()
|
|
504
|
+
.optional()
|
|
505
|
+
.describe("label: the result id (a hit's `id`, or an item id from trace) to label with `valid`."),
|
|
506
|
+
valid: z
|
|
507
|
+
.boolean()
|
|
508
|
+
.optional()
|
|
509
|
+
.describe("label: true = the result answers the request (thumbs up), false = it does not."),
|
|
510
|
+
items: z
|
|
511
|
+
.array(z
|
|
512
|
+
.object({
|
|
513
|
+
id: z.string(),
|
|
514
|
+
valid: z.boolean(),
|
|
515
|
+
note: z.string().optional(),
|
|
516
|
+
})
|
|
517
|
+
.strict())
|
|
518
|
+
.optional()
|
|
519
|
+
.describe("label: several results at once."),
|
|
520
|
+
by: z
|
|
521
|
+
.string()
|
|
522
|
+
.optional()
|
|
523
|
+
.describe("label: who labels (an agent name)."),
|
|
524
|
+
note: z.string().optional(),
|
|
525
|
+
sparql: z
|
|
526
|
+
.string()
|
|
527
|
+
.optional()
|
|
528
|
+
.describe("golden: the query to freeze (the search text for surface=search)."),
|
|
529
|
+
surface: z
|
|
530
|
+
.enum(["sparql", "search"])
|
|
531
|
+
.optional()
|
|
532
|
+
.describe("golden: which surface the query runs on (default sparql)."),
|
|
533
|
+
embedding: z
|
|
534
|
+
.string()
|
|
535
|
+
.optional()
|
|
536
|
+
.describe("golden: the embedding, for surface=search."),
|
|
537
|
+
top_k: z
|
|
538
|
+
.number()
|
|
539
|
+
.int()
|
|
540
|
+
.positive()
|
|
541
|
+
.max(200)
|
|
542
|
+
.optional()
|
|
543
|
+
.describe("golden: the top_k of a search golden."),
|
|
544
|
+
request: z
|
|
545
|
+
.string()
|
|
546
|
+
.optional()
|
|
547
|
+
.describe("golden: the user's words the query answers."),
|
|
548
|
+
golden_id: z
|
|
549
|
+
.string()
|
|
550
|
+
.optional()
|
|
551
|
+
.describe("accept / delete: the golden id."),
|
|
552
|
+
limit: z.number().int().positive().optional(),
|
|
553
|
+
unlabeled: z
|
|
554
|
+
.boolean()
|
|
555
|
+
.optional()
|
|
556
|
+
.describe("traces: only traces without a label."),
|
|
557
|
+
consistency: z
|
|
558
|
+
.enum(["strong", "eventual"])
|
|
559
|
+
.optional()
|
|
560
|
+
.describe("run / accept: strong reads the head, eventual the last published commit."),
|
|
561
|
+
detail: detailSchema,
|
|
562
|
+
...graphScope,
|
|
563
|
+
},
|
|
564
|
+
annotations: MUTATING,
|
|
565
|
+
}, ({ action, trace_id, item, valid, items, by, note, sparql, surface, embedding, top_k, request, golden_id, limit, unlabeled, consistency, detail, graph, branch, }) => run(client, `lbb_evals.${action}`, detail, () => {
|
|
566
|
+
const target = scoped(client, graph, branch);
|
|
567
|
+
switch (action) {
|
|
568
|
+
case "summary":
|
|
569
|
+
return target.evals.summary();
|
|
570
|
+
case "traces":
|
|
571
|
+
return target.evals.traces({ limit, unlabeled });
|
|
572
|
+
case "trace":
|
|
573
|
+
if (!trace_id)
|
|
574
|
+
throw new Error("trace requires trace_id");
|
|
575
|
+
return target.evals.trace(trace_id);
|
|
576
|
+
case "label":
|
|
577
|
+
if (!trace_id)
|
|
578
|
+
throw new Error("label requires trace_id");
|
|
579
|
+
if (!item && !items?.length)
|
|
580
|
+
throw new Error("label requires item (with valid) or items");
|
|
581
|
+
if (item && valid === undefined)
|
|
582
|
+
throw new Error("label requires valid with item");
|
|
583
|
+
return target.evals.label(trace_id, {
|
|
584
|
+
...(item ? { item, valid } : {}),
|
|
585
|
+
...(items?.length ? { items } : {}),
|
|
586
|
+
by,
|
|
587
|
+
note,
|
|
588
|
+
});
|
|
589
|
+
case "judge":
|
|
590
|
+
return target.evals.judge({ traceId: trace_id, limit });
|
|
591
|
+
case "goldens":
|
|
592
|
+
return target.evals.goldens();
|
|
593
|
+
case "golden":
|
|
594
|
+
if (!sparql)
|
|
595
|
+
throw new Error("golden requires sparql");
|
|
596
|
+
if (surface === "search" && !embedding)
|
|
597
|
+
throw new Error("a search golden requires embedding");
|
|
598
|
+
return target.evals.createGolden({
|
|
599
|
+
sparql,
|
|
600
|
+
request,
|
|
601
|
+
...(surface ? { surface } : {}),
|
|
602
|
+
...(embedding ? { embedding } : {}),
|
|
603
|
+
...(top_k ? { top_k } : {}),
|
|
604
|
+
});
|
|
605
|
+
case "accept":
|
|
606
|
+
if (!golden_id)
|
|
607
|
+
throw new Error("accept requires golden_id");
|
|
608
|
+
return target.evals.acceptGolden(golden_id, { consistency });
|
|
609
|
+
case "delete":
|
|
610
|
+
if (!golden_id)
|
|
611
|
+
throw new Error("delete requires golden_id");
|
|
612
|
+
return target.evals.deleteGolden(golden_id);
|
|
613
|
+
case "run":
|
|
614
|
+
return target.evals.run({ consistency });
|
|
615
|
+
case "results":
|
|
616
|
+
return target.evals.results({ limit });
|
|
617
|
+
case "settings":
|
|
618
|
+
return target.evals.settings();
|
|
619
|
+
}
|
|
620
|
+
}));
|
|
314
621
|
server.registerTool("lbb_commit", {
|
|
315
622
|
description: "Write graph facts, retract them, or label ranked results. mode=facts writes triplets/embeddings/properties; mode=retract removes a wrongly-added fact (by edge or by entity) without a full reset; mode=search_feedback stores query/result relevance labels (Feedback grades: 3=ideal/good, 1=partial, 0=bad; include query, search_id when available, target, rank, score). Explicit idempotency_key wins; when omitted, MCP derives a stable content hash so content-identical retries dedupe. Facts mode defaults edge_idempotency to append; pass skip_unchanged for re-runnable backfills.",
|
|
316
623
|
inputSchema: {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@littlebigbrain/mcp",
|
|
3
|
-
"version": "0.5.
|
|
4
|
-
"description": "MCP server for little big brain
|
|
3
|
+
"version": "0.5.2",
|
|
4
|
+
"description": "MCP server for little big brain: search, graph queries, and data import",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"test:coverage": "npm run clean:test && tsc -p tsconfig.test.json && node --test --experimental-test-coverage --test-coverage-include=test-dist/*.js --test-coverage-exclude=test-dist/*.test.js --test-coverage-exclude=test-dist/stdio.js --test-coverage-exclude=test-dist/test-support.js --test-coverage-lines=90 --test-coverage-branches=70 --test-coverage-functions=85 \"test-dist/**/*.test.js\""
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
|
-
"@littlebigbrain/client": "^0.13.
|
|
55
|
+
"@littlebigbrain/client": "^0.13.2",
|
|
56
56
|
"@modelcontextprotocol/sdk": "^1",
|
|
57
57
|
"zod": "^3"
|
|
58
58
|
},
|