// @ts-nocheck
/**
 * MCPService - Model Context Protocol Server for Baasix
 *
 * This service provides MCP tools that call Baasix HTTP routes internally,
 * ensuring all route-level validation, permission checks, and cache
 * invalidation are applied consistently.
 *
 * Enable via environment variable: MCP_ENABLED=true
 * Access at: http://localhost:8056/mcp (or custom MCP_PATH)
 *
 * Note: @ts-nocheck is used because the MCP SDK + Zod combination causes
 * TypeScript to hang during compilation due to complex generic type inference.
 * The explicit type annotations are kept for documentation and IDE support.
 */
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import env from "../utils/env.js";
// ==================== Helper Functions ====================
// Session storage for authenticated MCP clients
const mcpSessions = new Map();
/**
 * Store accountability info for an MCP session
 */
export function setMCPSession(sessionId, accountability) {
    mcpSessions.set(sessionId, accountability);
}
/**
 * Get accountability info from session
 */
function getAccountabilityFromSession(sessionId) {
    return mcpSessions.get(sessionId);
}
/**
 * Get default accountability (admin for now, can be configured)
 */
function getDefaultAccountability() {
    return {
        user: null,
        role: {
            id: "public",
            name: "public",
            isTenantSpecific: false,
        },
        permissions: [],
        tenant: null,
        ipaddress: "127.0.0.1",
    };
}
/**
 * Remove an MCP session
 */
export function removeMCPSession(sessionId) {
    mcpSessions.delete(sessionId);
}
/**
 * Get accountability from extra context
 */
function getAccountability(extra) {
    if (extra.sessionId) {
        const session = getAccountabilityFromSession(extra.sessionId);
        if (session)
            return session;
    }
    return getDefaultAccountability();
}
/**
 * Create success response
 */
function successResult(data) {
    return {
        content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
    };
}
/**
 * Create error response
 */
function errorResult(error) {
    const message = typeof error === "string" ? error : error.message;
    return {
        content: [{ type: "text", text: JSON.stringify({ error: message }, null, 2) }],
        isError: true,
    };
}
// ==================== MCP Server Creation ====================
// Map every tool to its action category: "read" | "create" | "update" | "delete"
const TOOL_ACTION_MAP = {
    // Schema Management
    baasix_list_schemas: "read",
    baasix_get_schema: "read",
    baasix_export_schemas: "read",
    baasix_create_schema: "create",
    baasix_import_schemas: "create",
    baasix_add_index: "create",
    baasix_create_relationship: "create",
    baasix_update_schema: "update",
    baasix_update_relationship: "update",
    baasix_delete_schema: "delete",
    baasix_remove_index: "delete",
    baasix_delete_relationship: "delete",
    // Item Management
    baasix_list_items: "read",
    baasix_get_item: "read",
    baasix_create_item: "create",
    baasix_update_item: "update",
    baasix_delete_item: "delete",
    // File Management
    baasix_list_files: "read",
    baasix_get_file_info: "read",
    baasix_delete_file: "delete",
    // Authentication / Session
    baasix_auth_status: "read",
    baasix_refresh_auth: "read",
    baasix_login: "create",
    baasix_logout: "delete",
    baasix_get_current_user: "read",
    baasix_register_user: "create",
    baasix_send_invite: "create",
    baasix_verify_invite: "read",
    baasix_send_magic_link: "create",
    baasix_get_user_tenants: "read",
    baasix_switch_tenant: "update",
    // Reports and Analytics
    baasix_generate_report: "read",
    baasix_collection_stats: "read",
    // Notifications
    baasix_list_notifications: "read",
    baasix_send_notification: "create",
    baasix_mark_notification_seen: "update",
    // Settings
    baasix_get_settings: "read",
    baasix_update_settings: "update",
    // Email Templates
    baasix_list_templates: "read",
    baasix_get_template: "read",
    baasix_update_template: "update",
    // Permissions
    baasix_list_roles: "read",
    baasix_list_permissions: "read",
    baasix_get_permission: "read",
    baasix_get_permissions: "read",
    baasix_create_permission: "create",
    baasix_update_permission: "update",
    baasix_delete_permission: "delete",
    baasix_reload_permissions: "read",
    baasix_update_permissions: "update",
    // Utilities
    baasix_server_info: "read",
    baasix_sort_items: "update",
};
/**
 * Parse MCP_ENABLED_ACTIONS env var.
 * Accepts a comma-separated list of: "all", "read", "create", "update", "delete"
 * Returns null when all actions are enabled (default), or a Set of enabled action strings.
 */
function parseEnabledActions() {
    const raw = (env.get("MCP_ENABLED_ACTIONS") || "all").toLowerCase().trim();
    const actions = raw.split(",").map((a) => a.trim()).filter(Boolean);
    if (actions.includes("all"))
        return null; // null = everything enabled
    return new Set(actions);
}
/**
 * Check if a tool should be registered based on MCP_ENABLED_ACTIONS.
 */
function isToolEnabled(toolName, enabledActions) {
    if (!enabledActions)
        return true; // "all"
    const action = TOOL_ACTION_MAP[toolName];
    if (!action)
        return true; // unknown tools stay enabled
    return enabledActions.has(action);
}
/**
 * Create and configure the MCP server with all Baasix tools
 */
export function createMCPServer() {
    const enabledActions = parseEnabledActions();
    const server = new McpServer({
        name: "Baasix Backend-as-a-Service",
        version: "0.1.0",
        instructions: `You are connected to a Baasix server — an open-source Backend-as-a-Service (BaaS) that provides a complete backend via API.

KEY CONCEPTS:
- "Collection" = a database table (e.g., "products", "orders", "users")
- "Schema" = the definition/structure of a table (its columns, types, constraints)
- "Item" = a row/record in a table
- "Relationship" = a foreign key or junction table between two collections
- System collections are prefixed with "baasix_" (e.g., baasix_User, baasix_Role) — avoid modifying these unless explicitly asked

COMMON TASK MAPPING:
- "Create a table" or "create a collection" → use baasix_create_schema (this creates both the schema definition AND the database table)
- "Add a column/field" → use baasix_update_schema (⚠️ MUST first call baasix_get_schema to get ALL existing fields, then send complete schema with additions)
- "Remove a column/field" → use baasix_update_schema (retrieve full schema, remove the field, send complete schema without it)
- "Insert/add data" or "create a record" → use baasix_create_item
- "Query/list/fetch data" or "get rows" → use baasix_list_items
- "Sum", "count", "average", "total", "report", "stats", "analytics", "dashboard", "min/max" → use baasix_generate_report (NOT baasix_list_items)
- "Stats across multiple tables" → use baasix_collection_stats
- "Link two tables" or "add a foreign key" → use baasix_create_relationship
- "Add an index" → use baasix_add_index
- "Set permissions" → use baasix_create_permission or baasix_update_permissions
- "Check what tables exist" → use baasix_list_schemas

⚠️ CRITICAL — baasix_update_schema performs a FULL REPLACEMENT of the schema definition.
When adding/modifying/removing columns, you MUST:
1. Call baasix_get_schema first to get ALL existing fields
2. Include ALL existing fields in your update (not just new ones)
3. Add/modify/remove only the specific fields you need
Sending only new fields will DELETE all other field definitions from the schema.

WORKFLOW FOR CREATING A NEW TABLE:
1. baasix_create_schema — define the table name and fields
2. baasix_create_relationship — (optional) link it to other tables
3. baasix_add_index — (optional) add indexes for performance
4. baasix_create_permission — (optional) set role-based access

SCHEMA FIELD TYPES: String (VARCHAR), Text (unlimited), Integer, BigInt, Decimal (precision/scale), Float, Real, Double, Boolean, Date, DateTime, Time, UUID, SUID (short ID), JSONB, Array, Enum

DEFAULT VALUE TYPES: { type: "UUIDV4" }, { type: "SUID" }, { type: "NOW" }, { type: "AUTOINCREMENT" }, { type: "SQL", value: "..." }

EXAMPLE — Creating a "products" table:
Call baasix_create_schema with:
  collection: "products"
  schema: {
    "timestamps": true,
    "fields": {
      "id": { "type": "UUID", "primaryKey": true, "defaultValue": { "type": "UUIDV4" } },
      "name": { "type": "String", "allowNull": false, "values": { "length": 255 } },
      "price": { "type": "Decimal", "values": { "precision": 10, "scale": 2 }, "defaultValue": 0 },
      "inStock": { "type": "Boolean", "defaultValue": true }
    }
  }
`,
    });
    // ==================== Helper Functions ====================
    /**
     * Call a Baasix REST route with the MCP session's bearer token.
     * This ensures all schema/relationship operations go through the same
     * route logic (validation, processSchemaFlags, permission checks, cache invalidation)
     * without duplicating code.
     */
    async function callRoute(method, path, extra, body) {
        const { default: axios } = await import("axios");
        const baseUrl = `http://localhost:${env.get("PORT") || "8056"}`;
        const accountability = getAccountability(extra);
        const token = accountability.token;
        const headers = { 'Content-Type': 'application/json' };
        if (token) {
            headers['Authorization'] = `Bearer ${token}`;
        }
        try {
            const response = await axios({
                method,
                url: `${baseUrl}${path}`,
                headers,
                data: body,
                validateStatus: () => true, // don't throw on non-2xx
            });
            if (response.status >= 200 && response.status < 300) {
                return { ok: true, data: response.data, status: response.status };
            }
            const msg = response.data?.message || response.data?.error || JSON.stringify(response.data);
            return { ok: false, error: msg, status: response.status };
        }
        catch (err) {
            return { ok: false, error: err.message || 'Route call failed' };
        }
    }
    // ==================== Schema Management Tools ====================
    // All schema tools call the REST routes via HTTP with the session's bearer token.
    // This ensures route-level validation, processSchemaFlags, permission checks and cache
    // invalidation are applied exactly once (in the route) — zero duplicated logic.
    /**
     * Conditionally register a tool based on MCP_ENABLED_ACTIONS.
     * Wraps server.tool() — if the tool's action category is not enabled, skips registration entirely.
     */
    function registerTool(name, ...rest) {
        if (!isToolEnabled(name, enabledActions))
            return;
        server.tool(name, ...rest);
    }
    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_'.", {
        search: z.string().optional().describe("Search term to filter schemas by collection name"),
        page: z.number().optional().default(1).describe("Page number for pagination"),
        limit: z.number().optional().default(10).describe("Number of schemas per page"),
        sort: z.string().optional().default("collectionName:asc").describe("Sort field and direction"),
    }, async (args, extra) => {
        const { search, page, limit, sort } = args;
        const params = new URLSearchParams();
        if (search)
            params.set('search', search);
        if (page)
            params.set('page', String(page));
        if (limit)
            params.set('limit', String(limit));
        if (sort)
            params.set('sort', sort);
        const qs = params.toString();
        const res = await callRoute('GET', `/schemas${qs ? '?' + qs : ''}`, extra);
        if (!res.ok)
            return errorResult(res.error || 'Failed to list schemas');
        return successResult(res.data);
    });
    registerTool("baasix_get_schema", `Get the full schema definition (columns, types, constraints, relationships, indexes) for a specific database table/collection.

IMPORTANT: Always call this BEFORE using baasix_update_schema to get the current schema.
The update_schema tool performs a full replacement, so you need the complete current schema to avoid losing existing fields.`, {
        collection: z.string().describe("Collection name"),
    }, async (args, extra) => {
        const { collection } = args;
        const res = await callRoute('GET', `/schemas/${encodeURIComponent(collection)}`, extra);
        if (!res.ok)
            return errorResult(res.error || `Schema '${collection}' not found`);
        return successResult(res.data);
    });
    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".

This creates both the schema definition AND the actual PostgreSQL table with all specified columns.

FIELD TYPES:
- String: VARCHAR — requires values.length (e.g., { "type": "String", "values": { "length": 255 } })
- Text: Unlimited length text
- Integer, BigInt: Whole numbers
- Decimal: requires values.precision & values.scale (e.g., { "type": "Decimal", "values": { "precision": 10, "scale": 2 } })
- Float, Real, Double: Floating point numbers
- Boolean: true/false
- Date, DateTime, Time: Date/time values
- UUID: Use with defaultValue { "type": "UUIDV4" } for auto-generated IDs
- SUID: Short unique ID with defaultValue { "type": "SUID" }
- JSONB: JSON data with indexing support
- Array: Specify element type via values.type (e.g., { "type": "Array", "values": { "type": "String" } })
- Enum: Specify allowed values via values.values (e.g., { "type": "Enum", "values": { "values": ["active", "inactive"] } })

FIELD OPTIONS: allowNull (boolean), unique (boolean), primaryKey (boolean), defaultValue (value or { type: "UUIDV4"|"SUID"|"NOW"|"AUTOINCREMENT" })

ALWAYS include an "id" field as primary key. Add "timestamps": true in the schema for automatic createdAt/updatedAt. Add "paranoid": true for soft deletes (deletedAt).

EXAMPLE — Create a "products" table:
collection: "products"
schema: { "timestamps": true, "fields": { "id": { "type": "UUID", "primaryKey": true, "defaultValue": { "type": "UUIDV4" } }, "name": { "type": "String", "allowNull": false, "values": { "length": 255 } }, "price": { "type": "Decimal", "values": { "precision": 10, "scale": 2 }, "defaultValue": 0 }, "inStock": { "type": "Boolean", "defaultValue": true } } }`, {
        collection: z.string().describe("Collection name"),
        schema: z
            .object({
            fields: z.record(z.any()).describe("Field definitions"),
        })
            .passthrough()
            .describe("Schema definition"),
    }, async (args, extra) => {
        const { collection, schema } = args;
        const res = await callRoute('POST', '/schemas', extra, { collectionName: collection, schema });
        if (!res.ok)
            return errorResult(res.error || `Failed to create collection '${collection}'`);
        return successResult({
            success: true,
            message: `Collection '${collection}' created successfully`,
            collectionName: collection,
        });
    });
    registerTool("baasix_update_schema", `Modify an existing database table — add new columns, change column types, or remove columns.
Use this when asked to 'add a field', 'add a column', or 'alter a table'.

⚠️ CRITICAL: This performs a FULL REPLACEMENT of the schema definition, NOT a merge.
You MUST first call baasix_get_schema to retrieve the current schema, then include ALL existing fields
and add/modify/remove only the fields you need. If you send only new fields, ALL existing fields will
be LOST from the schema definition.

CORRECT WORKFLOW to add a column:
1. Call baasix_get_schema for the collection
2. Copy the entire existing schema (including name, timestamps, paranoid, and ALL fields)
3. Add/modify/remove the desired fields while keeping all other fields intact
4. Send the complete schema to baasix_update_schema

EXAMPLE — Adding a "description" field to an existing "products" table that has id, name, price:
{
  "collection": "products",
  "schema": {
    "name": "Product",
    "timestamps": true,
    "fields": {
      "id": { "type": "UUID", "primaryKey": true, "defaultValue": { "type": "UUIDV4" } },
      "name": { "type": "String", "allowNull": false, "values": { "length": 255 } },
      "price": { "type": "Decimal", "values": { "precision": 10, "scale": 2 } },
      "description": { "type": "Text", "allowNull": true }
    }
  }
}

❌ WRONG — This will DELETE the id, name, and price field definitions:
{
  "collection": "products",
  "schema": {
    "fields": {
      "description": { "type": "Text", "allowNull": true }
    }
  }
}`, {
        collection: z.string().describe("Collection name"),
        schema: z
            .object({
            fields: z.record(z.any()).optional().describe("The COMPLETE field definitions including ALL existing fields plus any additions/modifications. This REPLACES the entire schema — do NOT send partial fields."),
        })
            .passthrough()
            .describe("The COMPLETE schema definition. This REPLACES the entire schema — include ALL fields, not just changes."),
    }, async (args, extra) => {
        const { collection, schema } = args;
        const res = await callRoute('PATCH', `/schemas/${encodeURIComponent(collection)}`, extra, { schema });
        if (!res.ok)
            return errorResult(res.error || `Failed to update collection '${collection}'`);
        return successResult({ success: true, message: `Collection '${collection}' updated successfully` });
    });
    registerTool("baasix_delete_schema", `DROP/DELETE an entire database table and all its data permanently.
Use this when asked to 'drop a table', 'delete a collection', or 'remove a table'.

⚠️ WARNING: This is destructive and irreversible. The table, all its data, indexes, and relationships will be permanently deleted.
Do NOT use this to remove a column — use baasix_update_schema instead to modify the schema without the unwanted field.`, {
        collection: z.string().describe("Collection name to delete"),
    }, async (args, extra) => {
        const { collection } = args;
        const res = await callRoute('DELETE', `/schemas/${encodeURIComponent(collection)}`, extra);
        if (!res.ok)
            return errorResult(res.error || `Failed to delete collection '${collection}'`);
        return successResult({ success: true, message: `Collection '${collection}' deleted successfully` });
    });
    registerTool("baasix_add_index", `Add a database index to a table for better query performance.
This is an ADDITIVE operation — it only adds the new index without affecting existing indexes or schema fields.
Supports btree (default), hash, gin, and gist index types.

To create a UNIQUE index, set "unique": true in the indexDefinition. This enforces that no two rows can have the same value(s) in the indexed field(s).

EXAMPLE — Add a unique index on email (prevents duplicate emails):
{ "collection": "users", "indexDefinition": { "name": "users_email_unique", "fields": ["email"], "unique": true } }

EXAMPLE — Add a composite unique index (unique combination of fields):
{ "collection": "orders", "indexDefinition": { "name": "orders_user_product_unique", "fields": ["user_Id", "product_Id"], "unique": true } }

EXAMPLE — Add a regular (non-unique) index for query performance:
{ "collection": "orders", "indexDefinition": { "name": "orders_status_date", "fields": ["status", "createdAt"] } }

EXAMPLE — Add a GIN index for JSONB or array fields:
{ "collection": "products", "indexDefinition": { "name": "products_tags_gin", "fields": ["tags"], "type": "gin" } }`, {
        collection: z.string().describe("Collection name of the table to add the index to"),
        indexDefinition: z
            .object({
            name: z.string().describe("Index name — use a descriptive name like {table}_{fields}_{type} e.g. 'users_email_unique'"),
            fields: z.array(z.string()).describe("Array of field names to index (e.g., ['email'] or ['status', 'createdAt'] for composite)"),
            unique: z.boolean().optional().describe("Whether the index should enforce uniqueness (default: false). Set to true to prevent duplicate values."),
            type: z.enum(["btree", "hash", "gin", "gist"]).optional().describe("Index type (default: btree). Use 'gin' for JSONB/array fields, 'gist' for geospatial."),
            nullsNotDistinct: z.boolean().optional().describe("When true, NULL values are considered equal for unique indexes (PostgreSQL 15+). Only applies when unique is true."),
        })
            .describe("Index definition with fields and options"),
    }, async (args, extra) => {
        const { collection, indexDefinition } = args;
        const res = await callRoute('POST', `/schemas/${encodeURIComponent(collection)}/indexes`, extra, indexDefinition);
        if (!res.ok)
            return errorResult(res.error || `Failed to add index to '${collection}'`);
        return successResult({ success: true, message: `Index added to '${collection}' successfully` });
    });
    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.", {
        collection: z.string().describe("Collection name"),
        indexName: z.string().describe("Name of the index to remove"),
    }, async (args, extra) => {
        const { collection, indexName } = args;
        const res = await callRoute('DELETE', `/schemas/${encodeURIComponent(collection)}/indexes/${encodeURIComponent(indexName)}`, extra);
        if (!res.ok)
            return errorResult(res.error || `Failed to remove index '${indexName}' from '${collection}'`);
        return successResult({ success: true, message: `Index '${indexName}' removed from '${collection}'` });
    });
    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'.

RELATIONSHIP TYPES:
- M2O (Many-to-One / BelongsTo): Adds a foreign key column. Example: products.category_Id → categories.id
- O2M (One-to-Many / HasMany): Virtual reverse of M2O. Example: categories.products → products
- O2O (One-to-One / HasOne): Unique foreign key. Example: users.profile_Id → profiles.id
- M2M (Many-to-Many): Auto-creates a junction table. Example: products ↔ tags
- M2A (Many-to-Any): Polymorphic — one source to multiple target tables

PARAMETERS:
- sourceCollection: the table that gets the foreign key (e.g., "products")
- name: the field name for the FK (e.g., "category" creates "category_Id" column)
- type: relationship type (M2O, O2M, O2O, M2M, M2A)
- target: the related table (e.g., "categories")
- alias: reverse-access name on the target table (e.g., "products" so categories.products works)
- onDelete: CASCADE | RESTRICT | SET NULL

EXAMPLE — products belongsTo categories:
sourceCollection: "products"
relationshipData: { "name": "category", "type": "M2O", "target": "categories", "alias": "products", "onDelete": "CASCADE" }`, {
        sourceCollection: z.string().describe("Source collection name"),
        relationshipData: z
            .object({
            name: z.string().describe("Relationship field name"),
            type: z.enum(["M2O", "O2M", "O2O", "M2M", "M2A"]).describe("Relationship type"),
            target: z.string().optional().describe("Target collection name"),
            alias: z.string().optional().describe("Alias for reverse relationship"),
            onDelete: z.enum(["CASCADE", "RESTRICT", "SET NULL"]).optional().describe("Delete behavior"),
            onUpdate: z.enum(["CASCADE", "RESTRICT", "SET NULL"]).optional().describe("Update behavior"),
            tables: z.array(z.string()).optional().describe("Target tables for M2A relationships"),
            through: z.string().optional().describe("Custom junction table name for M2M/M2A"),
        })
            .describe("Relationship configuration"),
    }, async (args, extra) => {
        const { sourceCollection, relationshipData } = args;
        const res = await callRoute('POST', `/schemas/${encodeURIComponent(sourceCollection)}/relationships`, extra, relationshipData);
        if (!res.ok)
            return errorResult(res.error || `Failed to create relationship '${relationshipData.name}' on '${sourceCollection}'`);
        return successResult({
            success: true,
            message: `Relationship '${relationshipData.name}' created on '${sourceCollection}'`,
        });
    });
    registerTool("baasix_delete_relationship", `Remove a foreign key / relationship from a database table.
This drops the foreign key constraint and the FK column (e.g., category_Id) from the source table.
For M2M relationships, this also removes the junction table.

⚠️ WARNING: This will remove the FK column and any data stored in it. Use baasix_get_schema first to confirm the relationship field name.
Do NOT confuse with baasix_delete_schema (which deletes an entire table).`, {
        sourceCollection: z.string().describe("Source collection name"),
        fieldName: z.string().describe("Relationship field name"),
    }, async (args, extra) => {
        const { sourceCollection, fieldName } = args;
        const res = await callRoute('DELETE', `/schemas/${encodeURIComponent(sourceCollection)}/relationships/${encodeURIComponent(fieldName)}`, extra);
        if (!res.ok)
            return errorResult(res.error || `Failed to remove relationship '${fieldName}' from '${sourceCollection}'`);
        return successResult({ success: true, message: `Relationship '${fieldName}' removed from '${sourceCollection}'` });
    });
    registerTool("baasix_export_schemas", "Export all table definitions as JSON for backup or migration.", {}, async (_args, extra) => {
        const res = await callRoute('GET', '/schemas-export', extra);
        if (!res.ok)
            return errorResult(res.error || 'Failed to export schemas');
        return successResult(res.data);
    });
    registerTool("baasix_import_schemas", "Import table definitions from JSON data to recreate tables.", {
        schemas: z.record(z.any()).describe("Schema data to import"),
    }, async (args, extra) => {
        const { schemas } = args;
        // The import route expects a file upload (multipart/form-data).
        // Use native Node.js FormData + Blob (available in Node 18+).
        const { default: axios } = await import("axios");
        const baseUrl = `http://localhost:${env.get("PORT") || "8056"}`;
        const accountability = getAccountability(extra);
        const token = accountability.token;
        const jsonStr = JSON.stringify(schemas);
        const blob = new Blob([jsonStr], { type: 'application/json' });
        const form = new FormData();
        form.append('schema', blob, 'schema.json');
        const headers = {};
        if (token) {
            headers['Authorization'] = `Bearer ${token}`;
        }
        try {
            const response = await axios.post(`${baseUrl}/schemas-import`, form, {
                headers,
                validateStatus: () => true,
            });
            if (response.status >= 200 && response.status < 300) {
                return successResult(response.data);
            }
            const msg = response.data?.message || response.data?.error || JSON.stringify(response.data);
            return errorResult(msg);
        }
        catch (err) {
            return errorResult(err.message || 'Failed to import schemas');
        }
    });
    // ==================== Item Management Tools ====================
    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.

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.

--- FIELDS (selecting columns & related data) ---
Use dot notation to include related table data. Wildcards expand to all columns.
- ["*"] → all columns on the main table
- ["name", "price"] → only specific columns
- ["*", "category.*"] → all main columns + all columns from the related "category" table
- ["name", "category.name", "category.id"] → specific columns from main + related table
- ["*", "author.profile.*"] → deep nested relations (up to 7 levels)
The primary key column is always returned even if not listed.

--- FILTER OPERATORS (case-sensitive, use exactly as shown) ---
Comparison: eq, ne, gt, gte, lt, lte
String: like, notLike, iLike, notILike, startsWith, endsWith, nstartsWith, nendsWith
Null check: isNull (true/false), isNotNull (true/false)
List: in, notIn, between, notBetween
Misc: not, is
Array (PostgreSQL arrays): arraycontains, arraycontained
JSONB: jsonbContains, jsonbContainedBy, jsonbHasKey, jsonbHasAnyKeys, jsonbHasAllKeys, jsonbPathExists, jsonbPathMatch, jsonbNotContains, jsonbKeyEquals, jsonbKeyNotEquals, jsonbKeyGt, jsonbKeyGte, jsonbKeyLt, jsonbKeyLte, jsonbKeyIn, jsonbKeyNotIn, jsonbKeyLike, jsonbKeyIsNull, jsonbKeyIsNotNull, jsonbArrayLength, jsonbTypeOf, jsonbDeepValue
Geospatial: dwithin, intersects, nIntersects, within, containsGEO
Logical (combine conditions): AND, OR

Type casting in filters — add "cast" to force column type:
{"price": {"cast": "numeric", "gt": 100}}
{"createdAt": {"cast": "date", "gte": "2024-01-01"}}
Valid cast types: text, varchar, integer, int, bigint, numeric, decimal, real, float, double precision, boolean, date, timestamp, timestamptz, time, uuid, json, jsonb, text[], varchar[], integer[], bigint[], uuid[]

--- DYNAMIC VARIABLES (use as string values in filters) ---
$CURRENT_USER → current authenticated user's ID
$CURRENT_USER.fieldName → any field on current user (e.g., $CURRENT_USER.tenant_Id)
$CURRENT_ROLE → current role ID
$CURRENT_ROLE.fieldName → any field on current role
$NOW → current ISO timestamp
$NOW+DAYS_7 → 7 days from now, $NOW-DAYS_7 → 7 days ago
Pattern: $NOW[+/-](YEARS|MONTHS|WEEKS|DAYS|HOURS|MINUTES|SECONDS)_N

--- SORT ---
Format: "fieldName:asc" or "fieldName:desc"
Relation sort: "category.name:asc" (sort by related table field)

--- SEARCH (PostgreSQL full-text search) ---
The search parameter performs full-text search with prefix matching.
By default searches all text/varchar/uuid columns. Use searchFields to limit which columns are searched.

--- AGGREGATE (SQL aggregation functions) ---
Format: {"aliasName": {"function": "functionName", "field": "columnName"}}
Functions: count, countDistinct, sum, avg, min, max, array_agg
Examples:
{"total": {"function": "count", "field": "*"}} → count all rows
{"avgPrice": {"function": "avg", "field": "price"}} → average price
{"uniqueCategories": {"function": "countDistinct", "field": "category"}} → count distinct
Relation aggregation: {"orderTotal": {"function": "sum", "field": "orders.total"}} → sum via join

--- GROUPBY ---
Format: array of field names. Supports date extraction: ["date:year:createdAt", "status"]
Date parts: year, month, day, hour. Relation paths: ["category.name"]

--- RELCONDITIONS (filter on related/joined tables) ---
Filter rows based on conditions on their related records. Keys are relation names, values are filter objects.
{"category": {"status": {"eq": "active"}}} → only rows whose related category has status=active
{"author": {"verified": {"eq": true}}} → only rows whose author is verified
Nested: {"author": {"department": {"name": {"eq": "Engineering"}}}} → filter by author's department name
Supports same operators and dynamic variables as filter.

--- FILTER EXAMPLES ---
{"status": {"eq": "active"}}  →  exact match
{"status": {"ne": "archived"}}  →  not equal
{"AND": [{"price": {"gte": 10}}, {"price": {"lte": 100}}]}  →  range
{"OR": [{"status": {"eq": "active"}}, {"status": {"eq": "pending"}}]}  →  either condition
{"name": {"iLike": "%john%"}}  →  case-insensitive pattern
{"email": {"startsWith": "admin"}}  →  prefix match
{"deletedAt": {"isNull": true}}  →  null check
{"category": {"in": ["books", "electronics"]}}  →  list membership
{"tags": {"arraycontains": ["featured"]}}  →  PostgreSQL array contains
{"metadata": {"jsonbHasKey": "color"}}  →  JSONB key exists
{"age": {"between": [18, 65]}}  →  between range
{"userId": {"eq": "$CURRENT_USER"}}  →  current user's records
{"createdAt": {"gte": "$NOW-DAYS_30"}}  →  last 30 days`, {
        collection: z.string().describe("Table/collection name to query"),
        filter: z.record(z.any()).optional().describe("Filter object using operators like {field: {operator: value}}. Combine with AND/OR for complex queries."),
        fields: z.array(z.string()).optional().describe("Columns to return. Use [\"*\"] for all, dot notation for relations: [\"*\", \"category.*\"]"),
        sort: z.string().optional().describe("Sort as 'field:asc' or 'field:desc'. Supports relation paths like 'category.name:asc'"),
        page: z.number().optional().default(1).describe("Page number for pagination (starts at 1)"),
        limit: z.number().optional().default(10).describe("Rows per page. Use -1 to fetch ALL rows (no pagination)"),
        search: z.string().optional().describe("Full-text search query string. Searches text/varchar columns by default."),
        searchFields: z.array(z.string()).optional().describe("Limit search to specific columns: [\"name\", \"description\"]"),
        aggregate: z.record(z.any()).optional().describe("Aggregation: {alias: {function: 'count|sum|avg|min|max|countDistinct|array_agg', field: 'columnName'}}"),
        groupBy: z.array(z.string()).optional().describe("Group results by fields: [\"status\"] or date extraction [\"date:year:createdAt\"]"),
        relConditions: z.record(z.any()).optional().describe("Filter by related table data: {relationName: {field: {operator: value}}}"),
    }, async (args, extra) => {
        const { collection, filter, fields, sort, page, limit, search, searchFields, aggregate, groupBy, relConditions } = args;
        try {
            const params = new URLSearchParams();
            if (page)
                params.set('page', String(page));
            if (limit)
                params.set('limit', String(limit));
            if (filter)
                params.set('filter', JSON.stringify(filter));
            if (fields)
                params.set('fields', JSON.stringify(fields));
            if (sort) {
                const [field, direction] = sort.split(":");
                params.set('sort', JSON.stringify([direction?.toLowerCase() === "desc" ? `-${field}` : field]));
            }
            if (search)
                params.set('search', search);
            if (searchFields)
                params.set('searchFields', searchFields.join(','));
            if (aggregate)
                params.set('aggregate', JSON.stringify(aggregate));
            if (groupBy)
                params.set('groupBy', groupBy.join(','));
            if (relConditions)
                params.set('relConditions', JSON.stringify(relConditions));
            const qs = params.toString();
            const res = await callRoute('GET', `/items/${encodeURIComponent(collection)}${qs ? '?' + qs : ''}`, extra);
            if (!res.ok)
                return errorResult(res.error || 'Failed to list items');
            return successResult(res.data);
        }
        catch (error) {
            return errorResult(error);
        }
    });
    registerTool("baasix_get_item", `Get a single row/record by its ID from a database table. Optionally include related data from linked tables.

FIELDS — use dot notation to include related data:
- ["*"] → all columns on this table
- ["*", "category.*"] → all columns + all related category columns
- ["name", "category.name", "author.email"] → specific fields from main + related tables
- ["*", "author.profile.*"] → deep nested relations (up to 7 levels)
The primary key is always returned.`, {
        collection: z.string().describe("Table/collection name"),
        id: z.string().describe("Row ID (UUID)"),
        fields: z.array(z.string()).optional().describe("Columns to return. Use [\"*\"] for all, dot notation for relations: [\"*\", \"category.*\"]"),
    }, async (args, extra) => {
        const { collection, id, fields } = args;
        try {
            const params = new URLSearchParams();
            if (fields)
                params.set('fields', JSON.stringify(fields));
            const qs = params.toString();
            const res = await callRoute('GET', `/items/${encodeURIComponent(collection)}/${encodeURIComponent(id)}${qs ? '?' + qs : ''}`, extra);
            if (!res.ok)
                return errorResult(res.error || 'Failed to get item');
            return successResult(res.data);
        }
        catch (error) {
            return errorResult(error);
        }
    });
    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'.

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.
For tables with timestamps: true, createdAt and updatedAt are auto-managed.
For foreign key fields, use the "_Id" suffixed column name (e.g., category_Id, author_Id).

EXAMPLE: collection: "products", data: {"name": "Widget", "price": 9.99, "category_Id": "<uuid>", "inStock": true}`, {
        collection: z.string().describe("Table/collection name to insert into"),
        data: z.record(z.any()).describe("Row data as {columnName: value}. FK columns use _Id suffix (e.g., category_Id)."),
    }, async (args, extra) => {
        const { collection, data } = args;
        try {
            const res = await callRoute('POST', `/items/${encodeURIComponent(collection)}`, extra, data);
            if (!res.ok)
                return errorResult(res.error || 'Failed to create item');
            return successResult(res.data);
        }
        catch (error) {
            return errorResult(error);
        }
    });
    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.

For tables with timestamps: true, updatedAt is automatically updated.
For foreign key fields, use the "_Id" suffixed column name (e.g., category_Id).`, {
        collection: z.string().describe("Table/collection name"),
        id: z.string().describe("Row ID (UUID) to update"),
        data: z.record(z.any()).describe("Fields to update as {columnName: newValue}. Only changed fields needed."),
    }, async (args, extra) => {
        const { collection, id, data } = args;
        try {
            const res = await callRoute('PATCH', `/items/${encodeURIComponent(collection)}/${encodeURIComponent(id)}`, extra, data);
            if (!res.ok)
                return errorResult(res.error || 'Failed to update item');
            return successResult(res.data);
        }
        catch (error) {
            return errorResult(error);
        }
    });
    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.", {
        collection: z.string().describe("Table/collection name"),
        id: z.string().describe("Row ID (UUID) to delete"),
    }, async (args, extra) => {
        const { collection, id } = args;
        try {
            const res = await callRoute('DELETE', `/items/${encodeURIComponent(collection)}/${encodeURIComponent(id)}`, extra);
            if (!res.ok)
                return errorResult(res.error || 'Failed to delete item');
            return successResult({ success: true, message: `Item '${id}' deleted` });
        }
        catch (error) {
            return errorResult(error);
        }
    });
    // ==================== File Management Tools ====================
    registerTool("baasix_list_files", `List uploaded files with metadata (filename, size, type, dimensions, storage location).

Filter examples:
By type: {"type": {"startsWith": "image/"}}
By name: {"originalFilename": {"iLike": "%report%"}}
Public files only: {"isPublic": {"eq": true}}`, {
        filter: z.record(z.any()).optional().describe("Filter using same operators as baasix_list_items: {\"type\": {\"startsWith\": \"image/\"}}"),
        page: z.number().optional().default(1).describe("Page number"),
        limit: z.number().optional().default(10).describe("Files per page. Use -1 for all."),
    }, async (args, extra) => {
        const { filter, page, limit } = args;
        try {
            const params = new URLSearchParams();
            if (page)
                params.set('page', String(page));
            if (limit)
                params.set('limit', String(limit));
            if (filter)
                params.set('filter', JSON.stringify(filter));
            const qs = params.toString();
            const res = await callRoute('GET', `/files${qs ? '?' + qs : ''}`, extra);
            if (!res.ok)
                return errorResult(res.error || 'Failed to list files');
            return successResult(res.data);
        }
        catch (error) {
            return errorResult(error);
        }
    });
    registerTool("baasix_get_file_info", "Get detailed metadata about a specific uploaded file — filename, size, MIME type, dimensions, storage location, and who uploaded it.", {
        id: z.string().describe("File UUID"),
    }, async (args, extra) => {
        const { id } = args;
        try {
            const res = await callRoute('GET', `/files/${encodeURIComponent(id)}`, extra);
            if (!res.ok)
                return errorResult(res.error || 'Failed to get file info');
            return successResult(res.data);
        }
        catch (error) {
            return errorResult(error);
        }
    });
    registerTool("baasix_delete_file", "Delete an uploaded file by its UUID. Removes both the file record and the actual file from storage.", {
        id: z.string().describe("File UUID"),
    }, async (args, extra) => {
        const { id } = args;
        try {
            const res = await callRoute('DELETE', `/files/${encodeURIComponent(id)}`, extra);
            if (!res.ok)
                return errorResult(res.error || 'Failed to delete file');
            return successResult({ success: true, message: `File '${id}' deleted` });
        }
        catch (error) {
            return errorResult(error);
        }
    });
    // ==================== Permission Tools ====================
    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) => {
        try {
            const res = await callRoute('GET', '/items/baasix_Role?limit=-1', extra);
            if (!res.ok)
                return errorResult(res.error || 'Failed to list roles');
            return successResult({ data: res.data?.data || res.data });
        }
        catch (error) {
            return errorResult(error);
        }
    });
    registerTool("baasix_list_permissions", `List all access control permission rules with optional filtering.

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).

Filter examples:
All permissions for a role: {"role_Id": {"eq": "<role-uuid>"}}
All read permissions: {"action": {"eq": "read"}}
Permissions for a table: {"collection": {"eq": "products"}}`, {
        filter: z.record(z.any()).optional().describe("Filter: {\"role_Id\": {\"eq\": \"<uuid>\"}}, {\"collection\": {\"eq\": \"products\"}}, {\"action\": {\"eq\": \"read\"}}"),
        sort: z.string().optional().describe("Sort as 'field:asc' or 'field:desc'"),
        page: z.number().optional().default(1).describe("Page number"),
        limit: z.number().optional().default(10).describe("Permissions per page. Use -1 for all."),
    }, async (args, extra) => {
        const { filter, sort, page, limit } = args;
        try {
            const params = new URLSearchParams();
            if (page)
                params.set('page', String(page));
            if (limit)
                params.set('limit', String(limit));
            if (filter)
                params.set('filter', JSON.stringify(filter));
            if (sort) {
                const [field, direction] = sort.split(":");
                params.set('sort', JSON.stringify([direction?.toLowerCase() === "desc" ? `-${field}` : field]));
            }
            const qs = params.toString();
            const res = await callRoute('GET', `/permissions${qs ? '?' + qs : ''}`, extra);
            if (!res.ok)
                return errorResult(res.error || 'Failed to list permissions');
            return successResult(res.data);
        }
        catch (error) {
            return errorResult(error);
        }
    });
    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).

First use baasix_list_roles to get the role UUID, then create permissions for that role.

--- ACTIONS ---
create: allow inserting new rows
read: allow querying/viewing rows
update: allow modifying existing rows
delete: allow deleting rows
Each action needs its own permission rule. A role with no permissions for a table cannot access it at all.

--- FIELDS (column-level access control) ---
["*"] → allow access to ALL columns
["name", "price", "status"] → allow access to ONLY these columns
For read: controls which columns are returned. For create/update: controls which columns can be written.

--- CONDITIONS (row-level security / RLS) ---
Uses the same filter operators as baasix_list_items. These conditions are enforced as security constraints — always ANDed with any user query, cannot be bypassed.
Only published: {"published": {"eq": true}}
Only own records: {"author_Id": {"eq": "$CURRENT_USER"}}
Only own tenant: {"tenant_Id": {"eq": "$CURRENT_USER.tenant_Id"}}
Multiple conditions: {"AND": [{"status": {"in": ["active", "draft"]}}, {"author_Id": {"eq": "$CURRENT_USER"}}]}

DYNAMIC VARIABLES in conditions:
$CURRENT_USER → current user's ID
$CURRENT_USER.fieldName → any field on the user (e.g., $CURRENT_USER.tenant_Id, $CURRENT_USER.department_Id)
$CURRENT_ROLE → current role ID
$NOW, $NOW+DAYS_7, $NOW-MONTHS_1 → timestamp math

--- RELCONDITIONS (row-level security on RELATED tables) ---
Restrict access based on data in related tables. Keys are relation names.
{"category": {"isPublic": {"eq": true}}} → only allow access if the related category is public
{"organization": {"members": {"user_Id": {"eq": "$CURRENT_USER"}}}} → nested relation check
These are merged with any query-level relConditions using AND.

--- DEFAULTVALUES (auto-injected values on create/update) ---
Values automatically set when this role creates or updates records. User-provided values override these.
{"status": "draft"} → new records default to draft status
{"author_Id": "$CURRENT_USER"} → auto-set author to current user
{"tenant_Id": "$CURRENT_USER.tenant_Id"} → auto-set tenant
Supports all dynamic variables.

--- EXAMPLES ---
Allow editors to read all products:
role_Id: "<uuid>", collection: "products", action: "read", fields: ["*"]

Allow users to only read their own posts:
role_Id: "<uuid>", collection: "posts", action: "read", fields: ["*"], conditions: {"author_Id": {"eq": "$CURRENT_USER"}}

Allow users to create posts with auto-set author:
role_Id: "<uuid>", collection: "posts", action: "create", fields: ["title", "content"], defaultValues: {"author_Id": "$CURRENT_USER", "status": "draft"}

Allow users to only update their own posts, only title and content:
role_Id: "<uuid>", collection: "posts", action: "update", fields: ["title", "content"], conditions: {"author_Id": {"eq": "$CURRENT_USER"}}`, {
        role_Id: z.string().describe("Role UUID — get this from baasix_list_roles"),
        collection: z.string().describe("Table/collection name this permission applies to"),
        action: z.enum(["create", "read", "update", "delete"]).describe("The CRUD action to allow"),
        fields: z.array(z.string()).optional().describe("Allowed columns: [\"*\"] for all, or [\"name\", \"price\"] for specific. Omit for all."),
        conditions: z.record(z.any()).optional().describe("Row-level security filter (same operators as filter in baasix_list_items). Always enforced."),
        defaultValues: z.record(z.any()).optional().describe("Auto-injected values on create/update: {\"author_Id\": \"$CURRENT_USER\", \"status\": \"draft\"}"),
        relConditions: z.record(z.any()).optional().describe("Row-level security on related tables: {\"category\": {\"isPublic\": {\"eq\": true}}}"),
    }, async (args, extra) => {
        const { role_Id, collection, action, fields, conditions, defaultValues, relConditions } = args;
        try {
            const data = { role_Id, collection, action };
            if (fields)
                data.fields = fields;
            if (conditions)
                data.conditions = conditions;
            if (defaultValues)
                data.defaultValues = defaultValues;
            if (relConditions)
                data.relConditions = relConditions;
            const res = await callRoute('POST', '/permissions', extra, data);
            if (!res.ok)
                return errorResult(res.error || 'Failed to create permission');
            return successResult(res.data);
        }
        catch (error) {
            return errorResult(error);
        }
    });
    registerTool("baasix_update_permission", `Update an existing permission rule. Only pass the fields you want to change — unspecified fields remain unchanged.

See baasix_create_permission for full documentation of conditions, relConditions, defaultValues, and fields options.
Use baasix_list_permissions or baasix_get_permissions to find the permission ID to update.`, {
        id: z.string().describe("Permission UUID — get from baasix_list_permissions or baasix_get_permissions"),
        role_Id: z.string().optional().describe("Change which role this permission applies to"),
        collection: z.string().optional().describe("Change which table this permission applies to"),
        action: z.enum(["create", "read", "update", "delete"]).optional().describe("Change the CRUD action"),
        fields: z.array(z.string()).optional().describe("Change allowed columns: [\"*\"] for all, or specific column names"),
        conditions: z.record(z.any()).optional().describe("Change row-level security conditions (same operators as filter)"),
        defaultValues: z.record(z.any()).optional().describe("Change auto-injected values on create/update"),
        relConditions: z.record(z.any()).optional().describe("Change row-level security on related tables"),
    }, async (args, extra) => {
        const { id, ...updateData } = args;
        try {
            const res = await callRoute('PATCH', `/permissions/${encodeURIComponent(id)}`, extra, updateData);
            if (!res.ok)
                return errorResult(res.error || 'Failed to update permission');
            return successResult(res.data);
        }
        catch (error) {
            return errorResult(error);
        }
    });
    registerTool("baasix_delete_permission", "Delete a permission rule by its UUID. This revokes the access it granted. The permission cache is automatically reloaded.", {
        id: z.string().describe("Permission UUID — get from baasix_list_permissions or baasix_get_permissions"),
    }, async (args, extra) => {
        const { id } = args;
        try {
            const res = await callRoute('DELETE', `/permissions/${encodeURIComponent(id)}`, extra);
            if (!res.ok)
                return errorResult(res.error || 'Failed to delete permission');
            return successResult({ success: true, message: `Permission '${id}' deleted` });
        }
        catch (error) {
            return errorResult(error);
        }
    });
    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) => {
        try {
            const res = await callRoute('POST', '/permissions/reload', extra);
            if (!res.ok)
                return errorResult(res.error || 'Failed to reload permissions');
            return successResult({ success: true, message: "Permissions reloaded" });
        }
        catch (error) {
            return errorResult(error);
        }
    });
    // ==================== Settings Tools ====================
    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.", {
        key: z.string().optional().describe("Specific setting key to retrieve (e.g., 'project_name', 'auth_password_policy'). Omit for all settings."),
    }, async (args, extra) => {
        const { key } = args;
        try {
            const path = key ? `/settings?key=${encodeURIComponent(key)}` : '/settings';
            const res = await callRoute('GET', path, extra);
            if (!res.ok)
                return errorResult(res.error || 'Failed to get settings');
            return successResult({ data: res.data });
        }
        catch (error) {
            return errorResult(error);
        }
    });
    registerTool("baasix_update_settings", "Update application settings. Pass an object with the settings keys and their new values. Only specified keys are updated.", {
        settings: z.record(z.any()).describe("Settings to update as {key: value}. Use baasix_get_settings first to see available keys."),
    }, async (args, extra) => {
        const { settings } = args;
        try {
            const res = await callRoute('PATCH', '/settings', extra, settings);
            if (!res.ok)
                return errorResult(res.error || 'Failed to update settings');
            return successResult({
                success: true,
                message: "Settings updated",
            });
        }
        catch (error) {
            return errorResult(error);
        }
    });
    // ==================== Email Template Tools ====================
    registerTool("baasix_list_templates", "List all email templates (invitation, password reset, welcome, etc.).", {
        filter: z.record(z.any()).optional().describe("Filter criteria"),
        page: z.number().optional().default(1).describe("Page number"),
        limit: z.number().optional().default(10).describe("Templates per page"),
    }, async (args, extra) => {
        const { filter, page, limit } = args;
        try {
            const params = new URLSearchParams();
            if (page)
                params.set('page', String(page));
            if (limit)
                params.set('limit', String(limit));
            if (filter)
                params.set('filter', JSON.stringify(filter));
            const qs = params.toString();
            const res = await callRoute('GET', `/items/baasix_Template${qs ? '?' + qs : ''}`, extra);
            if (!res.ok)
                return errorResult(res.error || 'Failed to list templates');
            return successResult(res.data);
        }
        catch (error) {
            return errorResult(error);
        }
    });
    registerTool("baasix_get_template", "Get a specific email template by ID", {
        id: z.string().describe("Template ID (UUID)"),
    }, async (args, extra) => {
        const { id } = args;
        try {
            const res = await callRoute('GET', `/items/baasix_Template/${encodeURIComponent(id)}`, extra);
            if (!res.ok)
                return errorResult(res.error || 'Failed to get template');
            return successResult(res.data);
        }
        catch (error) {
            return errorResult(error);
        }
    });
    registerTool("baasix_update_template", `Update an email template's subject, description, or body content.

TEMPLATE TYPES:
- magic_link: Magic link authentication emails
- invite: User invitation emails
- password_reset: Password reset emails
- welcome: Welcome emails
- verification: Email verification emails

AVAILABLE VARIABLES:
- User: {{user.firstName}}, {{user.lastName}}, {{user.fullName}}, {{user.email}}
- Tenant: {{tenant.name}}, {{tenant.logo}}, {{tenant.website}}
- Auth: {{magicLink}}, {{magicCode}}, {{resetPasswordLink}}, {{inviteLink}}`, {
        id: z.string().describe("Template ID (UUID)"),
        subject: z.string().optional().describe("Email subject line"),
        description: z.string().optional().describe("Template description"),
        body: z.string().optional().describe("Template body as HTML"),
        isActive: z.boolean().optional().describe("Whether the template is active"),
    }, async (args, extra) => {
        const { id, ...updateData } = args;
        try {
            const res = await callRoute('PATCH', `/items/baasix_Template/${encodeURIComponent(id)}`, extra, updateData);
            if (!res.ok)
                return errorResult(res.error || 'Failed to update template');
            return successResult(res.data);
        }
        catch (error) {
            return errorResult(error);
        }
    });
    // ==================== Notification Tools ====================
    registerTool("baasix_list_notifications", "List in-app notifications for the current authenticated user. Filter by seen/unseen status to find unread notifications.", {
        page: z.number().optional().default(1).describe("Page number"),
        limit: z.number().optional().default(10).describe("Notifications per page. Use -1 for all."),
        seen: z.boolean().optional().describe("Filter: true = only seen, false = only unseen, omit = all"),
    }, async (args, extra) => {
        const { page, limit, seen } = args;
        try {
            const params = new URLSearchParams();
            if (page)
                params.set('page', String(page));
            if (limit)
                params.set('limit', String(limit));
            if (seen !== undefined)
                params.set('seen', String(seen));
            const qs = params.toString();
            const res = await callRoute('GET', `/notifications${qs ? '?' + qs : ''}`, extra);
            if (!res.ok)
                return errorResult(res.error || 'Failed to list notifications');
            return successResult(res.data);
        }
        catch (error) {
            return errorResult(error);
        }
    });
    registerTool("baasix_mark_notification_seen", "Mark a notification as seen/read by its UUID.", {
        id: z.string().describe("Notification UUID"),
    }, async (args, extra) => {
        const { id } = args;
        try {
            const res = await callRoute('POST', '/notifications/mark-seen', extra, { notificationIds: [id] });
            if (!res.ok)
                return errorResult(res.error || 'Failed to mark notification as seen');
            return successResult(res.data);
        }
        catch (error) {
            return errorResult(error);
        }
    });
    // ==================== Utility Tools ====================
    registerTool("baasix_server_info", "Get server health, version, uptime, and memory usage.", {}, async (_args, _extra) => {
        try {
            const info = {
                name: "baasix",
                version: env.get("npm_package_version") || "0.1.0",
                environment: env.get("NODE_ENV") || "development",
                timestamp: new Date().toISOString(),
                uptime: process.uptime(),
                memory: process.memoryUsage(),
                nodejs: process.version,
                mcp: {
                    enabled: true,
                    version: "0.1.0",
                },
            };
            return successResult(info);
        }
        catch (error) {
            return errorResult(error);
        }
    });
    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.

MODE:
- "before" (default): Places item directly before the target row
- "after": Places item directly after the target row

EXAMPLE: Move task "abc" before task "xyz":
  collection: "tasks", item: "abc", to: "xyz", mode: "before"`, {
        collection: z.string().describe("Table/collection name (must have a sort field)"),
        item: z.string().describe("UUID of the row to move"),
        to: z.string().describe("UUID of the target row to position relative to"),
        mode: z.enum(["before", "after"]).optional().default("before").describe("'before' = place item before target (default), 'after' = place item after target"),
    }, async (args, extra) => {
        const { collection, item, to, mode } = args;
        try {
            const { sortItems } = await import("../utils/sortUtils.js");
            const accountability = getAccountability(extra);
            const result = await sortItems({
                collection,
                item,
                to,
                mode: mode || 'before',
                accountability,
            });
            return successResult({
                success: true,
                item: result.item,
                collection: result.collection,
                newSort: result.newSort,
                message: `Item '${item}' moved ${mode || 'before'} '${to}'`,
            });
        }
        catch (error) {
            return errorResult(error);
        }
    });
    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.

QUERY OPTIONS:
- fields: Columns to return. Default ["*"]. Use dot notation for relations: ["category.name", "status"].
- filter: Row filter (same operators as baasix_list_items). Applied BEFORE aggregation.
- sort: Sort results, e.g. ["count:desc", "name:asc"].
- limit: Max rows returned. Default -1 (all).
- page: Page number (works with limit).
- aggregate: Aggregation functions. Each key is an alias, value is {function, field}.
    Functions: "count", "sum", "avg", "min", "max"
    Field: column name or "*" for count.
- groupBy: Array of columns to group by. Required when using aggregate.

EXAMPLES:

1. Count orders grouped by status:
   collection: "orders"
   groupBy: ["status"]
   aggregate: {"count": {"function": "count", "field": "*"}}

2. Average price per category, only active products:
   collection: "products"
   groupBy: ["category"]
   aggregate: {"avg_price": {"function": "avg", "field": "price"}, "count": {"function": "count", "field": "*"}}
   filter: {"status": {"eq": "active"}}

3. Total revenue by month (raw data for client-side grouping):
   collection: "orders"
   fields: ["createdAt", "total"]
   filter: {"createdAt": {"gte": "2026-01-01"}}
   sort: ["createdAt:asc"]

4. Sum of amounts per user with relational field:
   collection: "payments"
   groupBy: ["user.name"]
   aggregate: {"total": {"function": "sum", "field": "amount"}}
   fields: ["user.name"]

5. Min/max price:
   collection: "products"
   aggregate: {"min_price": {"function": "min", "field": "price"}, "max_price": {"function": "max", "field": "price"}}`, {
        collection: z.string().describe("Table/collection name to report on"),
        fields: z.array(z.string()).optional().describe('Columns to return. Default ["*"]. Use dot notation for relations: ["category.name"].'),
        filter: z.record(z.any()).optional().describe("Row filter applied before aggregation. Same operators as baasix_list_items."),
        sort: z.array(z.string()).optional().describe('Sort results. Format: ["field:direction"]. E.g. ["count:desc", "name:asc"].'),
        limit: z.number().optional().describe("Max rows returned. Default -1 (all). Set to limit grouped results."),
        page: z.number().optional().describe("Page number for pagination (works with limit)."),
        aggregate: z.record(z.object({
            function: z.enum(["count", "sum", "avg", "min", "max"]).describe("Aggregation function"),
            field: z.string().describe('Column to aggregate, or "*" for count'),
        })).optional().describe("Aggregation definitions. Each key is the result alias."),
        groupBy: z.array(z.string()).optional().describe("Columns to group by. Required when using aggregate."),
    }, async (args, extra) => {
        const { collection, fields, filter, sort, limit, page, aggregate, groupBy } = args;
        try {
            const body = {};
            if (fields)
                body.fields = fields;
            if (filter)
                body.filter = filter;
            if (sort) {
                // Convert "field:desc" → "-field", "field:asc" → "field" for ItemsService
                body.sort = sort.map(s => {
                    const [field, dir] = s.split(':');
                    return dir?.toLowerCase() === 'desc' ? `-${field}` : field;
                });
            }
            if (limit !== undefined)
                body.limit = limit;
            if (page !== undefined)
                body.page = page;
            if (aggregate)
                body.aggregate = aggregate;
            if (groupBy)
                body.groupBy = groupBy;
            const res = await callRoute('POST', `/reports/${encodeURIComponent(collection)}`, extra, body);
            if (!res.ok)
                return errorResult(res.error || `Failed to generate report for '${collection}'`);
            return successResult(res.data);
        }
        catch (error) {
            return errorResult(error);
        }
    });
    // ==================== Collection Stats Tool ====================
    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.).

Each stats entry requires:
- name: A unique label for this stat in the results (e.g. "total_orders", "active_users")
- collection: The table to query
- query: Full report query object with fields, filter, aggregate, groupBy, sort, limit, page

AGGREGATE FUNCTIONS: "count", "sum", "avg", "min", "max"

EXAMPLES:

1. Get total counts for two tables:
   stats: [
     {"name": "total_products", "collection": "products", "query": {"aggregate": {"total": {"function": "count", "field": "*"}}}},
     {"name": "total_orders", "collection": "orders", "query": {"aggregate": {"total": {"function": "count", "field": "*"}}}}
   ]

2. Count active users + total revenue:
   stats: [
     {"name": "active_users", "collection": "users", "query": {"filter": {"status": {"eq": "active"}}, "aggregate": {"count": {"function": "count", "field": "*"}}}},
     {"name": "total_revenue", "collection": "orders", "query": {"aggregate": {"revenue": {"function": "sum", "field": "total"}}}}
   ]

3. Recent signups (last 7 days):
   stats: [
     {"name": "recent_signups", "collection": "users", "query": {"filter": {"createdAt": {"gte": "$NOW-DAYS_7"}}, "aggregate": {"count": {"function": "count", "field": "*"}}}}
   ]`, {
        stats: z.array(z.object({
            name: z.string().describe("Unique label for this stat in the results (e.g. 'total_orders')"),
            collection: z.string().describe("Table/collection to query"),
            query: z.record(z.any()).describe("Report query: {fields?, filter?, sort?, limit?, page?, aggregate?, groupBy?}. See baasix_generate_report for full options."),
        })).describe("Array of stat queries to run across tables"),
    }, async (args, extra) => {
        const { stats } = args;
        try {
            const res = await callRoute('POST', '/reports/stats', extra, { stats });
            if (!res.ok)
                return errorResult(res.error || 'Failed to generate stats');
            return successResult(res.data);
        }
        catch (error) {
            return errorResult(error);
        }
    });
    // ==================== Send Notification Tool ====================
    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.", {
        recipients: z.array(z.string()).describe("Array of user UUIDs to notify"),
        title: z.string().describe("Notification title"),
        message: z.string().describe("Notification message body"),
        type: z.string().optional().default("info").describe("Notification type: 'info', 'warning', 'error', 'success'"),
    }, async (args, extra) => {
        const { recipients, title, message, type } = args;
        try {
            const res = await callRoute('POST', '/notifications/send', extra, {
                type: type || "info",
                title,
                message,
                userIds: recipients,
            });
            if (!res.ok)
                return errorResult(res.error || 'Failed to send notifications');
            return successResult({
                success: true,
                sent: recipients.length,
                notificationIds: res.data?.notificationIds || res.data,
            });
        }
        catch (error) {
            return errorResult(error);
        }
    });
    // ==================== Get Permission Tool ====================
    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.", {
        id: z.string().describe("Permission UUID"),
    }, async (args, extra) => {
        const { id } = args;
        try {
            const res = await callRoute('GET', `/permissions/${encodeURIComponent(id)}`, extra);
            if (!res.ok)
                return errorResult(res.error || `Permission '${id}' not found`);
            return successResult(res.data?.data || res.data);
        }
        catch (error) {
            return errorResult(error);
        }
    });
    // ==================== Get Permissions for Role Tool ====================
    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.

Accepts either the role name (e.g., "editor", "public") or the role UUID.`, {
        role: z.string().describe("Role name (e.g., 'editor', 'public') or role UUID"),
    }, async (args, extra) => {
        const { role } = args;
        try {
            // Find role by name or id
            const roleParams = new URLSearchParams();
            roleParams.append('filter', JSON.stringify({ OR: [{ name: { eq: role } }, { id: { eq: role } }] }));
            roleParams.append('limit', '1');
            const rolesRes = await callRoute('GET', `/items/baasix_Role?${roleParams}`, extra);
            if (!rolesRes.ok)
                return errorResult(rolesRes.error || `Failed to look up role '${role}'`);
            const rolesData = rolesRes.data?.data || rolesRes.data;
            if (!Array.isArray(rolesData) || !rolesData.length) {
                return errorResult(`Role '${role}' not found`);
            }
            const roleId = rolesData[0].id;
            // Get permissions for this role
            const permParams = new URLSearchParams();
            permParams.append('filter', JSON.stringify({ role_Id: { eq: roleId } }));
            permParams.append('limit', '-1');
            const permRes = await callRoute('GET', `/permissions?${permParams}`, extra);
            if (!permRes.ok)
                return errorResult(permRes.error || 'Failed to get permissions');
            const permData = permRes.data?.data || permRes.data;
            return successResult({
                role: rolesData[0],
                permissions: Array.isArray(permData) ? permData : [],
                totalCount: Array.isArray(permData) ? permData.length : 0,
            });
        }
        catch (error) {
            return errorResult(error);
        }
    });
    // ==================== Update Permissions for Role Tool ====================
    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.

Accepts role name (e.g., "editor") or role UUID. Each permission in the array specifies a table + action combination.
If a permission for that role+table+action already exists, it is updated. Otherwise, a new one is created.
Fields default to ["*"] (all columns) if omitted.

EXAMPLE — Give "editor" full CRUD on products, read-only on categories:
role: "editor"
permissions: [
  {"collection": "products", "action": "create", "fields": ["*"]},
  {"collection": "products", "action": "read", "fields": ["*"]},
  {"collection": "products", "action": "update", "fields": ["name", "price"]},
  {"collection": "products", "action": "delete"},
  {"collection": "categories", "action": "read", "fields": ["*"]}
]

With row-level security:
permissions: [{"collection": "posts", "action": "read", "conditions": {"author_Id": {"eq": "$CURRENT_USER"}}}]`, {
        role: z.string().describe("Role name (e.g., 'editor', 'public') or role UUID"),
        permissions: z
            .array(z.object({
            collection: z.string().describe("Table/collection name"),
            action: z.enum(["create", "read", "update", "delete"]).describe("CRUD action to allow"),
            fields: z.array(z.string()).optional().describe("Allowed columns: [\"*\"] for all, or specific names. Defaults to [\"*\"]."),
            conditions: z.record(z.any()).optional().describe("Row-level security conditions (same operators as filter). Supports $CURRENT_USER, $NOW."),
        }))
            .describe("Array of permission rules to create or update for this role"),
    }, async (args, extra) => {
        const { role, permissions } = args;
        try {
            // Find role
            const roleParams = new URLSearchParams();
            roleParams.append('filter', JSON.stringify({ OR: [{ name: { eq: role } }, { id: { eq: role } }] }));
            roleParams.append('limit', '1');
            const rolesRes = await callRoute('GET', `/items/baasix_Role?${roleParams}`, extra);
            if (!rolesRes.ok)
                return errorResult(rolesRes.error || `Failed to look up role '${role}'`);
            const rolesData = rolesRes.data?.data || rolesRes.data;
            if (!Array.isArray(rolesData) || !rolesData.length) {
                return errorResult(`Role '${role}' not found`);
            }
            const roleData = rolesData[0];
            const roleId = roleData.id;
            // Create/update permissions
            const results = await Promise.all(permissions.map(async (perm) => {
                // Check if permission exists for this role+collection+action
                const existParams = new URLSearchParams();
                existParams.append('filter', JSON.stringify({
                    AND: [{ role_Id: { eq: roleId } }, { collection: { eq: perm.collection } }, { action: { eq: perm.action } }],
                }));
                existParams.append('limit', '1');
                const existRes = await callRoute('GET', `/permissions?${existParams}`, extra);
                const existData = existRes.data?.data || existRes.data;
                if (Array.isArray(existData) && existData.length) {
                    // Update existing via PATCH /permissions/:id (route reloads cache)
                    return callRoute('PATCH', `/permissions/${encodeURIComponent(existData[0].id)}`, extra, {
                        fields: perm.fields || ["*"],
                        conditions: perm.conditions || {},
                    });
                }
                else {
                    // Create new via POST /permissions (route reloads cache)
                    return callRoute('POST', '/permissions', extra, {
                        role_Id: roleId,
                        collection: perm.collection,
                        action: perm.action,
                        fields: perm.fields || ["*"],
                        conditions: perm.conditions || {},
                    });
                }
            }));
            // Reload permissions cache explicitly
            await callRoute('POST', '/permissions/reload', extra);
            return successResult({
                success: true,
                role: roleData.name,
                updated: results.length,
            });
        }
        catch (error) {
            return errorResult(error);
        }
    });
    // ==================== Auth Tools ====================
    registerTool("baasix_get_current_user", `Get the currently authenticated user's profile, role, and permissions.

Use fields to include related data:
- ["*"] → all user fields
- ["*", "role.*"] → user fields + full role object
- ["email", "firstName", "role.name"] → specific fields

Returns authenticated: false if no user is logged in (public/anonymous access).`, {
        fields: z.array(z.string()).optional().describe("Fields to return. Default: [\"*\", \"role.*\"]. Use dot notation for relations."),
    }, async (args, extra) => {
        const { fields } = args;
        try {
            const accountability = getAccountability(extra);
            if (!accountability.user) {
                return successResult({
                    authenticated: false,
                    role: accountability.role,
                    admin: false,
                });
            }
            // Call /auth/me which returns user, role, permissions, tenant
            const res = await callRoute('GET', '/auth/me', extra);
            if (!res.ok) {
                // If 401, user is not authenticated
                return successResult({
                    authenticated: false,
                    role: accountability.role,
                    admin: false,
                });
            }
            return successResult({
                authenticated: true,
                user: res.data?.user || res.data,
                role: res.data?.role || accountability.role,
                admin: accountability.user?.isAdmin || false,
            });
        }
        catch (error) {
            return errorResult(error);
        }
    });
    registerTool("baasix_register_user", "Register a new user", {
        email: z.string().email().describe("User email address"),
        password: z.string().describe("User password"),
        firstName: z.string().optional().describe("User first name"),
        lastName: z.string().optional().describe("User last name"),
        roleName: z.string().optional().describe("Role name to assign"),
    }, async (args, _extra) => {
        const { email, password, firstName, lastName, roleName } = args;
        try {
            const { default: axios } = await import("axios");
            const baseUrl = `http://localhost:${env.get("PORT") || "8056"}`;
            const response = await axios.post(`${baseUrl}/auth/register`, {
                email,
                password,
                firstName,
                lastName,
                roleName,
            });
            return successResult(response.data);
        }
        catch (error) {
            const axiosError = error;
            return errorResult(axiosError.response?.data?.message || axiosError.message || "Unknown error");
        }
    });
    registerTool("baasix_send_invite", "Send an invitation to a user", {
        email: z.string().email().describe("Email address to invite"),
        role_Id: z.string().describe("Role ID to assign"),
        tenant_Id: z.string().optional().describe("Tenant ID"),
        link: z.string().url().describe("Application URL for the invitation link"),
    }, async (args, _extra) => {
        const { email, role_Id, tenant_Id, link } = args;
        try {
            const { default: axios } = await import("axios");
            const baseUrl = `http://localhost:${env.get("PORT") || "8056"}`;
            const response = await axios.post(`${baseUrl}/auth/invite`, {
                email,
                role_Id,
                tenant_Id,
                link,
            });
            return successResult(response.data);
        }
        catch (error) {
            const axiosError = error;
            return errorResult(axiosError.response?.data?.message || axiosError.message || "Unknown error");
        }
    });
    registerTool("baasix_verify_invite", "Verify an invitation token", {
        token: z.string().describe("Invitation token"),
        link: z.string().url().optional().describe("Application URL to validate"),
    }, async (args, _extra) => {
        const { token, link } = args;
        try {
            const { default: axios } = await import("axios");
            const baseUrl = `http://localhost:${env.get("PORT") || "8056"}`;
            const params = new URLSearchParams();
            if (link)
                params.append("link", link);
            const response = await axios.get(`${baseUrl}/auth/verify-invite/${token}?${params}`);
            return successResult(response.data);
        }
        catch (error) {
            const axiosError = error;
            return errorResult(axiosError.response?.data?.message || axiosError.message || "Unknown error");
        }
    });
    registerTool("baasix_send_magic_link", "Send magic link or code for authentication", {
        email: z.string().email().describe("User email address"),
        link: z.string().url().optional().describe("Application URL for magic link"),
        mode: z.enum(["link", "code"]).optional().default("link").describe("Magic authentication mode"),
    }, async (args, _extra) => {
        const { email, link, mode } = args;
        try {
            const { default: axios } = await import("axios");
            const baseUrl = `http://localhost:${env.get("PORT") || "8056"}`;
            const response = await axios.post(`${baseUrl}/auth/magiclink`, {
                email,
                link,
                mode,
            });
            return successResult(response.data);
        }
        catch (error) {
            const axiosError = error;
            return errorResult(axiosError.response?.data?.message || axiosError.message || "Unknown error");
        }
    });
    registerTool("baasix_get_user_tenants", "Get available tenants for the current user", {}, async (_args, _extra) => {
        try {
            const { default: axios } = await import("axios");
            const baseUrl = `http://localhost:${env.get("PORT") || "8056"}`;
            const response = await axios.get(`${baseUrl}/auth/tenants`);
            return successResult(response.data);
        }
        catch (error) {
            const axiosError = error;
            return errorResult(axiosError.response?.data?.message || axiosError.message || "Unknown error");
        }
    });
    registerTool("baasix_switch_tenant", "Switch to a different tenant context", {
        tenant_Id: z.string().describe("Tenant ID to switch to"),
    }, async (args, _extra) => {
        const { tenant_Id } = args;
        try {
            const { default: axios } = await import("axios");
            const baseUrl = `http://localhost:${env.get("PORT") || "8056"}`;
            const response = await axios.post(`${baseUrl}/auth/switch-tenant`, {
                tenant_Id,
            });
            return successResult(response.data);
        }
        catch (error) {
            const axiosError = error;
            return errorResult(axiosError.response?.data?.message || axiosError.message || "Unknown error");
        }
    });
    // ==================== Auth Status & Session Tools ====================
    registerTool("baasix_auth_status", "Check if the current MCP session is authenticated, and show the current user, role, and admin status.", {}, async (_args, extra) => {
        try {
            const accountability = getAccountability(extra);
            return successResult({
                authenticated: !!accountability.user,
                userId: accountability.user?.id || null,
                role: accountability.role,
                admin: accountability.user?.isAdmin || false,
                sessionId: extra.sessionId || null,
            });
        }
        catch (error) {
            return errorResult(error);
        }
    });
    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.", {
        email: z.string().email().describe("User email address"),
        password: z.string().describe("User password"),
    }, async (args, extra) => {
        const { email, password } = args;
        try {
            const { default: axios } = await import("axios");
            const baseUrl = `http://localhost:${env.get("PORT") || "8056"}`;
            const response = await axios.post(`${baseUrl}/auth/login`, { email, password });
            const data = response.data;
            if (!data || !data.user) {
                return errorResult("Login failed: invalid credentials");
            }
            // Build accountability from login response and store in session
            const isAdmin = data.role?.name === "administrator";
            const accountability = {
                user: {
                    id: data.user.id,
                    email: data.user.email,
                    firstName: data.user.firstName,
                    lastName: data.user.lastName,
                    isAdmin,
                    role: data.role?.name || "public",
                },
                role: data.role ? {
                    id: data.role.id,
                    name: data.role.name,
                    isTenantSpecific: data.role.isTenantSpecific,
                } : null,
                permissions: data.permissions || [],
                tenant: data.tenant?.id || null,
                ipaddress: "127.0.0.1",
                token: data.token,
            };
            // Update the MCP session with the new accountability
            if (extra.sessionId) {
                setMCPSession(extra.sessionId, accountability);
            }
            return successResult({
                token: data.token,
                user: data.user,
                role: data.role,
                message: "Login successful. Session accountability updated.",
            });
        }
        catch (error) {
            const axiosError = error;
            return errorResult(axiosError.response?.data?.message || axiosError.message || "Login failed");
        }
    });
    registerTool("baasix_logout", "Logout and invalidate current session", {}, async (_args, extra) => {
        try {
            // Remove MCP session if exists
            if (extra.sessionId) {
                removeMCPSession(extra.sessionId);
            }
            return successResult({ success: true, message: "Logged out successfully. Session cleared." });
        }
        catch (error) {
            const err = error;
            return errorResult(err.message || "Logout failed");
        }
    });
    registerTool("baasix_refresh_auth", "Refresh authentication token", {
        refreshToken: z.string().optional().describe("Refresh token (if not using cookies)"),
    }, async (_args, extra) => {
        try {
            // In MCP context, the session is already maintained via session IDs.
            // Refresh is not needed since accountability is stored per-session.
            const accountability = getAccountability(extra);
            return successResult({
                authenticated: !!accountability.user,
                userId: accountability.user?.id || null,
                role: accountability.role,
                message: "MCP sessions are managed internally. Use baasix_login to re-authenticate if needed.",
            });
        }
        catch (error) {
            const err = error;
            return errorResult(err.message || "Refresh failed");
        }
    });
    // ==================== Update Relationship Tool ====================
    registerTool("baasix_update_relationship", "Modify an existing foreign key / relationship between two database tables (change delete behavior, alias, etc.).", {
        sourceCollection: z.string().describe("Source collection name"),
        relationshipName: z.string().describe("Relationship field name to update"),
        relationshipData: z
            .object({
            alias: z.string().optional().describe("Alias for reverse relationship"),
            onDelete: z.enum(["CASCADE", "RESTRICT", "SET NULL"]).optional().describe("Delete behavior"),
            onUpdate: z.enum(["CASCADE", "RESTRICT", "SET NULL"]).optional().describe("Update behavior"),
            description: z.string().optional().describe("Relationship description"),
        })
            .describe("Updated relationship configuration"),
    }, async (args, extra) => {
        const { sourceCollection, relationshipName, relationshipData } = args;
        const res = await callRoute('PATCH', `/schemas/${encodeURIComponent(sourceCollection)}/relationships/${encodeURIComponent(relationshipName)}`, extra, relationshipData);
        if (!res.ok)
            return errorResult(res.error || `Failed to update relationship '${relationshipName}' on '${sourceCollection}'`);
        return successResult({
            success: true,
            collection: sourceCollection,
            relationship: res.data,
        });
    });
    return server;
}
export default { createMCPServer, setMCPSession, removeMCPSession };
//# sourceMappingURL=MCPService.js.map