@littlebigbrain/mcp 0.9.0 → 0.11.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.
@@ -2,8 +2,14 @@ import { z } from "zod";
2
2
  export const DEFAULT_DETAIL = "compact";
3
3
  export const HARD_OUTPUT_CHARS = 80_000;
4
4
  export const MAX_QUERY_ROW_LIMIT = 5_000;
5
- /** The most rows `POST /v1/query/rewrite` returns from a run. */
5
+ /** The most rows each query of `POST /v1/query/ask` returns. */
6
6
  export const QUESTION_MAX_ROWS = 1_000;
7
+ /** Entity IRIs one question may anchor, and the characters of one. */
8
+ export const QUESTION_MAX_ANCHORS = 10;
9
+ export const QUESTION_MAX_ANCHOR_CHARS = 2_000;
10
+ /** Dated points one question's timeline may carry, and the characters of a label. */
11
+ export const QUESTION_MAX_TIMELINE = 200;
12
+ export const QUESTION_MAX_TIMELINE_LABEL_CHARS = 200;
7
13
  export const READ_ONLY = { readOnlyHint: true };
8
14
  export const IDEMPOTENT_WRITE = {
9
15
  readOnlyHint: false,
@@ -398,7 +404,18 @@ export const inspectInputSchema = z.discriminatedUnion("action", [
398
404
  export const SPARQL_IRI_GUIDE = 'IRI scheme: relations are <https://littlebigbrain.com/r/NAME> (NAME lowercased, e.g. writes_to; reverse a relation with the ^ path operator, no stored inverse triple). Types are <https://littlebigbrain.com/class/NAME> (lowercased), matched as `?x a <…/class/NAME>` with explicit entailment=subclass, rdfs, or owl for inference (default none). Property fields are <https://littlebigbrain.com/p/NAME> (lowercased). The local name is ALWAYS lowercase — an uppercase one (e.g. <…/r/FOR_CLIENT>) is a different, non-existent IRI that silently matches nothing; this tool auto-lowercases the local name of /r/, /class/, and /p/ IRIs for you and adds a `notes` entry when it does, so a stray uppercase still resolves. (Structured mode\'s `predicate` is case-insensitive on its own.) Entities are content-addressed <https://littlebigbrain.com/e/HASH> — never build an entity IRI from a name; anchor a named entity by its label instead: `?e <http://www.w3.org/2000/01/rdf-schema#label> "Acme"`. Discover the exact relation and type names with lbb_inspect action=ontology. SELECT and ASK only (CONSTRUCT/DESCRIBE are rejected).';
399
405
  // Search by meaning runs inside SPARQL text as a pattern, so an agent can
400
406
  // filter and join the hits in one query instead of a search then a lookup.
401
- export const SPARQL_SEARCH_GUIDE = 'Search by meaning inside the query (the graph needs an embedding; see lbb_embeddings action=list): PREFIX search: <https://littlebigbrain.com/search#> SELECT ?x ?score WHERE { ?x search:similarTo "card payments" ; search:score ?score . ?x <https://littlebigbrain.com/r/calls> ?y } ORDER BY DESC(?score) LIMIT 5. The other patterns filter and join the hits. One search:similarTo per query, with a variable subject, in the main group (not inside OPTIONAL, UNION, MINUS or a subquery). The object is text, an entity IRI (records like that one) or a vector literal. search:top N sets the hits (1 to 1000; default the query LIMIT, then 10) and search:embedding "name" searches one embedding. FILTER(?score > 0.8) keeps close hits. The result\'s search field reports the plan (nearest, filter_first or search_first), the hits and complete.';
407
+ export const SPARQL_SEARCH_GUIDE = 'Search by meaning inside the query (the graph needs an embedding; see lbb_embeddings action=list): PREFIX search: <https://littlebigbrain.com/search#> SELECT ?x ?score WHERE { ?x search:similarTo "card payments" ; search:score ?score . ?x <https://littlebigbrain.com/r/calls> ?y } ORDER BY DESC(?score) LIMIT 5. The other patterns filter and join the hits. One search:similarTo per query, with a variable subject, in the main group (not inside OPTIONAL, UNION, MINUS or a subquery). The object is text, an entity IRI (records like that one) or a vector literal. search:top N sets the hits (1 to 1000; default the query LIMIT, then 10) and search:embedding "name" searches one embedding. FILTER(?score > 0.8) keeps close hits. The result\'s search field reports the plan (nearest, filter_first or search_first), the hits and complete. For a question in words, add search:rerank true ; search:relevance ?r and ORDER BY DESC(?r): the rerank model (Jev) reads the best hits with the text, keeps the top that answer it and binds its answer (0 to 1); search.rerank reports the status. Only a text query reranks.';
408
+ /** One point of `lbb_query mode=compare`: a commit, a date or a moment. */
409
+ const comparePointSchema = z
410
+ .object({
411
+ as_of_commit_seq: z.number().int().min(0).optional(),
412
+ date: z
413
+ .string()
414
+ .regex(/^\d{4}-\d{2}-\d{2}$/)
415
+ .optional(),
416
+ moment: z.string().optional(),
417
+ })
418
+ .strict();
402
419
  export const queryInputSchema = z.discriminatedUnion("mode", [
403
420
  z
404
421
  .object({
@@ -441,7 +458,7 @@ export const queryInputSchema = z.discriminatedUnion("mode", [
441
458
  query: z
442
459
  .string()
443
460
  .optional()
444
- .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. ${SPARQL_SEARCH_GUIDE}`),
461
+ .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. ${SPARQL_SEARCH_GUIDE} compare: the SELECT to run at both points; leave out LIMIT.`),
445
462
  // Kept for an actionable error when an older connector sends this field.
446
463
  as_of: z
447
464
  .string()
@@ -468,7 +485,7 @@ export const queryInputSchema = z.discriminatedUnion("mode", [
468
485
  question: z
469
486
  .string()
470
487
  .min(1)
471
- .describe("question: the question in plain words (required), 1 to 4,000 characters. The server writes the SPARQL query from a description of this graph, checks it, and runs it."),
488
+ .describe("question: the question in plain words (required), 1 to 4,000 characters. The server's model runs queries on this graph, reads their rows, and answers. describe: the question whose classes and properties to describe."),
472
489
  context: z
473
490
  .string()
474
491
  .optional()
@@ -483,18 +500,41 @@ export const queryInputSchema = z.discriminatedUnion("mode", [
483
500
  "unanswerable",
484
501
  ])
485
502
  .optional()
486
- .describe("question: the kind of query, when you know it. Omit it and the router model selects it."),
503
+ .describe("question: the kind of question, when you know it. Omit it and the router model picks it."),
487
504
  limit: z
488
505
  .number()
489
506
  .int()
490
507
  .min(1)
491
508
  .max(QUESTION_MAX_ROWS)
492
509
  .optional()
493
- .describe("question: rows the run returns, 1 to 1,000. Defaults by detail: compact=20, standard=100, full=1000."),
494
- run: z
495
- .boolean()
510
+ .describe("question: rows each query of the server's loop returns, 1 to 1,000. Defaults by detail: compact=20, standard=100, full=1000. names: candidates per name, 1 to 10 (default 5). compare: entries of each list per page, 1 to 1,000 (defaults by detail: 20, 100, 500)."),
511
+ anchor: z
512
+ .array(z.string().min(1).max(QUESTION_MAX_ANCHOR_CHARS))
513
+ .max(QUESTION_MAX_ANCHORS)
514
+ .optional()
515
+ .describe("question: entity IRIs the question is about, at most 10 (for example the record the user has open). The server reads each one, and its queries use these IRIs directly instead of matching their names."),
516
+ timeline: z
517
+ .array(z
518
+ .object({
519
+ date: z
520
+ .string()
521
+ .regex(/^\d{4}-\d{2}-\d{2}$/)
522
+ .describe("YYYY-MM-DD."),
523
+ as_of_commit_seq: z
524
+ .number()
525
+ .int()
526
+ .min(0)
527
+ .describe("The commit that holds the graph as it was on that date."),
528
+ label: z
529
+ .string()
530
+ .max(QUESTION_MAX_TIMELINE_LABEL_CHARS)
531
+ .optional()
532
+ .describe("A name for the point, such as a milestone."),
533
+ })
534
+ .strict())
535
+ .max(QUESTION_MAX_TIMELINE)
496
536
  .optional()
497
- .describe("question: run the query and return its rows. Defaults to true; false returns the query only."),
537
+ .describe("question: dated points that stand for the graph's commits, at most 200. A question about a date reads the commit of the latest point on or before it. Use it when the commits stand for other dates than the days they were written (a demo's milestones, an import of old records); without it the server reads the last commit written by the end of that day."),
498
538
  ...readScope,
499
539
  })
500
540
  .strict(),
@@ -508,7 +548,7 @@ export const queryInputSchema = z.discriminatedUnion("mode", [
508
548
  text: z
509
549
  .string()
510
550
  .optional()
511
- .describe("The query text (required); embedded with the graph's model."),
551
+ .describe("search: the query text (required); embedded with the graph's model. names: the question, or the names, to find (required)."),
512
552
  top_k: z.number().int().positive().max(200).optional(),
513
553
  probe: z
514
554
  .number()
@@ -572,6 +612,82 @@ export const queryInputSchema = z.discriminatedUnion("mode", [
572
612
  ...readScope,
573
613
  })
574
614
  .strict(),
615
+ z
616
+ .object({
617
+ mode: z.literal("names"),
618
+ text: z
619
+ .string()
620
+ .min(1)
621
+ .max(4_000)
622
+ .describe("names: the question, or the names, to find (required)."),
623
+ limit: z.number().int().min(1).max(10).optional(),
624
+ ...readScope,
625
+ })
626
+ .strict(),
627
+ z
628
+ .object({
629
+ mode: z.literal("describe"),
630
+ question: z.string().max(4_000).optional(),
631
+ classes: z
632
+ .array(z.string().min(1))
633
+ .max(20)
634
+ .optional()
635
+ .describe("describe: class IRIs to describe, at most 20."),
636
+ properties: z
637
+ .array(z.string().min(1))
638
+ .max(50)
639
+ .optional()
640
+ .describe("describe: property IRIs to describe, at most 50."),
641
+ ...readScope,
642
+ })
643
+ .strict(),
644
+ z
645
+ .object({
646
+ mode: z.literal("commit_at"),
647
+ date: z
648
+ .string()
649
+ .regex(/^\d{4}-\d{2}-\d{2}$/)
650
+ .optional()
651
+ .describe("commit_at: YYYY-MM-DD; the last commit written by the end of that day (UTC)."),
652
+ moment: z
653
+ .string()
654
+ .optional()
655
+ .describe("commit_at: RFC 3339, e.g. 2026-06-18T12:00:00Z; the last commit written at or before it."),
656
+ ...readScope,
657
+ })
658
+ .strict(),
659
+ z
660
+ .object({
661
+ mode: z.literal("compare"),
662
+ query: z
663
+ .string()
664
+ .min(1)
665
+ .describe("compare: the SELECT to run at both points. Leave out LIMIT: the server reads up to max_rows rows a point, page by page."),
666
+ before: comparePointSchema.describe('compare: the earlier point (required): {"as_of_commit_seq": n}, {"date": "YYYY-MM-DD"} or {"moment": "RFC 3339"}.'),
667
+ after: comparePointSchema
668
+ .optional()
669
+ .describe("compare: the later point; defaults to the latest commit."),
670
+ key: z
671
+ .array(z.string().min(1))
672
+ .max(8)
673
+ .optional()
674
+ .describe('compare: the variables that identify a row\'s entity, e.g. ["contact"]. With a key the rows are paired: added and removed hold the entities at one point only, changed the entities whose values differ. Without one whole rows are compared.'),
675
+ entailment: z.enum(["none", "subclass", "rdfs", "owl"]).optional(),
676
+ max_rows: z
677
+ .number()
678
+ .int()
679
+ .min(1)
680
+ .max(50_000)
681
+ .optional()
682
+ .describe("compare: rows to read per point, 1 to 50,000 (default 20,000). Past it the result says truncated."),
683
+ limit: z.number().int().min(1).max(1_000).optional(),
684
+ compare_cursor: z
685
+ .string()
686
+ .optional()
687
+ .describe("compare: the cursor of the page before, from next; pass it with the same arguments."),
688
+ ...readScope,
689
+ })
690
+ .strict(),
575
691
  ]);
576
692
  export const configureInputSchema = z.discriminatedUnion("action", [
577
693
  z
@@ -695,6 +811,48 @@ export const configureInputSchema = z.discriminatedUnion("action", [
695
811
  ...graphScope,
696
812
  })
697
813
  .strict(),
814
+ z
815
+ .object({
816
+ action: z.literal("get_rewrite_profile"),
817
+ ...graphScope,
818
+ })
819
+ .strict(),
820
+ z
821
+ .object({
822
+ action: z.literal("set_rewrite_profile"),
823
+ notes: z
824
+ .string()
825
+ .max(8000)
826
+ .optional()
827
+ .describe("What the data means for questions: which property holds the current state, what 'me' or 'my' means, which records to leave out. At most 8,000 characters"),
828
+ examples: z
829
+ .array(z
830
+ .object({
831
+ question: z.string().min(1).max(1000),
832
+ sparql: z
833
+ .string()
834
+ .min(1)
835
+ .max(4000)
836
+ .describe("A SELECT or ASK query with its PREFIX lines; the server parses it"),
837
+ note: z.string().max(1000).optional(),
838
+ })
839
+ .strict())
840
+ .max(20)
841
+ .optional()
842
+ .describe("Up to 20 worked examples: a question and the query that answers it on this graph"),
843
+ expected_version: z
844
+ .number()
845
+ .int()
846
+ .nonnegative()
847
+ .optional()
848
+ .describe("The version get_rewrite_profile returned (0 for none): another stored version answers 409 conflict and stores nothing"),
849
+ dry_run: z
850
+ .boolean()
851
+ .optional()
852
+ .describe("Check the profile and answer what a write would store, without storing it"),
853
+ ...graphScope,
854
+ })
855
+ .strict(),
698
856
  ]);
699
857
  /**
700
858
  * MCP's `registerTool` advertises a JSON Schema for an input only when the
@@ -660,7 +660,7 @@ export async function guide(scopedClient) {
660
660
  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.",
661
661
  inspect: "Use lbb_inspect for ontology, schema metadata, the published conformance report, metadata, entities, and this guide. Use lbb_query with SPARQL property paths for exact path queries.",
662
662
  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 graph'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.",
663
- 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. To search and filter in one query, use SPARQL text: PREFIX search: <https://littlebigbrain.com/search#> SELECT ?x ?score WHERE { ?x search:similarTo \"words\" ; search:score ?score . ?x a <CLASS> } ORDER BY DESC(?score) LIMIT 10. Any other pattern on ?x filters the hits; the result's search field reports the plan and complete. (4) For a question in words, rerank=true lets the rerank model (Jev) put first the hits that answer it; each hit then carries relevance (0 to 1), and the search takes about 0.3 s more. Without rerank the graph's search setting decides.",
663
+ 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. To search and filter in one query, use SPARQL text: PREFIX search: <https://littlebigbrain.com/search#> SELECT ?x ?score WHERE { ?x search:similarTo \"words\" ; search:score ?score . ?x a <CLASS> } ORDER BY DESC(?score) LIMIT 10. Any other pattern on ?x filters the hits; the result's search field reports the plan and complete. (4) For a question in words, rerank=true lets the rerank model (Jev) put first the hits that answer it; each hit then carries relevance (0 to 1), and the search takes about 0.3 s more. Without rerank the graph's search setting decides. In SPARQL text, add search:rerank true ; search:relevance ?r to the search triples and ORDER BY DESC(?r); a SPARQL search reranks only then.",
664
664
  query: "Use lbb_query for questions in plain words (mode=question), structured SPARQL-subset bodies, SPARQL text, and canned analysis. SPARQL is the only query surface; mode=question also returns the SPARQL query the server wrote for the question. 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.",
665
665
  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.",
666
666
  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.",