@littlebigbrain/mcp 0.4.3 → 0.4.4
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 +6 -0
- package/dist/tool-contracts.js +8 -4
- package/dist/tools.js +20 -28
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -68,6 +68,12 @@ missing.
|
|
|
68
68
|
|
|
69
69
|
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.
|
|
70
70
|
|
|
71
|
+
Both `lbb_query` SPARQL modes (`sparql` and `structured`) support retained commit reads
|
|
72
|
+
through `as_of_commit_seq`. When omitted, the connector pins the current head
|
|
73
|
+
commit and reuses it for cursor pages. Valid-time `as_of` is unsupported and is
|
|
74
|
+
rejected before an API call, including when carried in an old cursor. Start a
|
|
75
|
+
new query without that selector or choose a retained commit sequence.
|
|
76
|
+
|
|
71
77
|
## Embed the server
|
|
72
78
|
|
|
73
79
|
For self-hosting behind your own auth, the package also serves the tools over HTTP:
|
package/dist/tool-contracts.js
CHANGED
|
@@ -390,11 +390,11 @@ export const queryInputSchema = z.discriminatedUnion("mode", [
|
|
|
390
390
|
'{ "value": <typed> } — and <typed> is exactly one wrapper: { "str": "…" }, { "i64": 5 }, { "f64": 0.9 }, { "bool": true }, { "date_time": "2026-01-01" } (RFC3339), or { "entity": { "entity_type": "T", "name": "N" } }. ' +
|
|
391
391
|
'Complete runnable example — deals whose amount ≥ 1000000: { "patterns": [{ "subject": { "var": "d" }, "predicate": "for_client", "object": { "var": "c" } }], "filters": [{ "compare": { "op": "ge", "left": { "property": { "var": "d", "field": "amount" } }, "right": { "value": { "f64": 1000000 } } } }] }. ' +
|
|
392
392
|
"Comparisons use the field's real declared type (numbers as numbers, datetimes as instants), so they run server-side. " +
|
|
393
|
-
'GROUP BY supports both entity-identity keys (group_by: ["s"]) and typed scalar keys via group_keys: a property value ({ property: { var, field, as } }) or a calendar bucket of a datetime property ({ date_bucket: { var, field, granularity: year|month|week|day|hour, as } }). Scalar keys come back per group under value_keys[as] — so a per-area breakdown or a commits-per-month time series is one server-side query, no client-side bucketing. Worked example -- commits per area per month in one query: { "patterns": [{ "subject": { "var": "c" }, "predicate": "committed_to", "object": { "var": "repo" } }], "group_keys": [{ "date_bucket": { "var": "c", "field": "committed_at", "granularity": "month", "as": "m" } }, { "property": { "var": "c", "field": "area", "as": "area" } }], "aggregates": [{ "func": "count", "as": "n" }], "order_by": [{ "var": "m" }] } -- area and committed_at are typed entity attributes (set via entity_properties; readable flat under attributes, never a nested metadata blob), and each group returns value_keys.m + value_keys.area + aggregates.n. `having: [...]` takes the same filter shape over the aggregated groups (e.g. { "compare": { "op": "gt", "left": { "var": "n" }, "right": { "value": { "i64": 10 } } } }). A `combinators` key (UNION/OPTIONAL/MINUS/EXISTS) is rejected here; express those with SPARQL text under mode=sparql. Cheap aggregate count: pair an equality having (e.g. { "compare": { "op": "eq", "left": { "var": "n" }, "right": { "value": { "i64": 4 } } } }) with row_limit: 1 -- the response row_page.total reports how many groups match without materializing them all, so you read the count off row_page.total instead of paging every matching row. For snapshot pinning
|
|
393
|
+
'GROUP BY supports both entity-identity keys (group_by: ["s"]) and typed scalar keys via group_keys: a property value ({ property: { var, field, as } }) or a calendar bucket of a datetime property ({ date_bucket: { var, field, granularity: year|month|week|day|hour, as } }). Scalar keys come back per group under value_keys[as] — so a per-area breakdown or a commits-per-month time series is one server-side query, no client-side bucketing. Worked example -- commits per area per month in one query: { "patterns": [{ "subject": { "var": "c" }, "predicate": "committed_to", "object": { "var": "repo" } }], "group_keys": [{ "date_bucket": { "var": "c", "field": "committed_at", "granularity": "month", "as": "m" } }, { "property": { "var": "c", "field": "area", "as": "area" } }], "aggregates": [{ "func": "count", "as": "n" }], "order_by": [{ "var": "m" }] } -- area and committed_at are typed entity attributes (set via entity_properties; readable flat under attributes, never a nested metadata blob), and each group returns value_keys.m + value_keys.area + aggregates.n. `having: [...]` takes the same filter shape over the aggregated groups (e.g. { "compare": { "op": "gt", "left": { "var": "n" }, "right": { "value": { "i64": 10 } } } }). A `combinators` key (UNION/OPTIONAL/MINUS/EXISTS) is rejected here; express those with SPARQL text under mode=sparql. Cheap aggregate count: pair an equality having (e.g. { "compare": { "op": "eq", "left": { "var": "n" }, "right": { "value": { "i64": 4 } } } }) with row_limit: 1 -- the response row_page.total reports how many groups match without materializing them all, so you read the count off row_page.total instead of paging every matching row. For snapshot pinning use the top-level `as_of_commit_seq` argument or the same body field. Valid-time `as_of` and `as_of_valid_time` selectors are unsupported and rejected before HTTP.'),
|
|
394
394
|
as_of: z
|
|
395
395
|
.string()
|
|
396
396
|
.optional()
|
|
397
|
-
.describe("
|
|
397
|
+
.describe("Unsupported in structured and SPARQL text modes; use as_of_commit_seq for a retained commit snapshot."),
|
|
398
398
|
as_of_commit_seq: z
|
|
399
399
|
.number()
|
|
400
400
|
.int()
|
|
@@ -412,8 +412,12 @@ export const queryInputSchema = z.discriminatedUnion("mode", [
|
|
|
412
412
|
query: z
|
|
413
413
|
.string()
|
|
414
414
|
.optional()
|
|
415
|
-
.describe(`SPARQL 1.1 query text (SELECT or ASK). ${SPARQL_IRI_GUIDE} Example: SELECT ?service ?db WHERE { ?service <https://littlebigbrain.com/r/writes_to> ?db } LIMIT 10`),
|
|
416
|
-
|
|
415
|
+
.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`),
|
|
416
|
+
// Kept for an actionable error when an older connector sends this field.
|
|
417
|
+
as_of: z
|
|
418
|
+
.string()
|
|
419
|
+
.optional()
|
|
420
|
+
.describe("Unsupported in SPARQL text mode; use as_of_commit_seq."),
|
|
417
421
|
as_of_commit_seq: z
|
|
418
422
|
.number()
|
|
419
423
|
.int()
|
package/dist/tools.js
CHANGED
|
@@ -124,38 +124,36 @@ export function registerLbbTools(server, client) {
|
|
|
124
124
|
stableJson(args.body) !== stableJson(cursor.body)) {
|
|
125
125
|
throw new Error("cursor body does not match the supplied body argument");
|
|
126
126
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
throw new Error("the structured body has an `as_of` key, which the server ignores — use the top-level `as_of` argument (valid-time, RFC3339) or rename it to `as_of_valid_time` inside the body");
|
|
127
|
+
if (args.as_of !== undefined ||
|
|
128
|
+
cursor?.as_of !== undefined ||
|
|
129
|
+
body.as_of !== undefined ||
|
|
130
|
+
body.as_of_valid_time !== undefined) {
|
|
131
|
+
throw new Error("structured SPARQL valid-time selectors are not supported; use as_of_commit_seq for a retained commit snapshot, or start a new query without the valid-time selector");
|
|
133
132
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
133
|
+
// Resolve the top-level or body commit pin once and retain it
|
|
134
|
+
// across cursor pages. The API validates its exact RDF lineage.
|
|
135
|
+
if (body.as_of_commit_seq !== undefined &&
|
|
136
|
+
body.as_of_commit_seq !== null &&
|
|
137
|
+
(typeof body.as_of_commit_seq !== "number" ||
|
|
138
|
+
!Number.isSafeInteger(body.as_of_commit_seq) ||
|
|
139
|
+
body.as_of_commit_seq < 0)) {
|
|
140
|
+
throw new Error("body as_of_commit_seq must be a nonnegative safe integer");
|
|
138
141
|
}
|
|
139
|
-
// Commit-seq pin: top-level arg, else the body field, pinned for
|
|
140
|
-
// continuation. Valid-time pin: cursor, else top-level arg, else the
|
|
141
|
-
// body's `as_of_valid_time`. Both are resolved here and set
|
|
142
|
-
// explicitly so the request never depends on the body's spelling.
|
|
143
142
|
const requestedCommitSeq = args.as_of_commit_seq ??
|
|
144
143
|
(typeof body.as_of_commit_seq === "number"
|
|
145
144
|
? body.as_of_commit_seq
|
|
146
145
|
: undefined);
|
|
146
|
+
if (cursor &&
|
|
147
|
+
args.as_of_commit_seq !== undefined &&
|
|
148
|
+
args.as_of_commit_seq !== cursor.as_of_commit_seq) {
|
|
149
|
+
throw new Error("cursor as_of_commit_seq does not match the supplied as_of_commit_seq argument");
|
|
150
|
+
}
|
|
147
151
|
const asOfCommitSeq = await queryCommitPin(target, requestedCommitSeq, cursor);
|
|
148
|
-
const asOfValidTime = cursor?.as_of ??
|
|
149
|
-
args.as_of ??
|
|
150
|
-
(typeof body.as_of_valid_time === "string"
|
|
151
|
-
? body.as_of_valid_time
|
|
152
|
-
: undefined);
|
|
153
152
|
const request = {
|
|
154
153
|
...body,
|
|
155
154
|
limit: rowLimit,
|
|
156
155
|
offset,
|
|
157
156
|
as_of_commit_seq: asOfCommitSeq,
|
|
158
|
-
as_of_valid_time: asOfValidTime ?? null,
|
|
159
157
|
};
|
|
160
158
|
// The analytics route is gone; structured bodies run only on the
|
|
161
159
|
// SPARQL-select path, which rejects unknown fields. Name the
|
|
@@ -175,7 +173,6 @@ export function registerLbbTools(server, client) {
|
|
|
175
173
|
detail,
|
|
176
174
|
row_limit: rowLimit,
|
|
177
175
|
body,
|
|
178
|
-
as_of: asOfValidTime,
|
|
179
176
|
as_of_commit_seq: asOfCommitSeq,
|
|
180
177
|
};
|
|
181
178
|
const next = rowPageNext(cursorBase, rowPage);
|
|
@@ -193,21 +190,17 @@ export function registerLbbTools(server, client) {
|
|
|
193
190
|
normalizeLbbIris(args.query).query !== cursor.query) {
|
|
194
191
|
throw new Error("cursor query does not match the supplied query argument");
|
|
195
192
|
}
|
|
196
|
-
if (cursor
|
|
197
|
-
|
|
198
|
-
args.as_of !== cursor.as_of) {
|
|
199
|
-
throw new Error("cursor as_of does not match the supplied as_of argument");
|
|
193
|
+
if (args.as_of !== undefined || cursor?.as_of !== undefined) {
|
|
194
|
+
throw new Error("SPARQL text valid-time as_of is not supported; use as_of_commit_seq for a retained commit snapshot, or start a new query without as_of");
|
|
200
195
|
}
|
|
201
196
|
if (cursor &&
|
|
202
197
|
args.as_of_commit_seq !== undefined &&
|
|
203
198
|
args.as_of_commit_seq !== cursor.as_of_commit_seq) {
|
|
204
199
|
throw new Error("cursor as_of_commit_seq does not match the supplied as_of_commit_seq argument");
|
|
205
200
|
}
|
|
206
|
-
const asOf = cursor?.as_of ?? args.as_of;
|
|
207
201
|
const asOfCommitSeq = await queryCommitPin(target, args.as_of_commit_seq, cursor);
|
|
208
202
|
const response = await target.sparqlText({
|
|
209
203
|
query,
|
|
210
|
-
as_of_valid_time: asOf ?? null,
|
|
211
204
|
as_of_commit_seq: asOfCommitSeq ?? null,
|
|
212
205
|
limit: rowLimit,
|
|
213
206
|
offset,
|
|
@@ -222,7 +215,6 @@ export function registerLbbTools(server, client) {
|
|
|
222
215
|
detail,
|
|
223
216
|
row_limit: rowLimit,
|
|
224
217
|
query,
|
|
225
|
-
as_of: asOf,
|
|
226
218
|
as_of_commit_seq: asOfCommitSeq,
|
|
227
219
|
};
|
|
228
220
|
const next = rowPageNext(cursorBase, rowPage);
|