@centrali-io/centrali-mcp 5.3.0 → 5.5.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.
@@ -621,9 +621,9 @@ export function registerDescribeTools(server: McpServer) {
621
621
 
622
622
  // ── Records ────────────────────────────────────────────────────────
623
623
 
624
- registerTool<any>(server,
624
+ registerTool<any>(server,
625
625
  "describe_records",
626
- "Get the schema reference for Centrali record operations. Explains filter syntax, sort syntax, pagination, expand, and the data. prefix convention.",
626
+ "Get the schema reference for Centrali record operations. Explains the canonical QueryDefinition body used by query_records, the operator vocabulary, sort/page/select clauses, and the data. prefix convention.",
627
627
  {},
628
628
  async () => ({
629
629
  content: [
@@ -633,7 +633,7 @@ export function registerDescribeTools(server: McpServer) {
633
633
  {
634
634
  domain: "Records",
635
635
  description:
636
- "Records are rows of data stored in collections. All custom field values live under the 'data' namespace.",
636
+ "Records are rows of data stored in collections. All custom field values live under the 'data' namespace. The query_records tool accepts a canonical QueryDefinition body; the underlying engine (POST /records/query) is shared with the SDK, console, compute, and Pages.",
637
637
  record_shape: {
638
638
  id: "UUID — auto-generated",
639
639
  collectionId: "UUID — the collection this record belongs to",
@@ -644,77 +644,85 @@ export function registerDescribeTools(server: McpServer) {
644
644
  updatedBy: "UUID — user who last updated",
645
645
  isDeleted: "boolean — true if soft-deleted",
646
646
  },
647
- filter_syntax: {
648
- description:
649
- "Filters are passed as key-value pairs. Keys use the 'data.' prefix for custom fields and bracket notation for operators.",
650
- examples: {
651
- "Exact match": { "data.status": "active" },
652
- "Greater than": { "data.price[gt]": 100 },
653
- "Less than or equal": { "data.age[lte]": 30 },
654
- "Not equal": { "data.type[ne]": "draft" },
655
- "In list": { "data.category[in]": "electronics,books" },
656
- "Not in list": { "data.status[nin]": "deleted,archived" },
657
- "Contains (text)": { "data.name[contains]": "john" },
658
- "Starts with": { "data.email[startswith]": "admin" },
659
- "Ends with": { "data.domain[endswith]": ".com" },
660
- },
661
- operators: [
662
- "eq (default, no bracket needed)",
663
- "ne (not equal)",
664
- "gt (greater than)",
665
- "gte (greater than or equal)",
666
- "lt (less than)",
667
- "lte (less than or equal)",
668
- "in (comma-separated list)",
669
- "nin (not in comma-separated list)",
670
- "contains (substring match)",
671
- "startswith (prefix match)",
672
- "endswith (suffix match)",
673
- ],
674
- },
675
- sort_syntax: {
647
+ query_definition: {
676
648
  description:
677
- "Sort by a single field. Prefix with '-' for descending order.",
678
- examples: {
679
- "Ascending by date": "createdAt",
680
- "Descending by date": "-createdAt",
681
- "By custom field asc": "data.name",
682
- "By custom field desc": "-data.price",
649
+ "Canonical QueryDefinition shape passed to query_records. Field paths are dotted strings ('data.status', 'data.customer.email'). Each FieldCondition uses exactly one operator (no '$' prefix).",
650
+ shape: {
651
+ resource: "string collection slug",
652
+ where: "WhereExpression FieldConditionMap | { and: [...] } | { or: [...] } | { not: ... }",
653
+ text: "{ query: string, fields?: string[], typoTolerance?: boolean }",
654
+ sort: "[{ field: string, direction: 'asc' | 'desc' }]",
655
+ page: "{ limit: number, offset?: number } | { limit: number, cursor?: string }",
656
+ select: "{ fields: string[] }",
657
+ include: "[{ relation: string }] — relation expansion (collection-defined relations)",
683
658
  },
684
659
  },
685
- pagination: {
686
- page: "1-indexed page number (default: 1)",
687
- pageSize: "Records per page (default: 50, max: 500)",
688
- response_includes:
689
- "total, page, pageSize, totalPages in the response metadata",
690
- },
691
- expand: {
660
+ operators: {
692
661
  description:
693
- "Comma-separated list of reference field names to expand (join). Returns the full referenced record instead of just the ID.",
694
- example: "expand: 'customer,product'",
662
+ "Field operators. Use exactly one per FieldCondition.",
663
+ list: [
664
+ "eq — equal",
665
+ "ne — not equal",
666
+ "gt — greater than",
667
+ "gte — greater than or equal",
668
+ "lt — less than",
669
+ "lte — less than or equal",
670
+ "in — value in array",
671
+ "nin — value not in array",
672
+ "contains — substring match",
673
+ "startsWith — prefix match",
674
+ "endsWith — suffix match",
675
+ "hasAny — array field has any of these",
676
+ "hasAll — array field has all of these",
677
+ "exists — boolean: field is set",
678
+ ],
679
+ boolean_tree: ["and", "or", "not"],
695
680
  },
696
- dateWindow: {
697
- description:
698
- "Date range filter. Restricts results to records where a date field falls within a given range. Both 'from' and 'to' are optional (open-ended ranges allowed).",
699
- shape: {
700
- field: "string — date field to filter on (e.g., 'createdAt', 'updatedAt', or a custom date field)",
701
- from: "string? ISO 8601 lower bound (inclusive)",
702
- to: "string? ISO 8601 upper bound (inclusive)",
681
+ examples: {
682
+ "Exact match": { "data.status": { eq: "active" } },
683
+ "Greater than": { "data.price": { gt: 100 } },
684
+ "Boolean tree": {
685
+ and: [
686
+ { "data.status": { eq: "open" } },
687
+ { or: [{ "data.amount": { gte: 100 } }, { "data.priority": { eq: "high" } }] },
688
+ ],
703
689
  },
704
- examples: {
705
- "Last 30 days": { field: "createdAt", from: "2024-03-01T00:00:00Z" },
706
- "Specific range": { field: "updatedAt", from: "2024-01-01T00:00:00Z", to: "2024-03-31T23:59:59Z" },
690
+ "Date window": {
691
+ and: [
692
+ { createdAt: { gte: "2024-03-01T00:00:00Z" } },
693
+ { createdAt: { lte: "2024-03-31T23:59:59Z" } },
694
+ ],
695
+ },
696
+ "Sort + page": {
697
+ sort: [{ field: "createdAt", direction: "desc" }],
698
+ page: { limit: 50 },
707
699
  },
700
+ "Projection": { select: { fields: ["id", "data.status", "data.amount"] } },
701
+ "Full-text search": { text: { query: "urgent shipping", fields: ["data.notes"] } },
708
702
  },
709
- includeDeleted: {
710
- description: "Set to true to include soft-deleted records in the results (default: false). Alias: includeArchived, all.",
703
+ response_envelope: {
704
+ description: "All canonical queries return { data, meta }.",
705
+ shape: {
706
+ data: "T[]",
707
+ meta: {
708
+ limit: "number",
709
+ offset: "number? (offset mode)",
710
+ cursor: "string? (cursor mode)",
711
+ nextCursor: "string?",
712
+ hasMore: "boolean?",
713
+ total: "number? (when computable; depends on executor)",
714
+ processingTimeMs: "number?",
715
+ mode: "'filter' | 'search' | 'hybrid'",
716
+ },
717
+ },
711
718
  },
712
- includeTotal: {
713
- description: "Set to true to include the total record count in response metadata (default: false).",
719
+ page_defaults: {
720
+ limit_default: 50,
721
+ limit_max: 500,
714
722
  },
715
723
  upsert: {
716
724
  description:
717
- "Atomic create-or-update. Provide 'match' fields to find existing record and 'data' for the full record body.",
725
+ "Atomic create-or-update via upsert_record. Provide 'match' fields to find an existing record and 'data' for the full record body.",
718
726
  example: {
719
727
  match: { sku: "WIDGET-001" },
720
728
  data: { sku: "WIDGET-001", name: "Widget", price: 9.99 },
@@ -724,6 +732,7 @@ export function registerDescribeTools(server: McpServer) {
724
732
  tips: [
725
733
  "Always use the 'data.' prefix when filtering on custom fields",
726
734
  "System fields (id, createdAt, updatedAt) don't need the 'data.' prefix",
735
+ "Each FieldCondition must use exactly one operator — '{ eq: 1, ne: 2 }' is rejected",
727
736
  "Use upsert_record for idempotent imports — it won't create duplicates",
728
737
  "Soft-deleted records can be restored with restore_record",
729
738
  "Use get_records_by_ids for batch lookups (more efficient than individual get_record calls)",
@@ -1054,9 +1063,9 @@ export function registerDescribeTools(server: McpServer) {
1054
1063
 
1055
1064
  // ── Smart Queries ──────────────────────────────────────────────────
1056
1065
 
1057
- registerTool<any>(server,
1066
+ registerTool<any>(server,
1058
1067
  "describe_smart_queries",
1059
- "Get the schema reference for Centrali smart queries. Explains parameterized queries, variable substitution, and execution.",
1068
+ "Get the schema reference for Centrali saved (a.k.a. smart) queries. Explains the canonical QueryDefinition body, variable substitution, and execution.",
1060
1069
  {},
1061
1070
  async () => ({
1062
1071
  content: [
@@ -1064,71 +1073,75 @@ export function registerDescribeTools(server: McpServer) {
1064
1073
  type: "text",
1065
1074
  text: JSON.stringify(
1066
1075
  {
1067
- domain: "Smart Queries",
1076
+ domain: "Saved Queries (a.k.a. Smart Queries)",
1068
1077
  description:
1069
- "Smart queries are reusable, parameterized queries defined in the Centrali console. They allow complex filtering and aggregation logic to be saved and executed with variable substitution.",
1070
- smart_query_shape: {
1078
+ "Saved queries are reusable, parameterized queries defined in the Centrali console. They store a canonical QueryDefinition plus optional variable declarations and are executed under the caller's permissions. Tool names retain the `_smart_query` suffix for backwards compatibility.",
1079
+ saved_query_shape: {
1071
1080
  id: "UUID",
1072
1081
  name: "string — display name",
1073
1082
  recordSlug: "string — the collection this query targets",
1074
1083
  description: "string | null",
1075
- query:
1076
- "object — the query definition with filters, sort, fields, and aggregations",
1077
- variables:
1078
- "object[] — declared variables with name, type, and default values",
1084
+ queryDefinition:
1085
+ "object — canonical QueryDefinition (without 'resource' filled in from recordSlug). Variables are referenced as '{{varName}}' placeholders inside operator values; the engine infers them from the body.",
1079
1086
  },
1087
+ query_definition_reference:
1088
+ "Use describe_records for the full QueryDefinition shape, operator vocabulary, and examples.",
1080
1089
  variable_syntax: {
1081
1090
  description:
1082
- "Variables use double-brace syntax: {{variableName}}. When executing, pass a variables object to substitute values.",
1083
- example: {
1084
- query_filter: {
1085
- "data.status": "{{status}}",
1086
- "data.amount[gte]": "{{minAmount}}",
1087
- },
1088
- execution_variables: {
1089
- status: "active",
1090
- minAmount: "1000",
1091
+ "Variables use double-brace syntax: '{{variableName}}'. They appear inside operator values; the engine infers the variable list from the body and resolves substitutions at execution time. There is no separate variable declaration — placeholders in the QueryDefinition are the source of truth.",
1092
+ example_query_definition: {
1093
+ where: {
1094
+ and: [
1095
+ { "data.status": { eq: "{{statusFilter}}" } },
1096
+ { "data.amount": { gte: "{{minAmount}}" } },
1097
+ ],
1091
1098
  },
1099
+ sort: [{ field: "createdAt", direction: "desc" }],
1100
+ page: { limit: 100 },
1101
+ },
1102
+ example_execution_variables: {
1103
+ statusFilter: "active",
1104
+ minAmount: 1000,
1092
1105
  },
1093
- note: "Variable values are always passed as strings the query engine handles type coercion",
1106
+ note: "Pass values via the 'variables' arg on execute_smart_query / test_smart_query. Values are substituted before the engine validates the query.",
1094
1107
  },
1095
1108
  crud_tools: {
1096
1109
  get_smart_query: {
1097
- description: "Get a smart query by ID, including its full definition",
1110
+ description: "Get a saved query by ID, including its full canonical definition",
1098
1111
  required_params: ["recordSlug", "queryId"],
1099
1112
  },
1100
1113
  create_smart_query: {
1101
- description: "Create a new smart query for a collection",
1114
+ description: "Create a new saved query for a collection",
1102
1115
  required_params: ["recordSlug", "name", "queryDefinition"],
1103
1116
  optional_params: ["description"],
1104
1117
  queryDefinition_example: {
1105
- where: { status: { "$eq": "active" } },
1118
+ where: { "data.status": { eq: "active" } },
1106
1119
  sort: [{ field: "createdAt", direction: "desc" }],
1107
- limit: 100,
1120
+ page: { limit: 100 },
1108
1121
  },
1109
1122
  },
1110
1123
  update_smart_query: {
1111
- description: "Update an existing smart query. Partial updates supported.",
1124
+ description: "Update an existing saved query. Partial updates supported.",
1112
1125
  required_params: ["recordSlug", "queryId"],
1113
1126
  optional_params: ["name", "description", "queryDefinition"],
1114
1127
  },
1115
1128
  delete_smart_query: {
1116
- description: "Delete a smart query by ID",
1129
+ description: "Delete a saved query by ID",
1117
1130
  required_params: ["recordSlug", "queryId"],
1118
1131
  },
1119
1132
  test_smart_query: {
1120
- description: "Test execute a query definition without saving it. Preview results before creating.",
1133
+ description: "Test execute a canonical query definition without saving it. Preview results before creating.",
1121
1134
  required_params: ["recordSlug", "queryDefinition"],
1122
1135
  optional_params: ["variables"],
1123
1136
  },
1124
1137
  },
1125
1138
  tips: [
1126
1139
  "Use list_smart_queries to discover available queries (optionally filter by collection slug)",
1127
- "Smart queries return the same result shape as query_records",
1128
- "Variables are defined when creating the query in the console check the query's variables array to see what's expected",
1129
- "Prefer smart queries over ad-hoc query_records filters for complex/reusable logic",
1130
- "Use test_smart_query to validate query syntax and preview results before saving",
1131
- "Use create_smart_query to save reusable queries that can be executed with execute_smart_query",
1140
+ "Saved queries return the same canonical { data, meta } envelope as query_records",
1141
+ "Variables are declared when creating the query — read the query's 'variables' field to see what's expected",
1142
+ "Prefer saved queries over ad-hoc query_records filters for reusable logic",
1143
+ "Use test_smart_query to validate canonical syntax and preview results before saving",
1144
+ "DO NOT author new saved queries with legacy '$'-prefixed operators ('$eq', '$gte', …). The data service still translates them server-side during the deprecation window, but new saved queries must use canonical operators ('eq', 'gte', …)",
1132
1145
  ],
1133
1146
  },
1134
1147
  null,