@littlebigbrain/mcp 0.5.0 → 0.5.1
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 +7 -0
- package/dist/tool-runtime.js +60 -94
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -69,6 +69,13 @@ missing.
|
|
|
69
69
|
|
|
70
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
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
|
+
|
|
72
79
|
Both `lbb_query` SPARQL modes (`sparql` and `structured`) support retained commit reads
|
|
73
80
|
through `as_of_commit_seq`. When omitted, the connector pins the current head
|
|
74
81
|
commit and reuses it for cursor pages. Valid-time `as_of` is unsupported and is
|
package/dist/tool-runtime.js
CHANGED
|
@@ -312,104 +312,70 @@ export function envelope(label, value, detailArg, next) {
|
|
|
312
312
|
};
|
|
313
313
|
}
|
|
314
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))
|
|
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
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
327
|
+
const rows = key === "bindings" ? results?.bindings : key ? source[key] : undefined;
|
|
328
|
+
if (!rowPage || !repage || !Array.isArray(rows)) {
|
|
329
|
+
return { ...envelope(label, value, detailArg, next), row_page: rowPage };
|
|
330
|
+
}
|
|
331
|
+
if (rows.length !== rowPage.returned) {
|
|
332
|
+
throw new Error("Query row count does not match its page; refusing a cursor that could skip rows.");
|
|
333
|
+
}
|
|
334
|
+
const flags = serverTruncationFlags(value);
|
|
335
|
+
const build = (count) => {
|
|
336
|
+
const hasMore = count < rows.length || rowPage.has_more;
|
|
337
|
+
const page = {
|
|
338
|
+
...rowPage,
|
|
339
|
+
returned: count,
|
|
340
|
+
has_more: hasMore,
|
|
341
|
+
next_offset: hasMore ? rowPage.offset + count : undefined,
|
|
390
342
|
};
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
:
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
row_page: rowPage,
|
|
405
|
-
rows_shown: returned !== undefined ? 0 : undefined,
|
|
406
|
-
truncated: true,
|
|
407
|
-
next: partialRows
|
|
408
|
-
? next
|
|
409
|
-
: repage
|
|
410
|
-
? continuationNext(repage, 0, 1)
|
|
343
|
+
const selected = rows.slice(0, count);
|
|
344
|
+
const data = key === "bindings"
|
|
345
|
+
? { ...source, results: { ...results, bindings: selected } }
|
|
346
|
+
: { ...source, [key]: selected, row_page: page };
|
|
347
|
+
return {
|
|
348
|
+
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]" : ""}`,
|
|
349
|
+
data,
|
|
350
|
+
counts: countsFor(data),
|
|
351
|
+
row_page: page,
|
|
352
|
+
rows_shown: count < rows.length ? count : undefined,
|
|
353
|
+
truncated: hasMore || flags.length > 0 || undefined,
|
|
354
|
+
next: hasMore
|
|
355
|
+
? continuationNext(repage, page.offset + count, repage.row_limit)
|
|
411
356
|
: undefined,
|
|
357
|
+
};
|
|
412
358
|
};
|
|
359
|
+
// Leave room for tool-level normalization notes added by the caller. Count
|
|
360
|
+
// UTF-8 bytes, including multibyte literals, not JavaScript code units.
|
|
361
|
+
const fits = (page) => Buffer.byteLength(JSON.stringify(page, null, 2), "utf8") <=
|
|
362
|
+
HARD_OUTPUT_CHARS - 2048;
|
|
363
|
+
const full = build(rows.length);
|
|
364
|
+
if (fits(full))
|
|
365
|
+
return full;
|
|
366
|
+
let lo = 0;
|
|
367
|
+
let hi = rows.length;
|
|
368
|
+
while (lo < hi) {
|
|
369
|
+
const mid = Math.ceil((lo + hi) / 2);
|
|
370
|
+
if (fits(build(mid)))
|
|
371
|
+
lo = mid;
|
|
372
|
+
else
|
|
373
|
+
hi = mid - 1;
|
|
374
|
+
}
|
|
375
|
+
if (lo === 0) {
|
|
376
|
+
throw new Error("One query row exceeds the MCP output budget. Project fewer fields or use the SPARQL HTTP API for this value; no rows were skipped.");
|
|
377
|
+
}
|
|
378
|
+
return build(lo);
|
|
413
379
|
}
|
|
414
380
|
export function toolResult(value) {
|
|
415
381
|
return {
|