@centrali-io/centrali-mcp 5.4.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.
package/README.md CHANGED
@@ -115,7 +115,7 @@ After connecting, call `describe_centrali` first — it returns the full capabil
115
115
  ### Records
116
116
  | Tool | Description |
117
117
  |------|-------------|
118
- | `query_records` | Query records with filters, sorting, pagination |
118
+ | `query_records` | Query records using the canonical Centrali query language (POST /records/query). Accepts a `QueryDefinition` body (`where`, `text`, `sort`, `page`, `select`). |
119
119
  | `get_record` | Get a single record by ID |
120
120
  | `get_records_by_ids` | Get multiple records by IDs |
121
121
  | `create_record` | Create a new record |
@@ -154,16 +154,19 @@ After connecting, call `describe_centrali` first — it returns the full capabil
154
154
  | `invoke_endpoint` | Call a sync compute endpoint by path (returns response inline) |
155
155
  | `remove_allowed_domain` | Remove a domain from the allowlist |
156
156
 
157
- ### Smart Queries
157
+ ### Saved Queries (a.k.a. Smart Queries)
158
+
159
+ Saved queries store a canonical `QueryDefinition` plus optional variables. Tool names keep the `_smart_query` suffix for backwards compatibility, but inputs and outputs are the canonical shape.
160
+
158
161
  | Tool | Description |
159
162
  |------|-------------|
160
- | `list_smart_queries` | List smart queries, optionally by collection |
161
- | `get_smart_query` | Get a smart query by ID |
162
- | `create_smart_query` | Create a reusable parameterized query |
163
- | `update_smart_query` | Update a smart query |
164
- | `delete_smart_query` | Delete a smart query |
165
- | `execute_smart_query` | Execute a smart query with optional variables |
166
- | `test_smart_query` | Test a query definition without saving |
163
+ | `list_smart_queries` | List saved queries, optionally by collection |
164
+ | `get_smart_query` | Get a saved query by ID |
165
+ | `create_smart_query` | Create a reusable parameterized query (canonical body) |
166
+ | `update_smart_query` | Update a saved query (canonical body) |
167
+ | `delete_smart_query` | Delete a saved query |
168
+ | `execute_smart_query` | Execute a saved query with optional variables |
169
+ | `test_smart_query` | Test a canonical query definition without saving |
167
170
 
168
171
  ### Orchestrations
169
172
  | Tool | Description |
@@ -19,6 +19,15 @@ export type ToolResult = {
19
19
  *
20
20
  * Call sites annotate the arg shape explicitly via the `Args` generic so the
21
21
  * handler body stays fully type-safe.
22
+ *
23
+ * CEN-1099 / CEN-1186 — re-investigated when query tool inputs were tightened
24
+ * to the canonical `QueryDefinition` shape (small, finite). Direct
25
+ * `server.tool(...)` still trips `TS2589 Type instantiation is excessively
26
+ * deep and possibly infinite` against the smaller schema, so this wrapper
27
+ * stays. Repro:
28
+ * import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
29
+ * server.tool("x", "x", { resource: z.string() }, async (a) => ({ content: [] }));
30
+ * // → TS2589 from `ShapeOutput` over `ZodRawShape`.
22
31
  */
23
32
  export declare function registerTool<Args>(server: McpServer, name: string, description: string, schema: ZodRawShape, handler: (args: Args) => Promise<ToolResult>): void;
24
33
  export declare function formatError(error: unknown, context: string): string;
@@ -14,6 +14,15 @@ exports.formatError = formatError;
14
14
  *
15
15
  * Call sites annotate the arg shape explicitly via the `Args` generic so the
16
16
  * handler body stays fully type-safe.
17
+ *
18
+ * CEN-1099 / CEN-1186 — re-investigated when query tool inputs were tightened
19
+ * to the canonical `QueryDefinition` shape (small, finite). Direct
20
+ * `server.tool(...)` still trips `TS2589 Type instantiation is excessively
21
+ * deep and possibly infinite` against the smaller schema, so this wrapper
22
+ * stays. Repro:
23
+ * import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
24
+ * server.tool("x", "x", { resource: z.string() }, async (a) => ({ content: [] }));
25
+ * // → TS2589 from `ShapeOutput` over `ZodRawShape`.
17
26
  */
18
27
  function registerTool(server, name, description, schema, handler) {
19
28
  server.tool(name, description, schema, handler);
@@ -585,14 +585,14 @@ function registerDescribeTools(server) {
585
585
  });
586
586
  }));
587
587
  // ── Records ────────────────────────────────────────────────────────
588
- (0, _register_js_1.registerTool)(server, "describe_records", "Get the schema reference for Centrali record operations. Explains filter syntax, sort syntax, pagination, expand, and the data. prefix convention.", {}, () => __awaiter(this, void 0, void 0, function* () {
588
+ (0, _register_js_1.registerTool)(server, "describe_records", "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.", {}, () => __awaiter(this, void 0, void 0, function* () {
589
589
  return ({
590
590
  content: [
591
591
  {
592
592
  type: "text",
593
593
  text: JSON.stringify({
594
594
  domain: "Records",
595
- description: "Records are rows of data stored in collections. All custom field values live under the 'data' namespace.",
595
+ description: "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.",
596
596
  record_shape: {
597
597
  id: "UUID — auto-generated",
598
598
  collectionId: "UUID — the collection this record belongs to",
@@ -603,71 +603,82 @@ function registerDescribeTools(server) {
603
603
  updatedBy: "UUID — user who last updated",
604
604
  isDeleted: "boolean — true if soft-deleted",
605
605
  },
606
- filter_syntax: {
607
- description: "Filters are passed as key-value pairs. Keys use the 'data.' prefix for custom fields and bracket notation for operators.",
608
- examples: {
609
- "Exact match": { "data.status": "active" },
610
- "Greater than": { "data.price[gt]": 100 },
611
- "Less than or equal": { "data.age[lte]": 30 },
612
- "Not equal": { "data.type[ne]": "draft" },
613
- "In list": { "data.category[in]": "electronics,books" },
614
- "Not in list": { "data.status[nin]": "deleted,archived" },
615
- "Contains (text)": { "data.name[contains]": "john" },
616
- "Starts with": { "data.email[startswith]": "admin" },
617
- "Ends with": { "data.domain[endswith]": ".com" },
618
- },
619
- operators: [
620
- "eq (default, no bracket needed)",
621
- "ne (not equal)",
622
- "gt (greater than)",
623
- "gte (greater than or equal)",
624
- "lt (less than)",
625
- "lte (less than or equal)",
626
- "in (comma-separated list)",
627
- "nin (not in comma-separated list)",
628
- "contains (substring match)",
629
- "startswith (prefix match)",
630
- "endswith (suffix match)",
606
+ query_definition: {
607
+ description: "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).",
608
+ shape: {
609
+ resource: "string collection slug",
610
+ where: "WhereExpression — FieldConditionMap | { and: [...] } | { or: [...] } | { not: ... }",
611
+ text: "{ query: string, fields?: string[], typoTolerance?: boolean }",
612
+ sort: "[{ field: string, direction: 'asc' | 'desc' }]",
613
+ page: "{ limit: number, offset?: number } | { limit: number, cursor?: string }",
614
+ select: "{ fields: string[] }",
615
+ include: "[{ relation: string }] — relation expansion (collection-defined relations)",
616
+ },
617
+ },
618
+ operators: {
619
+ description: "Field operators. Use exactly one per FieldCondition.",
620
+ list: [
621
+ "eq equal",
622
+ "ne not equal",
623
+ "gt greater than",
624
+ "gte greater than or equal",
625
+ "lt less than",
626
+ "lte less than or equal",
627
+ "in value in array",
628
+ "nin value not in array",
629
+ "contains substring match",
630
+ "startsWith prefix match",
631
+ "endsWith — suffix match",
632
+ "hasAny — array field has any of these",
633
+ "hasAll — array field has all of these",
634
+ "exists — boolean: field is set",
631
635
  ],
636
+ boolean_tree: ["and", "or", "not"],
632
637
  },
633
- sort_syntax: {
634
- description: "Sort by a single field. Prefix with '-' for descending order.",
635
- examples: {
636
- "Ascending by date": "createdAt",
637
- "Descending by date": "-createdAt",
638
- "By custom field asc": "data.name",
639
- "By custom field desc": "-data.price",
638
+ examples: {
639
+ "Exact match": { "data.status": { eq: "active" } },
640
+ "Greater than": { "data.price": { gt: 100 } },
641
+ "Boolean tree": {
642
+ and: [
643
+ { "data.status": { eq: "open" } },
644
+ { or: [{ "data.amount": { gte: 100 } }, { "data.priority": { eq: "high" } }] },
645
+ ],
640
646
  },
641
- },
642
- pagination: {
643
- page: "1-indexed page number (default: 1)",
644
- pageSize: "Records per page (default: 50, max: 500)",
645
- response_includes: "total, page, pageSize, totalPages in the response metadata",
646
- },
647
- expand: {
648
- description: "Comma-separated list of reference field names to expand (join). Returns the full referenced record instead of just the ID.",
649
- example: "expand: 'customer,product'",
650
- },
651
- dateWindow: {
652
- description: "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).",
653
- shape: {
654
- field: "string — date field to filter on (e.g., 'createdAt', 'updatedAt', or a custom date field)",
655
- from: "string? — ISO 8601 lower bound (inclusive)",
656
- to: "string? — ISO 8601 upper bound (inclusive)",
647
+ "Date window": {
648
+ and: [
649
+ { createdAt: { gte: "2024-03-01T00:00:00Z" } },
650
+ { createdAt: { lte: "2024-03-31T23:59:59Z" } },
651
+ ],
657
652
  },
658
- examples: {
659
- "Last 30 days": { field: "createdAt", from: "2024-03-01T00:00:00Z" },
660
- "Specific range": { field: "updatedAt", from: "2024-01-01T00:00:00Z", to: "2024-03-31T23:59:59Z" },
653
+ "Sort + page": {
654
+ sort: [{ field: "createdAt", direction: "desc" }],
655
+ page: { limit: 50 },
661
656
  },
657
+ "Projection": { select: { fields: ["id", "data.status", "data.amount"] } },
658
+ "Full-text search": { text: { query: "urgent shipping", fields: ["data.notes"] } },
662
659
  },
663
- includeDeleted: {
664
- description: "Set to true to include soft-deleted records in the results (default: false). Alias: includeArchived, all.",
660
+ response_envelope: {
661
+ description: "All canonical queries return { data, meta }.",
662
+ shape: {
663
+ data: "T[]",
664
+ meta: {
665
+ limit: "number",
666
+ offset: "number? (offset mode)",
667
+ cursor: "string? (cursor mode)",
668
+ nextCursor: "string?",
669
+ hasMore: "boolean?",
670
+ total: "number? (when computable; depends on executor)",
671
+ processingTimeMs: "number?",
672
+ mode: "'filter' | 'search' | 'hybrid'",
673
+ },
674
+ },
665
675
  },
666
- includeTotal: {
667
- description: "Set to true to include the total record count in response metadata (default: false).",
676
+ page_defaults: {
677
+ limit_default: 50,
678
+ limit_max: 500,
668
679
  },
669
680
  upsert: {
670
- description: "Atomic create-or-update. Provide 'match' fields to find existing record and 'data' for the full record body.",
681
+ description: "Atomic create-or-update via upsert_record. Provide 'match' fields to find an existing record and 'data' for the full record body.",
671
682
  example: {
672
683
  match: { sku: "WIDGET-001" },
673
684
  data: { sku: "WIDGET-001", name: "Widget", price: 9.99 },
@@ -677,6 +688,7 @@ function registerDescribeTools(server) {
677
688
  tips: [
678
689
  "Always use the 'data.' prefix when filtering on custom fields",
679
690
  "System fields (id, createdAt, updatedAt) don't need the 'data.' prefix",
691
+ "Each FieldCondition must use exactly one operator — '{ eq: 1, ne: 2 }' is rejected",
680
692
  "Use upsert_record for idempotent imports — it won't create duplicates",
681
693
  "Soft-deleted records can be restored with restore_record",
682
694
  "Use get_records_by_ids for batch lookups (more efficient than individual get_record calls)",
@@ -974,73 +986,77 @@ function registerDescribeTools(server) {
974
986
  });
975
987
  }));
976
988
  // ── Smart Queries ──────────────────────────────────────────────────
977
- (0, _register_js_1.registerTool)(server, "describe_smart_queries", "Get the schema reference for Centrali smart queries. Explains parameterized queries, variable substitution, and execution.", {}, () => __awaiter(this, void 0, void 0, function* () {
989
+ (0, _register_js_1.registerTool)(server, "describe_smart_queries", "Get the schema reference for Centrali saved (a.k.a. smart) queries. Explains the canonical QueryDefinition body, variable substitution, and execution.", {}, () => __awaiter(this, void 0, void 0, function* () {
978
990
  return ({
979
991
  content: [
980
992
  {
981
993
  type: "text",
982
994
  text: JSON.stringify({
983
- domain: "Smart Queries",
984
- description: "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.",
985
- smart_query_shape: {
995
+ domain: "Saved Queries (a.k.a. Smart Queries)",
996
+ description: "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.",
997
+ saved_query_shape: {
986
998
  id: "UUID",
987
999
  name: "string — display name",
988
1000
  recordSlug: "string — the collection this query targets",
989
1001
  description: "string | null",
990
- query: "object — the query definition with filters, sort, fields, and aggregations",
991
- variables: "object[] — declared variables with name, type, and default values",
1002
+ queryDefinition: "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.",
992
1003
  },
1004
+ query_definition_reference: "Use describe_records for the full QueryDefinition shape, operator vocabulary, and examples.",
993
1005
  variable_syntax: {
994
- description: "Variables use double-brace syntax: {{variableName}}. When executing, pass a variables object to substitute values.",
995
- example: {
996
- query_filter: {
997
- "data.status": "{{status}}",
998
- "data.amount[gte]": "{{minAmount}}",
999
- },
1000
- execution_variables: {
1001
- status: "active",
1002
- minAmount: "1000",
1006
+ description: "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.",
1007
+ example_query_definition: {
1008
+ where: {
1009
+ and: [
1010
+ { "data.status": { eq: "{{statusFilter}}" } },
1011
+ { "data.amount": { gte: "{{minAmount}}" } },
1012
+ ],
1003
1013
  },
1014
+ sort: [{ field: "createdAt", direction: "desc" }],
1015
+ page: { limit: 100 },
1016
+ },
1017
+ example_execution_variables: {
1018
+ statusFilter: "active",
1019
+ minAmount: 1000,
1004
1020
  },
1005
- note: "Variable values are always passed as strings the query engine handles type coercion",
1021
+ note: "Pass values via the 'variables' arg on execute_smart_query / test_smart_query. Values are substituted before the engine validates the query.",
1006
1022
  },
1007
1023
  crud_tools: {
1008
1024
  get_smart_query: {
1009
- description: "Get a smart query by ID, including its full definition",
1025
+ description: "Get a saved query by ID, including its full canonical definition",
1010
1026
  required_params: ["recordSlug", "queryId"],
1011
1027
  },
1012
1028
  create_smart_query: {
1013
- description: "Create a new smart query for a collection",
1029
+ description: "Create a new saved query for a collection",
1014
1030
  required_params: ["recordSlug", "name", "queryDefinition"],
1015
1031
  optional_params: ["description"],
1016
1032
  queryDefinition_example: {
1017
- where: { status: { "$eq": "active" } },
1033
+ where: { "data.status": { eq: "active" } },
1018
1034
  sort: [{ field: "createdAt", direction: "desc" }],
1019
- limit: 100,
1035
+ page: { limit: 100 },
1020
1036
  },
1021
1037
  },
1022
1038
  update_smart_query: {
1023
- description: "Update an existing smart query. Partial updates supported.",
1039
+ description: "Update an existing saved query. Partial updates supported.",
1024
1040
  required_params: ["recordSlug", "queryId"],
1025
1041
  optional_params: ["name", "description", "queryDefinition"],
1026
1042
  },
1027
1043
  delete_smart_query: {
1028
- description: "Delete a smart query by ID",
1044
+ description: "Delete a saved query by ID",
1029
1045
  required_params: ["recordSlug", "queryId"],
1030
1046
  },
1031
1047
  test_smart_query: {
1032
- description: "Test execute a query definition without saving it. Preview results before creating.",
1048
+ description: "Test execute a canonical query definition without saving it. Preview results before creating.",
1033
1049
  required_params: ["recordSlug", "queryDefinition"],
1034
1050
  optional_params: ["variables"],
1035
1051
  },
1036
1052
  },
1037
1053
  tips: [
1038
1054
  "Use list_smart_queries to discover available queries (optionally filter by collection slug)",
1039
- "Smart queries return the same result shape as query_records",
1040
- "Variables are defined when creating the query in the console check the query's variables array to see what's expected",
1041
- "Prefer smart queries over ad-hoc query_records filters for complex/reusable logic",
1042
- "Use test_smart_query to validate query syntax and preview results before saving",
1043
- "Use create_smart_query to save reusable queries that can be executed with execute_smart_query",
1055
+ "Saved queries return the same canonical { data, meta } envelope as query_records",
1056
+ "Variables are declared when creating the query — read the query's 'variables' field to see what's expected",
1057
+ "Prefer saved queries over ad-hoc query_records filters for reusable logic",
1058
+ "Use test_smart_query to validate canonical syntax and preview results before saving",
1059
+ "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', …)",
1044
1060
  ],
1045
1061
  }, null, 2),
1046
1062
  },
@@ -1,3 +1,38 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { CentraliSDK } from "@centrali-io/centrali-sdk";
3
+ declare function parseLegacyFilters(filters: unknown): unknown;
4
+ declare function parseLegacySort(sort: string): Array<{
5
+ field: string;
6
+ direction: "asc" | "desc";
7
+ }>;
8
+ type QueryRecordsArgs = {
9
+ resource?: string;
10
+ recordSlug?: string;
11
+ where?: unknown;
12
+ text?: unknown;
13
+ sort?: unknown;
14
+ page?: unknown;
15
+ pageSize?: number;
16
+ select?: unknown;
17
+ include?: unknown;
18
+ filters?: Record<string, unknown>;
19
+ expand?: string;
20
+ dateWindow?: {
21
+ field: string;
22
+ from?: string;
23
+ to?: string;
24
+ };
25
+ includeDeleted?: boolean;
26
+ includeTotal?: boolean;
27
+ };
28
+ declare function translateQueryRecordsArgs(args: QueryRecordsArgs): {
29
+ resource: string;
30
+ definition: Record<string, unknown>;
31
+ };
32
+ export declare const _internal: {
33
+ translateQueryRecordsArgs: typeof translateQueryRecordsArgs;
34
+ parseLegacyFilters: typeof parseLegacyFilters;
35
+ parseLegacySort: typeof parseLegacySort;
36
+ };
3
37
  export declare function registerRecordTools(server: McpServer, sdk: CentraliSDK, centraliUrl: string, workspaceId: string): void;
38
+ export {};