@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 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:
@@ -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 prefer the top-level `as_of` / `as_of_commit_seq` arguments below; a bare `as_of` key inside the body is rejected (the body\'s valid-time field is `as_of_valid_time`).'),
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("Snapshot pin (valid-time, RFC3339): evaluate the body as of this instant. Folded into the request's `as_of_valid_time`. Top-level here is the supported spelling — a bare `as_of` inside the body is rejected, since the server silently ignores it."),
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
- as_of: z.string().optional(),
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
- // The body's valid-time field is `as_of_valid_time`; the server
128
- // ignores a bare `as_of` key, so a naive caller would chart
129
- // head-snapshot data and never know. Turn that silent no-op into a
130
- // clear error pointing at the right spelling.
131
- if (body.as_of !== undefined) {
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
- if (cursor &&
135
- args.as_of !== undefined &&
136
- args.as_of !== cursor.as_of) {
137
- throw new Error("cursor as_of does not match the supplied as_of argument");
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
- args.as_of !== undefined &&
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@littlebigbrain/mcp",
3
- "version": "0.4.3",
3
+ "version": "0.4.4",
4
4
  "description": "MCP server for little big brain — graph and hybrid search tools for agents",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {