@baasix/baasix 0.1.50 → 0.1.52

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.
@@ -83,10 +83,99 @@ function errorResult(error) {
83
83
  };
84
84
  }
85
85
  // ==================== MCP Server Creation ====================
86
+ // Map every tool to its action category: "read" | "create" | "update" | "delete"
87
+ const TOOL_ACTION_MAP = {
88
+ // Schema Management
89
+ baasix_list_schemas: "read",
90
+ baasix_get_schema: "read",
91
+ baasix_export_schemas: "read",
92
+ baasix_create_schema: "create",
93
+ baasix_import_schemas: "create",
94
+ baasix_add_index: "create",
95
+ baasix_create_relationship: "create",
96
+ baasix_update_schema: "update",
97
+ baasix_update_relationship: "update",
98
+ baasix_delete_schema: "delete",
99
+ baasix_remove_index: "delete",
100
+ baasix_delete_relationship: "delete",
101
+ // Item Management
102
+ baasix_list_items: "read",
103
+ baasix_get_item: "read",
104
+ baasix_create_item: "create",
105
+ baasix_update_item: "update",
106
+ baasix_delete_item: "delete",
107
+ // File Management
108
+ baasix_list_files: "read",
109
+ baasix_get_file_info: "read",
110
+ baasix_delete_file: "delete",
111
+ // Authentication / Session
112
+ baasix_auth_status: "read",
113
+ baasix_refresh_auth: "read",
114
+ baasix_login: "create",
115
+ baasix_logout: "delete",
116
+ baasix_get_current_user: "read",
117
+ baasix_register_user: "create",
118
+ baasix_send_invite: "create",
119
+ baasix_verify_invite: "read",
120
+ baasix_send_magic_link: "create",
121
+ baasix_get_user_tenants: "read",
122
+ baasix_switch_tenant: "update",
123
+ // Reports and Analytics
124
+ baasix_generate_report: "read",
125
+ baasix_collection_stats: "read",
126
+ // Notifications
127
+ baasix_list_notifications: "read",
128
+ baasix_send_notification: "create",
129
+ baasix_mark_notification_seen: "update",
130
+ // Settings
131
+ baasix_get_settings: "read",
132
+ baasix_update_settings: "update",
133
+ // Email Templates
134
+ baasix_list_templates: "read",
135
+ baasix_get_template: "read",
136
+ baasix_update_template: "update",
137
+ // Permissions
138
+ baasix_list_roles: "read",
139
+ baasix_list_permissions: "read",
140
+ baasix_get_permission: "read",
141
+ baasix_get_permissions: "read",
142
+ baasix_create_permission: "create",
143
+ baasix_update_permission: "update",
144
+ baasix_delete_permission: "delete",
145
+ baasix_reload_permissions: "read",
146
+ baasix_update_permissions: "update",
147
+ // Utilities
148
+ baasix_server_info: "read",
149
+ baasix_sort_items: "update",
150
+ };
151
+ /**
152
+ * Parse MCP_ENABLED_ACTIONS env var.
153
+ * Accepts a comma-separated list of: "all", "read", "create", "update", "delete"
154
+ * Returns null when all actions are enabled (default), or a Set of enabled action strings.
155
+ */
156
+ function parseEnabledActions() {
157
+ const raw = (env.get("MCP_ENABLED_ACTIONS") || "all").toLowerCase().trim();
158
+ const actions = raw.split(",").map((a) => a.trim()).filter(Boolean);
159
+ if (actions.includes("all"))
160
+ return null; // null = everything enabled
161
+ return new Set(actions);
162
+ }
163
+ /**
164
+ * Check if a tool should be registered based on MCP_ENABLED_ACTIONS.
165
+ */
166
+ function isToolEnabled(toolName, enabledActions) {
167
+ if (!enabledActions)
168
+ return true; // "all"
169
+ const action = TOOL_ACTION_MAP[toolName];
170
+ if (!action)
171
+ return true; // unknown tools stay enabled
172
+ return enabledActions.has(action);
173
+ }
86
174
  /**
87
175
  * Create and configure the MCP server with all Baasix tools
88
176
  */
89
177
  export function createMCPServer() {
178
+ const enabledActions = parseEnabledActions();
90
179
  const server = new McpServer({
91
180
  name: "Baasix Backend-as-a-Service",
92
181
  version: "0.1.0",
@@ -181,7 +270,16 @@ Call baasix_create_schema with:
181
270
  // All schema tools call the REST routes via HTTP with the session's bearer token.
182
271
  // This ensures route-level validation, processSchemaFlags, permission checks and cache
183
272
  // invalidation are applied exactly once (in the route) — zero duplicated logic.
184
- server.tool("baasix_list_schemas", "List all database tables (collections) and their schema definitions. Use this to discover what tables exist. System tables are prefixed with 'baasix_'.", {
273
+ /**
274
+ * Conditionally register a tool based on MCP_ENABLED_ACTIONS.
275
+ * Wraps server.tool() — if the tool's action category is not enabled, skips registration entirely.
276
+ */
277
+ function registerTool(name, ...rest) {
278
+ if (!isToolEnabled(name, enabledActions))
279
+ return;
280
+ server.tool(name, ...rest);
281
+ }
282
+ registerTool("baasix_list_schemas", "List all database tables (collections) and their schema definitions. Use this to discover what tables exist. System tables are prefixed with 'baasix_'.", {
185
283
  search: z.string().optional().describe("Search term to filter schemas by collection name"),
186
284
  page: z.number().optional().default(1).describe("Page number for pagination"),
187
285
  limit: z.number().optional().default(10).describe("Number of schemas per page"),
@@ -203,7 +301,7 @@ Call baasix_create_schema with:
203
301
  return errorResult(res.error || 'Failed to list schemas');
204
302
  return successResult(res.data);
205
303
  });
206
- server.tool("baasix_get_schema", `Get the full schema definition (columns, types, constraints, relationships, indexes) for a specific database table/collection.
304
+ registerTool("baasix_get_schema", `Get the full schema definition (columns, types, constraints, relationships, indexes) for a specific database table/collection.
207
305
 
208
306
  IMPORTANT: Always call this BEFORE using baasix_update_schema to get the current schema.
209
307
  The update_schema tool performs a full replacement, so you need the complete current schema to avoid losing existing fields.`, {
@@ -215,7 +313,7 @@ The update_schema tool performs a full replacement, so you need the complete cur
215
313
  return errorResult(res.error || `Schema '${collection}' not found`);
216
314
  return successResult(res.data);
217
315
  });
218
- server.tool("baasix_create_schema", `CREATE A NEW DATABASE TABLE. This is the tool to use when asked to "create a table", "create a collection", "add a new model", or "define a new entity".
316
+ registerTool("baasix_create_schema", `CREATE A NEW DATABASE TABLE. This is the tool to use when asked to "create a table", "create a collection", "add a new model", or "define a new entity".
219
317
 
220
318
  This creates both the schema definition AND the actual PostgreSQL table with all specified columns.
221
319
 
@@ -258,7 +356,7 @@ schema: { "timestamps": true, "fields": { "id": { "type": "UUID", "primaryKey":
258
356
  collectionName: collection,
259
357
  });
260
358
  });
261
- server.tool("baasix_update_schema", `Modify an existing database table — add new columns, change column types, or remove columns.
359
+ registerTool("baasix_update_schema", `Modify an existing database table — add new columns, change column types, or remove columns.
262
360
  Use this when asked to 'add a field', 'add a column', or 'alter a table'.
263
361
 
264
362
  ⚠️ CRITICAL: This performs a FULL REPLACEMENT of the schema definition, NOT a merge.
@@ -310,7 +408,7 @@ EXAMPLE — Adding a "description" field to an existing "products" table that ha
310
408
  return errorResult(res.error || `Failed to update collection '${collection}'`);
311
409
  return successResult({ success: true, message: `Collection '${collection}' updated successfully` });
312
410
  });
313
- server.tool("baasix_delete_schema", `DROP/DELETE an entire database table and all its data permanently.
411
+ registerTool("baasix_delete_schema", `DROP/DELETE an entire database table and all its data permanently.
314
412
  Use this when asked to 'drop a table', 'delete a collection', or 'remove a table'.
315
413
 
316
414
  ⚠️ WARNING: This is destructive and irreversible. The table, all its data, indexes, and relationships will be permanently deleted.
@@ -323,7 +421,7 @@ Do NOT use this to remove a column — use baasix_update_schema instead to modif
323
421
  return errorResult(res.error || `Failed to delete collection '${collection}'`);
324
422
  return successResult({ success: true, message: `Collection '${collection}' deleted successfully` });
325
423
  });
326
- server.tool("baasix_add_index", `Add a database index to a table for better query performance.
424
+ registerTool("baasix_add_index", `Add a database index to a table for better query performance.
327
425
  This is an ADDITIVE operation — it only adds the new index without affecting existing indexes or schema fields.
328
426
  Supports btree (default), hash, gin, and gist index types.
329
427
 
@@ -357,7 +455,7 @@ EXAMPLE — Add a GIN index for JSONB or array fields:
357
455
  return errorResult(res.error || `Failed to add index to '${collection}'`);
358
456
  return successResult({ success: true, message: `Index added to '${collection}' successfully` });
359
457
  });
360
- server.tool("baasix_remove_index", "Remove an existing index from a database table. This only removes the index — it does NOT delete any data or columns. Use baasix_get_schema first to see the list of existing indexes if you don't know the index name.", {
458
+ registerTool("baasix_remove_index", "Remove an existing index from a database table. This only removes the index — it does NOT delete any data or columns. Use baasix_get_schema first to see the list of existing indexes if you don't know the index name.", {
361
459
  collection: z.string().describe("Collection name"),
362
460
  indexName: z.string().describe("Name of the index to remove"),
363
461
  }, async (args, extra) => {
@@ -367,7 +465,7 @@ EXAMPLE — Add a GIN index for JSONB or array fields:
367
465
  return errorResult(res.error || `Failed to remove index '${indexName}' from '${collection}'`);
368
466
  return successResult({ success: true, message: `Index '${indexName}' removed from '${collection}'` });
369
467
  });
370
- server.tool("baasix_create_relationship", `Create a foreign key / relationship between two database tables. Use this when asked to 'link tables', 'add a foreign key', 'create a relation', or 'connect collections'.
468
+ registerTool("baasix_create_relationship", `Create a foreign key / relationship between two database tables. Use this when asked to 'link tables', 'add a foreign key', 'create a relation', or 'connect collections'.
371
469
 
372
470
  RELATIONSHIP TYPES:
373
471
  - M2O (Many-to-One / BelongsTo): Adds a foreign key column. Example: products.category_Id → categories.id
@@ -410,7 +508,7 @@ relationshipData: { "name": "category", "type": "M2O", "target": "categories", "
410
508
  message: `Relationship '${relationshipData.name}' created on '${sourceCollection}'`,
411
509
  });
412
510
  });
413
- server.tool("baasix_delete_relationship", `Remove a foreign key / relationship from a database table.
511
+ registerTool("baasix_delete_relationship", `Remove a foreign key / relationship from a database table.
414
512
  This drops the foreign key constraint and the FK column (e.g., category_Id) from the source table.
415
513
  For M2M relationships, this also removes the junction table.
416
514
 
@@ -425,13 +523,13 @@ Do NOT confuse with baasix_delete_schema (which deletes an entire table).`, {
425
523
  return errorResult(res.error || `Failed to remove relationship '${fieldName}' from '${sourceCollection}'`);
426
524
  return successResult({ success: true, message: `Relationship '${fieldName}' removed from '${sourceCollection}'` });
427
525
  });
428
- server.tool("baasix_export_schemas", "Export all table definitions as JSON for backup or migration.", {}, async (_args, extra) => {
526
+ registerTool("baasix_export_schemas", "Export all table definitions as JSON for backup or migration.", {}, async (_args, extra) => {
429
527
  const res = await callRoute('GET', '/schemas-export', extra);
430
528
  if (!res.ok)
431
529
  return errorResult(res.error || 'Failed to export schemas');
432
530
  return successResult(res.data);
433
531
  });
434
- server.tool("baasix_import_schemas", "Import table definitions from JSON data to recreate tables.", {
532
+ registerTool("baasix_import_schemas", "Import table definitions from JSON data to recreate tables.", {
435
533
  schemas: z.record(z.any()).describe("Schema data to import"),
436
534
  }, async (args, extra) => {
437
535
  const { schemas } = args;
@@ -465,7 +563,7 @@ Do NOT confuse with baasix_delete_schema (which deletes an entire table).`, {
465
563
  }
466
564
  });
467
565
  // ==================== Item Management Tools ====================
468
- server.tool("baasix_list_items", `Query/list/fetch rows from a database table. Use this when asked to 'get data', 'list records', 'query items', 'search', or 'fetch rows' from any table.
566
+ registerTool("baasix_list_items", `Query/list/fetch rows from a database table. Use this when asked to 'get data', 'list records', 'query items', 'search', or 'fetch rows' from any table.
469
567
 
470
568
  NOTE: For analytics, summaries, totals, sums, averages, counts, min/max, grouped reports, or dashboards → use baasix_generate_report instead. Use this tool (list_items) for fetching actual row data.
471
569
 
@@ -592,7 +690,7 @@ Supports same operators and dynamic variables as filter.
592
690
  return errorResult(error);
593
691
  }
594
692
  });
595
- server.tool("baasix_get_item", `Get a single row/record by its ID from a database table. Optionally include related data from linked tables.
693
+ registerTool("baasix_get_item", `Get a single row/record by its ID from a database table. Optionally include related data from linked tables.
596
694
 
597
695
  FIELDS — use dot notation to include related data:
598
696
  - ["*"] → all columns on this table
@@ -619,7 +717,7 @@ The primary key is always returned.`, {
619
717
  return errorResult(error);
620
718
  }
621
719
  });
622
- server.tool("baasix_create_item", `Insert a new row/record into a database table. Use this when asked to 'add data', 'insert a record', or 'create an entry'.
720
+ registerTool("baasix_create_item", `Insert a new row/record into a database table. Use this when asked to 'add data', 'insert a record', or 'create an entry'.
623
721
 
624
722
  Pass field values as a key-value object. For UUID primary keys with UUIDV4 default, you can omit the id field — it will be auto-generated.
625
723
  For tables with timestamps: true, createdAt and updatedAt are auto-managed.
@@ -640,7 +738,7 @@ EXAMPLE: collection: "products", data: {"name": "Widget", "price": 9.99, "catego
640
738
  return errorResult(error);
641
739
  }
642
740
  });
643
- server.tool("baasix_update_item", `Update/modify an existing row/record in a database table by its ID. Only pass the fields you want to change — unspecified fields remain unchanged.
741
+ registerTool("baasix_update_item", `Update/modify an existing row/record in a database table by its ID. Only pass the fields you want to change — unspecified fields remain unchanged.
644
742
 
645
743
  For tables with timestamps: true, updatedAt is automatically updated.
646
744
  For foreign key fields, use the "_Id" suffixed column name (e.g., category_Id).`, {
@@ -659,7 +757,7 @@ For foreign key fields, use the "_Id" suffixed column name (e.g., category_Id).`
659
757
  return errorResult(error);
660
758
  }
661
759
  });
662
- server.tool("baasix_delete_item", "Delete a row/record from a database table by its ID. For tables with paranoid: true (soft delete), the record is marked as deleted (deletedAt is set) rather than permanently removed.", {
760
+ registerTool("baasix_delete_item", "Delete a row/record from a database table by its ID. For tables with paranoid: true (soft delete), the record is marked as deleted (deletedAt is set) rather than permanently removed.", {
663
761
  collection: z.string().describe("Table/collection name"),
664
762
  id: z.string().describe("Row ID (UUID) to delete"),
665
763
  }, async (args, extra) => {
@@ -675,7 +773,7 @@ For foreign key fields, use the "_Id" suffixed column name (e.g., category_Id).`
675
773
  }
676
774
  });
677
775
  // ==================== File Management Tools ====================
678
- server.tool("baasix_list_files", `List uploaded files with metadata (filename, size, type, dimensions, storage location).
776
+ registerTool("baasix_list_files", `List uploaded files with metadata (filename, size, type, dimensions, storage location).
679
777
 
680
778
  Filter examples:
681
779
  By type: {"type": {"startsWith": "image/"}}
@@ -704,7 +802,7 @@ Public files only: {"isPublic": {"eq": true}}`, {
704
802
  return errorResult(error);
705
803
  }
706
804
  });
707
- server.tool("baasix_get_file_info", "Get detailed metadata about a specific uploaded file — filename, size, MIME type, dimensions, storage location, and who uploaded it.", {
805
+ registerTool("baasix_get_file_info", "Get detailed metadata about a specific uploaded file — filename, size, MIME type, dimensions, storage location, and who uploaded it.", {
708
806
  id: z.string().describe("File UUID"),
709
807
  }, async (args, extra) => {
710
808
  const { id } = args;
@@ -718,7 +816,7 @@ Public files only: {"isPublic": {"eq": true}}`, {
718
816
  return errorResult(error);
719
817
  }
720
818
  });
721
- server.tool("baasix_delete_file", "Delete an uploaded file by its UUID. Removes both the file record and the actual file from storage.", {
819
+ registerTool("baasix_delete_file", "Delete an uploaded file by its UUID. Removes both the file record and the actual file from storage.", {
722
820
  id: z.string().describe("File UUID"),
723
821
  }, async (args, extra) => {
724
822
  const { id } = args;
@@ -733,7 +831,7 @@ Public files only: {"isPublic": {"eq": true}}`, {
733
831
  }
734
832
  });
735
833
  // ==================== Permission Tools ====================
736
- server.tool("baasix_list_roles", "List all user roles with their IDs, names, and descriptions. Use this to get role UUIDs needed for permission tools. Default roles: 'administrator' (full access), 'public' (unauthenticated access). Custom roles can be created for granular access control.", {}, async (_args, extra) => {
834
+ registerTool("baasix_list_roles", "List all user roles with their IDs, names, and descriptions. Use this to get role UUIDs needed for permission tools. Default roles: 'administrator' (full access), 'public' (unauthenticated access). Custom roles can be created for granular access control.", {}, async (_args, extra) => {
737
835
  try {
738
836
  const res = await callRoute('GET', '/items/baasix_Role?limit=-1', extra);
739
837
  if (!res.ok)
@@ -744,7 +842,7 @@ Public files only: {"isPublic": {"eq": true}}`, {
744
842
  return errorResult(error);
745
843
  }
746
844
  });
747
- server.tool("baasix_list_permissions", `List all access control permission rules with optional filtering.
845
+ registerTool("baasix_list_permissions", `List all access control permission rules with optional filtering.
748
846
 
749
847
  Each permission defines: role_Id (which role), collection (which table), action (create/read/update/delete), fields (which columns), conditions (row-level security), defaultValues (auto-set values), and relConditions (related table security).
750
848
 
@@ -780,7 +878,7 @@ Permissions for a table: {"collection": {"eq": "products"}}`, {
780
878
  return errorResult(error);
781
879
  }
782
880
  });
783
- server.tool("baasix_create_permission", `Grant a role permission to perform an action on a table. Use this to set up access control (RBAC + row-level security).
881
+ registerTool("baasix_create_permission", `Grant a role permission to perform an action on a table. Use this to set up access control (RBAC + row-level security).
784
882
 
785
883
  First use baasix_list_roles to get the role UUID, then create permissions for that role.
786
884
 
@@ -862,7 +960,7 @@ role_Id: "<uuid>", collection: "posts", action: "update", fields: ["title", "con
862
960
  return errorResult(error);
863
961
  }
864
962
  });
865
- server.tool("baasix_update_permission", `Update an existing permission rule. Only pass the fields you want to change — unspecified fields remain unchanged.
963
+ registerTool("baasix_update_permission", `Update an existing permission rule. Only pass the fields you want to change — unspecified fields remain unchanged.
866
964
 
867
965
  See baasix_create_permission for full documentation of conditions, relConditions, defaultValues, and fields options.
868
966
  Use baasix_list_permissions or baasix_get_permissions to find the permission ID to update.`, {
@@ -886,7 +984,7 @@ Use baasix_list_permissions or baasix_get_permissions to find the permission ID
886
984
  return errorResult(error);
887
985
  }
888
986
  });
889
- server.tool("baasix_delete_permission", "Delete a permission rule by its UUID. This revokes the access it granted. The permission cache is automatically reloaded.", {
987
+ registerTool("baasix_delete_permission", "Delete a permission rule by its UUID. This revokes the access it granted. The permission cache is automatically reloaded.", {
890
988
  id: z.string().describe("Permission UUID — get from baasix_list_permissions or baasix_get_permissions"),
891
989
  }, async (args, extra) => {
892
990
  const { id } = args;
@@ -900,7 +998,7 @@ Use baasix_list_permissions or baasix_get_permissions to find the permission ID
900
998
  return errorResult(error);
901
999
  }
902
1000
  });
903
- server.tool("baasix_reload_permissions", "Force-reload the permission cache from the database. Normally not needed as create/update/delete permission tools auto-reload. Use this if permissions seem stale.", {}, async (_args, extra) => {
1001
+ registerTool("baasix_reload_permissions", "Force-reload the permission cache from the database. Normally not needed as create/update/delete permission tools auto-reload. Use this if permissions seem stale.", {}, async (_args, extra) => {
904
1002
  try {
905
1003
  const res = await callRoute('POST', '/permissions/reload', extra);
906
1004
  if (!res.ok)
@@ -912,7 +1010,7 @@ Use baasix_list_permissions or baasix_get_permissions to find the permission ID
912
1010
  }
913
1011
  });
914
1012
  // ==================== Settings Tools ====================
915
- server.tool("baasix_get_settings", "Get application settings. Pass a key to get a specific setting, or omit to get all settings. Settings include project info, auth config, email config, etc.", {
1013
+ registerTool("baasix_get_settings", "Get application settings. Pass a key to get a specific setting, or omit to get all settings. Settings include project info, auth config, email config, etc.", {
916
1014
  key: z.string().optional().describe("Specific setting key to retrieve (e.g., 'project_name', 'auth_password_policy'). Omit for all settings."),
917
1015
  }, async (args, extra) => {
918
1016
  const { key } = args;
@@ -927,7 +1025,7 @@ Use baasix_list_permissions or baasix_get_permissions to find the permission ID
927
1025
  return errorResult(error);
928
1026
  }
929
1027
  });
930
- server.tool("baasix_update_settings", "Update application settings. Pass an object with the settings keys and their new values. Only specified keys are updated.", {
1028
+ registerTool("baasix_update_settings", "Update application settings. Pass an object with the settings keys and their new values. Only specified keys are updated.", {
931
1029
  settings: z.record(z.any()).describe("Settings to update as {key: value}. Use baasix_get_settings first to see available keys."),
932
1030
  }, async (args, extra) => {
933
1031
  const { settings } = args;
@@ -945,7 +1043,7 @@ Use baasix_list_permissions or baasix_get_permissions to find the permission ID
945
1043
  }
946
1044
  });
947
1045
  // ==================== Email Template Tools ====================
948
- server.tool("baasix_list_templates", "List all email templates (invitation, password reset, welcome, etc.).", {
1046
+ registerTool("baasix_list_templates", "List all email templates (invitation, password reset, welcome, etc.).", {
949
1047
  filter: z.record(z.any()).optional().describe("Filter criteria"),
950
1048
  page: z.number().optional().default(1).describe("Page number"),
951
1049
  limit: z.number().optional().default(10).describe("Templates per page"),
@@ -969,7 +1067,7 @@ Use baasix_list_permissions or baasix_get_permissions to find the permission ID
969
1067
  return errorResult(error);
970
1068
  }
971
1069
  });
972
- server.tool("baasix_get_template", "Get a specific email template by ID", {
1070
+ registerTool("baasix_get_template", "Get a specific email template by ID", {
973
1071
  id: z.string().describe("Template ID (UUID)"),
974
1072
  }, async (args, extra) => {
975
1073
  const { id } = args;
@@ -983,7 +1081,7 @@ Use baasix_list_permissions or baasix_get_permissions to find the permission ID
983
1081
  return errorResult(error);
984
1082
  }
985
1083
  });
986
- server.tool("baasix_update_template", `Update an email template's subject, description, or body content.
1084
+ registerTool("baasix_update_template", `Update an email template's subject, description, or body content.
987
1085
 
988
1086
  TEMPLATE TYPES:
989
1087
  - magic_link: Magic link authentication emails
@@ -1014,7 +1112,7 @@ AVAILABLE VARIABLES:
1014
1112
  }
1015
1113
  });
1016
1114
  // ==================== Notification Tools ====================
1017
- server.tool("baasix_list_notifications", "List in-app notifications for the current authenticated user. Filter by seen/unseen status to find unread notifications.", {
1115
+ registerTool("baasix_list_notifications", "List in-app notifications for the current authenticated user. Filter by seen/unseen status to find unread notifications.", {
1018
1116
  page: z.number().optional().default(1).describe("Page number"),
1019
1117
  limit: z.number().optional().default(10).describe("Notifications per page. Use -1 for all."),
1020
1118
  seen: z.boolean().optional().describe("Filter: true = only seen, false = only unseen, omit = all"),
@@ -1038,7 +1136,7 @@ AVAILABLE VARIABLES:
1038
1136
  return errorResult(error);
1039
1137
  }
1040
1138
  });
1041
- server.tool("baasix_mark_notification_seen", "Mark a notification as seen/read by its UUID.", {
1139
+ registerTool("baasix_mark_notification_seen", "Mark a notification as seen/read by its UUID.", {
1042
1140
  id: z.string().describe("Notification UUID"),
1043
1141
  }, async (args, extra) => {
1044
1142
  const { id } = args;
@@ -1053,7 +1151,7 @@ AVAILABLE VARIABLES:
1053
1151
  }
1054
1152
  });
1055
1153
  // ==================== Utility Tools ====================
1056
- server.tool("baasix_server_info", "Get server health, version, uptime, and memory usage.", {}, async (_args, _extra) => {
1154
+ registerTool("baasix_server_info", "Get server health, version, uptime, and memory usage.", {}, async (_args, _extra) => {
1057
1155
  try {
1058
1156
  const info = {
1059
1157
  name: "baasix",
@@ -1074,7 +1172,7 @@ AVAILABLE VARIABLES:
1074
1172
  return errorResult(error);
1075
1173
  }
1076
1174
  });
1077
- server.tool("baasix_sort_items", `Reorder a row within a sortable table — move it before or after another row. The table must have a "sort" field.
1175
+ registerTool("baasix_sort_items", `Reorder a row within a sortable table — move it before or after another row. The table must have a "sort" field.
1078
1176
 
1079
1177
  MODE:
1080
1178
  - "before" (default): Places item directly before the target row
@@ -1110,7 +1208,7 @@ EXAMPLE: Move task "abc" before task "xyz":
1110
1208
  return errorResult(error);
1111
1209
  }
1112
1210
  });
1113
- server.tool("baasix_generate_report", `Run an aggregate/analytics query on a database table. ALWAYS use this tool (not baasix_list_items) when the user asks for sums, totals, counts, averages, min/max, grouped data, dashboards, reports, or any analytics/summary query.
1211
+ registerTool("baasix_generate_report", `Run an aggregate/analytics query on a database table. ALWAYS use this tool (not baasix_list_items) when the user asks for sums, totals, counts, averages, min/max, grouped data, dashboards, reports, or any analytics/summary query.
1114
1212
 
1115
1213
  QUERY OPTIONS:
1116
1214
  - fields: Columns to return. Default ["*"]. Use dot notation for relations: ["category.name", "status"].
@@ -1195,7 +1293,7 @@ EXAMPLES:
1195
1293
  }
1196
1294
  });
1197
1295
  // ==================== Collection Stats Tool ====================
1198
- server.tool("baasix_collection_stats", `Run multiple aggregate queries across different tables in a single call. Each query uses the full report engine (filter, aggregate, groupBy, etc.).
1296
+ registerTool("baasix_collection_stats", `Run multiple aggregate queries across different tables in a single call. Each query uses the full report engine (filter, aggregate, groupBy, etc.).
1199
1297
 
1200
1298
  Each stats entry requires:
1201
1299
  - name: A unique label for this stat in the results (e.g. "total_orders", "active_users")
@@ -1240,7 +1338,7 @@ EXAMPLES:
1240
1338
  }
1241
1339
  });
1242
1340
  // ==================== Send Notification Tool ====================
1243
- server.tool("baasix_send_notification", "Send an in-app notification to one or more users by their user UUIDs. Notifications appear in the user's notification list and can be marked as seen.", {
1341
+ registerTool("baasix_send_notification", "Send an in-app notification to one or more users by their user UUIDs. Notifications appear in the user's notification list and can be marked as seen.", {
1244
1342
  recipients: z.array(z.string()).describe("Array of user UUIDs to notify"),
1245
1343
  title: z.string().describe("Notification title"),
1246
1344
  message: z.string().describe("Notification message body"),
@@ -1267,7 +1365,7 @@ EXAMPLES:
1267
1365
  }
1268
1366
  });
1269
1367
  // ==================== Get Permission Tool ====================
1270
- server.tool("baasix_get_permission", "Get a specific permission rule by its UUID. Returns the full permission object including role_Id, collection, action, fields, conditions, defaultValues, and relConditions.", {
1368
+ registerTool("baasix_get_permission", "Get a specific permission rule by its UUID. Returns the full permission object including role_Id, collection, action, fields, conditions, defaultValues, and relConditions.", {
1271
1369
  id: z.string().describe("Permission UUID"),
1272
1370
  }, async (args, extra) => {
1273
1371
  const { id } = args;
@@ -1282,7 +1380,7 @@ EXAMPLES:
1282
1380
  }
1283
1381
  });
1284
1382
  // ==================== Get Permissions for Role Tool ====================
1285
- server.tool("baasix_get_permissions", `Get all permission rules assigned to a specific role. Shows which tables the role can access and what actions (create/read/update/delete) are allowed, including any row-level security conditions, field restrictions, default values, and relationship conditions.
1383
+ registerTool("baasix_get_permissions", `Get all permission rules assigned to a specific role. Shows which tables the role can access and what actions (create/read/update/delete) are allowed, including any row-level security conditions, field restrictions, default values, and relationship conditions.
1286
1384
 
1287
1385
  Accepts either the role name (e.g., "editor", "public") or the role UUID.`, {
1288
1386
  role: z.string().describe("Role name (e.g., 'editor', 'public') or role UUID"),
@@ -1320,7 +1418,7 @@ Accepts either the role name (e.g., "editor", "public") or the role UUID.`, {
1320
1418
  }
1321
1419
  });
1322
1420
  // ==================== Update Permissions for Role Tool ====================
1323
- server.tool("baasix_update_permissions", `Bulk set/update access control permissions for a role — define which tables a role can create, read, update, or delete. Creates new permissions or updates existing ones. Automatically reloads the permission cache.
1421
+ registerTool("baasix_update_permissions", `Bulk set/update access control permissions for a role — define which tables a role can create, read, update, or delete. Creates new permissions or updates existing ones. Automatically reloads the permission cache.
1324
1422
 
1325
1423
  Accepts role name (e.g., "editor") or role UUID. Each permission in the array specifies a table + action combination.
1326
1424
  If a permission for that role+table+action already exists, it is updated. Otherwise, a new one is created.
@@ -1404,7 +1502,7 @@ permissions: [{"collection": "posts", "action": "read", "conditions": {"author_I
1404
1502
  }
1405
1503
  });
1406
1504
  // ==================== Auth Tools ====================
1407
- server.tool("baasix_get_current_user", `Get the currently authenticated user's profile, role, and permissions.
1505
+ registerTool("baasix_get_current_user", `Get the currently authenticated user's profile, role, and permissions.
1408
1506
 
1409
1507
  Use fields to include related data:
1410
1508
  - ["*"] → all user fields
@@ -1445,7 +1543,7 @@ Returns authenticated: false if no user is logged in (public/anonymous access).`
1445
1543
  return errorResult(error);
1446
1544
  }
1447
1545
  });
1448
- server.tool("baasix_register_user", "Register a new user", {
1546
+ registerTool("baasix_register_user", "Register a new user", {
1449
1547
  email: z.string().email().describe("User email address"),
1450
1548
  password: z.string().describe("User password"),
1451
1549
  firstName: z.string().optional().describe("User first name"),
@@ -1470,7 +1568,7 @@ Returns authenticated: false if no user is logged in (public/anonymous access).`
1470
1568
  return errorResult(axiosError.response?.data?.message || axiosError.message || "Unknown error");
1471
1569
  }
1472
1570
  });
1473
- server.tool("baasix_send_invite", "Send an invitation to a user", {
1571
+ registerTool("baasix_send_invite", "Send an invitation to a user", {
1474
1572
  email: z.string().email().describe("Email address to invite"),
1475
1573
  role_Id: z.string().describe("Role ID to assign"),
1476
1574
  tenant_Id: z.string().optional().describe("Tenant ID"),
@@ -1493,7 +1591,7 @@ Returns authenticated: false if no user is logged in (public/anonymous access).`
1493
1591
  return errorResult(axiosError.response?.data?.message || axiosError.message || "Unknown error");
1494
1592
  }
1495
1593
  });
1496
- server.tool("baasix_verify_invite", "Verify an invitation token", {
1594
+ registerTool("baasix_verify_invite", "Verify an invitation token", {
1497
1595
  token: z.string().describe("Invitation token"),
1498
1596
  link: z.string().url().optional().describe("Application URL to validate"),
1499
1597
  }, async (args, _extra) => {
@@ -1512,7 +1610,7 @@ Returns authenticated: false if no user is logged in (public/anonymous access).`
1512
1610
  return errorResult(axiosError.response?.data?.message || axiosError.message || "Unknown error");
1513
1611
  }
1514
1612
  });
1515
- server.tool("baasix_send_magic_link", "Send magic link or code for authentication", {
1613
+ registerTool("baasix_send_magic_link", "Send magic link or code for authentication", {
1516
1614
  email: z.string().email().describe("User email address"),
1517
1615
  link: z.string().url().optional().describe("Application URL for magic link"),
1518
1616
  mode: z.enum(["link", "code"]).optional().default("link").describe("Magic authentication mode"),
@@ -1533,7 +1631,7 @@ Returns authenticated: false if no user is logged in (public/anonymous access).`
1533
1631
  return errorResult(axiosError.response?.data?.message || axiosError.message || "Unknown error");
1534
1632
  }
1535
1633
  });
1536
- server.tool("baasix_get_user_tenants", "Get available tenants for the current user", {}, async (_args, _extra) => {
1634
+ registerTool("baasix_get_user_tenants", "Get available tenants for the current user", {}, async (_args, _extra) => {
1537
1635
  try {
1538
1636
  const { default: axios } = await import("axios");
1539
1637
  const baseUrl = `http://localhost:${env.get("PORT") || "8056"}`;
@@ -1545,7 +1643,7 @@ Returns authenticated: false if no user is logged in (public/anonymous access).`
1545
1643
  return errorResult(axiosError.response?.data?.message || axiosError.message || "Unknown error");
1546
1644
  }
1547
1645
  });
1548
- server.tool("baasix_switch_tenant", "Switch to a different tenant context", {
1646
+ registerTool("baasix_switch_tenant", "Switch to a different tenant context", {
1549
1647
  tenant_Id: z.string().describe("Tenant ID to switch to"),
1550
1648
  }, async (args, _extra) => {
1551
1649
  const { tenant_Id } = args;
@@ -1563,7 +1661,7 @@ Returns authenticated: false if no user is logged in (public/anonymous access).`
1563
1661
  }
1564
1662
  });
1565
1663
  // ==================== Auth Status & Session Tools ====================
1566
- server.tool("baasix_auth_status", "Check if the current MCP session is authenticated, and show the current user, role, and admin status.", {}, async (_args, extra) => {
1664
+ registerTool("baasix_auth_status", "Check if the current MCP session is authenticated, and show the current user, role, and admin status.", {}, async (_args, extra) => {
1567
1665
  try {
1568
1666
  const accountability = getAccountability(extra);
1569
1667
  return successResult({
@@ -1578,7 +1676,7 @@ Returns authenticated: false if no user is logged in (public/anonymous access).`
1578
1676
  return errorResult(error);
1579
1677
  }
1580
1678
  });
1581
- server.tool("baasix_login", "Authenticate with email and password to get admin or role-based access. Required before performing write operations if not already authenticated via headers.", {
1679
+ registerTool("baasix_login", "Authenticate with email and password to get admin or role-based access. Required before performing write operations if not already authenticated via headers.", {
1582
1680
  email: z.string().email().describe("User email address"),
1583
1681
  password: z.string().describe("User password"),
1584
1682
  }, async (args, extra) => {
@@ -1628,7 +1726,7 @@ Returns authenticated: false if no user is logged in (public/anonymous access).`
1628
1726
  return errorResult(axiosError.response?.data?.message || axiosError.message || "Login failed");
1629
1727
  }
1630
1728
  });
1631
- server.tool("baasix_logout", "Logout and invalidate current session", {}, async (_args, extra) => {
1729
+ registerTool("baasix_logout", "Logout and invalidate current session", {}, async (_args, extra) => {
1632
1730
  try {
1633
1731
  // Remove MCP session if exists
1634
1732
  if (extra.sessionId) {
@@ -1641,7 +1739,7 @@ Returns authenticated: false if no user is logged in (public/anonymous access).`
1641
1739
  return errorResult(err.message || "Logout failed");
1642
1740
  }
1643
1741
  });
1644
- server.tool("baasix_refresh_auth", "Refresh authentication token", {
1742
+ registerTool("baasix_refresh_auth", "Refresh authentication token", {
1645
1743
  refreshToken: z.string().optional().describe("Refresh token (if not using cookies)"),
1646
1744
  }, async (_args, extra) => {
1647
1745
  try {
@@ -1661,7 +1759,7 @@ Returns authenticated: false if no user is logged in (public/anonymous access).`
1661
1759
  }
1662
1760
  });
1663
1761
  // ==================== Update Relationship Tool ====================
1664
- server.tool("baasix_update_relationship", "Modify an existing foreign key / relationship between two database tables (change delete behavior, alias, etc.).", {
1762
+ registerTool("baasix_update_relationship", "Modify an existing foreign key / relationship between two database tables (change delete behavior, alias, etc.).", {
1665
1763
  sourceCollection: z.string().describe("Source collection name"),
1666
1764
  relationshipName: z.string().describe("Relationship field name to update"),
1667
1765
  relationshipData: z