@littlebigbrain/mcp 0.5.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +119 -126
- package/dist/http-server.d.ts +0 -1
- package/dist/http-server.js +0 -1
- package/dist/index.d.ts +1 -0
- package/dist/metadata-pages.d.ts +0 -2
- package/dist/metadata-pages.js +2 -12
- package/dist/query-observer.d.ts +33 -0
- package/dist/query-observer.js +95 -0
- package/dist/rdf-tool.js +2 -3
- package/dist/server.d.ts +2 -1
- package/dist/server.js +2 -2
- package/dist/stdio.js +1 -2
- package/dist/tool-contracts.js +67 -6
- package/dist/tool-runtime.js +54 -56
- package/dist/tools.d.ts +2 -1
- package/dist/tools.js +339 -131
- 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,147 +55,126 @@ 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` to change
|
|
68
|
+
the default graph 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
|
-
Query pages preserve complete RDF values and may contain fewer than `row_limit`
|
|
73
|
-
rows to fit the 80 KB UTF-8 output budget. Follow the returned `next` arguments
|
|
74
|
-
until absent; the cursor advances by rows actually delivered. A single row that
|
|
75
|
-
exceeds the budget fails explicitly: project fewer fields or use the direct
|
|
76
|
-
SPARQL HTTP endpoint for that row.
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
Both `lbb_query` SPARQL modes (`sparql` and `structured`) support retained commit reads
|
|
80
|
-
through `as_of_commit_seq`. When omitted, the connector pins the current head
|
|
81
|
-
commit and reuses it for cursor pages. Valid-time `as_of` is unsupported and is
|
|
82
|
-
rejected before an API call, including when carried in an old cursor. Start a
|
|
83
|
-
new query without that selector or choose a retained commit sequence.
|
|
84
|
-
|
|
85
|
-
## Create and evolve an ontology through MCP
|
|
86
|
-
|
|
87
|
-
Start with `lbb_inspect action=guide`; `action=graphs` helps select an existing
|
|
88
|
-
scope, and a missing graph returns bootstrap guidance. Decide what questions
|
|
89
|
-
the graph must answer before choosing classes and relations. Distinguish
|
|
90
|
-
source-backed facts from hypotheses, and preserve evidence and dates.
|
|
91
|
-
|
|
92
|
-
Native metadata and stored RDF axioms are separate:
|
|
93
|
-
|
|
94
|
-
- `lbb_configure action=define_ontology` accepts a friendly `spec`, including
|
|
95
|
-
class `super_types`. Unknown spec fields fail explicitly. `lbb_json` expects
|
|
96
|
-
an internal serialized ontology, not a friendly spec. Raw OWL supplied to
|
|
97
|
-
configure is reduced to native metadata; it is not stored as a full document.
|
|
98
|
-
- `lbb_rdf action=import` stores the complete Turtle, N-Triples, N-Quads, or TriG
|
|
99
|
-
document as queryable graph facts, including RDF lists, annotations, and OWL
|
|
100
|
-
axioms. Pass `source`; the published RDF tier supports only the default RDF
|
|
101
|
-
graph. Dataset formats must contain only default-graph quads. The first RDF
|
|
102
|
-
data write selects RDF-native storage, which refuses
|
|
103
|
-
later property-graph commits; choose the write workflow before bootstrap.
|
|
104
|
-
- `lbb_configure action=evolve_ontology` supports explicit native changes,
|
|
105
|
-
including `add_super_types`. `dry_run: true` previews define/evolve/publish;
|
|
106
|
-
the same flag previews `lbb_commit mode=facts` without writing.
|
|
107
|
-
- `lbb_rdf action=update` submits SPARQL Update unchanged; currently only
|
|
108
|
-
`INSERT DATA` is supported. DELETE, WHERE, and graph replacement are refused.
|
|
109
|
-
Re-importing is
|
|
110
|
-
additive and does not remove obsolete axioms. Content-based retry keys are
|
|
111
|
-
automatic; use a new explicit key for an intentional repeat after other edits.
|
|
112
|
-
|
|
113
|
-
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:
|
|
114
76
|
|
|
115
77
|
```json
|
|
116
78
|
{
|
|
117
|
-
"action": "
|
|
118
|
-
"
|
|
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"
|
|
119
84
|
}
|
|
120
85
|
```
|
|
121
86
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
After applying, inspect `action=publication`, then verify both asserted axioms
|
|
133
|
-
and expected inferred answers. `lbb_query mode=sparql` accepts explicit
|
|
134
|
-
`entailment: "none" | "subclass" | "rdfs" | "owl"` (default `none`),
|
|
135
|
-
`consistency: "eventual" | "strong"`, and `min_indexed_seq`. Cursors retain
|
|
136
|
-
these controls. OWL is the server's supported inference profile, not arbitrary
|
|
137
|
-
OWL DL. An upload acknowledgement is not proof of successful reasoning.
|
|
138
|
-
|
|
139
|
-
To read asserted axioms in the default graph, query with `entailment: "none"`
|
|
140
|
-
and follow all returned row cursors:
|
|
141
|
-
|
|
142
|
-
```sparql
|
|
143
|
-
SELECT ?s ?p ?o WHERE {
|
|
144
|
-
?s ?p ?o
|
|
145
|
-
} 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
|
+
}
|
|
146
96
|
```
|
|
147
97
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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_evals` | Label query results and check whether later queries return the expected answers. |
|
|
124
|
+
| `lbb_models` | Compare retrieval settings and read model training datasets. |
|
|
125
|
+
|
|
126
|
+
See [search by meaning](https://docs.littlebigbrain.com/guides/search-by-meaning/)
|
|
127
|
+
for embedding setup. To define constraints with the Shapes Constraint Language
|
|
128
|
+
(SHACL), see [query and validation](https://docs.littlebigbrain.com/guides/sparql-and-shacl/).
|
|
129
|
+
|
|
130
|
+
RDF imports and JSON facts use different write workflows. A graph first written
|
|
131
|
+
through `lbb_rdf` does not accept JSON fact writes through `lbb_commit`. Choose
|
|
132
|
+
the workflow when creating the graph. RDF updates
|
|
133
|
+
support additive `INSERT DATA`;
|
|
134
|
+
see the [RDF guide](https://docs.littlebigbrain.com/guides/load-rdf/) for format and
|
|
135
|
+
update limits.
|
|
136
|
+
|
|
137
|
+
## Pages and saved queries
|
|
138
|
+
|
|
139
|
+
When a result includes `next`, pass those arguments to the same tool to read the
|
|
140
|
+
next page. Continue until `next` is absent. `row_limit` sets a maximum; large
|
|
141
|
+
values can make a page shorter.
|
|
142
|
+
|
|
143
|
+
SPARQL queries keep the same commit across pages. Save the query and its returned
|
|
144
|
+
commit sequence to check an answer later, then use `as_of_commit_seq` to select
|
|
145
|
+
that version. See [history and replay](https://docs.littlebigbrain.com/guides/time-travel-audit/)
|
|
146
|
+
for retention and evidence handling.
|
|
156
147
|
|
|
157
148
|
## Embed the server
|
|
158
149
|
|
|
159
|
-
|
|
150
|
+
To serve the tools from your own Node.js process:
|
|
160
151
|
|
|
161
152
|
```ts
|
|
162
153
|
import { createMcpHttpServer } from "@littlebigbrain/mcp";
|
|
163
154
|
|
|
164
155
|
createMcpHttpServer({
|
|
165
|
-
baseUrl:
|
|
156
|
+
baseUrl: process.env.LBB_BASE_URL!,
|
|
166
157
|
mcpPath: "/mcp",
|
|
167
158
|
allowedHosts: ["127.0.0.1", "localhost", "::1"],
|
|
168
159
|
}).listen(8080, "127.0.0.1");
|
|
169
160
|
```
|
|
170
161
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
162
|
+
Clients connect to `http://127.0.0.1:8080/mcp` and send a stack API key in
|
|
163
|
+
`Authorization: Bearer <key>`. See the
|
|
164
|
+
[Node.js integration guide](https://docs.littlebigbrain.com/sdks/mcp/#embed-in-a-node-process)
|
|
165
|
+
for HTTP options and integration with an existing MCP server.
|
|
174
166
|
|
|
175
|
-
##
|
|
167
|
+
## Development
|
|
176
168
|
|
|
177
|
-
From
|
|
178
|
-
real-server MCP test (it creates and removes its own temporary data root):
|
|
169
|
+
From a clone of this repository:
|
|
179
170
|
|
|
180
171
|
```sh
|
|
181
|
-
|
|
182
|
-
npm run build
|
|
183
|
-
|
|
172
|
+
npm ci
|
|
173
|
+
npm run build
|
|
174
|
+
npm run typecheck
|
|
175
|
+
npm test
|
|
184
176
|
```
|
|
185
177
|
|
|
186
|
-
|
|
187
|
-
|
|
178
|
+
## License
|
|
179
|
+
|
|
180
|
+
[Apache-2.0](LICENSE).
|
package/dist/http-server.d.ts
CHANGED
package/dist/http-server.js
CHANGED
|
@@ -150,7 +150,6 @@ export function createMcpHttpServer(options) {
|
|
|
150
150
|
baseUrl,
|
|
151
151
|
apiKey,
|
|
152
152
|
graph: url.searchParams.get("graph") ?? undefined,
|
|
153
|
-
branch: url.searchParams.get("branch") ?? undefined,
|
|
154
153
|
};
|
|
155
154
|
const client = options.clientFactory
|
|
156
155
|
? options.clientFactory(clientOptions)
|
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";
|
package/dist/metadata-pages.d.ts
CHANGED
|
@@ -4,7 +4,6 @@ import { metadataPageSchema } from "./tool-contracts.js";
|
|
|
4
4
|
type Request = z.infer<z.ZodObject<typeof metadataPageSchema>> & {
|
|
5
5
|
action: "ontology" | "schema";
|
|
6
6
|
graph?: string;
|
|
7
|
-
branch?: string;
|
|
8
7
|
};
|
|
9
8
|
/** Stateless pagination of complete entries, including their full nested values.
|
|
10
9
|
* A digest rejects mixed-version reads instead of skipping/repeating definitions
|
|
@@ -24,7 +23,6 @@ export declare function metadataPage(client: LbbClient, args: Request): Promise<
|
|
|
24
23
|
next: {
|
|
25
24
|
action: "ontology" | "schema";
|
|
26
25
|
graph: string | undefined;
|
|
27
|
-
branch: string | undefined;
|
|
28
26
|
section: string | undefined;
|
|
29
27
|
page_size: number;
|
|
30
28
|
cursor: string;
|
package/dist/metadata-pages.js
CHANGED
|
@@ -7,7 +7,6 @@ const cursorSchema = z
|
|
|
7
7
|
v: z.literal(1),
|
|
8
8
|
action: z.enum(["ontology", "schema"]),
|
|
9
9
|
graph: z.string().optional(),
|
|
10
|
-
branch: z.string().optional(),
|
|
11
10
|
section: z.string().optional(),
|
|
12
11
|
page_size: z.number().int().min(1).max(500),
|
|
13
12
|
offset: z.number().int().nonnegative().safe(),
|
|
@@ -27,23 +26,16 @@ export async function metadataPage(client, args) {
|
|
|
27
26
|
catch {
|
|
28
27
|
throw new Error("invalid lbb_inspect cursor; restart without cursor");
|
|
29
28
|
}
|
|
30
|
-
for (const key of [
|
|
31
|
-
"action",
|
|
32
|
-
"graph",
|
|
33
|
-
"branch",
|
|
34
|
-
"section",
|
|
35
|
-
"page_size",
|
|
36
|
-
]) {
|
|
29
|
+
for (const key of ["action", "graph", "section", "page_size"]) {
|
|
37
30
|
if (args[key] !== undefined && args[key] !== cursor[key]) {
|
|
38
31
|
throw new Error(`cursor ${key} does not match the supplied ${key}`);
|
|
39
32
|
}
|
|
40
33
|
}
|
|
41
34
|
}
|
|
42
35
|
const graph = cursor?.graph ?? args.graph;
|
|
43
|
-
const branch = cursor?.branch ?? args.branch;
|
|
44
36
|
const section = cursor?.section ?? args.section;
|
|
45
37
|
const pageSize = cursor?.page_size ?? args.page_size ?? 50;
|
|
46
|
-
const target = client.withScope({ graph
|
|
38
|
+
const target = client.withScope({ graph });
|
|
47
39
|
const value = (args.action === "ontology"
|
|
48
40
|
? await target.ontologyView({ counts: true })
|
|
49
41
|
: await target.schema.view());
|
|
@@ -62,7 +54,6 @@ export async function metadataPage(client, args) {
|
|
|
62
54
|
v: 1,
|
|
63
55
|
action: args.action,
|
|
64
56
|
graph,
|
|
65
|
-
branch,
|
|
66
57
|
section,
|
|
67
58
|
page_size: pageSize,
|
|
68
59
|
fingerprint,
|
|
@@ -94,7 +85,6 @@ export async function metadataPage(client, args) {
|
|
|
94
85
|
? {
|
|
95
86
|
action: args.action,
|
|
96
87
|
graph,
|
|
97
|
-
branch,
|
|
98
88
|
section,
|
|
99
89
|
page_size: pageSize,
|
|
100
90
|
cursor: Buffer.from(JSON.stringify({
|
|
@@ -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/rdf-tool.js
CHANGED
|
@@ -49,13 +49,12 @@ export function registerRdfTool(server, client) {
|
|
|
49
49
|
return errorResult(parsed.error);
|
|
50
50
|
const args = parsed.data;
|
|
51
51
|
return run(client, `lbb_rdf.${args.action}`, args.detail, async () => {
|
|
52
|
-
const target = scoped(client, args.graph
|
|
52
|
+
const target = scoped(client, args.graph);
|
|
53
53
|
const { idempotency_key } = args;
|
|
54
54
|
const operation = { ...args };
|
|
55
55
|
delete operation.detail;
|
|
56
56
|
delete operation.idempotency_key;
|
|
57
|
-
const key = idempotency_key ??
|
|
58
|
-
contentHashKey({ graph: args.graph, branch: args.branch }, operation);
|
|
57
|
+
const key = idempotency_key ?? contentHashKey({ graph: args.graph }, operation);
|
|
59
58
|
if (args.action === "import") {
|
|
60
59
|
return target.importRdf(args.source, {
|
|
61
60
|
format: args.format ?? "turtle",
|
package/dist/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/stdio.js
CHANGED
|
@@ -12,7 +12,7 @@ const BASE_URL_HELP = "LBB_BASE_URL is required. Copy endpoint_url from your Lit
|
|
|
12
12
|
*
|
|
13
13
|
* LBB_BASE_URL (required for tool calls; copy endpoint_url from the stack Connect page)
|
|
14
14
|
* LBB_API_KEY stack API key (lbb_sk_live_…) or single-mode token
|
|
15
|
-
* LBB_GRAPH
|
|
15
|
+
* LBB_GRAPH (optional; server defaults to main)
|
|
16
16
|
*
|
|
17
17
|
* The base URL is required lazily, at the first tool invocation, not at
|
|
18
18
|
* process start: MCP hosts launch servers to complete the initialize
|
|
@@ -44,7 +44,6 @@ async function main() {
|
|
|
44
44
|
baseUrl,
|
|
45
45
|
apiKey: process.env.LBB_API_KEY,
|
|
46
46
|
graph: process.env.LBB_GRAPH,
|
|
47
|
-
branch: process.env.LBB_BRANCH,
|
|
48
47
|
})
|
|
49
48
|
: unconfiguredClient();
|
|
50
49
|
const server = buildLbbServer(client);
|