{
  "server": "mediflo",
  "serverVersion": "0.6.0",
  "apiVersion": "v1",
  "protocolVersions": [
    "2025-11-25",
    "2025-06-18",
    "2025-03-26",
    "2024-11-05",
    "2024-10-07"
  ],
  "tools": [
    {
      "name": "get_server_info",
      "requiredScope": null,
      "description": "Describe this Mediflo MCP server: product, mirrored REST API version, negotiable MCP protocol revisions, and the tools the calling API key is allowed to use. Call this first if a tool you expected is missing — it is usually a missing scope on the key rather than a missing tool. `hints` names capabilities this key cannot see and how to obtain them.",
      "descriptionPl": "Zwraca informacje o serwerze: wersję API, negocjowane wersje protokołu i listę narzędzi dostępnych dla użytego klucza.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {}
      }
    },
    {
      "name": "get_recommended_defaults",
      "requiredScope": "bots.read",
      "description": "Mediflo's recommended out-of-the-box settings for a bot (phone or website chat) — use these instead of inventing values, and read them at setup time rather than copying them into your own code, so a platform change reaches your bots. Returns the recommended `turnTakingMode` and the `rag` retrieval defaults. The models are configured by Mediflo: a Mediflo staff connection also gets the model stack (`modelConfig`, the `extraction` model policy). This is the same bundle GET /api/external/v1/model-catalog publishes as `data.recommendedDefaults`. NOTE: create_bot already applies all of this to a new bot, so you mainly need this to inspect what a bot started from, or to bring an OLDER bot up to the current recommendation via update_bot_config.",
      "descriptionPl": "Zwraca ustawienia rekomendowane przez Mediflo dla bota głosowego — modele, sposób prowadzenia rozmowy, analizę i wyszukiwanie w bazie wiedzy. Nowe boty startują już z tymi ustawieniami.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {}
      }
    },
    {
      "name": "list_extraction_models",
      "requiredScope": "bots.read",
      "description": "Mediflo staff only — the models are configured by Mediflo, and any other connection is refused. The models selectable for POST-call extraction — this is where a valid `remarksLlmModelId` comes from, and publishing a bot REQUIRES that field. Do not confuse this catalog with the live voice pipeline: `modelConfig` picks the models that talk to the caller, while these analyse the finished call. The list is CURATED (a write outside it is rejected) and returned in the recommended order, with the recommended pick flagged `isDefault`. Pass a row `id` — the uuid — as `remarksLlmModelId`, not the `modelId` string. create_bot already seeds the default, so you only need this to choose a different one.",
      "descriptionPl": "Lista modeli dostępnych do analizy po rozmowie, z oznaczonym modelem rekomendowanym.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {}
      }
    },
    {
      "name": "list_bots",
      "requiredScope": "bots.read",
      "description": "List the tenant bots — website chat, phone, or both — with their `channels`, location, bound phone number (null when the bot does not place calls) and publish pointer (`currentConfigSnapshotId` — null means the bot has never been published and cannot take a real conversation or call). A WEB_CHAT bot does not need a phone number. Mirrors GET /api/external/v1/bots.",
      "descriptionPl": "Lista botów organizacji (telefon i czat) wraz z lokalizacją, numerem telefonu i informacją, czy bot ma opublikowaną wersję.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "page": {
            "default": 1,
            "type": "integer",
            "minimum": 1,
            "maximum": 9007199254740991
          },
          "limit": {
            "default": 50,
            "type": "integer",
            "minimum": 1,
            "maximum": 100
          }
        }
      }
    },
    {
      "name": "get_bot",
      "requiredScope": "bots.read",
      "description": "Read one bot, plus `publishState`: whether a published version exists, and which snapshot version is currently LIVE. Note there is no \"draft has unpublished changes\" flag — `updatedAt` also bumps on the publish itself, so it cannot answer that question honestly. Compare the draft from get_bot_config against the live version yourself, or simply publish again (publishing is versioned and reversible). Mirrors GET /api/external/v1/bots/{botId}.",
      "descriptionPl": "Zwraca dane jednego bota i wskazuje, na której opublikowanej wersji obecnie działa.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "botId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
          }
        },
        "required": [
          "botId"
        ]
      }
    },
    {
      "name": "create_bot",
      "requiredScope": "bots.write",
      "description": "Create a bot for the channels you pass (`WEB_CHAT`, `VOICE_INBOUND`, `VOICE_OUTBOUND`). Omit `channels` and it is created on the channels this organization can use: website chat when the plan includes it (on an older organization, when the website-chat feature is on), phone when voice bots are included and there is a phone number, or phone when website chat is not available. A chat-only organization does not get a voice bot, and you must not pass phone channels it cannot use. It arrives PRE-CONFIGURED with Mediflo's recommended platform defaults — the models (configured by Mediflo), the recommended turn-taking mode and the default post-call extraction model are seeded as part of the create, so you do NOT need to set any of them. What is still yours to write: the prompt. The ordered path from here: 1) update_bot_config (agentPromptText, greetingConfig, guardrailsConfig; for WEB_CHAT also channelSettings.webChat — greeting, handoffMessage, guardrailFallbackMessage. remarksOutputSchemaJson is OPTIONAL — omit it to extract only the always-on system analysis fields. Publishing still REQUIRES the prompt and the model settings, but creation seeded the model settings, so you only write the prompt), 2) publish_bot_config — nothing you configured runs before this step, 3) for WEB_CHAT, bind the chat widget in the Portal (Settings → Chat widget: this bot, allowed domains, colours). Do NOT bind a phone number for a chat-only bot. For VOICE_OUTBOUND, bind a phone number with update_bot (phoneNumberId). Check get_bot_readiness at any point (readyForWebChat vs readyForOutbound). Widget binding, a website knowledge crawl, the website-chat preview and the Terminarz scheduling toolset are Portal-only. Conversation workflows (create_workflow_from_template / activate_workflow) are an OPTIONAL structuring tool for rigid, form-like flows — most bots run on the prompt alone; do not add one unless the flow genuinely needs it. Mirrors POST /api/external/v1/bots.",
      "descriptionPl": "Tworzy bota (telefon lub czat) od razu skonfigurowanego zgodnie z rekomendacjami Mediflo.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1
          },
          "locationId": {
            "anyOf": [
              {
                "anyOf": [
                  {
                    "type": "string",
                    "format": "uuid",
                    "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$"
                  },
                  {
                    "type": "string",
                    "const": ""
                  }
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "phoneNumberId": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "maxConcurrentCalls": {
            "default": 5,
            "type": "number",
            "minimum": 1,
            "maximum": 100
          },
          "isActive": {
            "default": true,
            "type": "boolean"
          },
          "priority": {
            "default": 50,
            "type": "number",
            "minimum": 0,
            "maximum": 100
          },
          "description": {
            "type": "string"
          },
          "operatingHoursStart": {
            "type": "string"
          },
          "operatingHoursEnd": {
            "type": "string"
          },
          "operatingDays": {
            "type": "array",
            "items": {
              "type": "number"
            }
          },
          "country": {
            "default": "PL",
            "type": "string",
            "minLength": 2,
            "maxLength": 2
          },
          "channels": {
            "minItems": 1,
            "maxItems": 3,
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "WEB_CHAT",
                "VOICE_INBOUND",
                "VOICE_OUTBOUND"
              ]
            }
          }
        },
        "required": [
          "name"
        ],
        "additionalProperties": false
      }
    },
    {
      "name": "update_bot",
      "requiredScope": "bots.write",
      "description": "Update a bot’s deployment settings: name, description, isActive, priority, maxConcurrentCalls, country, locationId, phoneNumberId, operating hours and channels. These take effect immediately — they are NOT part of the configuration draft, so this call does not need publish_bot_config. The prompt, greeting, guardrails and channelSettings still go through update_bot_config and then publish_bot_config. `channels` replaces the list (`WEB_CHAT`, `VOICE_INBOUND`, `VOICE_OUTBOUND`; at least one). You can only ADD a channel this organization can use: website chat when the plan includes it (on an older organization, the website-chat feature); phone channels when voice bots are included and there is a phone number, or when website chat is not available. A channel the bot already serves can stay. Removing a channel that is still in use is refused: the chat widget (unbind it in the Portal, Settings → Chat widget), an inbound number’s flow, or the caller-ID number (clear phoneNumberId in the same call). phoneNumberId binds the outbound caller ID and turns VOICE_OUTBOUND on. The number must already exist and be connected to a trunk. Provisioning numbers is not part of this tool. Bind the chat widget, its allowed domains and its colours in the Portal (Settings → Chat widget). That binding is not available over MCP. Preview the website chat from the bot’s Test tab in the Portal. locationId null clears the location; a bot running on a Mediflo template must keep one. Mirrors PATCH /api/external/v1/bots/{botId}.",
      "descriptionPl": "Zmienia nazwę bota, to czy jest włączony, oraz jego kanały. Działa od razu.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1
          },
          "locationId": {
            "anyOf": [
              {
                "anyOf": [
                  {
                    "type": "string",
                    "format": "uuid",
                    "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$"
                  },
                  {
                    "type": "string",
                    "const": ""
                  }
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "phoneNumberId": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "maxConcurrentCalls": {
            "type": "number",
            "minimum": 1,
            "maximum": 100
          },
          "isActive": {
            "type": "boolean"
          },
          "priority": {
            "type": "number",
            "minimum": 0,
            "maximum": 100
          },
          "description": {
            "type": "string"
          },
          "operatingHoursStart": {
            "type": "string"
          },
          "operatingHoursEnd": {
            "type": "string"
          },
          "operatingDays": {
            "type": "array",
            "items": {
              "type": "number"
            }
          },
          "country": {
            "type": "string",
            "minLength": 2,
            "maxLength": 2
          },
          "channels": {
            "minItems": 1,
            "maxItems": 3,
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "WEB_CHAT",
                "VOICE_INBOUND",
                "VOICE_OUTBOUND"
              ]
            }
          },
          "botId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
          }
        },
        "required": [
          "botId"
        ],
        "additionalProperties": false
      }
    },
    {
      "name": "get_bot_config",
      "requiredScope": "bots.read",
      "description": "Read the bot DRAFT configuration: agentPromptText, greetingConfig (the shared greeting plus an optional per-direction byDirection.inbound / byDirection.outbound — static, llm, or off = the bot waits for the caller (after ~4 s of silence it improvises an opener); a missing direction uses the shared greeting; web chat ignores byDirection), channelSettings (per-channel overrides; today only webChat.greeting, webChat.handoffMessage and webChat.guardrailFallbackMessage — null when unset; the website-chat greeting replaces greetingConfig for that channel only), guardrailsConfig, toolsConfig (tools and MCP servers), turnTakingMode, promptVariableMetadata and the post-call extraction fields (remarksPromptText, remarksLlmModelId, remarksOutputSchemaJson), plus entryProcedureId — the DETERMINISTIC procedure that drives the whole call, if any (list_procedures reads the procedures themselves). Tool gates live inside toolsConfig, as mcp_servers[].tool_gates (keyed by tool name) and http_tools[].gates — the http_tools ones do not take effect until the bot-side http_tools executor ships. The bot’s models are configured by Mediflo: `modelConfig` comes back only on a Mediflo staff connection. This is the DRAFT, which may differ from the version the bot is currently running — `currentConfigSnapshotId` names the live one. MCP server secrets come back masked and never as ciphertext. Feed `updatedAt` back to update_bot_config as `expectedUpdatedAt` for optimistic concurrency. Mirrors GET /api/external/v1/bots/{botId}/config.",
      "descriptionPl": "Zwraca roboczą konfigurację bota: prompt, powitanie, ustawienia czatu, zabezpieczenia, narzędzia i analizę po rozmowie.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "botId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
          }
        },
        "required": [
          "botId"
        ]
      }
    },
    {
      "name": "get_prompting_guidance",
      "requiredScope": "bots.read",
      "description": "How to WRITE a bot prompt — read this before you write or rewrite agentPromptText, a workflow node instruction, or remarksPromptText. Returns two blocks. `general` is the voice-bot rules: the bot speaks Polish over TTS, so static content goes first and every {{variable}} LAST (the prompt cache only hits an unchanged prefix), replies are short spoken prose with no markdown, brevity rules are scoped to their section, and knowledge belongs in the knowledge base rather than the prompt. `chat` is the website-chat rules: the visitor reads the reply, so do not write for speech; set channelSettings.webChat.greeting, handoffMessage and guardrailFallbackMessage; keep the instructions short. Use `general` when the bot answers the phone, `chat` when it serves WEB_CHAT, and both when it does both. A website crawl is Portal-only. The models a bot runs are managed by Mediflo; a Mediflo staff connection additionally gets the model-specific rules for them (pass `botId`, or `model` for a catalog key), while any other connection gets the general and chat rules whatever it passes. This tool only READS guidance; update_bot_config is what writes the prompt.",
      "descriptionPl": "Zwraca zasady pisania promptu bota — reguły dla głosu i dla czatu na stronie, a dla pracowników Mediflo także wymagania modelu, na którym bot działa.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "model": {
            "type": "string",
            "minLength": 1
          },
          "botId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
          }
        }
      }
    },
    {
      "name": "update_bot_config",
      "requiredScope": "bots.write",
      "description": "Edit the bot draft configuration — prompt, greeting, guardrails, tools/MCP servers, turn taking, and the post-call extraction prompt and schema. Only the fields you send are touched. The bot’s models (modelConfig and the extraction model) are configured by Mediflo and are not part of this tool. greetingConfig is the shared greeting ({type: static, text} | {type: llm, prompt} | {type: none} = improvise) plus an optional byDirection: {inbound?, outbound?} that overrides it per call direction — each entry is static, llm, or {type: off}, where off means the bot does NOT speak first and waits for the caller, improvising an opener only if the caller stays silent for ~4 s (none is not accepted per direction). Omit a direction key to keep the shared greeting for it; send the whole greetingConfig object, it replaces the stored one. Web chat has no direction and ignores byDirection. channelSettings is the website-chat override and is returned by get_bot_config. webChat.greeting is {type: static, text} | {type: llm, prompt} | {type: none} and replaces greetingConfig for website chat only ({type: none} posts no automatic opener). handoffMessage is the notice shown when the bot hands the thread to a person. guardrailFallbackMessage replaces a blocked guardrail’s own fallback on this channel, so a sentence written for ending a phone call is not what a chat visitor reads. Send the whole channelSettings object; it replaces the stored one. null clears every override. The Terminarz scheduling toolset (browse/book/modify) is switched on in the Portal on the bot’s Tools tab. Writing toolsConfig cannot create or delete that managed entry. Each MCP server entry also carries async execution controls (async_mode: none|all|selected + async_tools) — async tools let the bot keep talking while a slow tool call runs instead of going silent. sessionInitConfig is an optional per-call HTTPS hook fired once at dispatch: its JSON response merges into session variables and its `opener` key overrides the greeting; it ALWAYS fails open (a broken endpoint never fails a call). http_tools are generic HTTPS tools executed bot-side without an MCP server; parameter_bindings on any tool pin named parameters to platform values (caller phone, session id) so the LLM cannot omit or invent them — bound parameters are removed from the LLM-visible schema. TOOL GATES are preconditions on side-effectful tools: mcp_servers[].tool_gates is {toolName: [gate]} and http_tools[].gates is [gate], at most 4 per tool. A gate is {requires: {variable, operator, value?, label?}, message?}; a denied call gets a corrective message instead of running, so every denial costs a conversational turn — gate PHI access and irreversible actions, nothing else. `requires.label` is the human name of the fact, spoken in the default Polish denial; without it the bot reads out the raw variable key, which sounds wrong. `message` replaces the whole denial line and makes `label` unused. Prefer BOOLEAN facts with is_true/is_false — is_true uses truthy-string semantics, so a gate on a free-text variable passes on any non-empty answer. A gated fact must be a platform builtin, a key you declared in promptVariableMetadata, or identity_verified (set only by an identity check, never claimable by the model); anything else is refused here, and gating on identity_verified without an identity step anywhere in the call flow is refused at publish. Gates on http_tools[] are stored and validated but NOT YET ENFORCED — the bot evaluates gates at the MCP call wrapper and has no http_tools executor yet, so those tools are effectively ungated until it ships (publish warns HTTP_TOOL_GATES_NOT_ENFORCED). entryProcedureId names the DETERMINISTIC procedure that runs as the whole call flow (create it with create_procedure first); null clears it. A bot cannot have both an entry procedure and an active workflow — publish refuses that combination. Publishing is required for any draft change to affect live calls. Writes the DRAFT. The bot keeps running the last published version until you call publish_bot_config — including Bot Preview, which always uses the published version, never the draft. Pass `expectedUpdatedAt` (from get_bot_config) for optimistic concurrency — a conflicting write fails instead of silently overwriting a change you never saw; `forceOverwrite: true` is the explicit escape. Unknown keys are REJECTED rather than ignored, so a misspelled field never looks like a success. remarksOutputSchemaJson is the post-call extraction contract and it is OPTIONAL: omit it (or send null) and Mediflo still extracts its always-on system analysis fields on every call. When provided, it is a JSON Schema, not free-form JSON. REQUIRED SHAPE: a root object — {\"type\":\"object\",\"properties\":{...},\"required\":[...]}; every `required` entry must name a declared property, and a field is made optional by LEAVING IT OUT of `required`, never by `type:[\"string\",\"null\"]`. Each field's `description` is the extraction instruction the model follows, so write it. SUPPORTED: type, description, enum, items, nested properties/required. SILENTLY REWRITTEN ON SAVE (the response reports each one under `extractionSchema.normalizedChanges`, so what you read back is not byte-identical to what you sent): `title`, `examples` and `$schema` are stripped; `type:[...]` arrays and `anyOf`-with-siblings are rewritten. AVOID: `$ref`, `$defs`, `allOf` and numeric `minimum`/`maximum` — they are not rewritten and have no equivalent in the provider schema subset; inline the shape and put ranges in `description`. RESERVED: Mediflo always extracts sentiment, summary, resolution, needs_human_followup, follow_up_reason, red_flags, issue_detected, issues, quality, topics, language, unanswered_questions and force-merges its own definitions over yours, so do not redefine them. Ceiling: 64 KB serialized. A schema that cannot work is refused with the offending path rather than accepted and left to fail on every call. Before writing agentPromptText or remarksPromptText, call get_prompting_guidance with this botId. When you write agentPromptText or greetingConfig, also decide whether the bot says it is an AI: EU AI Act Article 50 expects the person to be told. get_bot_readiness reports that as the informational `ai_disclosure` check — advisory only, it blocks nothing, and the wording is the operator’s to choose. Mirrors PATCH /api/external/v1/bots/{botId}/config. Template-managed bots (blueprintRole INSTANCE) reject this call; change them through their template settings in the Portal.",
      "descriptionPl": "Edytuje roboczą konfigurację bota. Zmiana zacznie działać dopiero po opublikowaniu konfiguracji.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "agentPromptText": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "remarksPromptText": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "remarksOutputSchemaJson": {
            "anyOf": [
              {
                "type": "object",
                "propertyNames": {
                  "type": "string"
                },
                "additionalProperties": {}
              },
              {
                "type": "null"
              }
            ]
          },
          "toolsConfig": {
            "anyOf": [
              {
                "type": "object",
                "properties": {
                  "end_call": {
                    "type": "object",
                    "properties": {
                      "enabled": {
                        "type": "boolean",
                        "const": true
                      },
                      "description": {
                        "type": "string",
                        "maxLength": 4000
                      },
                      "disableInterruptions": {
                        "type": "boolean"
                      },
                      "responseTimeoutSecs": {
                        "type": "integer",
                        "minimum": 1,
                        "maximum": 120
                      }
                    },
                    "required": [
                      "enabled"
                    ]
                  },
                  "detected_answering_machine": {
                    "type": "object",
                    "properties": {
                      "enabled": {
                        "type": "boolean"
                      },
                      "description": {
                        "type": "string",
                        "maxLength": 4000
                      },
                      "disableInterruptions": {
                        "type": "boolean"
                      },
                      "responseTimeoutSecs": {
                        "type": "integer",
                        "minimum": 1,
                        "maximum": 120
                      }
                    },
                    "required": [
                      "enabled"
                    ]
                  },
                  "transfer_to_consultant": {
                    "type": "object",
                    "properties": {
                      "enabled": {
                        "type": "boolean",
                        "const": true
                      },
                      "ringTimeoutSecs": {
                        "default": 30,
                        "type": "integer",
                        "minimum": 10,
                        "maximum": 120
                      },
                      "onNoAnswer": {
                        "default": "resume",
                        "type": "string",
                        "enum": [
                          "offer_message",
                          "resume"
                        ]
                      },
                      "destination": {
                        "oneOf": [
                          {
                            "type": "object",
                            "properties": {
                              "kind": {
                                "type": "string",
                                "const": "consultant_queue"
                              },
                              "queueId": {
                                "type": "string",
                                "format": "uuid",
                                "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
                              }
                            },
                            "required": [
                              "kind",
                              "queueId"
                            ],
                            "additionalProperties": false
                          },
                          {
                            "type": "object",
                            "properties": {
                              "kind": {
                                "type": "string",
                                "const": "phone"
                              },
                              "mode": {
                                "type": "string",
                                "enum": [
                                  "warm",
                                  "cold_bridge",
                                  "cold_handoff"
                                ]
                              },
                              "number": {
                                "oneOf": [
                                  {
                                    "type": "object",
                                    "properties": {
                                      "type": {
                                        "type": "string",
                                        "const": "static"
                                      },
                                      "value": {
                                        "type": "string",
                                        "pattern": "^\\+[1-9]\\d{6,14}$"
                                      }
                                    },
                                    "required": [
                                      "type",
                                      "value"
                                    ],
                                    "additionalProperties": false
                                  },
                                  {
                                    "type": "object",
                                    "properties": {
                                      "type": {
                                        "type": "string",
                                        "const": "session_variable"
                                      },
                                      "name": {
                                        "type": "string",
                                        "pattern": "^[a-zA-Z_][a-zA-Z0-9_]*$"
                                      }
                                    },
                                    "required": [
                                      "type",
                                      "name"
                                    ],
                                    "additionalProperties": false
                                  }
                                ]
                              },
                              "gate": {
                                "default": "dtmf",
                                "type": "string",
                                "enum": [
                                  "dtmf",
                                  "amd",
                                  "dtmf_amd",
                                  "none"
                                ]
                              },
                              "voicemailRescue": {
                                "default": true,
                                "type": "boolean"
                              },
                              "briefing": {
                                "type": "object",
                                "properties": {
                                  "preAccept": {
                                    "default": [
                                      "clinic_only",
                                      "topic"
                                    ],
                                    "type": "array",
                                    "items": {
                                      "type": "string",
                                      "enum": [
                                        "clinic_only",
                                        "caller_number",
                                        "topic"
                                      ]
                                    }
                                  },
                                  "postAccept": {
                                    "default": "topic",
                                    "type": "string",
                                    "enum": [
                                      "none",
                                      "topic",
                                      "summary"
                                    ]
                                  }
                                }
                              }
                            },
                            "required": [
                              "kind",
                              "mode",
                              "number"
                            ],
                            "additionalProperties": false
                          }
                        ]
                      },
                      "description": {
                        "type": "string",
                        "maxLength": 4000
                      },
                      "disableInterruptions": {
                        "type": "boolean"
                      },
                      "responseTimeoutSecs": {
                        "type": "integer",
                        "minimum": 1,
                        "maximum": 120
                      }
                    },
                    "required": [
                      "enabled",
                      "destination"
                    ]
                  },
                  "mcp_servers": {
                    "maxItems": 10,
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "format": "uuid",
                          "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
                        },
                        "name": {
                          "type": "string",
                          "minLength": 1,
                          "maxLength": 120
                        },
                        "enabled": {
                          "type": "boolean"
                        },
                        "url": {
                          "type": "string",
                          "maxLength": 2000,
                          "format": "uri"
                        },
                        "transport": {
                          "default": "auto",
                          "type": "string",
                          "enum": [
                            "auto",
                            "streamable_http",
                            "sse"
                          ]
                        },
                        "allowed_tools": {
                          "default": null,
                          "anyOf": [
                            {
                              "type": "array",
                              "items": {
                                "type": "string",
                                "minLength": 1,
                                "maxLength": 200
                              }
                            },
                            {
                              "type": "null"
                            }
                          ]
                        },
                        "async_mode": {
                          "default": "none",
                          "description": "Which of this MCP server's tools run asynchronously — the bot keeps talking to the caller while the tool call runs, voices an interim acknowledgment for slow calls, and delivers the result when ready. 'none' (default): every call blocks the conversation. 'all': every exposed tool runs async. 'selected': only the tools listed in async_tools. Unknown values fail closed to 'none'.",
                          "type": "string",
                          "enum": [
                            "none",
                            "all",
                            "selected"
                          ]
                        },
                        "async_tools": {
                          "default": null,
                          "description": "Tool names that run asynchronously when async_mode is 'selected'; null otherwise. Only tools also exposed by the server (and allowed by allowed_tools) take effect. 'selected' with an empty or null list means nothing runs async (fail closed).",
                          "anyOf": [
                            {
                              "type": "array",
                              "items": {
                                "type": "string",
                                "minLength": 1,
                                "maxLength": 200
                              }
                            },
                            {
                              "type": "null"
                            }
                          ]
                        },
                        "discovered_tools": {
                          "default": [],
                          "maxItems": 500,
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "name": {
                                "type": "string",
                                "minLength": 1,
                                "maxLength": 200
                              },
                              "description": {
                                "type": "string",
                                "maxLength": 2000
                              },
                              "input_schema": {
                                "type": "object",
                                "propertyNames": {
                                  "type": "string"
                                },
                                "additionalProperties": {}
                              }
                            },
                            "required": [
                              "name"
                            ]
                          }
                        },
                        "client_session_timeout_seconds": {
                          "type": "number",
                          "minimum": 1,
                          "maximum": 300
                        },
                        "parameter_bindings": {
                          "type": "object",
                          "propertyNames": {
                            "type": "string",
                            "minLength": 1
                          },
                          "additionalProperties": {
                            "type": "object",
                            "propertyNames": {
                              "type": "string",
                              "minLength": 1
                            },
                            "additionalProperties": {
                              "oneOf": [
                                {
                                  "type": "object",
                                  "properties": {
                                    "source": {
                                      "type": "string",
                                      "const": "session_variable"
                                    },
                                    "name": {
                                      "type": "string",
                                      "pattern": "^[a-zA-Z_][a-zA-Z0-9_]*$"
                                    }
                                  },
                                  "required": [
                                    "source",
                                    "name"
                                  ]
                                },
                                {
                                  "type": "object",
                                  "properties": {
                                    "source": {
                                      "type": "string",
                                      "const": "constant"
                                    },
                                    "value": {
                                      "anyOf": [
                                        {
                                          "type": "string"
                                        },
                                        {
                                          "type": "number"
                                        },
                                        {
                                          "type": "boolean"
                                        }
                                      ]
                                    }
                                  },
                                  "required": [
                                    "source",
                                    "value"
                                  ]
                                }
                              ]
                            }
                          }
                        },
                        "response_variables": {
                          "type": "object",
                          "propertyNames": {
                            "type": "string",
                            "minLength": 1
                          },
                          "additionalProperties": {
                            "maxItems": 16,
                            "type": "array",
                            "items": {
                              "type": "object",
                              "properties": {
                                "from": {
                                  "type": "string",
                                  "minLength": 1
                                },
                                "to": {
                                  "type": "string",
                                  "pattern": "^[a-zA-Z_][a-zA-Z0-9_]*$"
                                }
                              },
                              "required": [
                                "from",
                                "to"
                              ]
                            }
                          }
                        },
                        "tool_gates": {
                          "type": "object",
                          "propertyNames": {
                            "type": "string",
                            "minLength": 1,
                            "maxLength": 200
                          },
                          "additionalProperties": {
                            "maxItems": 4,
                            "type": "array",
                            "items": {
                              "type": "object",
                              "properties": {
                                "requires": {
                                  "type": "object",
                                  "properties": {
                                    "variable": {
                                      "type": "string",
                                      "minLength": 1,
                                      "maxLength": 64,
                                      "description": "The session fact to test: identity_verified, a declared workflow/procedure variable, or a seeded session key."
                                    },
                                    "operator": {
                                      "type": "string",
                                      "enum": [
                                        "eq",
                                        "neq",
                                        "gt",
                                        "gte",
                                        "lt",
                                        "lte",
                                        "is_true",
                                        "is_false",
                                        "is_set",
                                        "is_empty"
                                      ],
                                      "description": "Comparison to apply. The unary operators (is_true/is_false/is_set/is_empty) take no value."
                                    },
                                    "value": {
                                      "description": "Value to compare against. Required for the binary operators, forbidden for the unary ones.",
                                      "type": "string",
                                      "maxLength": 200
                                    },
                                    "label": {
                                      "description": "Human wording of the fact for the default spoken denial (\"najpierw musimy ustalić: {label}\"). Without it the runner de-snake-cases the variable key, which reads badly aloud. Ignored when `message` is set.",
                                      "type": "string",
                                      "minLength": 1,
                                      "maxLength": 80
                                    }
                                  },
                                  "required": [
                                    "variable",
                                    "operator"
                                  ],
                                  "additionalProperties": false
                                },
                                "message": {
                                  "description": "Polish steer returned to the model when the gate denies the call. Say what must happen FIRST. Stored as a single line. Omit for the standard wording.",
                                  "type": "string",
                                  "maxLength": 300
                                }
                              },
                              "required": [
                                "requires"
                              ],
                              "additionalProperties": false
                            }
                          },
                          "description": "Preconditions per tool name: {\"book_visit\": [{requires: {...}, label?, message?}]}. At most 4 gates on one tool — every denial costs a conversational turn, so gates are for PHI access and irreversible actions, not for shaping the conversation. `requires.label` is the human name of the fact, spoken in the default Polish denial (\"najpierw musimy ustalić: {label}\"); without it the runner de-snake-cases the variable key, which reads badly aloud. `message` replaces the whole denial line and makes `label` unused. Prefer BOOLEAN facts with is_true/is_false: the runner evaluates is_true with truthy-string semantics (mirroring the workflow walker), so a gate on a free-text variable passes on any non-empty answer."
                        },
                        "managed": {
                          "type": "object",
                          "properties": {
                            "kind": {
                              "type": "string",
                              "const": "mediflo_scheduling"
                            },
                            "capabilities": {
                              "type": "object",
                              "properties": {
                                "browse": {
                                  "type": "boolean"
                                },
                                "book": {
                                  "type": "boolean"
                                },
                                "modify": {
                                  "type": "boolean"
                                },
                                "contact": {
                                  "default": false,
                                  "type": "boolean"
                                },
                                "cases": {
                                  "type": "boolean"
                                }
                              },
                              "required": [
                                "browse",
                                "book",
                                "modify"
                              ]
                            },
                            "credential_ref": {
                              "type": "string",
                              "const": "scheduling-toolset"
                            }
                          },
                          "required": [
                            "kind",
                            "capabilities",
                            "credential_ref"
                          ],
                          "additionalProperties": false
                        },
                        "secrets": {
                          "default": [],
                          "maxItems": 20,
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "string",
                                "format": "uuid",
                                "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
                              },
                              "key": {
                                "type": "string",
                                "pattern": "^[A-Za-z][A-Za-z0-9_-]{0,63}$"
                              },
                              "value": {
                                "anyOf": [
                                  {
                                    "type": "string",
                                    "minLength": 1,
                                    "maxLength": 4096
                                  },
                                  {
                                    "type": "object",
                                    "properties": {
                                      "encrypted": {
                                        "type": "string",
                                        "minLength": 1
                                      },
                                      "iv": {
                                        "type": "string",
                                        "minLength": 1
                                      },
                                      "tag": {
                                        "type": "string",
                                        "minLength": 1
                                      }
                                    },
                                    "required": [
                                      "encrypted",
                                      "iv",
                                      "tag"
                                    ]
                                  }
                                ]
                              }
                            },
                            "required": [
                              "id",
                              "key"
                            ]
                          }
                        }
                      },
                      "required": [
                        "id",
                        "name",
                        "enabled",
                        "url"
                      ]
                    }
                  },
                  "http_tools": {
                    "maxItems": 10,
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "format": "uuid",
                          "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
                        },
                        "name": {
                          "type": "string",
                          "pattern": "^[A-Za-z0-9_-]{1,64}$"
                        },
                        "enabled": {
                          "default": true,
                          "type": "boolean"
                        },
                        "description": {
                          "type": "string",
                          "minLength": 1,
                          "maxLength": 2000
                        },
                        "url": {
                          "type": "string",
                          "format": "uri",
                          "pattern": "^https:\\/\\/.*"
                        },
                        "method": {
                          "default": "POST",
                          "type": "string",
                          "enum": [
                            "GET",
                            "POST",
                            "PUT",
                            "PATCH",
                            "DELETE"
                          ]
                        },
                        "queryParams": {
                          "maxItems": 10,
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "key": {
                                "type": "string"
                              },
                              "value": {
                                "type": "string"
                              }
                            },
                            "required": [
                              "key",
                              "value"
                            ]
                          }
                        },
                        "bodySchema": {
                          "type": "object",
                          "propertyNames": {
                            "type": "string"
                          },
                          "additionalProperties": {}
                        },
                        "argsOnly": {
                          "default": true,
                          "type": "boolean"
                        },
                        "timeoutMs": {
                          "default": 8000,
                          "type": "integer",
                          "minimum": 1000,
                          "maximum": 20000
                        },
                        "retries": {
                          "default": 0,
                          "type": "integer",
                          "minimum": 0,
                          "maximum": 2
                        },
                        "parameter_bindings": {
                          "type": "object",
                          "propertyNames": {
                            "type": "string",
                            "minLength": 1
                          },
                          "additionalProperties": {
                            "type": "object",
                            "propertyNames": {
                              "type": "string",
                              "minLength": 1
                            },
                            "additionalProperties": {
                              "oneOf": [
                                {
                                  "type": "object",
                                  "properties": {
                                    "source": {
                                      "type": "string",
                                      "const": "session_variable"
                                    },
                                    "name": {
                                      "type": "string",
                                      "pattern": "^[a-zA-Z_][a-zA-Z0-9_]*$"
                                    }
                                  },
                                  "required": [
                                    "source",
                                    "name"
                                  ]
                                },
                                {
                                  "type": "object",
                                  "properties": {
                                    "source": {
                                      "type": "string",
                                      "const": "constant"
                                    },
                                    "value": {
                                      "anyOf": [
                                        {
                                          "type": "string"
                                        },
                                        {
                                          "type": "number"
                                        },
                                        {
                                          "type": "boolean"
                                        }
                                      ]
                                    }
                                  },
                                  "required": [
                                    "source",
                                    "value"
                                  ]
                                }
                              ]
                            }
                          }
                        },
                        "response_variables": {
                          "maxItems": 16,
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "from": {
                                "type": "string",
                                "minLength": 1
                              },
                              "to": {
                                "type": "string",
                                "pattern": "^[a-zA-Z_][a-zA-Z0-9_]*$"
                              }
                            },
                            "required": [
                              "from",
                              "to"
                            ]
                          }
                        },
                        "gates": {
                          "description": "Preconditions on this tool (at most 4). A denied call returns a corrective message to the model instead of running — it never raises. NOT YET ENFORCED: gates take effect only once the bot-side http_tools executor ships (the runner evaluates gates at the MCP call wrapper today), so a gate declared here is stored and validated but the tool is still callable — publish warns about it. `requires.label` names the fact in the default Polish denial; `message` replaces the whole line. Prefer boolean facts: is_true uses truthy-string semantics, so a gate on free text passes on any non-empty answer.",
                          "maxItems": 4,
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "requires": {
                                "type": "object",
                                "properties": {
                                  "variable": {
                                    "type": "string",
                                    "minLength": 1,
                                    "maxLength": 64,
                                    "description": "The session fact to test: identity_verified, a declared workflow/procedure variable, or a seeded session key."
                                  },
                                  "operator": {
                                    "type": "string",
                                    "enum": [
                                      "eq",
                                      "neq",
                                      "gt",
                                      "gte",
                                      "lt",
                                      "lte",
                                      "is_true",
                                      "is_false",
                                      "is_set",
                                      "is_empty"
                                    ],
                                    "description": "Comparison to apply. The unary operators (is_true/is_false/is_set/is_empty) take no value."
                                  },
                                  "value": {
                                    "description": "Value to compare against. Required for the binary operators, forbidden for the unary ones.",
                                    "type": "string",
                                    "maxLength": 200
                                  },
                                  "label": {
                                    "description": "Human wording of the fact for the default spoken denial (\"najpierw musimy ustalić: {label}\"). Without it the runner de-snake-cases the variable key, which reads badly aloud. Ignored when `message` is set.",
                                    "type": "string",
                                    "minLength": 1,
                                    "maxLength": 80
                                  }
                                },
                                "required": [
                                  "variable",
                                  "operator"
                                ],
                                "additionalProperties": false
                              },
                              "message": {
                                "description": "Polish steer returned to the model when the gate denies the call. Say what must happen FIRST. Stored as a single line. Omit for the standard wording.",
                                "type": "string",
                                "maxLength": 300
                              }
                            },
                            "required": [
                              "requires"
                            ],
                            "additionalProperties": false
                          }
                        },
                        "headers": {
                          "maxItems": 10,
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "string",
                                "format": "uuid",
                                "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
                              },
                              "key": {
                                "type": "string",
                                "pattern": "^[A-Za-z][A-Za-z0-9_-]{0,63}$"
                              },
                              "value": {
                                "anyOf": [
                                  {
                                    "type": "string",
                                    "minLength": 1,
                                    "maxLength": 4096
                                  },
                                  {
                                    "type": "object",
                                    "properties": {
                                      "encrypted": {
                                        "type": "string",
                                        "minLength": 1
                                      },
                                      "iv": {
                                        "type": "string",
                                        "minLength": 1
                                      },
                                      "tag": {
                                        "type": "string",
                                        "minLength": 1
                                      }
                                    },
                                    "required": [
                                      "encrypted",
                                      "iv",
                                      "tag"
                                    ]
                                  }
                                ]
                              }
                            },
                            "required": [
                              "id",
                              "key"
                            ]
                          }
                        }
                      },
                      "required": [
                        "id",
                        "name",
                        "description",
                        "url"
                      ]
                    }
                  }
                },
                "required": [
                  "end_call",
                  "detected_answering_machine"
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "guardrailsConfig": {
            "anyOf": [
              {
                "type": "object",
                "properties": {
                  "enabled": {
                    "type": "boolean"
                  },
                  "focus": {
                    "type": "object",
                    "properties": {
                      "enabled": {
                        "type": "boolean"
                      },
                      "executionMode": {
                        "default": "streaming",
                        "type": "string",
                        "enum": [
                          "streaming",
                          "blocking"
                        ]
                      },
                      "action": {
                        "default": "end_call",
                        "type": "string",
                        "enum": [
                          "end_call",
                          "retry"
                        ]
                      },
                      "fallbackMessage": {
                        "type": "string",
                        "maxLength": 1000
                      }
                    },
                    "required": [
                      "enabled"
                    ]
                  },
                  "manipulation": {
                    "type": "object",
                    "properties": {
                      "enabled": {
                        "type": "boolean"
                      },
                      "executionMode": {
                        "default": "streaming",
                        "type": "string",
                        "enum": [
                          "streaming",
                          "blocking"
                        ]
                      },
                      "action": {
                        "default": "end_call",
                        "type": "string",
                        "enum": [
                          "end_call",
                          "retry"
                        ]
                      },
                      "fallbackMessage": {
                        "type": "string",
                        "maxLength": 1000
                      }
                    },
                    "required": [
                      "enabled"
                    ]
                  },
                  "content": {
                    "type": "object",
                    "properties": {
                      "enabled": {
                        "type": "boolean"
                      },
                      "executionMode": {
                        "default": "streaming",
                        "type": "string",
                        "enum": [
                          "streaming",
                          "blocking"
                        ]
                      },
                      "action": {
                        "default": "end_call",
                        "type": "string",
                        "enum": [
                          "end_call",
                          "retry"
                        ]
                      },
                      "fallbackMessage": {
                        "type": "string",
                        "maxLength": 1000
                      },
                      "categories": {
                        "type": "object",
                        "properties": {
                          "sexual": {
                            "type": "object",
                            "properties": {
                              "enabled": {
                                "type": "boolean"
                              },
                              "severity": {
                                "default": "medium",
                                "type": "string",
                                "enum": [
                                  "low",
                                  "medium",
                                  "high"
                                ]
                              }
                            },
                            "required": [
                              "enabled"
                            ]
                          },
                          "violence": {
                            "type": "object",
                            "properties": {
                              "enabled": {
                                "type": "boolean"
                              },
                              "severity": {
                                "default": "medium",
                                "type": "string",
                                "enum": [
                                  "low",
                                  "medium",
                                  "high"
                                ]
                              }
                            },
                            "required": [
                              "enabled"
                            ]
                          },
                          "harassment": {
                            "type": "object",
                            "properties": {
                              "enabled": {
                                "type": "boolean"
                              },
                              "severity": {
                                "default": "medium",
                                "type": "string",
                                "enum": [
                                  "low",
                                  "medium",
                                  "high"
                                ]
                              }
                            },
                            "required": [
                              "enabled"
                            ]
                          },
                          "self_harm": {
                            "type": "object",
                            "properties": {
                              "enabled": {
                                "type": "boolean"
                              },
                              "severity": {
                                "default": "medium",
                                "type": "string",
                                "enum": [
                                  "low",
                                  "medium",
                                  "high"
                                ]
                              }
                            },
                            "required": [
                              "enabled"
                            ]
                          },
                          "profanity": {
                            "type": "object",
                            "properties": {
                              "enabled": {
                                "type": "boolean"
                              },
                              "severity": {
                                "default": "medium",
                                "type": "string",
                                "enum": [
                                  "low",
                                  "medium",
                                  "high"
                                ]
                              }
                            },
                            "required": [
                              "enabled"
                            ]
                          },
                          "politics_religion": {
                            "type": "object",
                            "properties": {
                              "enabled": {
                                "type": "boolean"
                              },
                              "severity": {
                                "default": "medium",
                                "type": "string",
                                "enum": [
                                  "low",
                                  "medium",
                                  "high"
                                ]
                              }
                            },
                            "required": [
                              "enabled"
                            ]
                          }
                        },
                        "required": [
                          "sexual",
                          "violence",
                          "harassment",
                          "self_harm",
                          "profanity",
                          "politics_religion"
                        ]
                      }
                    },
                    "required": [
                      "enabled",
                      "categories"
                    ]
                  },
                  "custom": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "minLength": 1
                        },
                        "name": {
                          "type": "string",
                          "minLength": 1,
                          "maxLength": 120
                        },
                        "prompt": {
                          "type": "string",
                          "minLength": 1,
                          "maxLength": 4000
                        },
                        "enabled": {
                          "type": "boolean"
                        },
                        "executionMode": {
                          "default": "streaming",
                          "type": "string",
                          "enum": [
                            "streaming",
                            "blocking"
                          ]
                        },
                        "action": {
                          "default": "end_call",
                          "type": "string",
                          "enum": [
                            "end_call",
                            "retry"
                          ]
                        },
                        "fallbackMessage": {
                          "type": "string",
                          "maxLength": 1000
                        }
                      },
                      "required": [
                        "id",
                        "name",
                        "prompt",
                        "enabled"
                      ]
                    }
                  }
                },
                "required": [
                  "enabled",
                  "focus",
                  "manipulation",
                  "content",
                  "custom"
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "greetingConfig": {
            "anyOf": [
              {
                "oneOf": [
                  {
                    "type": "object",
                    "properties": {
                      "type": {
                        "type": "string",
                        "const": "static"
                      },
                      "text": {
                        "type": "string",
                        "minLength": 1
                      },
                      "allowInterruptions": {
                        "default": false,
                        "type": "boolean"
                      },
                      "byDirection": {
                        "type": "object",
                        "properties": {
                          "inbound": {
                            "oneOf": [
                              {
                                "type": "object",
                                "properties": {
                                  "type": {
                                    "type": "string",
                                    "const": "static"
                                  },
                                  "text": {
                                    "type": "string",
                                    "minLength": 1
                                  },
                                  "allowInterruptions": {
                                    "default": false,
                                    "type": "boolean"
                                  }
                                },
                                "required": [
                                  "type",
                                  "text"
                                ],
                                "additionalProperties": false
                              },
                              {
                                "type": "object",
                                "properties": {
                                  "type": {
                                    "type": "string",
                                    "const": "llm"
                                  },
                                  "prompt": {
                                    "type": "string",
                                    "minLength": 1
                                  },
                                  "allowInterruptions": {
                                    "default": false,
                                    "type": "boolean"
                                  }
                                },
                                "required": [
                                  "type",
                                  "prompt"
                                ],
                                "additionalProperties": false
                              },
                              {
                                "type": "object",
                                "properties": {
                                  "type": {
                                    "type": "string",
                                    "const": "off"
                                  }
                                },
                                "required": [
                                  "type"
                                ],
                                "additionalProperties": false
                              }
                            ]
                          },
                          "outbound": {
                            "oneOf": [
                              {
                                "type": "object",
                                "properties": {
                                  "type": {
                                    "type": "string",
                                    "const": "static"
                                  },
                                  "text": {
                                    "type": "string",
                                    "minLength": 1
                                  },
                                  "allowInterruptions": {
                                    "default": false,
                                    "type": "boolean"
                                  }
                                },
                                "required": [
                                  "type",
                                  "text"
                                ],
                                "additionalProperties": false
                              },
                              {
                                "type": "object",
                                "properties": {
                                  "type": {
                                    "type": "string",
                                    "const": "llm"
                                  },
                                  "prompt": {
                                    "type": "string",
                                    "minLength": 1
                                  },
                                  "allowInterruptions": {
                                    "default": false,
                                    "type": "boolean"
                                  }
                                },
                                "required": [
                                  "type",
                                  "prompt"
                                ],
                                "additionalProperties": false
                              },
                              {
                                "type": "object",
                                "properties": {
                                  "type": {
                                    "type": "string",
                                    "const": "off"
                                  }
                                },
                                "required": [
                                  "type"
                                ],
                                "additionalProperties": false
                              }
                            ]
                          }
                        },
                        "additionalProperties": false
                      }
                    },
                    "required": [
                      "type",
                      "text"
                    ]
                  },
                  {
                    "type": "object",
                    "properties": {
                      "type": {
                        "type": "string",
                        "const": "llm"
                      },
                      "prompt": {
                        "type": "string",
                        "minLength": 1
                      },
                      "allowInterruptions": {
                        "default": false,
                        "type": "boolean"
                      },
                      "byDirection": {
                        "type": "object",
                        "properties": {
                          "inbound": {
                            "oneOf": [
                              {
                                "type": "object",
                                "properties": {
                                  "type": {
                                    "type": "string",
                                    "const": "static"
                                  },
                                  "text": {
                                    "type": "string",
                                    "minLength": 1
                                  },
                                  "allowInterruptions": {
                                    "default": false,
                                    "type": "boolean"
                                  }
                                },
                                "required": [
                                  "type",
                                  "text"
                                ],
                                "additionalProperties": false
                              },
                              {
                                "type": "object",
                                "properties": {
                                  "type": {
                                    "type": "string",
                                    "const": "llm"
                                  },
                                  "prompt": {
                                    "type": "string",
                                    "minLength": 1
                                  },
                                  "allowInterruptions": {
                                    "default": false,
                                    "type": "boolean"
                                  }
                                },
                                "required": [
                                  "type",
                                  "prompt"
                                ],
                                "additionalProperties": false
                              },
                              {
                                "type": "object",
                                "properties": {
                                  "type": {
                                    "type": "string",
                                    "const": "off"
                                  }
                                },
                                "required": [
                                  "type"
                                ],
                                "additionalProperties": false
                              }
                            ]
                          },
                          "outbound": {
                            "oneOf": [
                              {
                                "type": "object",
                                "properties": {
                                  "type": {
                                    "type": "string",
                                    "const": "static"
                                  },
                                  "text": {
                                    "type": "string",
                                    "minLength": 1
                                  },
                                  "allowInterruptions": {
                                    "default": false,
                                    "type": "boolean"
                                  }
                                },
                                "required": [
                                  "type",
                                  "text"
                                ],
                                "additionalProperties": false
                              },
                              {
                                "type": "object",
                                "properties": {
                                  "type": {
                                    "type": "string",
                                    "const": "llm"
                                  },
                                  "prompt": {
                                    "type": "string",
                                    "minLength": 1
                                  },
                                  "allowInterruptions": {
                                    "default": false,
                                    "type": "boolean"
                                  }
                                },
                                "required": [
                                  "type",
                                  "prompt"
                                ],
                                "additionalProperties": false
                              },
                              {
                                "type": "object",
                                "properties": {
                                  "type": {
                                    "type": "string",
                                    "const": "off"
                                  }
                                },
                                "required": [
                                  "type"
                                ],
                                "additionalProperties": false
                              }
                            ]
                          }
                        },
                        "additionalProperties": false
                      }
                    },
                    "required": [
                      "type",
                      "prompt"
                    ]
                  },
                  {
                    "type": "object",
                    "properties": {
                      "type": {
                        "type": "string",
                        "const": "none"
                      },
                      "byDirection": {
                        "type": "object",
                        "properties": {
                          "inbound": {
                            "oneOf": [
                              {
                                "type": "object",
                                "properties": {
                                  "type": {
                                    "type": "string",
                                    "const": "static"
                                  },
                                  "text": {
                                    "type": "string",
                                    "minLength": 1
                                  },
                                  "allowInterruptions": {
                                    "default": false,
                                    "type": "boolean"
                                  }
                                },
                                "required": [
                                  "type",
                                  "text"
                                ],
                                "additionalProperties": false
                              },
                              {
                                "type": "object",
                                "properties": {
                                  "type": {
                                    "type": "string",
                                    "const": "llm"
                                  },
                                  "prompt": {
                                    "type": "string",
                                    "minLength": 1
                                  },
                                  "allowInterruptions": {
                                    "default": false,
                                    "type": "boolean"
                                  }
                                },
                                "required": [
                                  "type",
                                  "prompt"
                                ],
                                "additionalProperties": false
                              },
                              {
                                "type": "object",
                                "properties": {
                                  "type": {
                                    "type": "string",
                                    "const": "off"
                                  }
                                },
                                "required": [
                                  "type"
                                ],
                                "additionalProperties": false
                              }
                            ]
                          },
                          "outbound": {
                            "oneOf": [
                              {
                                "type": "object",
                                "properties": {
                                  "type": {
                                    "type": "string",
                                    "const": "static"
                                  },
                                  "text": {
                                    "type": "string",
                                    "minLength": 1
                                  },
                                  "allowInterruptions": {
                                    "default": false,
                                    "type": "boolean"
                                  }
                                },
                                "required": [
                                  "type",
                                  "text"
                                ],
                                "additionalProperties": false
                              },
                              {
                                "type": "object",
                                "properties": {
                                  "type": {
                                    "type": "string",
                                    "const": "llm"
                                  },
                                  "prompt": {
                                    "type": "string",
                                    "minLength": 1
                                  },
                                  "allowInterruptions": {
                                    "default": false,
                                    "type": "boolean"
                                  }
                                },
                                "required": [
                                  "type",
                                  "prompt"
                                ],
                                "additionalProperties": false
                              },
                              {
                                "type": "object",
                                "properties": {
                                  "type": {
                                    "type": "string",
                                    "const": "off"
                                  }
                                },
                                "required": [
                                  "type"
                                ],
                                "additionalProperties": false
                              }
                            ]
                          }
                        },
                        "additionalProperties": false
                      }
                    },
                    "required": [
                      "type"
                    ]
                  }
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "channelSettings": {
            "anyOf": [
              {
                "type": "object",
                "properties": {
                  "webChat": {
                    "type": "object",
                    "properties": {
                      "greeting": {
                        "oneOf": [
                          {
                            "type": "object",
                            "properties": {
                              "type": {
                                "type": "string",
                                "const": "static"
                              },
                              "text": {
                                "type": "string",
                                "minLength": 1,
                                "maxLength": 2000
                              }
                            },
                            "required": [
                              "type",
                              "text"
                            ]
                          },
                          {
                            "type": "object",
                            "properties": {
                              "type": {
                                "type": "string",
                                "const": "llm"
                              },
                              "prompt": {
                                "type": "string",
                                "minLength": 1,
                                "maxLength": 2000
                              }
                            },
                            "required": [
                              "type",
                              "prompt"
                            ]
                          },
                          {
                            "type": "object",
                            "properties": {
                              "type": {
                                "type": "string",
                                "const": "none"
                              }
                            },
                            "required": [
                              "type"
                            ]
                          }
                        ]
                      },
                      "handoffMessage": {
                        "type": "string",
                        "minLength": 1,
                        "maxLength": 500
                      },
                      "guardrailFallbackMessage": {
                        "type": "string",
                        "minLength": 1,
                        "maxLength": 500
                      }
                    },
                    "additionalProperties": false
                  }
                },
                "additionalProperties": false
              },
              {
                "type": "null"
              }
            ]
          },
          "sessionInitConfig": {
            "anyOf": [
              {
                "type": "object",
                "properties": {
                  "enabled": {
                    "type": "boolean",
                    "const": true
                  },
                  "url": {
                    "type": "string",
                    "format": "uri",
                    "pattern": "^https:\\/\\/.*"
                  },
                  "timeout_ms": {
                    "default": 8000,
                    "type": "integer",
                    "minimum": 1000,
                    "maximum": 15000
                  },
                  "secrets": {
                    "default": [],
                    "maxItems": 10,
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "format": "uuid",
                          "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
                        },
                        "key": {
                          "type": "string",
                          "pattern": "^[A-Za-z][A-Za-z0-9_-]{0,63}$"
                        },
                        "value": {
                          "anyOf": [
                            {
                              "type": "string",
                              "minLength": 1,
                              "maxLength": 4096
                            },
                            {
                              "type": "object",
                              "properties": {
                                "encrypted": {
                                  "type": "string",
                                  "minLength": 1
                                },
                                "iv": {
                                  "type": "string",
                                  "minLength": 1
                                },
                                "tag": {
                                  "type": "string",
                                  "minLength": 1
                                }
                              },
                              "required": [
                                "encrypted",
                                "iv",
                                "tag"
                              ]
                            }
                          ]
                        }
                      },
                      "required": [
                        "id",
                        "key"
                      ]
                    }
                  }
                },
                "required": [
                  "enabled",
                  "url"
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "turnTakingMode": {
            "type": "string",
            "enum": [
              "eager",
              "regular",
              "lazy"
            ]
          },
          "promptVariableMetadata": {
            "anyOf": [
              {
                "type": "object",
                "propertyNames": {
                  "type": "string",
                  "pattern": "^[a-zA-Z_][a-zA-Z0-9_]*$"
                },
                "additionalProperties": {
                  "type": "object",
                  "properties": {
                    "label": {
                      "type": "string",
                      "maxLength": 100
                    },
                    "description": {
                      "type": "string",
                      "maxLength": 500
                    },
                    "defaultValue": {
                      "type": "string",
                      "maxLength": 1000
                    },
                    "type": {
                      "type": "string",
                      "enum": [
                        "string",
                        "boolean",
                        "number",
                        "date",
                        "time",
                        "datetime",
                        "email",
                        "url",
                        "phone",
                        "currency"
                      ]
                    }
                  }
                }
              },
              {
                "type": "null"
              }
            ]
          },
          "entryProcedureId": {
            "description": "Id of the DETERMINISTIC procedure that runs as this bot’s whole call flow. null means the bot has no entry procedure and follows its prompt (or its active legacy workflow) instead. A bot cannot have BOTH an entry procedure and an active workflow — publish refuses that combination (PROCEDURES_WORKFLOW_CONFLICT).",
            "anyOf": [
              {
                "type": "string",
                "format": "uuid",
                "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
              },
              {
                "type": "null"
              }
            ]
          },
          "expectedUpdatedAt": {
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
          },
          "forceOverwrite": {
            "type": "boolean"
          },
          "botId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
          }
        },
        "required": [
          "botId"
        ],
        "additionalProperties": false
      }
    },
    {
      "name": "create_workflow_from_template",
      "requiredScope": "bots.write",
      "description": "Create a conversation workflow for a bot from a server-side template, so you start from a working graph instead of authoring nodes and edges. `inbound-intake` is the general inbound flow for non-clinic tenants (greet → what is the call about → answer a question from the knowledge base, or take a message → goodbye), and `blank` is a minimal valid starting graph; both are available to every tenant. `visit-confirmation` (the appointment-confirmation flow: identity → confirm/reschedule → prep info → goodbye) and `registration` (the self-registration flow) are CLINIC-ONLY and are refused for a tenant without the clinic modules. Templates carry the same Polish copy the Portal editor uses (the clinic flows: Mediflo-approved copy; inbound-intake: a starter for the tenant to adapt); edit it afterwards with update_workflow. A created workflow is valid but NOT active — call activate_workflow. Workflows are NOT part of the configuration snapshot: this change reaches the next call on its own, so publish_bot_config is neither required nor sufficient for it (publishing only freezes prompt/model/greeting/tools). Mirrors POST /api/external/v1/bots/{botId}/workflows. Template-managed bots (blueprintRole INSTANCE) reject this call; change them through their template settings in the Portal.",
      "descriptionPl": "Tworzy przepływ rozmowy dla bota na podstawie gotowego szablonu (potwierdzanie wizyt, rejestracja albo pusty).",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "botId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
          },
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 255
          },
          "description": {
            "type": "string",
            "maxLength": 2000
          },
          "template": {
            "type": "string",
            "enum": [
              "visit-confirmation",
              "registration",
              "inbound-intake",
              "blank"
            ]
          }
        },
        "required": [
          "botId",
          "name",
          "template"
        ]
      }
    },
    {
      "name": "get_workflow",
      "requiredScope": "bots.read",
      "description": "Read one conversation workflow, including its full node/edge `definition`. `version` is the optimistic-concurrency token — feed it back to update_workflow as `expectedVersion`. `isActive` tells you whether this is the graph the bot actually runs. Mirrors GET /api/external/v1/bot-workflows/{workflowId}.",
      "descriptionPl": "Zwraca jeden przepływ rozmowy wraz ze wszystkimi krokami.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "workflowId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
          }
        },
        "required": [
          "workflowId"
        ]
      }
    },
    {
      "name": "update_workflow",
      "requiredScope": "bots.write",
      "description": "Update a workflow name, description and/or `definition`. The definition runs through the same canonical validator the Portal editor uses and is refused if it fails — use validate_workflow to iterate before saving. Pass `expectedVersion` (from get_workflow) so a retry cannot silently overwrite a change you never saw; the conflict tells you the current version. Workflows are NOT part of the configuration snapshot: this change reaches the next call on its own, so publish_bot_config is neither required nor sufficient for it (publishing only freezes prompt/model/greeting/tools). Mirrors PUT /api/external/v1/bot-workflows/{workflowId}. Template-managed bots (blueprintRole INSTANCE) reject this call; change them through their template settings in the Portal.",
      "descriptionPl": "Edytuje przepływ rozmowy. Nieprawidłowe przepływy są odrzucane.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "workflowId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
          },
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 255
          },
          "description": {
            "anyOf": [
              {
                "type": "string",
                "maxLength": 2000
              },
              {
                "type": "null"
              }
            ]
          },
          "definition": {},
          "expectedVersion": {
            "type": "integer",
            "minimum": 0,
            "maximum": 9007199254740991
          }
        },
        "required": [
          "workflowId"
        ]
      }
    },
    {
      "name": "validate_workflow",
      "requiredScope": "bots.read",
      "description": "Check a workflow definition WITHOUT saving anything. Returns `{ valid, issues }`, where each issue carries the `nodeId` and `field` it belongs to, so you can iterate until green before touching a stored workflow. An invalid definition is a normal result here, not an error. Stateless — nothing is persisted and no bot is affected. Mirrors POST /api/external/v1/bot-workflows/validate.",
      "descriptionPl": "Sprawdza przepływ rozmowy pod kątem błędów, niczego nie zapisując.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "definition": {}
        },
        "required": [
          "definition"
        ]
      }
    },
    {
      "name": "activate_workflow",
      "requiredScope": "bots.write",
      "description": "Make this workflow the one the bot runs, atomically deactivating any other workflow on the same bot. A definition that fails validation is refused, so real calls can never be routed into a broken graph. A bot whose config draft names an entry procedure is refused too: dispatch runs the compiled procedure flow, so the workflow would be listed as active and never run a call — clear entryProcedureId with update_bot_config first. Activation takes effect on the next call. Workflows are NOT part of the configuration snapshot: this change reaches the next call on its own, so publish_bot_config is neither required nor sufficient for it (publishing only freezes prompt/model/greeting/tools). Mirrors POST /api/external/v1/bot-workflows/{workflowId}/activate. Template-managed bots (blueprintRole INSTANCE) reject this call; change them through their template settings in the Portal.",
      "descriptionPl": "Ustawia przepływ rozmowy jako obowiązujący dla bota, wyłączając pozostałe.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "workflowId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
          }
        },
        "required": [
          "workflowId"
        ]
      }
    },
    {
      "name": "deactivate_workflow",
      "requiredScope": "bots.write",
      "description": "Return the bot to prompt-only mode: clear the active flag on this workflow WITHOUT deleting it. The workflow stays available to reactivate later. Idempotent — deactivating an already-inactive workflow succeeds and reports wasActive: false. Takes effect on the next call. Workflows are NOT part of the configuration snapshot: this change reaches the next call on its own, so publish_bot_config is neither required nor sufficient for it (publishing only freezes prompt/model/greeting/tools). Mirrors POST /api/external/v1/bot-workflows/{workflowId}/deactivate. Template-managed bots (blueprintRole INSTANCE) reject this call; change them through their template settings in the Portal.",
      "descriptionPl": "Wyłącza przepływ rozmowy — bot działa wtedy wyłącznie na podstawie promptu; przepływ można później włączyć ponownie.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "workflowId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
          }
        },
        "required": [
          "workflowId"
        ]
      }
    },
    {
      "name": "delete_workflow",
      "requiredScope": "bots.write",
      "description": "Soft-delete a workflow. Deleting the ACTIVE workflow is allowed here: it is deactivated and deleted in one step, returning the bot to prompt-only mode. The definition is retained internally (soft delete) but the workflow disappears from every list and cannot be reactivated. Takes effect on the next call. Workflows are NOT part of the configuration snapshot: this change reaches the next call on its own, so publish_bot_config is neither required nor sufficient for it (publishing only freezes prompt/model/greeting/tools). Mirrors DELETE /api/external/v1/bot-workflows/{workflowId}. Template-managed bots (blueprintRole INSTANCE) reject this call; change them through their template settings in the Portal.",
      "descriptionPl": "Usuwa przepływ rozmowy — aktywny przepływ jest wyłączany i usuwany w jednym kroku.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "workflowId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
          }
        },
        "required": [
          "workflowId"
        ]
      }
    },
    {
      "name": "list_procedures",
      "requiredScope": "bots.read",
      "description": "List a bot’s procedures, in the order they are authored, with their full content. A procedure is one of two things. A FREE_FORM procedure is a playbook the model reaches for MID-CALL when its `trigger` matches what the caller is asking about — the trigger is the ONLY part the model ever sees, and the body is injected as a framed tool result on activation; it steers behaviour and grants no new capability. A DETERMINISTIC procedure is an ordered flow compiled into the call itself at publish; the model cannot activate one, so it runs only as the bot’s entry flow (get_bot_config → entryProcedureId) or when another deterministic procedure calls it as a sub_procedure. `entryProcedureId` in the response names the entry flow, if any. Mirrors GET /api/external/v1/bots/{botId}/procedures.",
      "descriptionPl": "Lista umiejętności i procedur bota w kolejności priorytetu, z rodzajem, statusem i stanem walidacji.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "botId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
          }
        },
        "required": [
          "botId"
        ]
      }
    },
    {
      "name": "get_procedure",
      "requiredScope": "bots.read",
      "description": "Read one procedure with its full content. content depends on `type`. FREE_FORM: {\"markdown\": \"...\"} — the playbook body, at most 6000 characters. DETERMINISTIC: {\"steps\": [...]} — ordered steps, each one of: `ask` (collect answers into named fields), `say` (verbatim wording), `tell` (a brief the model phrases itself), `branch` (arms + a REQUIRED fallback; routing \"expression\" tests a variable, routing \"llm\" lets the model classify the reply; a branch may not contain another branch), `tool_call` (a tool the bot already exposes — procedures cannot add tools), `verify_identity` (onFailure is REQUIRED), `sub_procedure` (inlines another deterministic procedure of the same bot), and `end_call` (must be the last step of its chain). Mirrors GET /api/external/v1/bot-procedures/{procedureId}.",
      "descriptionPl": "Zwraca jedną umiejętność lub procedurę w całości — wyzwalacz, treść albo kroki oraz bramki narzędzi.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "procedureId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
          }
        },
        "required": [
          "procedureId"
        ]
      }
    },
    {
      "name": "create_procedure",
      "requiredScope": "bots.write",
      "description": "Add a procedure to a bot. Pick the tier deliberately: FREE_FORM when the caller may or may not raise the topic and the bot should handle it well when they do (preparation instructions, parking, what a referral requires) — its natural-language `trigger` describes when to use it and is the only thing the model sees; the trigger may stay empty while DRAFTING, but publish_bot_config refuses a free-form procedure without one. DETERMINISTIC when the steps must happen in order every time (verify identity, collect the details, then book) — it must have NO trigger, since the model can never choose to run one; it runs as the entry flow or via sub_procedure. content depends on `type`. FREE_FORM: {\"markdown\": \"...\"} — the playbook body, at most 6000 characters. DETERMINISTIC: {\"steps\": [...]} — ordered steps, each one of: `ask` (collect answers into named fields), `say` (verbatim wording), `tell` (a brief the model phrases itself), `branch` (arms + a REQUIRED fallback; routing \"expression\" tests a variable, routing \"llm\" lets the model classify the reply; a branch may not contain another branch), `tool_call` (a tool the bot already exposes — procedures cannot add tools), `verify_identity` (onFailure is REQUIRED), `sub_procedure` (inlines another deterministic procedure of the same bot), and `end_call` (must be the last step of its chain). `name` is a dashboard label and is NEVER sent to the model. New procedures are enabled and appended last; a bot may hold at most 20. To make a deterministic procedure the call flow, set entryProcedureId with update_bot_config afterwards. Writes the DRAFT. Procedures ARE part of the configuration snapshot, so this change reaches no call until you call publish_bot_config — including Bot Preview, which always runs the published version. (Workflows are the opposite: they take effect on their own. Do not carry that habit over.) Mirrors POST /api/external/v1/bots/{botId}/procedures. Template-managed bots (blueprintRole INSTANCE) reject this call; change them through their template settings in the Portal.",
      "descriptionPl": "Dodaje umiejętność (swobodne wskazówki, które bot wczytuje na żądanie) albo procedurę deterministyczną (kroki kompilowane do przepływu rozmowy). Zapisywana jako robocza; zaczyna działać po publikacji.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "botId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
          },
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 80
          },
          "type": {
            "type": "string",
            "enum": [
              "FREE_FORM",
              "DETERMINISTIC"
            ]
          },
          "trigger": {
            "anyOf": [
              {
                "type": "string",
                "maxLength": 240
              },
              {
                "type": "null"
              }
            ]
          },
          "content": {},
          "enabled": {
            "type": "boolean"
          }
        },
        "required": [
          "botId",
          "name",
          "type",
          "content"
        ]
      }
    },
    {
      "name": "update_procedure",
      "requiredScope": "bots.write",
      "description": "Edit a procedure: name, trigger, content, position and/or `enabled`. Only the fields you send are touched, but the WHOLE procedure is re-validated, so a rename cannot leave a body that breaks its tier’s rules. `type` cannot be changed — the content shape depends on it; delete and re-create instead. `enabled: false` is a LIVE kill switch and the one exception to the publish rule below: every NEW call drops the procedure from the already-published snapshot at once, no re-publish needed. Flipping it back on is just as live — the row is still in the published snapshot, so the next call serves it again. It is an override, not a stored intent, and that is what bites LATER: the next publish loads only enabled procedures, so a procedure left disabled across a publish is gone from the new snapshot and re-enabling it then takes another publish. Disabling the bot’s ENTRY procedure does NOT stop the bot: no compiled flow is served and the legacy workflow stays suppressed, so calls keep landing and run prompt-only, with no identity flow. Everything else here needs a publish. content depends on `type`. FREE_FORM: {\"markdown\": \"...\"} — the playbook body, at most 6000 characters. DETERMINISTIC: {\"steps\": [...]} — ordered steps, each one of: `ask` (collect answers into named fields), `say` (verbatim wording), `tell` (a brief the model phrases itself), `branch` (arms + a REQUIRED fallback; routing \"expression\" tests a variable, routing \"llm\" lets the model classify the reply; a branch may not contain another branch), `tool_call` (a tool the bot already exposes — procedures cannot add tools), `verify_identity` (onFailure is REQUIRED), `sub_procedure` (inlines another deterministic procedure of the same bot), and `end_call` (must be the last step of its chain). Writes the DRAFT. Procedures ARE part of the configuration snapshot, so this change reaches no call until you call publish_bot_config — including Bot Preview, which always runs the published version. (Workflows are the opposite: they take effect on their own. Do not carry that habit over.) Mirrors PATCH /api/external/v1/bot-procedures/{procedureId}. Template-managed bots (blueprintRole INSTANCE) reject this call; change them through their template settings in the Portal.",
      "descriptionPl": "Edytuje roboczą umiejętność lub procedurę. Wersja robocza może być niekompletna — walidacja odbywa się przy publikacji.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "procedureId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
          },
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 80
          },
          "trigger": {
            "anyOf": [
              {
                "type": "string",
                "maxLength": 240
              },
              {
                "type": "null"
              }
            ]
          },
          "content": {},
          "position": {
            "type": "integer",
            "minimum": 0,
            "maximum": 9007199254740991
          },
          "enabled": {
            "type": "boolean"
          }
        },
        "required": [
          "procedureId"
        ]
      }
    },
    {
      "name": "delete_procedure",
      "requiredScope": "bots.write",
      "description": "Soft-delete a procedure. The row is retained internally but disappears from every list and from the next published snapshot. Deleting the bot’s ENTRY procedure is refused: it IS the call flow, so clear entryProcedureId with update_bot_config first, then delete. To stop a procedure RIGHT NOW without deleting it, use update_procedure with `enabled: false` — every new call drops it at once, though the next publish then drops it from the snapshot as well. Writes the DRAFT. Procedures ARE part of the configuration snapshot, so this change reaches no call until you call publish_bot_config — including Bot Preview, which always runs the published version. (Workflows are the opposite: they take effect on their own. Do not carry that habit over.) Mirrors DELETE /api/external/v1/bot-procedures/{procedureId}. Template-managed bots (blueprintRole INSTANCE) reject this call; change them through their template settings in the Portal.",
      "descriptionPl": "Usuwa umiejętność lub procedurę z wersji roboczej. Opublikowana wersja zachowuje ją do następnej publikacji.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "procedureId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
          }
        },
        "required": [
          "procedureId"
        ]
      }
    },
    {
      "name": "reorder_procedures",
      "requiredScope": "bots.write",
      "description": "Set the authoring order of a bot’s procedures. Pass EVERY non-deleted procedure id exactly once, in the order you want — a partial list is refused rather than silently leaving stale positions, because position also drives the node ids of the compiled call flow. Order is an authoring convenience: it does not decide which free-form procedure the model reaches for (the trigger does). Writes the DRAFT. Procedures ARE part of the configuration snapshot, so this change reaches no call until you call publish_bot_config — including Bot Preview, which always runs the published version. (Workflows are the opposite: they take effect on their own. Do not carry that habit over.) Mirrors POST /api/external/v1/bots/{botId}/procedures/reorder. Template-managed bots (blueprintRole INSTANCE) reject this call; change them through their template settings in the Portal.",
      "descriptionPl": "Ustala kolejność priorytetów umiejętności i procedur bota. Lista musi zawierać wszystkie z nich.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "botId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
          },
          "procedureIds": {
            "minItems": 1,
            "maxItems": 20,
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid",
              "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
            }
          }
        },
        "required": [
          "botId",
          "procedureIds"
        ]
      }
    },
    {
      "name": "publish_bot_config",
      "requiredScope": "bots.write",
      "description": "Freeze the current draft into a new immutable version and point the bot at it. THIS is the step that makes configuration real: the snapshot captures agentPromptText, the model settings, greeting (shared AND its per-direction byDirection overrides), channelSettings (website-chat greeting, handoffMessage, guardrailFallbackMessage), guardrails, tools/MCP servers, turn taking, prompt variables and the whole post-call extraction chain, and from the moment it exists every dispatch — real calls AND Bot Preview — runs the snapshot rather than the draft. A bot with no published version cannot place or take a real call at all. Every publish is a numbered version with an optional label, and any of them can be restored later with rollback_bot_config, so publishing is reversible rather than risky. Publishing REQUIRES agentPromptText and the model settings create_bot seeds (configured by Mediflo) — an incomplete draft is refused with the exact missing fields. remarksOutputSchemaJson is OPTIONAL: omit it to extract only Mediflo's always-on system analysis fields; when provided it must be a JSON Schema (update_bot_config carries the rules). PROCEDURES ARE part of the snapshot: every enabled procedure is frozen here, and a deterministic entry procedure is COMPILED into the call flow at this moment. That is also when the cross-procedure checks run — sub-procedure references and cycles, reachability from the entry, the per-call context budget, gate satisfiability, and whether the primary LLM honours forced tool choice. A publish blocked this way returns the blockers with the procedure and path each belongs to; non-blocking findings come back as `procedureWarnings` on a SUCCESSFUL publish, so read them. procedureWarnings is not only procedure findings: it also carries chat prompt warnings (PROMPT_TIME_VARIABLE_IN_INSTRUCTIONS, PROMPT_LONG_FOR_CHAT, PINNED_KNOWLEDGE_LONG_FOR_CHAT), Terminarz and website-chat booking warnings (SCHEDULING_TOOLS_WITHOUT_SOURCE, SCHEDULING_KEY_MISSING, WEB_CHAT_BOOKING_WIDGET_MISSING, WEB_CHAT_BOOKING_SMS_MISSING — the SMS one only when the tenant requires SMS verification) and the AI-disclosure advisory (AI_DISCLOSURE_NOT_FOUND). None of those block the publish. The booking widget, the SMS integration and the chat-widget binding are fixed in the Portal, not by another tool call. Workflows and knowledge sources are NOT part of the snapshot; they take effect on their own. Mirrors POST /api/external/v1/bots/{botId}/config/publish. Template-managed bots (blueprintRole INSTANCE) reject this call; change them through their template settings in the Portal.",
      "descriptionPl": "Publikuje roboczą konfigurację jako nową wersję. Do tego momentu bot — również w podglądzie — działa na poprzedniej wersji.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "botId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
          },
          "label": {
            "type": "string",
            "maxLength": 255
          }
        },
        "required": [
          "botId"
        ]
      }
    },
    {
      "name": "list_config_versions",
      "requiredScope": "bots.read",
      "description": "List the published configuration versions for a bot, newest first, each marked with `isLive` for the one the bot is actually running. Only identity, label and publish time come back — the frozen payload is not shipped (it would re-expose the MCP secret surface for no benefit); read the current draft with get_bot_config instead. Feed an `id` from here into rollback_bot_config. Mirrors GET /api/external/v1/bots/{botId}/config/snapshots.",
      "descriptionPl": "Lista opublikowanych wersji konfiguracji bota z oznaczeniem wersji obecnie używanej.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "botId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
          },
          "page": {
            "default": 1,
            "type": "integer",
            "minimum": 1,
            "maximum": 9007199254740991
          },
          "limit": {
            "default": 50,
            "type": "integer",
            "minimum": 1,
            "maximum": 100
          }
        },
        "required": [
          "botId"
        ]
      }
    },
    {
      "name": "rollback_bot_config",
      "requiredScope": "bots.write",
      "description": "Restore a previously published version. It is copied back onto the draft AND re-pointed as the live version in one step, so it takes effect immediately — no publish_bot_config is needed afterwards. Get the `snapshotId` from list_config_versions. Rolling back does not delete anything: the version you rolled away from is still listed and can be restored the same way. PROCEDURES roll back with the snapshot — the frozen bundle and the compiled call flow are what every call runs, and `entryProcedureId` is restored on the draft too. The procedure ROWS themselves are deliberately left alone: they are authored content, and rewriting them would discard edits made since. So after a rollback list_procedures may show newer bodies than the call actually uses, until the next publish_bot_config. Conversation WORKFLOWS are not snapshotted and do not roll back — but a workflow node’s `skillRef` resolves against whichever snapshot is live, so a successful rollback may return `skillRefWarnings` naming pins this older version cannot serve. Mirrors POST /api/external/v1/bots/{botId}/config/rollback. Template-managed bots (blueprintRole INSTANCE) reject this call; change them through their template settings in the Portal.",
      "descriptionPl": "Przywraca wcześniejszą opublikowaną wersję konfiguracji. Zmiana działa od razu.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "botId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
          },
          "snapshotId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
          }
        },
        "required": [
          "botId",
          "snapshotId"
        ]
      }
    },
    {
      "name": "get_bot_readiness",
      "requiredScope": "bots.read",
      "description": "Preflight checklist, channel-aware. Three verdicts, and they are not interchangeable: `ready` means the bot can serve at all (published config and an active bot; a chat-only bot also needs its chat widget bound; a location is optional — a workflow is NOT required; prompt-only bots are first-class). `readyForOutbound` additionally requires the bot to serve VOICE_OUTBOUND and a bound phone number whose SIP trunk is dispatch-ready. `readyForWebChat` requires WEB_CHAT and a chat widget bound to this bot (active, at least one allowed domain, bot mode not off). A chat-only bot is legitimately `readyForOutbound: false` — do not bind a phone number. An inbound-only bot is legitimately `ready: true, readyForOutbound: false`. `chat_widget_bound` tells you to bind the widget in the Portal (Settings → Chat widget: this bot, domains, colours). Widget binding, domains, colours, the website-chat preview (Test tab), a website knowledge crawl and the Terminarz scheduling toolset are Portal-only. When website chat has booking on, `web_chat_booking_widget` and `web_chat_booking_sms` report WEB_CHAT_BOOKING_WIDGET_MISSING and WEB_CHAT_BOOKING_SMS_MISSING; they are informational (chat still works) and both are fixed in the Portal, not over MCP. Each check carries an actionable message; `informational` checks never gate a verdict. One of them, `ai_disclosure`, is an ADVISORY rather than a precondition: it sweeps agentPromptText, greetingConfig and the active workflow for wording that tells the person they are talking to an AI, because EU AI Act Article 50 expects that disclosure. It is a Polish/English keyword heuristic, so `ok: false` means \"we could not find one, please confirm\" — it is not a compliance verdict and not legal advice. The obligation and the wording belong to the operator; Mediflo neither enforces nor verifies them, and nothing is ever blocked by this check. Mirrors GET /api/external/v1/bots/{botId}/readiness.",
      "descriptionPl": "Sprawdza gotowość bota na jego kanałach: widget czatu dla strony oraz numer telefonu tylko wtedy, gdy bot wykonuje połączenia.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "botId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
          }
        },
        "required": [
          "botId"
        ]
      }
    },
    {
      "name": "list_knowledge_sources",
      "requiredScope": "knowledge.read",
      "description": "List the knowledge sources attached to a bot: title, article type, usage mode, publish state, tags, freshness window and indexing status. Body text is not included — this is the inventory view. Mirrors GET /api/external/v1/bots/{botId}/knowledge/sources.",
      "descriptionPl": "Lista artykułów w bazie wiedzy bota wraz ze statusem indeksowania.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "botId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
          },
          "page": {
            "default": 1,
            "type": "integer",
            "minimum": 1,
            "maximum": 9007199254740991
          },
          "limit": {
            "default": 50,
            "type": "integer",
            "minimum": 1,
            "maximum": 100
          }
        },
        "required": [
          "botId"
        ]
      }
    },
    {
      "name": "add_knowledge_source",
      "requiredScope": "knowledge.write",
      "description": "Add a text (or structured) knowledge article to a bot knowledge base. The knowledge base is created on first use, so there is nothing to provision first. `type` is always \"text\" (file uploads are Portal-only). Crawling a website into the knowledge base is also Portal-only — open the bot’s knowledge page in the Portal. This tool cannot start a crawl. `articleType=text` requires `bodyText`. Structured article types (`pricing`, `info`, `faq`, `doctors`) require `structuredContent` and must omit `bodyText` — submitting them without `structuredContent` is rejected and retrying the same payload will not work. `usageMode` controls whether the article is retrieved on demand (\"auto\") or always in context; `publishState` controls whether it is live. Indexing is asynchronous — the returned `status` is the current indexing state, not a failure. Knowledge sources are NOT part of the configuration snapshot — this reaches the bot through indexing on its own, with no publish_bot_config step. Mirrors POST /api/external/v1/bots/{botId}/knowledge/sources.",
      "descriptionPl": "Dodaje artykuł tekstowy do bazy wiedzy bota.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "type": {
            "type": "string",
            "const": "text"
          },
          "title": {
            "type": "string",
            "minLength": 1,
            "maxLength": 255
          },
          "bodyText": {
            "type": "string",
            "minLength": 1,
            "maxLength": 500000
          },
          "articleType": {
            "default": "text",
            "type": "string",
            "enum": [
              "text",
              "faq",
              "info",
              "pricing",
              "doctors"
            ]
          },
          "structuredContent": {},
          "questionVariants": {
            "maxItems": 12,
            "type": "array",
            "items": {
              "type": "string",
              "minLength": 3,
              "maxLength": 300
            }
          },
          "tags": {
            "maxItems": 16,
            "type": "array",
            "items": {
              "type": "string",
              "minLength": 1,
              "maxLength": 48
            }
          },
          "usageMode": {
            "default": "auto",
            "type": "string",
            "enum": [
              "auto",
              "prompt"
            ]
          },
          "publishState": {
            "default": "published",
            "type": "string",
            "enum": [
              "draft",
              "published"
            ]
          },
          "category": {
            "anyOf": [
              {
                "anyOf": [
                  {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 80
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "expiresAt": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time",
                "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
              },
              {
                "type": "null"
              }
            ]
          },
          "botId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
          }
        },
        "required": [
          "type",
          "title",
          "botId"
        ]
      }
    },
    {
      "name": "update_knowledge_settings",
      "requiredScope": "knowledge.write",
      "description": "Set the bot retrieval (RAG) settings: whether knowledge is `enabled`, the `retrievalMode` (\"auto\" retrieves when the model asks for it, \"every_turn\" always retrieves, \"off\" disables it), whether an answer must be grounded (`requireKnowledge`), and the retrieval budget (maxCharacters, maxChunks, maxVectorDistance, searchDepth, timeoutMs, requiredTimeoutMs). All fields are required — send the full object, starting from get_recommended_defaults.rag. Knowledge sources are NOT part of the configuration snapshot — this reaches the bot through indexing on its own, with no publish_bot_config step. Mirrors PATCH /api/external/v1/bots/{botId}/knowledge.",
      "descriptionPl": "Zmienia ustawienia wyszukiwania w bazie wiedzy bota, w tym to, czy baza jest w ogóle używana.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "enabled": {
            "type": "boolean"
          },
          "retrievalMode": {
            "type": "string",
            "enum": [
              "auto",
              "every_turn",
              "off"
            ]
          },
          "requireKnowledge": {
            "type": "boolean"
          },
          "maxCharacters": {
            "type": "integer",
            "minimum": 200,
            "maximum": 20000
          },
          "maxChunks": {
            "type": "integer",
            "minimum": 1,
            "maximum": 20
          },
          "maxVectorDistance": {
            "type": "number",
            "minimum": 0,
            "maximum": 2
          },
          "searchDepth": {
            "type": "integer",
            "minimum": 1,
            "maximum": 500
          },
          "timeoutMs": {
            "type": "integer",
            "minimum": 1,
            "maximum": 5000
          },
          "requiredTimeoutMs": {
            "type": "integer",
            "minimum": 1,
            "maximum": 5000
          },
          "botId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
          }
        },
        "required": [
          "enabled",
          "retrievalMode",
          "requireKnowledge",
          "maxCharacters",
          "maxChunks",
          "maxVectorDistance",
          "searchDepth",
          "timeoutMs",
          "requiredTimeoutMs",
          "botId"
        ]
      }
    },
    {
      "name": "list_appointment_types",
      "requiredScope": "scheduling.read",
      "description": "List the visit types this clinic takes online bookings for: name, duration in minutes, price (present ONLY for privately paid visits that have one — a missing `price` means do not quote a fee), a short description, and the ids of the specialists who perform it. Start here when the caller says what they need; you cannot book without an `appointmentTypeId` from this list. RULES. Identifiers and tokens (slotToken, actionToken, patientRef, operationId, specialistId, appointmentTypeId, locationId) come only from a tool result in this conversation — never from the caller, never invented, and a person's name is never an identifier. `null` is a value, not the text \"null\": omit a field you do not have. Offer at most three slots per turn and read each slot's `spokenLabel` verbatim — never derive a weekday or a date yourself. Mirrors GET /api/external/v1/scheduling/appointment-types.",
      "descriptionPl": "Zwraca typy wizyt dostępnych do rezerwacji online: czas trwania, cenę (tylko dla wizyt prywatnych), opis i specjalistów.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "specialistId": {
            "description": "Only types this specialist performs.",
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
          },
          "locationId": {
            "description": "Only types offered at this site.",
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
          }
        }
      }
    },
    {
      "name": "list_specialists",
      "requiredScope": "scheduling.read",
      "description": "List the specialists taking online bookings: name, title, specialization and the sites they work at. Use it when the caller asks for a particular doctor, or to say who is available for a visit type. Contact details are deliberately absent — the clinic's own number is the only one to give out. RULES. Identifiers and tokens (slotToken, actionToken, patientRef, operationId, specialistId, appointmentTypeId, locationId) come only from a tool result in this conversation — never from the caller, never invented, and a person's name is never an identifier. `null` is a value, not the text \"null\": omit a field you do not have. Offer at most three slots per turn and read each slot's `spokenLabel` verbatim — never derive a weekday or a date yourself. Mirrors GET /api/external/v1/scheduling/specialists.",
      "descriptionPl": "Zwraca specjalistów przyjmujących rezerwacje online wraz z tytułem, specjalizacją i placówkami.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "locationId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
          },
          "appointmentTypeId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
          }
        }
      }
    },
    {
      "name": "get_booking_requirements",
      "requiredScope": "scheduling.read",
      "description": "What this clinic needs before a booking can go through: which patient fields are required, which are optional, the notice policies (how far ahead a visit must be booked, how long a held slot lasts, how late a visit may still be cancelled or moved), the sites with their addresses, and each specialist's `calendarSyncMode`. Call it ONCE at the start of a booking conversation. CHECK `bookingEnabled` FIRST: when it is false the clinic cannot take any booking right now (`unavailableReason` says why) — tell the caller plainly and offer request_staff_callback instead of trying to book. Ask only for the fields it lists: asking for a field this clinic does not collect wastes the caller's time, and skipping a required one fails book_appointment with MISSING_FIELDS. Say \"the doctor will see it in their calendar\" only for a specialist whose `calendarSyncMode` is \"linked\". RULES. Identifiers and tokens (slotToken, actionToken, patientRef, operationId, specialistId, appointmentTypeId, locationId) come only from a tool result in this conversation — never from the caller, never invented, and a person's name is never an identifier. `null` is a value, not the text \"null\": omit a field you do not have. Offer at most three slots per turn and read each slot's `spokenLabel` verbatim — never derive a weekday or a date yourself. Mirrors GET /api/external/v1/scheduling/booking-requirements.",
      "descriptionPl": "Zwraca wymagane i opcjonalne dane pacjenta, zasady wyprzedzenia i odwołań, adresy placówek oraz informację, czy rezerwacje są w ogóle możliwe.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {}
      }
    },
    {
      "name": "get_available_slots",
      "requiredScope": "scheduling.read",
      "description": "Free slots for a visit type in a date range. Every slot carries a `slotToken` (the ONLY thing hold_slot, book_appointment and reschedule_appointment accept — a time you typed yourself is refused with INVALID_SLOT; on a phone call it is a short code like `k7m2qx`, copy it exactly and never build one) and a `spokenLabel` already written in Polish: read it out exactly as given. An empty `slots` list with `noAvailability: true` is a normal answer, not a failure — offer a different day, specialist or visit type. `degraded: true` means an external calendar could not be reached and the times shown may still turn out to be taken: say the appointment will be confirmed, and never present a degraded slot as certain. LOCATION. Pass `locationId` only for a location the caller chose (an id from get_booking_requirements or from `details.locations`); an assistant that serves one location searches it by itself, so omit it there. When the clinic has several locations and none was chosen, the tool answers `success: false` with `code: LOCATION_REQUIRED` and `details.locations`: ask the caller which location they want — say the names, never an id — and call again with its `locationId`. `LOCATION_NOT_SERVED` means this assistant does not book that location: do what its `message` says. RULES. Identifiers and tokens (slotToken, actionToken, patientRef, operationId, specialistId, appointmentTypeId, locationId) come only from a tool result in this conversation — never from the caller, never invented, and a person's name is never an identifier. `null` is a value, not the text \"null\": omit a field you do not have. Offer at most three slots per turn and read each slot's `spokenLabel` verbatim — never derive a weekday or a date yourself. Mirrors GET /api/external/v1/scheduling/availability.",
      "descriptionPl": "Zwraca wolne terminy dla wybranego typu wizyty wraz z tokenem terminu i gotową polską etykietą do przeczytania.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "appointmentTypeId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
          },
          "specialistId": {
            "description": "Omit to search every specialist who performs the type.",
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
          },
          "locationId": {
            "description": "Only for a location the caller chose. Omit it otherwise: an assistant that serves one location searches it by itself.",
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
          },
          "from": {
            "type": "string",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
            "description": "First day to search, YYYY-MM-DD, inclusive."
          },
          "to": {
            "type": "string",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
            "description": "Last day to search, YYYY-MM-DD, inclusive."
          },
          "timeOfDay": {
            "default": "any",
            "description": "Filter by part of the day when the caller asked for one. morning is before 12:00, afternoon 12:00–17:00, evening from 17:00.",
            "type": "string",
            "enum": [
              "morning",
              "afternoon",
              "evening",
              "any"
            ]
          },
          "limit": {
            "default": 10,
            "description": "How many slots to return. Present at most three per turn.",
            "type": "integer",
            "minimum": 1,
            "maximum": 25
          }
        },
        "required": [
          "appointmentTypeId",
          "from",
          "to"
        ]
      }
    },
    {
      "name": "hold_slot",
      "requiredScope": "scheduling.book",
      "description": "Reserve a slot for a few minutes while you collect the caller's details, so nobody else takes it mid-conversation. Pass a `slotToken` from get_available_slots; the hold expires on its own, so a call that ends badly leaves nothing behind. Holding is OPTIONAL — book_appointment works without one — but on a busy book it is the difference between a booking and a SLOT_TAKEN two minutes later. RULES. Identifiers and tokens (slotToken, actionToken, patientRef, operationId, specialistId, appointmentTypeId, locationId) come only from a tool result in this conversation — never from the caller, never invented, and a person's name is never an identifier. `null` is a value, not the text \"null\": omit a field you do not have. Offer at most three slots per turn and read each slot's `spokenLabel` verbatim — never derive a weekday or a date yourself. WRITE RULES. This tool changes a real appointment book. Claim a booked visit only when `success: true` AND an `appointment` is present. `success: true` with `operationId` and `status` PENDING/UNCERTAIN is not a confirmed booking — do not retry. On `success: false` read `code` and tell the caller what actually happened instead of announcing a booking. Before you call it, read the caller a summary of what is about to happen and get an explicit yes. Never call the same write twice to \"make sure\". Mirrors POST /api/external/v1/scheduling/holds.",
      "descriptionPl": "Blokuje wybrany termin na kilka minut, żeby nikt go nie zajął w trakcie rozmowy.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "slotToken": {
            "type": "string",
            "minLength": 1,
            "maxLength": 4096
          }
        },
        "required": [
          "slotToken"
        ]
      }
    },
    {
      "name": "find_patient",
      "requiredScope": "patients.read",
      "description": "Recognise a returning caller from the number they are calling from, and see the visits they already have booked. `contactPhone` is supplied by the platform. When the channel has no verified number the tool answers IDENTITY_UNAVAILABLE. `found: false` means this number is new to the clinic — that is normal, book them as a new patient. Names come back MASKED on purpose: use one to confirm (\"czy rozmawiam z panem Janem K.?\"), never to assume, because several people share one phone. A `patientRef` is how you book for someone already known — pass it to book_appointment instead of retyping their details. RULES. Identifiers and tokens (slotToken, actionToken, patientRef, operationId, specialistId, appointmentTypeId, locationId) come only from a tool result in this conversation — never from the caller, never invented, and a person's name is never an identifier. `null` is a value, not the text \"null\": omit a field you do not have. Offer at most three slots per turn and read each slot's `spokenLabel` verbatim — never derive a weekday or a date yourself. Mirrors GET /api/external/v1/patients/lookup.",
      "descriptionPl": "Rozpoznaje dzwoniącego po numerze telefonu i zwraca zamaskowane imię, referencję pacjenta oraz jego nadchodzące wizyty.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "contactPhone": {
            "description": "The caller's own number. Supplied by the platform — do NOT fill it in yourself. When the channel has no verified number the tool answers IDENTITY_UNAVAILABLE; that is expected on web chat, not an error to retry.",
            "type": "string"
          },
          "lastName": {
            "description": "Pass it when the caller gave a surname, to pick the right person on a shared phone.",
            "type": "string",
            "minLength": 1,
            "maxLength": 80
          }
        }
      }
    },
    {
      "name": "find_contact",
      "requiredScope": "contacts.lookup",
      "description": "Recognise a returning caller from the number they are calling from, and see the bookings they already have. `contactPhone` is supplied by the platform. When the channel has no verified number the tool answers IDENTITY_UNAVAILABLE. `found: false` means this number has no earlier booking — that is normal, book them as a new customer. Names come back MASKED on purpose: use one to confirm (\"czy rozmawiam z panem Janem K.?\"), never to assume, because several people share one phone. A `contactRef` is how you book for someone already known — pass it to book_appointment instead of retyping their details. RULES. Identifiers and tokens (slotToken, actionToken, contactRef, operationId, specialistId, appointmentTypeId, locationId) come only from a tool result in this conversation — never from the caller, never invented, and a person's name is never an identifier. `null` is a value, not the text \"null\": omit a field you do not have. Offer at most three slots per turn and read each slot's `spokenLabel` verbatim — never derive a weekday or a date yourself.",
      "descriptionPl": "Rozpoznaje dzwoniącego po numerze telefonu i zwraca zamaskowane imię, referencję klienta oraz jego nadchodzące rezerwacje.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "contactPhone": {
            "description": "The caller's own number. Supplied by the platform — do NOT fill it in yourself. When the channel has no verified number the tool answers IDENTITY_UNAVAILABLE; that is expected on web chat, not an error to retry.",
            "type": "string"
          },
          "lastName": {
            "description": "Pass it when the caller gave a surname, to pick the right person on a shared phone.",
            "type": "string",
            "minLength": 1,
            "maxLength": 80
          }
        }
      }
    },
    {
      "name": "book_appointment",
      "requiredScope": "scheduling.book",
      "description": "Book the appointment. This is the commitment — the patient is expected at the clinic afterwards and the visit goes into the clinic's calendar. Pass EITHER `slotToken` (from get_available_slots) or `holdId` (from hold_slot), and EITHER `patientRef` (from find_patient, for someone already known) or `patient` with the fields get_booking_requirements listed as required. Supply an optional field only when the caller actually gave it — never invent an e-mail address. `contactPhone` is supplied by the platform. When the channel has no verified number, a NEW patient may instead be booked on the number the caller dictates — put it in `patient.phone` as E.164 (+48…), after reading the digits back and getting a clear \"tak\". That stand-in is only for a new patient: with `patientRef`, or on web chat, the tool still answers IDENTITY_UNAVAILABLE. On success read back the returned `spokenLabel`, the address and any `instructions` verbatim. Booking twice for the same person, specialist and time returns the SAME appointment instead of double-booking. `idempotencyKey` is OPTIONAL and exists for a retry after a timeout: reusing a key MUST repeat the identical request — a different body under a used key is refused with IDEMPOTENCY_MISMATCH and books nothing. ⚠️ A `success: true` result that carries `operationId` and `status` PENDING/UNCERTAIN (and no appointment) means the outcome is not yet known: DO NOT CALL THIS TOOL AGAIN — that risks a second visit. Tell the caller the clinic will confirm the booking, offer request_staff_callback, and optionally call get_booking_status once with the returned `operationId`. RULES. Identifiers and tokens (slotToken, actionToken, patientRef, operationId, specialistId, appointmentTypeId, locationId) come only from a tool result in this conversation — never from the caller, never invented, and a person's name is never an identifier. `null` is a value, not the text \"null\": omit a field you do not have. Offer at most three slots per turn and read each slot's `spokenLabel` verbatim — never derive a weekday or a date yourself. WRITE RULES. This tool changes a real appointment book. Claim a booked visit only when `success: true` AND an `appointment` is present. `success: true` with `operationId` and `status` PENDING/UNCERTAIN is not a confirmed booking — do not retry. On `success: false` read `code` and tell the caller what actually happened instead of announcing a booking. Before you call it, read the caller a summary of what is about to happen and get an explicit yes. Never call the same write twice to \"make sure\". Mirrors POST /api/external/v1/scheduling/appointments.",
      "descriptionPl": "Rezerwuje wizytę: zapisuje pacjenta, wpisuje termin do kalendarza lekarza i uruchamia potwierdzenie.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "slotToken": {
            "type": "string",
            "minLength": 1,
            "maxLength": 4096
          },
          "holdId": {
            "type": "string",
            "minLength": 1,
            "maxLength": 8192
          },
          "contactPhone": {
            "description": "The caller's own number. Supplied by the platform — do NOT fill it in yourself. When the channel has no verified number the tool answers IDENTITY_UNAVAILABLE; that is expected on web chat, not an error to retry.",
            "type": "string"
          },
          "alternativePhone": {
            "description": "Only when the caller explicitly asks to be reached on a DIFFERENT number.",
            "type": "string",
            "pattern": "^\\+[1-9]\\d{6,14}$"
          },
          "patientRef": {
            "description": "From find_patient, for a patient the clinic already knows.",
            "type": "string",
            "minLength": 1,
            "maxLength": 1000
          },
          "patient": {
            "type": "object",
            "properties": {
              "firstName": {
                "type": "string",
                "minLength": 1,
                "maxLength": 80
              },
              "lastName": {
                "type": "string",
                "minLength": 1,
                "maxLength": 80
              },
              "dateOfBirth": {
                "description": "YYYY-MM-DD. Ask only when get_booking_requirements lists it.",
                "type": "string",
                "pattern": "^\\d{4}-\\d{2}-\\d{2}$"
              },
              "email": {
                "type": "string",
                "format": "email",
                "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
              },
              "pesel": {
                "description": "Ask only when get_booking_requirements lists it. Stored encrypted and never read back.",
                "type": "string",
                "pattern": "^\\d{11}$"
              },
              "phone": {
                "description": "The contact number the caller DICTATES, in E.164 (+48…). Used only when the platform has no verified number for this channel — on a normal phone call the platform already knows it and yours is ignored, so you never need to ask just to fill this in. Read the digits back and get a clear \"tak\" before booking: a wrong number sends this visit's confirmation to a stranger.",
                "type": "string",
                "pattern": "^\\+[1-9]\\d{6,14}$"
              }
            },
            "required": [
              "lastName"
            ]
          },
          "note": {
            "description": "What the caller says the visit is about, in their words. Stored as the visit note (max 200 characters).",
            "type": "string",
            "minLength": 1,
            "maxLength": 200
          },
          "idempotencyKey": {
            "description": "Optional retry key. Reusing it MUST repeat the identical request; a different body under the same key is refused with IDEMPOTENCY_MISMATCH.",
            "type": "string",
            "minLength": 1,
            "maxLength": 200
          }
        }
      }
    },
    {
      "name": "get_booking_status",
      "requiredScope": "scheduling.read",
      "description": "What happened to a booking whose outcome was not yet known. Pass the `operationId` that book_appointment returned with `success: true` and `status` PENDING or UNCERTAIN. `COMMITTED` means the visit exists and `appointment` holds it — read the `spokenLabel` and the address. `FAILED` means nothing was booked. `PENDING` or `UNCERTAIN` means it is still being resolved: call this AT MOST ONCE more, then tell the caller the clinic will confirm the booking and offer request_staff_callback. Never re-run book_appointment to find out — that risks a second visit. RULES. Identifiers and tokens (slotToken, actionToken, patientRef, operationId, specialistId, appointmentTypeId, locationId) come only from a tool result in this conversation — never from the caller, never invented, and a person's name is never an identifier. `null` is a value, not the text \"null\": omit a field you do not have. Offer at most three slots per turn and read each slot's `spokenLabel` verbatim — never derive a weekday or a date yourself. Mirrors GET /api/external/v1/scheduling/booking-operations/{operationId}.",
      "descriptionPl": "Sprawdza wynik rezerwacji, której skutek nie był jeszcze znany, na podstawie identyfikatora operacji.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "operationId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
          }
        },
        "required": [
          "operationId"
        ]
      }
    },
    {
      "name": "list_patient_appointments",
      "requiredScope": "scheduling.read",
      "description": "The upcoming visits booked on the number the caller is phoning from. `contactPhone` is supplied by the platform. When the channel has no verified number the tool answers IDENTITY_UNAVAILABLE. Each entry carries an `actionToken` — the ONLY way reschedule_appointment and cancel_appointment may name an appointment — and `patientMaskedName`, because several people share one phone: read the name and the date back and get a clear \"tak\" before you change anything. RULES. Identifiers and tokens (slotToken, actionToken, patientRef, operationId, specialistId, appointmentTypeId, locationId) come only from a tool result in this conversation — never from the caller, never invented, and a person's name is never an identifier. `null` is a value, not the text \"null\": omit a field you do not have. Offer at most three slots per turn and read each slot's `spokenLabel` verbatim — never derive a weekday or a date yourself. Mirrors GET /api/external/v1/scheduling/appointments.",
      "descriptionPl": "Zwraca nadchodzące wizyty przypisane do numeru dzwoniącego, każdą z tokenem akcji i zamaskowanym imieniem pacjenta.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "contactPhone": {
            "description": "The caller's own number. Supplied by the platform — do NOT fill it in yourself. When the channel has no verified number the tool answers IDENTITY_UNAVAILABLE; that is expected on web chat, not an error to retry.",
            "type": "string"
          }
        }
      }
    },
    {
      "name": "list_contact_appointments",
      "requiredScope": "contacts.lookup",
      "description": "The upcoming bookings made on the number the caller is phoning from. `contactPhone` is supplied by the platform. When the channel has no verified number the tool answers IDENTITY_UNAVAILABLE. Each entry carries an `actionToken` — the ONLY way reschedule_appointment and cancel_appointment may name a booking — and `contactMaskedName`, because several people share one phone: read the name and the date back and get a clear \"tak\" before you change anything. RULES. Identifiers and tokens (slotToken, actionToken, contactRef, operationId, specialistId, appointmentTypeId, locationId) come only from a tool result in this conversation — never from the caller, never invented, and a person's name is never an identifier. `null` is a value, not the text \"null\": omit a field you do not have. Offer at most three slots per turn and read each slot's `spokenLabel` verbatim — never derive a weekday or a date yourself. Mirrors POST /api/external/v1/scheduling/appointments/search.",
      "descriptionPl": "Zwraca nadchodzące rezerwacje przypisane do numeru dzwoniącego, każdą z tokenem akcji i zamaskowanym imieniem klienta.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "contactPhone": {
            "description": "The caller's own number. Supplied by the platform — do NOT fill it in yourself. When the channel has no verified number the tool answers IDENTITY_UNAVAILABLE; that is expected on web chat, not an error to retry.",
            "type": "string"
          }
        }
      }
    },
    {
      "name": "reschedule_appointment",
      "requiredScope": "scheduling.modify",
      "description": "Move an existing visit to a new slot. Name the appointment with the `actionToken` from list_patient_appointments — a bare id is refused. `slotToken` comes from get_available_slots. Read the caller the OLD time and the NEW time, confirm the patient name you were shown, and get an explicit yes before calling. `code: POLICY_WINDOW` with `handoff: true` means the visit is too close to be moved by phone — offer request_staff_callback instead of trying again. `code: STALE_VERSION` means the visit changed while you were talking: call list_patient_appointments again and start from the new state. RULES. Identifiers and tokens (slotToken, actionToken, patientRef, operationId, specialistId, appointmentTypeId, locationId) come only from a tool result in this conversation — never from the caller, never invented, and a person's name is never an identifier. `null` is a value, not the text \"null\": omit a field you do not have. Offer at most three slots per turn and read each slot's `spokenLabel` verbatim — never derive a weekday or a date yourself. WRITE RULES. This tool changes a real appointment book. Claim a booked visit only when `success: true` AND an `appointment` is present. `success: true` with `operationId` and `status` PENDING/UNCERTAIN is not a confirmed booking — do not retry. On `success: false` read `code` and tell the caller what actually happened instead of announcing a booking. Before you call it, read the caller a summary of what is about to happen and get an explicit yes. Never call the same write twice to \"make sure\". Mirrors POST /api/external/v1/scheduling/appointment-actions/reschedule.",
      "descriptionPl": "Przenosi wizytę na nowy termin na podstawie tokenu akcji.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "actionToken": {
            "description": "From list_patient_appointments or find_patient. The only way a bot may name an appointment.",
            "type": "string",
            "minLength": 1,
            "maxLength": 1000
          },
          "appointmentId": {
            "description": "Staff and partner keys (scheduling.manage) only.",
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
          },
          "expectedVersion": {
            "description": "Required with appointmentId: the version you last read. A mismatch answers STALE_VERSION instead of overwriting somebody else’s change.",
            "type": "integer",
            "minimum": 0,
            "maximum": 9007199254740991
          },
          "slotToken": {
            "type": "string",
            "minLength": 1,
            "maxLength": 1000
          },
          "contactPhone": {
            "description": "The caller's own number. Supplied by the platform — do NOT fill it in yourself. When the channel has no verified number the tool answers IDENTITY_UNAVAILABLE; that is expected on web chat, not an error to retry.",
            "type": "string"
          }
        },
        "required": [
          "slotToken"
        ]
      }
    },
    {
      "name": "cancel_appointment",
      "requiredScope": "scheduling.modify",
      "description": "Cancel a visit. It frees the slot, removes the doctor's calendar entry and notifies the patient — there is no undo, so CONFIRM with the caller first: say which visit, on which day, for which patient name, and get an explicit yes before calling. Name the appointment with the `actionToken` from list_patient_appointments — a bare id is refused. `code: POLICY_WINDOW` with `handoff: true` means it is too late to cancel by phone — offer request_staff_callback. RULES. Identifiers and tokens (slotToken, actionToken, patientRef, operationId, specialistId, appointmentTypeId, locationId) come only from a tool result in this conversation — never from the caller, never invented, and a person's name is never an identifier. `null` is a value, not the text \"null\": omit a field you do not have. Offer at most three slots per turn and read each slot's `spokenLabel` verbatim — never derive a weekday or a date yourself. WRITE RULES. This tool changes a real appointment book. Claim a booked visit only when `success: true` AND an `appointment` is present. `success: true` with `operationId` and `status` PENDING/UNCERTAIN is not a confirmed booking — do not retry. On `success: false` read `code` and tell the caller what actually happened instead of announcing a booking. Before you call it, read the caller a summary of what is about to happen and get an explicit yes. Never call the same write twice to \"make sure\". Mirrors POST /api/external/v1/scheduling/appointment-actions/cancel.",
      "descriptionPl": "Odwołuje wizytę na podstawie tokenu akcji, zwalnia termin i powiadamia pacjenta. Operacji nie da się cofnąć.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "actionToken": {
            "description": "From list_patient_appointments or find_patient. The only way a bot may name an appointment.",
            "type": "string",
            "minLength": 1,
            "maxLength": 1000
          },
          "appointmentId": {
            "description": "Staff and partner keys (scheduling.manage) only.",
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
          },
          "expectedVersion": {
            "description": "Required with appointmentId: the version you last read. A mismatch answers STALE_VERSION instead of overwriting somebody else’s change.",
            "type": "integer",
            "minimum": 0,
            "maximum": 9007199254740991
          },
          "contactPhone": {
            "description": "The caller's own number. Supplied by the platform — do NOT fill it in yourself. When the channel has no verified number the tool answers IDENTITY_UNAVAILABLE; that is expected on web chat, not an error to retry.",
            "type": "string"
          },
          "reason": {
            "description": "The caller's cancellation explanation, in their words. Omit if they did not say. Separate from the visit note (max 200 characters); this field takes up to 500.",
            "type": "string",
            "minLength": 1,
            "maxLength": 500
          }
        }
      }
    },
    {
      "name": "request_staff_callback",
      "requiredScope": "scheduling.read | contact-requests.write",
      "description": "Hand the caller over to a human: files a contact request the clinic's staff see and act on. Use it when a tool came back with `handoff: true` (a change too close to the visit, an unknown booking outcome, no caller identity on this channel), when the caller asks for a person, or when you genuinely cannot complete what they need. Say plainly that somebody from the clinic will call back — do NOT promise a time you were not given, and do not present a callback as a completed booking. RULES. Identifiers and tokens (slotToken, actionToken, patientRef, operationId, specialistId, appointmentTypeId, locationId) come only from a tool result in this conversation — never from the caller, never invented, and a person's name is never an identifier. `null` is a value, not the text \"null\": omit a field you do not have. Offer at most three slots per turn and read each slot's `spokenLabel` verbatim — never derive a weekday or a date yourself. WRITE RULES. This tool changes a real appointment book. Claim a booked visit only when `success: true` AND an `appointment` is present. `success: true` with `operationId` and `status` PENDING/UNCERTAIN is not a confirmed booking — do not retry. On `success: false` read `code` and tell the caller what actually happened instead of announcing a booking. Before you call it, read the caller a summary of what is about to happen and get an explicit yes. Never call the same write twice to \"make sure\". Mirrors POST /api/external/v1/scheduling/callback-requests.",
      "descriptionPl": "Zakłada prośbę o kontakt, gdy sprawą musi zająć się człowiek z rejestracji.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "reason": {
            "type": "string",
            "minLength": 1,
            "maxLength": 1000,
            "description": "What the caller wants, in their own words, so staff can act without listening to the call."
          },
          "appointmentId": {
            "description": "The visit this is about, when there is one.",
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
          },
          "contactPhone": {
            "description": "The caller's own number. Supplied by the platform — do NOT fill it in yourself. When the channel has no verified number the tool answers IDENTITY_UNAVAILABLE; that is expected on web chat, not an error to retry.",
            "type": "string"
          },
          "callbackPhone": {
            "description": "The number the caller gives you to be called back on, E.164 (+48…). Supply it ONLY when the platform has no verified number for this channel — otherwise the clinic calls back on the number they rang from. Read the digits back and get a clear \"tak\" first.",
            "type": "string",
            "pattern": "^\\+[1-9]\\d{6,14}$"
          },
          "callerName": {
            "description": "The caller’s name as they said it. Omit when not given.",
            "type": "string",
            "minLength": 1,
            "maxLength": 80
          },
          "urgent": {
            "description": "true only when the caller’s matter cannot wait for a normal callback.",
            "type": "boolean"
          }
        },
        "required": [
          "reason"
        ]
      }
    },
    {
      "name": "create_bot_session",
      "requiredScope": "bot-calls.write",
      "description": "PLACES A REAL PHONE CALL: dials `phoneNumber` (E.164) with the bot `botId` RIGHT NOW — there is no scheduling and no confirmation step, so only call this when the person is meant to receive a call at this moment. Returns the new bot session (`created: true`, the same shape as get_bot_session) as soon as the dispatcher accepts it; the call itself is still in progress at that point. `idempotencyKey` is REQUIRED and is what makes a retry safe: the same key never dials twice — a replay returns the existing session with `created: false` instead of placing a second call, and the same key with a different bot or number is refused (IDEMPOTENCY_KEY_REUSED). The key is consumed the moment the dispatcher accepts the request — even if the call then fails before anyone picks up (FAILED, no_answer, voicemail): a replay returns that session and never re-dials, so calling the same person again needs a NEW key. Use your own message or notification id. Calls are only placed between 08:00 and 20:00 in the tenant’s timezone; outside that window the tool refuses with OUTSIDE_CALL_WINDOW and does not queue anything. The bot must have an outbound phone number bound (BOT_NO_OUTBOUND_NUMBER otherwise — check get_bot_readiness.readyForOutbound) and a PUBLISHED configuration (BOT_NOT_PUBLISHED — run publish_bot_config first). `context` is what the bot knows about this call: `reason` (a slug the bot prompt or workflow branches on, rendered as {{reason}}), an optional `message` ({{message}}), an optional `appointment` (flattened to {{appointment_date}}, {{appointment_time}}, {{appointment_doctor_name}} (from `appointment.staffName`, or its medical alias `doctorName`), {{appointment_service_name}}, {{appointment_location_name}}, {{appointment_address}}, {{appointment_external_id}}) and optional `variables` rendered verbatim as {{name}} — reserved names are refused as an argument validation error. The bot must also be switched on (BOT_INACTIVE otherwise). The outcome is NOT in this response: it arrives on the `bot_call.completed` webhook (status completed / no_answer / voicemail / failed / stuck, `durationSec`, `outcome`, `links`) or by polling get_bot_session with the returned `dispatchId` until `status` leaves IN_PROGRESS. Mid-call actions such as rescheduling are the bot's own tools, configured on the bot — not arguments here. Sessions placed this way carry `attemptId` `ext-<dispatchId>`. Mirrors POST /api/external/v1/bot-sessions.",
      "descriptionPl": "Wykonuje prawdziwe połączenie telefoniczne na podany numer wybranym botem, natychmiast. Wymaga unikalnego klucza idempotencji, więc ponowione żądanie nigdy nie wykona drugiego połączenia; dozwolone tylko między 08:00 a 20:00 w strefie czasowej tenanta.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "botId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
          },
          "phoneNumber": {
            "type": "string",
            "pattern": "^\\+[1-9]\\d{6,14}$",
            "description": "The number to dial, E.164."
          },
          "idempotencyKey": {
            "type": "string",
            "minLength": 1,
            "maxLength": 128,
            "description": "Caller-chosen key that makes the request safe to retry: the same key never places a second call. Use your own message/notification id."
          },
          "context": {
            "type": "object",
            "properties": {
              "reason": {
                "type": "string",
                "pattern": "^[a-z][a-z0-9_]{0,63}$",
                "description": "Why the bot is calling, as a stable slug — e.g. `visit_reminder` or `visit_rescheduled` (clinics), `booking_reminder`, `order_status`; use the slugs the bot’s workflow expects. Rendered as `{{reason}}`; the bot prompt or workflow branches on it."
              },
              "message": {
                "description": "The information to convey, in the language of the person called. Rendered as `{{message}}`.",
                "type": "string",
                "minLength": 1,
                "maxLength": 1000
              },
              "appointment": {
                "description": "Flattened to `{{appointment_date}}`, `{{appointment_time}}`, …",
                "type": "object",
                "properties": {
                  "date": {
                    "type": "string",
                    "pattern": "^\\d{4}-\\d{2}-\\d{2}$"
                  },
                  "time": {
                    "type": "string",
                    "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$"
                  },
                  "staffName": {
                    "description": "Who the appointment is with (a staff member, specialist or consultant). Rendered as `{{appointment_doctor_name}}`.",
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 200
                  },
                  "doctorName": {
                    "description": "Medical name of `staffName` (the doctor the visit is with); same field, same `{{appointment_doctor_name}}`. Send one or the other — both are accepted only when equal.",
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 200
                  },
                  "serviceName": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 200
                  },
                  "locationName": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 200
                  },
                  "address": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 300
                  },
                  "externalId": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 128
                  }
                },
                "additionalProperties": false
              },
              "variables": {
                "description": "Extra template variables, rendered verbatim as `{{name}}`. At most 40 keys.",
                "type": "object",
                "propertyNames": {
                  "type": "string",
                  "pattern": "^[a-z][a-z0-9_]{0,63}$"
                },
                "additionalProperties": {
                  "anyOf": [
                    {
                      "type": "string",
                      "maxLength": 2000
                    },
                    {
                      "type": "number"
                    },
                    {
                      "type": "boolean"
                    }
                  ]
                }
              }
            },
            "required": [
              "reason"
            ],
            "additionalProperties": false
          }
        },
        "required": [
          "botId",
          "phoneNumber",
          "idempotencyKey",
          "context"
        ],
        "additionalProperties": false
      }
    },
    {
      "name": "list_bot_sessions",
      "requiredScope": "bot-calls.read",
      "description": "List this tenant's bot sessions — one \"session\" is one dispatch of one bot to one phone number — newest first, with pagination and optional botId / status / since / until filters. Returns the outcome-level facts (status, end reason, duration, whether a transcript, recording or telemetry exists), not the call contents. Use it to find the session an end user is complaining about, then get_bot_session_telemetry to see why it went that way. Mirrors GET /api/external/v1/bot-sessions. To COUNT or summarise sessions (per month / day / bot / result, minutes, connect rate) do NOT page through this tool: call aggregate_bot_sessions or get_bot_statistics (scope analytics.read; see get_server_info `hints` if they are missing from your tool list).",
      "descriptionPl": "Lista rozmów bota, od najnowszych, z filtrami po bocie, statusie i dacie — wraz z informacją, czy rozmowa ma transkrypcję, nagranie i dane diagnostyczne.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "botId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
          },
          "status": {
            "type": "string",
            "enum": [
              "IN_PROGRESS",
              "COMPLETED",
              "FAILED",
              "NO_ANSWER",
              "VOICEMAIL",
              "DISCONNECTED"
            ]
          },
          "since": {
            "description": "ISO 8601. Inclusive lower bound on the session start.",
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"
          },
          "until": {
            "description": "ISO 8601. Inclusive upper bound on the session start.",
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"
          },
          "page": {
            "default": 1,
            "type": "integer",
            "minimum": 1,
            "maximum": 9007199254740991
          },
          "limit": {
            "default": 50,
            "type": "integer",
            "minimum": 1,
            "maximum": 100
          }
        },
        "additionalProperties": false
      }
    },
    {
      "name": "get_bot_session",
      "requiredScope": "bot-calls.read",
      "description": "Read one bot session: the same outcome-level facts as list_bot_sessions, plus `outcome` — the bot's own structured result for the call, in the shape that bot's remarks schema defines — and `links` to its telemetry, transcript and recording (the recording link is REST only: it hands out a short-lived URL to call audio, which no MCP tool exposes). Still not the call contents: for why the call behaved the way it did call get_bot_session_telemetry, and for what was actually said call get_bot_session_transcript. Mirrors GET /api/external/v1/bot-sessions/{dispatchId}.",
      "descriptionPl": "Zwraca jedną rozmowę bota: jak się zakończyła, ile trwała i co bot z niej wywnioskował.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "dispatchId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
            "description": "The `dispatchId` from list_bot_sessions."
          }
        },
        "required": [
          "dispatchId"
        ]
      }
    },
    {
      "name": "get_bot_session_telemetry",
      "requiredScope": "bot-calls.read",
      "description": "The diagnostic record for one bot session: why the agent session closed (`closeReason`, `closeError`), why the worker shut down, any SIP-layer failure, whether the caller ever spoke, how often the bot timed out on silence, the error array (runtime error messages summarised to their type; Mediflo support sees the full text), `pipelineMetrics` (per-modality latency such as STT/LLM/TTS time-to-first-token) and `falseInterruptions` (how often the bot mistook noise for the caller). This is the tool to reach for when a call was silent, cut off, slow to answer, or interrupted the caller. CONVERSATION CONTENT IS NOT HERE: no utterances, no transcript, no tool arguments — get_bot_session_transcript is a separate call, so an integration that only needs to debug never has to receive caller speech. (`transcriptCounters` is a COUNT of recognized segments, not their text.) If what you see points at a platform problem on our side rather than a mistake in this bot's own configuration — the pipeline erroring, the call dropping, latency far outside the norm — you do not have to stop there: file it to Mediflo with create_support_ticket and pass this dispatchId in `botSessionIds`. Mediflo support will investigate the attached session. Mirrors GET /api/external/v1/bot-sessions/{dispatchId}/telemetry.",
      "descriptionPl": "Zwraca dane diagnostyczne jednej rozmowy bota — powód zakończenia, błędy, czasy odpowiedzi i fałszywe przerwania. Nie zawiera treści rozmowy.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "dispatchId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
            "description": "The `dispatchId` from list_bot_sessions."
          }
        },
        "required": [
          "dispatchId"
        ]
      }
    },
    {
      "name": "get_bot_session_transcript",
      "requiredScope": "bot-calls.read",
      "description": "The CONVERSATION for one bot session: what the caller and the bot actually said, turn by turn, with per-turn latency metrics and any tool calls the bot made. Deliberately a separate call from get_bot_session_telemetry, which carries every diagnostic without any of this — reach for that one first if you are debugging call quality, and come here only when the wording itself is the question (the bot said the wrong thing, misheard a name, took the wrong branch). This returns caller speech; handle it accordingly. `available: false` means the session simply has no transcript file, which is not an error. Mirrors GET /api/external/v1/bot-sessions/{dispatchId}/transcript.",
      "descriptionPl": "Zwraca treść jednej rozmowy bota — co powiedział dzwoniący, a co bot, wypowiedź po wypowiedzi.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "dispatchId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
            "description": "The `dispatchId` from list_bot_sessions."
          }
        },
        "required": [
          "dispatchId"
        ]
      }
    },
    {
      "name": "create_support_ticket",
      "requiredScope": "tickets.write",
      "description": "File a support ticket WITH MEDIFLO — a problem you want us to investigate, not a to-do inside the customer's own queue (that is create_task). Reach for this when a bot misbehaved for a reason that is not the bot's own configuration: the pipeline errored, calls drop, telemetry looks wrong, something on the platform is broken. ATTACH THE AFFECTED SESSION(S). Pass their dispatchIds in `botSessionIds` — an attached session is what makes a report actionable, because support can open the exact call instead of asking \"which one?\", and a report with none is markedly slower to resolve. Attach SEVERAL when the problem keeps happening: the pattern across calls is usually the diagnosis, and later occurrences go on the same ticket with attach_sessions_to_ticket rather than into a second one. Ids are validated against this tenant; an unknown one is rejected, never silently dropped. If you know WHICH PERSON is reporting, pass their user id as `reportedByUserId` (resolve one from the users list, GET /api/external/v1/users) so support can reply to a human. Omit it and the ticket is credited to the person behind this connection — the signed-in user, or whoever created the API key — and is still marked as filed by an agent. Track it afterwards with get_support_ticket, which is how you tell the person who complained that Mediflo is looking at it. Mirrors POST /api/external/v1/tickets.",
      "descriptionPl": "Zgłasza problem do Mediflo wraz z rozmowami, których dotyczy, żeby wsparcie mogło od razu je sprawdzić.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "title": {
            "type": "string",
            "minLength": 1,
            "maxLength": 500,
            "description": "Becomes the ticket subject."
          },
          "description": {
            "type": "string",
            "minLength": 1,
            "maxLength": 10000
          },
          "priority": {
            "default": "MEDIUM",
            "type": "string",
            "enum": [
              "LOW",
              "MEDIUM",
              "HIGH",
              "URGENT"
            ]
          },
          "reportedByUserId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
          },
          "botSessionIds": {
            "description": "Bot sessions that ARE the problem. Max 20. Each id is validated against your tenant; an unknown id is a 422.",
            "maxItems": 20,
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid",
              "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
            }
          },
          "contextUrl": {
            "type": "string",
            "maxLength": 2000
          },
          "contextMetadata": {
            "type": "object",
            "propertyNames": {
              "type": "string"
            },
            "additionalProperties": {}
          }
        },
        "required": [
          "title",
          "description"
        ],
        "additionalProperties": false
      }
    },
    {
      "name": "attach_sessions_to_ticket",
      "requiredScope": "tickets.write",
      "description": "Attach more bot sessions to a ticket you already filed — the \"it keeps happening\" case. Put the recurrence on the existing ticket rather than filing a second one: support is already looking at this thread, and the pattern across sessions is usually what identifies the cause. Re-attaching a session that is already on the ticket is a no-op, not an error, so a retry is safe. Ids are validated against this tenant. Mirrors POST /api/external/v1/tickets/{ticketId}/sessions.",
      "descriptionPl": "Dodaje kolejne rozmowy do zgłoszenia przekazanego już do Mediflo, gdy problem się powtarza.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "botSessionIds": {
            "minItems": 1,
            "maxItems": 20,
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid",
              "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
            }
          },
          "ticketId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
            "description": "The ticket `id` returned by create_support_ticket."
          }
        },
        "required": [
          "botSessionIds",
          "ticketId"
        ],
        "additionalProperties": false
      }
    },
    {
      "name": "list_support_tickets",
      "requiredScope": "tickets.read",
      "description": "List the support tickets this tenant has filed with Mediflo, newest first, with an optional status filter and pagination. Each ticket carries `linkedSessions` (the bot sessions attached to it) and `reportedVia`, which says whether a person filed it in the Portal or an agent filed it headlessly. Reads are tenant-wide, not per-reporter — an agent may file on anyone's behalf. Check here before filing a new ticket for a problem that may already be open. Mirrors GET /api/external/v1/tickets.",
      "descriptionPl": "Lista zgłoszeń przekazanych do Mediflo wraz ze statusem i rozmowami dołączonymi do każdego z nich.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "OPEN",
              "ACKNOWLEDGED",
              "IN_PROGRESS",
              "RESOLVED",
              "CLOSED"
            ]
          },
          "page": {
            "default": 1,
            "type": "integer",
            "minimum": 1,
            "maximum": 9007199254740991
          },
          "limit": {
            "default": 50,
            "type": "integer",
            "minimum": 1,
            "maximum": 100
          }
        },
        "additionalProperties": false
      }
    },
    {
      "name": "get_support_ticket",
      "requiredScope": "tickets.read",
      "description": "Read one support ticket: its `status` (OPEN, ACKNOWLEDGED, IN_PROGRESS, RESOLVED, CLOSED), `awaitingResponse` (whether the ball is with Mediflo support or with the customer), the sessions attached to it and when it was resolved. This is how an agent answers \"is anyone looking at this?\" for the person who reported the problem. Mirrors GET /api/external/v1/tickets/{ticketId}.",
      "descriptionPl": "Zwraca jedno zgłoszenie przekazane do Mediflo: jego status i to, czy jest po stronie wsparcia, czy czeka na odpowiedź organizacji.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "ticketId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
            "description": "The ticket `id` returned by create_support_ticket."
          }
        },
        "required": [
          "ticketId"
        ]
      }
    },
    {
      "name": "get_webhook_event_catalog",
      "requiredScope": null,
      "description": "READ THIS BEFORE WRITING A WEBHOOK RECEIVER. Returns everything needed to implement one correctly, and it is the only place that information lives: the full event vocabulary with a COMPLETE SAMPLE ENVELOPE per event type (so you can code against the exact wire shape before a real event exists), the optional content blocks each event family can carry and WHICH OF THEM ARE PHI, the signature scheme with copy-ready verification code in TypeScript and PHP, how the signing secret behaves, and the delivery contract (at-least-once, no ordering, the response deadline, the retry ladder, auto-disable, and why you must parse leniently). The verification snippets are the same ones our own suite runs against the signer that signs real deliveries — paste them rather than writing an HMAC check from the description. Available to any authenticated key: it is a contract description and no tenant data is in it; event families of areas your organisation has not enabled are left out. Mirrors GET /api/external/v1/meta/event-types, plus the receiver guidance the Portal shows human integrators.",
      "descriptionPl": "Zwraca katalog zdarzeń webhookowych z przykładowymi payloadami, opisem podpisu i zasadami dostarczania — wszystkim, czego potrzeba, aby napisać odbiornik.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {}
      }
    },
    {
      "name": "list_webhook_endpoints",
      "requiredScope": "webhooks.manage",
      "description": "List this tenant’s webhook endpoints, newest first, with their health: `status` (ENABLED, DISABLED, AUTO_DISABLED), `consecutiveFailures`, `lastSuccessAt`, `lastFailureAt`, `autoDisabledAt`, and `previousSecretExpiresAt` (non-null while a rotation grace window is open). Start here when deliveries stopped arriving — an AUTO_DISABLED endpoint receives nothing until update_webhook_endpoint re-enables it. Signing secrets are never returned by this or any other read. Mirrors GET /api/external/v1/webhook-endpoints.",
      "descriptionPl": "Lista adresów webhookowych organizacji wraz z ich kondycją: liczbą kolejnych błędów i datą ostatniego udanego dostarczenia.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "page": {
            "default": 1,
            "type": "integer",
            "minimum": 1,
            "maximum": 9007199254740991
          },
          "limit": {
            "default": 25,
            "type": "integer",
            "minimum": 1,
            "maximum": 100
          }
        }
      }
    },
    {
      "name": "get_webhook_endpoint",
      "requiredScope": "webhooks.manage",
      "description": "Read one webhook endpoint: its URL, subscriptions, `payloadConfig`, and the same health fields list_webhook_endpoints returns. Use it to confirm a change landed, or to check whether a rotation grace window is still open (`previousSecretExpiresAt`). The signing secret is NOT here and cannot be read back — see get_webhook_event_catalog. Mirrors GET /api/external/v1/webhook-endpoints/{endpointId}.",
      "descriptionPl": "Zwraca jeden adres webhookowy: subskrypcje zdarzeń, konfigurację payloadu i stan dostarczania.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "endpointId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
            "description": "The endpoint `id` returned by create_webhook_endpoint."
          }
        },
        "required": [
          "endpointId"
        ]
      }
    },
    {
      "name": "create_webhook_endpoint",
      "requiredScope": "webhooks.manage",
      "description": "Register an https receiver and subscribe it to events. ⚠️ THE RESPONSE CARRIES THE SIGNING SECRET, AND THIS IS THE ONLY TIME IT IS EVER SHOWN. It is stored encrypted and no call reads it back — give it to the person you are building this for, or write it into the receiver’s secret store, in this same turn. Losing it means rotate_webhook_secret and a redeploy. Do not print it into anything that gets logged. `subscribedEvents` takes catalogued event types or `[\"*\"]` for all of them; `payloadConfig` opts into optional content blocks per event family ({\"bot_call\": [\"outcome\"]}) and DEFAULTS TO CORE FIELDS ONLY — a block you do not name is never sent, which is how PHI stays out of a receiver that does not need it. Call get_webhook_event_catalog first for the valid type and block names, the sample envelopes, and the verification code. The URL must be https on a publicly resolvable host (localhost and private addresses are refused, so a local receiver needs a tunnel), and it is vetted at save time, not just at delivery. Mirrors POST /api/external/v1/webhook-endpoints.",
      "descriptionPl": "Rejestruje nowy adres webhookowy i zwraca klucz podpisujący — jedyny raz, kiedy jest on pokazywany.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "url": {
            "type": "string",
            "minLength": 1,
            "maxLength": 2048
          },
          "description": {
            "anyOf": [
              {
                "type": "string",
                "maxLength": 500
              },
              {
                "type": "null"
              }
            ]
          },
          "subscribedEvents": {
            "minItems": 1,
            "type": "array",
            "items": {
              "anyOf": [
                {
                  "type": "string",
                  "const": "*"
                },
                {
                  "type": "string",
                  "enum": [
                    "ping",
                    "bot_call.completed",
                    "call.completed",
                    "call.updated",
                    "call.transcript.ready",
                    "call.analysis.completed",
                    "task.created",
                    "task.updated",
                    "task.deleted",
                    "task.suggestion.resolved",
                    "case.created",
                    "case.updated",
                    "case.deleted",
                    "webhook_endpoint.auto_disabled"
                  ]
                }
              ]
            }
          },
          "payloadConfig": {
            "anyOf": [
              {
                "type": "object",
                "propertyNames": {
                  "type": "string",
                  "enum": [
                    "call",
                    "bot_call"
                  ]
                },
                "additionalProperties": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                }
              },
              {
                "type": "null"
              }
            ]
          }
        },
        "required": [
          "url",
          "subscribedEvents"
        ],
        "additionalProperties": false
      }
    },
    {
      "name": "send_webhook_test_event",
      "requiredScope": "webhooks.manage",
      "description": "Fire a REAL delivery at one of your endpoints — this is how an integration gets tested without waiting for a real call. It is not a simulation: the event goes through the production pipeline, so it is signed with the endpoint’s live secret, vetted, POSTed, and retried on the normal ladder if your server fails it. Omit `eventType` for a `ping` (smallest possible body, ideal for proving connectivity and the signature check). Pass an `eventType` from get_webhook_event_catalog to send that type’s SAMPLE PAYLOAD instead, so you can exercise a real parser — the values are obviously synthetic (zero-padded uuids, \"Jan Testowy\") and contain no personal data. The request carries `Mediflo-Event-Test`, which is ABSENT on real events, so a receiver can drop test traffic before its production processing — verify that branch here too. Returns 202-style immediately with a `deliveryId`: delivery is asynchronous, so READ THE RESULT with get_webhook_delivery a moment later rather than assuming success from this call. The endpoint must be ENABLED; a DISABLED or AUTO_DISABLED one is refused. Mirrors POST /api/external/v1/webhook-endpoints/{endpointId}/test.",
      "descriptionPl": "Wysyła prawdziwe, podpisane zdarzenie testowe na wskazany adres, aby sprawdzić działanie odbiornika.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "eventType": {
            "type": "string",
            "enum": [
              "ping",
              "bot_call.completed",
              "call.completed",
              "call.updated",
              "call.transcript.ready",
              "call.analysis.completed",
              "task.created",
              "task.updated",
              "task.deleted",
              "task.suggestion.resolved",
              "case.created",
              "case.updated",
              "case.deleted",
              "webhook_endpoint.auto_disabled"
            ]
          },
          "endpointId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
            "description": "The endpoint `id` returned by create_webhook_endpoint."
          }
        },
        "required": [
          "endpointId"
        ],
        "additionalProperties": false
      }
    },
    {
      "name": "list_webhook_deliveries",
      "requiredScope": "webhooks.manage",
      "description": "The delivery log, newest first — what we sent, when, and how it went. Filter by `endpointId`, by `status` (PENDING, RETRYING, SUCCEEDED, EXHAUSTED, SKIPPED) and by `eventType`. This is the first place to look when a customer says \"we are not receiving webhooks\": a page of EXHAUSTED rows means we tried and their server refused, no rows at all means nothing matched their subscription (or the endpoint is disabled), and RETRYING means it is still in flight. Each row carries the event identity (`event.id`, the `evt_…` value the envelope was deduplicated on, plus type and `occurredAt`) but NOT the payload we sent. Mirrors GET /api/external/v1/webhook-deliveries.",
      "descriptionPl": "Dziennik dostarczeń: co wysłaliśmy, kiedy i z jakim skutkiem, z filtrami po adresie, statusie i typie zdarzenia.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "page": {
            "default": 1,
            "type": "integer",
            "minimum": 1,
            "maximum": 9007199254740991
          },
          "limit": {
            "default": 25,
            "type": "integer",
            "minimum": 1,
            "maximum": 100
          },
          "endpointId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
          },
          "status": {
            "type": "string",
            "enum": [
              "PENDING",
              "RETRYING",
              "SUCCEEDED",
              "EXHAUSTED",
              "SKIPPED"
            ]
          },
          "eventType": {
            "type": "string",
            "enum": [
              "ping",
              "bot_call.completed",
              "call.completed",
              "call.updated",
              "call.transcript.ready",
              "call.analysis.completed",
              "task.created",
              "task.updated",
              "task.deleted",
              "task.suggestion.resolved",
              "case.created",
              "case.updated",
              "case.deleted",
              "webhook_endpoint.auto_disabled"
            ]
          }
        }
      }
    },
    {
      "name": "get_webhook_delivery",
      "requiredScope": "webhooks.manage",
      "description": "Read one delivery — the other half of send_webhook_test_event, and the call that tells you whether the receiver you just wrote actually works. ⚠️ WHAT YOU GET, AND WHAT YOU DO NOT. Available: `status`, `attemptCount`, `lastHttpStatus` (the status code your server returned), `lastErrorCode` (a stable classification — `timeout`, `redirect`, `ssrf_blocked`, `http_error`), `lastErrorMessage` (OUR short diagnostic), `lastLatencyMs`, `deliveredAt`, `nextAttemptAt`, and the event identity. NOT available, at all: your endpoint’s RESPONSE BODY — it is never retained, so do not look for it — and the raw bytes we transmitted. For the CONTENT that delivery carried, call get_webhook_delivered_payload (separate scope). Debug from the status code and your own server logs; correlate them using `event.id`, which is the `evt_…` value that arrived in `Mediflo-Event-Id`. If `lastHttpStatus` is 401 or 403 your signature check is rejecting us — re-read the verification snippet in get_webhook_event_catalog, and check you are hashing the RAW BYTES rather than a re-serialized object. Mirrors the delivery rows returned by GET /api/external/v1/webhook-deliveries, narrowed to one id.",
      "descriptionPl": "Zwraca jedno dostarczenie: status, liczbę prób, kod odpowiedzi serwera odbiorcy i naszą diagnostykę błędu.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "deliveryId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
            "description": "The delivery `id` — send_webhook_test_event returns one as `deliveryId`."
          }
        },
        "required": [
          "deliveryId"
        ]
      }
    },
    {
      "name": "get_webhook_delivered_payload",
      "requiredScope": "webhook-payloads.read",
      "description": "The CONTENT one delivery carried — use it when a receiver logged an event it could not parse, or when you need to see which fields an endpoint is actually being sent. ⚠️ READ THESE THREE THINGS BEFORE RELYING ON IT. (1) It is the PER-ENDPOINT PROJECTION, not a raw archive: we never store the bytes we transmitted, so this is rebuilt by re-running the delivery pipeline’s own projection over the stored event using THIS delivery’s endpoint `payloadConfig`. It is the same function that composes real deliveries, so it shows what that endpoint gets — and never the superset we hold internally. A block the endpoint did not opt into is absent here exactly as it was absent on the wire. (2) `payloadConfig` is read as it is NOW. If the endpoint’s block opt-ins were changed after this delivery went out, you are seeing what that endpoint would receive for this event TODAY, not a historical record — the response returns `payloadConfig` alongside `envelope` so you can tell. (3) It DISAPPEARS. Events and their deliveries are pruned after the retention window, so an old delivery answers \"not found\", and a delivery whose event was already pruned answers that its payload is gone. Do not build a datastore on this call — subscribe to the events instead. Byte-level debugging of a signature failure is NOT what this is for: the `envelope` here is a re-serialization, so its key order and spacing are not necessarily what was hashed. Verify signatures against the raw body your own server received. Requires the `webhook-payloads.read` scope, which is DELIBERATELY separate from `webhooks.manage` and is not granted by any wildcard, because for an endpoint subscribed to the `outcome` or `transcript` blocks this content includes personal data, including sensitive data. If you get a scope error, the key must be re-issued with that scope named explicitly. Mirrors GET /api/external/v1/webhook-deliveries/{deliveryId}/payload.",
      "descriptionPl": "Zwraca treść, którą przeniosło jedno dostarczenie, odtworzoną jako projekcja przypisana temu punktowi końcowemu — nigdy jako pełny zbiór wewnętrzny. Wymaga osobnego uprawnienia webhook-payloads.read, ponieważ treść może zawierać dane osobowe, w tym dane wrażliwe.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "deliveryId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
            "description": "The delivery `id` — send_webhook_test_event returns one as `deliveryId`."
          }
        },
        "required": [
          "deliveryId"
        ]
      }
    },
    {
      "name": "redeliver_webhook",
      "requiredScope": "webhooks.manage",
      "description": "Replay a delivery that already ran — the \"our receiver was broken, please send it again\" case, and the fastest way to re-test a fix without waiting for a new event. Only SUCCEEDED, EXHAUSTED, SKIPPED and RETRYING rows may be replayed; a PENDING one is still on its way and is refused. `attemptCount` is deliberately preserved rather than reset, so the delivery keeps its real history and a replay does not hand the endpoint a fresh ladder every time. The replay is QUEUED, not immediate — it goes out within about 90 seconds, so poll get_webhook_delivery rather than reading the result straight away. Remember the receiver must be idempotent: a redelivery is a second arrival of an event it may already have processed. Mirrors POST /api/external/v1/webhook-deliveries/{deliveryId}/redeliver.",
      "descriptionPl": "Ponawia wcześniejsze dostarczenie — po naprawie odbiornika, bez czekania na nowe zdarzenie.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "deliveryId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
            "description": "The delivery `id` — send_webhook_test_event returns one as `deliveryId`."
          }
        },
        "required": [
          "deliveryId"
        ]
      }
    },
    {
      "name": "update_webhook_endpoint",
      "requiredScope": "webhooks.manage",
      "description": "Change an endpoint’s URL, description, `subscribedEvents`, `payloadConfig`, or `status`. Sending `status: \"ENABLED\"` is also how you recover an AUTO_DISABLED endpoint: it clears the failure streak as well, so a freshly fixed receiver is not one failure away from being disabled again by a counter describing the old outage. You cannot set AUTO_DISABLED — that is ours. Fields you omit are left alone, but `subscribedEvents` and `payloadConfig` are REPLACED wholesale when present, never merged, so send the complete list you want. Changing the URL does not rotate the secret. Mirrors PATCH /api/external/v1/webhook-endpoints/{endpointId}.",
      "descriptionPl": "Zmienia adres, subskrypcje lub status punktu webhookowego; ustawienie statusu ENABLED przywraca punkt wyłączony automatycznie.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "url": {
            "type": "string",
            "minLength": 1,
            "maxLength": 2048
          },
          "description": {
            "anyOf": [
              {
                "type": "string",
                "maxLength": 500
              },
              {
                "type": "null"
              }
            ]
          },
          "subscribedEvents": {
            "minItems": 1,
            "type": "array",
            "items": {
              "anyOf": [
                {
                  "type": "string",
                  "const": "*"
                },
                {
                  "type": "string",
                  "enum": [
                    "ping",
                    "bot_call.completed",
                    "call.completed",
                    "call.updated",
                    "call.transcript.ready",
                    "call.analysis.completed",
                    "task.created",
                    "task.updated",
                    "task.deleted",
                    "task.suggestion.resolved",
                    "case.created",
                    "case.updated",
                    "case.deleted",
                    "webhook_endpoint.auto_disabled"
                  ]
                }
              ]
            }
          },
          "payloadConfig": {
            "anyOf": [
              {
                "type": "object",
                "propertyNames": {
                  "type": "string",
                  "enum": [
                    "call",
                    "bot_call"
                  ]
                },
                "additionalProperties": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                }
              },
              {
                "type": "null"
              }
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "ENABLED",
              "DISABLED"
            ]
          },
          "endpointId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
            "description": "The endpoint `id` returned by create_webhook_endpoint."
          }
        },
        "required": [
          "endpointId"
        ],
        "additionalProperties": false
      }
    },
    {
      "name": "rotate_webhook_secret",
      "requiredScope": "webhooks.manage",
      "description": "Mint a new signing secret for an endpoint. ⚠️ THE NEW SECRET IS RETURNED ONCE AND NEVER AGAIN — store it in the same turn. The OUTGOING secret keeps signing for a 24-hour grace window, during which `Mediflo-Signature` carries TWO `v1=` entries and either one verifying is a valid request. That is what lets you redeploy without dropping deliveries — but it only works if the receiver tries EVERY `v1=` against BOTH secrets; a verifier that reads only the first entry will reject roughly half the traffic mid-rotation. The snippet in get_webhook_event_catalog already handles this. `previousSecretExpiresAt` on the endpoint tells you when the old secret stops working. Mirrors POST /api/external/v1/webhook-endpoints/{endpointId}/rotate-secret.",
      "descriptionPl": "Generuje nowy klucz podpisujący; poprzedni działa jeszcze przez 24 godziny, więc odbiornik można wdrożyć bez przerwy w dostarczaniu.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "endpointId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
            "description": "The endpoint `id` returned by create_webhook_endpoint."
          }
        },
        "required": [
          "endpointId"
        ]
      }
    },
    {
      "name": "delete_webhook_endpoint",
      "requiredScope": "webhooks.manage",
      "description": "Stop delivering to an endpoint and remove it from the list. This is a SOFT delete: the endpoint is disabled and hidden, and its delivery log is deliberately kept so past deliveries stay auditable. It cannot be undone through this API — if you only want to pause traffic, use update_webhook_endpoint with `status: \"DISABLED\"` instead, which is reversible. Mirrors DELETE /api/external/v1/webhook-endpoints/{endpointId}.",
      "descriptionPl": "Usuwa adres webhookowy (usunięcie miękkie) — dostarczanie ustaje, a dziennik dostarczeń pozostaje do wglądu.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "endpointId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
            "description": "The endpoint `id` returned by create_webhook_endpoint."
          }
        },
        "required": [
          "endpointId"
        ]
      }
    },
    {
      "name": "list_trunk_providers",
      "requiredScope": "trunks.read",
      "description": "List the SIP carrier presets this organization can connect a trunk to, plus `connection` — the facts to give the carrier. Per provider: `key` (pass it as trunkProviderKey to create_carrier_trunk; GENERIC or null = a manual SIP trunk), display names, `supportsInbound` / `supportsOutbound`, `registrationRequired` (Mediflo REGISTERs to the carrier with the outbound credentials, and the carrier then delivers inbound calls over that registration) and `defaultTransport` when the preset fixes one. `connection.inbound` is where carriers send calls: forward each number to the given host on port 5060 over TCP, with no digest authentication — Mediflo authenticates the carrier by the source IP of its SIP signalling, so those IPs go into inbound.carrierSourceIps. `connection.outbound.signalingSourceIp` is null: Mediflo does not publish its outbound signalling IP here — the user should ask Mediflo support if the carrier needs to allow-list it.",
      "descriptionPl": "Lista operatorów SIP, z którymi można połączyć trunk, oraz informacje dla operatora (dokąd przekierować numery).",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {}
      }
    },
    {
      "name": "list_trunks",
      "requiredScope": "trunks.read",
      "description": "List the organization's SIP carrier trunks. Per trunk: id, name, description, providerKey (the preset; null = manual), isActive, timestamps and `portalUrl`; per direction (`inbound`, `outbound` — `{ enabled: false }` when not configured): isActive, transport, media and header settings, capacity, inbound `carrierSourceIps`, outbound `address` / `authUsername` / `hasAuthPassword` / `authRealm` / `requiresRegistration`, a `registration` summary (status and state code of the registration Mediflo holds with the carrier) and `updatedAt` (send it back as expectedUpdatedAt to update_carrier_trunk_direction). `phoneNumbers` lists each number with its ownership and its per-direction state, including the inbound IVR flow. `revision` is a token of the trunk's numbers and directions: send it back as expectedRevision to set_trunk_phone_numbers, and to update_carrier_trunk_direction when it sends phoneNumbers. Passwords are never returned. `legacySingleDirection: true` marks an old single-direction trunk; `managedInPortal: true` marks a peer-registration trunk that only the Portal changes. get_trunk adds diagnostics.",
      "descriptionPl": "Lista trunków SIP organizacji wraz z kierunkami i numerami telefonów. Hasła nigdy nie są pokazywane.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {}
      }
    },
    {
      "name": "get_trunk",
      "requiredScope": "trunks.read",
      "description": "Read one trunk — the list_trunks projection — plus `diagnostics`: per direction, `registration` (the SIP registration Mediflo keeps with the carrier: status, a state code, when it was observed and last succeeded — codes, never free text), `gatewaySync` (whether this configuration has reached Mediflo's SIP gateway: synced, pending — normally within minutes — or failed with an errorCode) and `readiness` (the Portal's readiness checklist with the same item codes); `recentCalls` for this trunk's numbers over the last 24 hours and 7 days (inbound and outbound totals, answered vs not, and the most frequent disconnect reasons — there are no per-call SIP response codes); and `hints`, short next steps (a failing gateway sync is filed with create_support_ticket). An unknown id, or another organization's, is not found.",
      "descriptionPl": "Pokazuje jeden trunk SIP z diagnostyką: rejestracją u operatora, synchronizacją z bramką, listą gotowości i liczbą ostatnich połączeń.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "trunkId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
            "description": "The trunk id from list_trunks."
          }
        },
        "required": [
          "trunkId"
        ]
      }
    },
    {
      "name": "create_carrier_trunk",
      "requiredScope": "trunks.write",
      "description": "Create a SIP carrier trunk for this organization — one trunk with an inbound and/or an outbound direction and its phone numbers — through the same service and validation as the Portal's trunk form. Unlike the Portal, BOTH `inbound.enabled` and `outbound.enabled` are required: decide each direction explicitly (an inbound-only carrier is outbound: { enabled: false }). Every number has an `ownership`: INBOUND (calls arrive on it), OUTBOUND (caller ID for calls out) or BOTH; each must belong to an enabled direction, and each enabled direction needs at least one. Give inbound numbers an IVR flow (inbound.activeIvrFlowId = an IVR flow family id, or inbound.createDefaultIvrFlow: true) — without one their calls reach no queue or bot. An inbound number's activeIvrFlowId must be the FAMILY id of an IVR flow of this organization that has an ACTIVE version — a number routed to a family without one drops every call, so it is refused; activate a version first with activate_ivr_flow_version, or pass createDefaultIvrFlow: true instead. A number keeping the flow it already has is not re-checked. `trunkProviderKey` is a preset from list_trunk_providers (null = manual SIP trunk); a preset can supply the carrier's address, realm and source IPs. Inbound: list the carrier's signalling IPs and tell the carrier to forward the numbers as list_trunk_providers `connection` describes; inbound always starts on TCP. Outbound: `address` is the carrier's SIP proxy (required unless the preset supplies one); authUsername / authPassword / authRealm when the carrier authenticates our calls; requiresRegistration when Mediflo must REGISTER to the carrier. carrierSourceIps: the carrier's SIP signalling IPs — each entry a single public IPv4/IPv6 address, or a public range no wider than /24 (IPv4) or /64 (IPv6) written as its network address (x.y.z.0/24, never x.y.z.7/24). Private, loopback, link-local, CGNAT, multicast and reserved addresses are refused: these sources are trusted by Mediflo's SIP edge and exempted from its flood protection. authPassword is WRITE-ONLY: it is stored for the carrier and no tool ever returns it (reads show only hasAuthPassword), and it is kept out of logs and the audit trail. If the user prefers that the password not pass through this conversation, omit it and give them `portalUrl` from the answer, where they type it into the Portal themselves. A number cannot take inbound calls here (ownership INBOUND or BOTH) while it is on a trunk of another organization, active or not: it is refused as not available, and the answer never says who holds it. Numbers of the Mediflo number pool, or managed by Mediflo, cannot be added at all. If the user says a refused number is theirs, they should contact Mediflo support. Peer registration is not available here (Portal only). The trunk is created and then pushed to Mediflo's SIP gateway in the background, so the answer's diagnostics.gatewaySync is normally `pending` — re-check with get_trunk. Returns the trunk as get_trunk shows it (recentCalls null) with its `portalUrl`.",
      "descriptionPl": "Tworzy trunk SIP wraz z numerami telefonów, z tymi samymi kontrolami co formularz w Portalu. Tylko dla administratorów.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1
          },
          "description": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "trunkProviderKey": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "phoneNumbers": {
            "minItems": 1,
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "e164": {
                  "type": "string",
                  "minLength": 1,
                  "description": "The number in E.164, e.g. +48123456789."
                },
                "ownership": {
                  "type": "string",
                  "enum": [
                    "BOTH",
                    "INBOUND",
                    "OUTBOUND"
                  ],
                  "description": "INBOUND (calls arrive on it), OUTBOUND (caller ID for calls out) or BOTH."
                },
                "inbound": {
                  "description": "Inbound side: activeIvrFlowId = the IVR flow family id the number routes into (null = none; the family must have an active version), or createDefaultIvrFlow: true; isActive.",
                  "type": "object",
                  "properties": {
                    "isActive": {
                      "type": "boolean"
                    },
                    "activeIvrFlowId": {
                      "anyOf": [
                        {
                          "type": "string",
                          "format": "uuid",
                          "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
                        },
                        {
                          "type": "null"
                        }
                      ]
                    },
                    "createDefaultIvrFlow": {
                      "type": "boolean"
                    }
                  },
                  "additionalProperties": false
                },
                "outbound": {
                  "description": "Outbound side: isDefaultOutbound (at most one — the organization's default caller ID); isActive.",
                  "type": "object",
                  "properties": {
                    "isActive": {
                      "type": "boolean"
                    },
                    "isDefaultOutbound": {
                      "type": "boolean"
                    }
                  },
                  "additionalProperties": false
                }
              },
              "required": [
                "e164",
                "ownership"
              ],
              "additionalProperties": false
            }
          },
          "inbound": {
            "type": "object",
            "properties": {
              "enabled": {
                "type": "boolean",
                "description": "Required: whether this trunk receives calls from the carrier."
              },
              "carrierSourceIps": {
                "description": "carrierSourceIps: the carrier's SIP signalling IPs — each entry a single public IPv4/IPv6 address, or a public range no wider than /24 (IPv4) or /64 (IPv6) written as its network address (x.y.z.0/24, never x.y.z.7/24). Private, loopback, link-local, CGNAT, multicast and reserved addresses are refused: these sources are trusted by Mediflo's SIP edge and exempted from its flood protection.",
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "mediaEncryption": {
                "type": "string"
              },
              "includeHeaders": {
                "type": "string"
              },
              "noiseReduction": {
                "description": "Noise reduction on the caller's audio for inbound calls.",
                "type": "boolean"
              },
              "maxConcurrentCalls": {
                "type": "integer",
                "exclusiveMinimum": 0,
                "maximum": 9007199254740991
              }
            },
            "required": [
              "enabled"
            ],
            "additionalProperties": false
          },
          "outbound": {
            "type": "object",
            "properties": {
              "enabled": {
                "type": "boolean",
                "description": "Required: whether this trunk places calls via the carrier."
              },
              "address": {
                "description": "The carrier's SIP proxy (host[:port]). Required unless the provider preset supplies one.",
                "type": "string"
              },
              "transport": {
                "description": "UDP, TCP, TLS or AUTO. Omitted: the provider preset's transport (list_trunk_providers defaultTransport), else TCP.",
                "type": "string"
              },
              "authUsername": {
                "type": "string"
              },
              "authPassword": {
                "description": "authPassword is WRITE-ONLY: it is stored for the carrier and no tool ever returns it (reads show only hasAuthPassword), and it is kept out of logs and the audit trail. If the user prefers that the password not pass through this conversation, omit it and give them `portalUrl` from the answer, where they type it into the Portal themselves.",
                "type": "string"
              },
              "authRealm": {
                "type": "string"
              },
              "requiresRegistration": {
                "description": "True when Mediflo must REGISTER to the carrier with authUsername/authPassword.",
                "type": "boolean"
              },
              "mediaEncryption": {
                "type": "string"
              },
              "includeHeaders": {
                "type": "string"
              },
              "maxConcurrentCalls": {
                "type": "integer",
                "exclusiveMinimum": 0,
                "maximum": 9007199254740991
              },
              "maxCallsPerSecond": {
                "type": "integer",
                "exclusiveMinimum": 0,
                "maximum": 9007199254740991
              },
              "cpsEnabled": {
                "type": "boolean"
              }
            },
            "required": [
              "enabled"
            ],
            "additionalProperties": false
          }
        },
        "required": [
          "name",
          "phoneNumbers",
          "inbound",
          "outbound"
        ]
      }
    },
    {
      "name": "update_carrier_trunk",
      "requiredScope": "trunks.write",
      "description": "Edit trunk-level fields — exactly what the Portal's trunk edit accepts: `name`, `description`, `trunkProviderKey` (a preset from list_trunk_providers; null = manual) and `isActive`. Only the fields you send change. `isActive: false` switches BOTH directions off and every call on the trunk stops — confirm that with the user first. Direction settings (address, credentials, source IPs, transport…) are edited with update_carrier_trunk_direction, numbers with set_trunk_phone_numbers. Refuses a legacy single-direction trunk (legacySingleDirection: true) — edit its direction instead. Peer-registration trunks (managedInPortal: true — the organization's PBX registers to Mediflo) are managed in the Portal; this tool refuses them. Returns the updated trunk (recentCalls null).",
      "descriptionPl": "Zmienia nazwę, opis, operatora lub stan włączenia trunku. Tylko dla administratorów.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1
          },
          "description": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "trunkProviderKey": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "isActive": {
            "type": "boolean"
          },
          "trunkId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
            "description": "The trunk id from list_trunks."
          }
        },
        "required": [
          "trunkId"
        ]
      }
    },
    {
      "name": "update_carrier_trunk_direction",
      "requiredScope": "trunks.write",
      "description": "Edit ONE direction of a trunk — the same fields and service as the Portal's direction panel: `name`, `description`, `isActive`, `trunkProviderKey` (a preset override for this direction; null = use the trunk's), `transport` (UDP, TCP, TLS or AUTO), `mediaEncryption` (disabled, enabled, required), `includeHeaders` (none, x, all), `maxConcurrentCalls`; inbound: `carrierSourceIps`, `noiseReduction`; outbound: `address`, `authUsername`, `authPassword`, `authRealm`, `requiresRegistration`, `maxCallsPerSecond`, `cpsEnabled`. Only the fields you send change. `expectedUpdatedAt` is REQUIRED: pass the direction's updatedAt from get_trunk or list_trunks; if the direction changed since, the edit is refused — re-read it and retry. Pass null only to CREATE a direction the trunk does not have yet (it then needs `phoneNumbers`). `phoneNumbers`, when sent, REPLACES this direction's whole number list (numbers left out are removed) — omit it to keep the numbers, and prefer set_trunk_phone_numbers for number changes. With phoneNumbers, expectedRevision is REQUIRED as well: `expectedRevision` is the trunk's `revision` from get_trunk or list_trunks: if any number or direction of the trunk changed since, the write is refused — re-read the trunk with get_trunk and send the list again. An inbound number's activeIvrFlowId must be the FAMILY id of an IVR flow of this organization that has an ACTIVE version — a number routed to a family without one drops every call, so it is refused; activate a version first with activate_ivr_flow_version, or pass createDefaultIvrFlow: true instead. A number keeping the flow it already has is not re-checked. carrierSourceIps: the carrier's SIP signalling IPs — each entry a single public IPv4/IPv6 address, or a public range no wider than /24 (IPv4) or /64 (IPv6) written as its network address (x.y.z.0/24, never x.y.z.7/24). Private, loopback, link-local, CGNAT, multicast and reserved addresses are refused: these sources are trusted by Mediflo's SIP edge and exempted from its flood protection. authPassword is WRITE-ONLY: it is stored for the carrier and no tool ever returns it (reads show only hasAuthPassword), and it is kept out of logs and the audit trail. If the user prefers that the password not pass through this conversation, omit it and give them `portalUrl` from the answer, where they type it into the Portal themselves. Omitting authPassword keeps the stored one. A number cannot take inbound calls here (ownership INBOUND or BOTH) while it is on a trunk of another organization, active or not: it is refused as not available, and the answer never says who holds it. Numbers of the Mediflo number pool, or managed by Mediflo, cannot be added at all. If the user says a refused number is theirs, they should contact Mediflo support. Peer-registration settings are not available here. Peer-registration trunks (managedInPortal: true — the organization's PBX registers to Mediflo) are managed in the Portal; this tool refuses them. Returns the updated trunk (recentCalls null).",
      "descriptionPl": "Zmienia jeden kierunek trunku — adres, dane logowania, adresy IP operatora, transport — z ochroną przed nadpisaniem cudzej zmiany. Tylko dla administratorów.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1
          },
          "description": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "isActive": {
            "type": "boolean"
          },
          "trunkProviderKey": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "address": {
            "type": "string"
          },
          "transport": {
            "type": "string"
          },
          "carrierSourceIps": {
            "description": "carrierSourceIps: the carrier's SIP signalling IPs — each entry a single public IPv4/IPv6 address, or a public range no wider than /24 (IPv4) or /64 (IPv6) written as its network address (x.y.z.0/24, never x.y.z.7/24). Private, loopback, link-local, CGNAT, multicast and reserved addresses are refused: these sources are trusted by Mediflo's SIP edge and exempted from its flood protection.",
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "mediaEncryption": {
            "type": "string"
          },
          "includeHeaders": {
            "type": "string"
          },
          "authUsername": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "authPassword": {
            "description": "authPassword is WRITE-ONLY: it is stored for the carrier and no tool ever returns it (reads show only hasAuthPassword), and it is kept out of logs and the audit trail. If the user prefers that the password not pass through this conversation, omit it and give them `portalUrl` from the answer, where they type it into the Portal themselves. Omitting it keeps the stored password.",
            "type": "string"
          },
          "authRealm": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "requiresRegistration": {
            "type": "boolean"
          },
          "maxConcurrentCalls": {
            "type": "integer",
            "exclusiveMinimum": 0,
            "maximum": 9007199254740991
          },
          "maxCallsPerSecond": {
            "type": "integer",
            "exclusiveMinimum": 0,
            "maximum": 9007199254740991
          },
          "cpsEnabled": {
            "type": "boolean"
          },
          "phoneNumbers": {
            "description": "REPLACES this direction's whole number list when sent; omit it to keep the numbers. Requires expectedRevision.",
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "e164": {
                  "type": "string",
                  "minLength": 1
                },
                "isActive": {
                  "type": "boolean"
                },
                "activeIvrFlowId": {
                  "anyOf": [
                    {
                      "type": "string",
                      "format": "uuid",
                      "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
                    },
                    {
                      "type": "null"
                    }
                  ]
                },
                "createDefaultIvrFlow": {
                  "type": "boolean"
                },
                "isDefaultOutbound": {
                  "type": "boolean"
                }
              },
              "required": [
                "e164"
              ],
              "additionalProperties": false
            }
          },
          "noiseReduction": {
            "description": "Noise reduction on the caller's audio for inbound calls.",
            "type": "boolean"
          },
          "trunkId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
            "description": "The trunk id from list_trunks."
          },
          "direction": {
            "type": "string",
            "enum": [
              "inbound",
              "outbound"
            ]
          },
          "expectedUpdatedAt": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time",
                "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
              },
              {
                "type": "null"
              }
            ],
            "description": "The direction's `updatedAt` from get_trunk / list_trunks. null ONLY to create a direction the trunk does not have yet."
          },
          "expectedRevision": {
            "description": "REQUIRED when phoneNumbers is sent: the trunk's `revision` from get_trunk / list_trunks.",
            "type": "string",
            "minLength": 1
          }
        },
        "required": [
          "trunkId",
          "direction",
          "expectedUpdatedAt"
        ]
      }
    },
    {
      "name": "set_trunk_phone_numbers",
      "requiredScope": "trunks.write",
      "description": "Set the trunk's phone numbers — the same as the Portal's number matrix. `rows` is the COMPLETE list: every number not in it is removed from the trunk, so read the current list with get_trunk first and send it back with your change. Per number: `e164`, `ownership` (INBOUND, OUTBOUND or BOTH — a direction the trunk lacks is created when a number needs it), `inbound` { activeIvrFlowId (the IVR flow family id the number routes into; null = none), createDefaultIvrFlow (create a default flow instead), isActive } and `outbound` { isDefaultOutbound (at most one: the organization's default caller ID), isActive }. An inbound number without an IVR flow reaches no queue or bot. An inbound number's activeIvrFlowId must be the FAMILY id of an IVR flow of this organization that has an ACTIVE version — a number routed to a family without one drops every call, so it is refused; activate a version first with activate_ivr_flow_version, or pass createDefaultIvrFlow: true instead. A number keeping the flow it already has is not re-checked. `expectedRevision` is the trunk's `revision` from get_trunk or list_trunks: if any number or direction of the trunk changed since, the write is refused — re-read the trunk with get_trunk and send the list again. A number cannot take inbound calls here (ownership INBOUND or BOTH) while it is on a trunk of another organization, active or not: it is refused as not available, and the answer never says who holds it. Numbers of the Mediflo number pool, or managed by Mediflo, cannot be added at all. If the user says a refused number is theirs, they should contact Mediflo support. Peer-registration trunks (managedInPortal: true — the organization's PBX registers to Mediflo) are managed in the Portal; this tool refuses them. Returns the updated trunk (recentCalls null).",
      "descriptionPl": "Ustawia pełną listę numerów trunku wraz z kierunkiem i przepływem IVR każdego numeru. Tylko dla administratorów.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "rows": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "e164": {
                  "type": "string",
                  "minLength": 1,
                  "description": "The number in E.164, e.g. +48123456789."
                },
                "ownership": {
                  "type": "string",
                  "enum": [
                    "BOTH",
                    "INBOUND",
                    "OUTBOUND"
                  ],
                  "description": "INBOUND (calls arrive on it), OUTBOUND (caller ID for calls out) or BOTH."
                },
                "inbound": {
                  "description": "Inbound side: activeIvrFlowId = the IVR flow family id the number routes into (null = none; the family must have an active version), or createDefaultIvrFlow: true; isActive.",
                  "type": "object",
                  "properties": {
                    "isActive": {
                      "type": "boolean"
                    },
                    "activeIvrFlowId": {
                      "anyOf": [
                        {
                          "type": "string",
                          "format": "uuid",
                          "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
                        },
                        {
                          "type": "null"
                        }
                      ]
                    },
                    "createDefaultIvrFlow": {
                      "type": "boolean"
                    }
                  },
                  "additionalProperties": false
                },
                "outbound": {
                  "description": "Outbound side: isDefaultOutbound (at most one — the organization's default caller ID); isActive.",
                  "type": "object",
                  "properties": {
                    "isActive": {
                      "type": "boolean"
                    },
                    "isDefaultOutbound": {
                      "type": "boolean"
                    }
                  },
                  "additionalProperties": false
                }
              },
              "required": [
                "e164",
                "ownership"
              ],
              "additionalProperties": false
            },
            "description": "The trunk's COMPLETE number list — every number not listed is removed."
          },
          "trunkId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
            "description": "The trunk id from list_trunks."
          },
          "expectedRevision": {
            "type": "string",
            "minLength": 1,
            "description": "The trunk's `revision` from get_trunk / list_trunks."
          }
        },
        "required": [
          "rows",
          "trunkId",
          "expectedRevision"
        ]
      }
    },
    {
      "name": "list_queues",
      "requiredScope": "queues.read",
      "description": "List the call queues of this tenant, newest first: id, name, queueType (INTERNAL or EXTERNAL), ring/DTMF timeouts, overflow queue, hold music, member counts, and `usedInIvrFlows` — the IVR flows whose queue node routes callers here. EXTERNAL queues with numberSource MEMBERS (the default) ring members ONE AT A TIME on their personal phone (User.externalPhoneNumber), and only USER members that have such a number AND have at least one availability window covering the current time — a member with no windows is never rung, and a PBX extension member is never dialled by an EXTERNAL queue. An EXTERNAL queue with numberSource API works differently: for each call it dials the phone numbers returned by the organization's own routing endpoint, and its members, their availability windows and their personal numbers are used ONLY AS THE FALLBACK when that endpoint fails — and only if routingFallback is MEMBERS (with EXIT_QUEUE the caller leaves the queue instead; members are rung only if the caller is still waiting long after that). routingConfigured false means no endpoint and secret are stored yet, so every call takes the fallback. numberSource, the endpoint, its secret and the fallback are set in the queue's page in the Mediflo Portal (Queues → the queue → Settings → Number source); no MCP tool configures them. INTERNAL queues offer the call to members signed in to Mediflo and to desk phones. Each queue carries `updatedAt`; pass it back as `expectedUpdatedAt` to update_queue.",
      "descriptionPl": "Lista kolejek połączeń organizacji z ich typem, limitami czasu, liczbą członków i przepływami IVR, które do nich kierują.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {}
      }
    },
    {
      "name": "get_queue",
      "requiredScope": "queues.read",
      "description": "Read one queue with its members (highest priority first) and each member's availability windows. A member is either a user (`userId`) or a desk phone (`pbxExtensionId`). For users, `hasExternalPhoneNumber` and `externalPhoneNumberMasked` (last 3 digits only) show which personal phone an EXTERNAL queue would ring — the full number is never returned by any tool. Check `numberSource` first: on an API-sourced EXTERNAL queue the members are only the fallback, and the answer carries a `numberSourceNote` saying so. EXTERNAL queues with numberSource MEMBERS (the default) ring members ONE AT A TIME on their personal phone (User.externalPhoneNumber), and only USER members that have such a number AND have at least one availability window covering the current time — a member with no windows is never rung, and a PBX extension member is never dialled by an EXTERNAL queue. An EXTERNAL queue with numberSource API works differently: for each call it dials the phone numbers returned by the organization's own routing endpoint, and its members, their availability windows and their personal numbers are used ONLY AS THE FALLBACK when that endpoint fails — and only if routingFallback is MEMBERS (with EXIT_QUEUE the caller leaves the queue instead; members are rung only if the caller is still waiting long after that). routingConfigured false means no endpoint and secret are stored yet, so every call takes the fallback. numberSource, the endpoint, its secret and the fallback are set in the queue's page in the Mediflo Portal (Queues → the queue → Settings → Number source); no MCP tool configures them. INTERNAL queues offer the call to members signed in to Mediflo and to desk phones. Windows use dayOfWeek 0 = Sunday … 6 = Saturday and HH:MM local to their timezone.",
      "descriptionPl": "Pokazuje jedną kolejkę z jej członkami, ich priorytetem i godzinami dostępności. Prywatne numery telefonów są maskowane (widać tylko 3 ostatnie cyfry).",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "queueId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
            "description": "Queue id, from list_queues."
          }
        },
        "required": [
          "queueId"
        ]
      }
    },
    {
      "name": "list_queue_member_candidates",
      "requiredScope": "queues.read",
      "description": "List who can be added to a queue with add_queue_member: the users of this tenant (id, name, role, invitation status, hasExternalPhoneNumber, masked personal number) and the active desk phones (PBX extensions). Pass `queueId` to leave out those already in that queue. For an EXTERNAL queue pick users that have an external phone number — or set one with set_user_external_phone_number first.",
      "descriptionPl": "Lista użytkowników i telefonów stacjonarnych, które można dodać do kolejki. Prywatne numery telefonów są maskowane.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "queueId": {
            "description": "Leave out the members already in this queue.",
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
          }
        }
      }
    },
    {
      "name": "create_queue",
      "requiredScope": "queues.write",
      "description": "Create a call queue. `queueType` (default INTERNAL) is fixed at creation and cannot be changed later. EXTERNAL queues with numberSource MEMBERS (the default) ring members ONE AT A TIME on their personal phone (User.externalPhoneNumber), and only USER members that have such a number AND have at least one availability window covering the current time — a member with no windows is never rung, and a PBX extension member is never dialled by an EXTERNAL queue. An EXTERNAL queue with numberSource API works differently: for each call it dials the phone numbers returned by the organization's own routing endpoint, and its members, their availability windows and their personal numbers are used ONLY AS THE FALLBACK when that endpoint fails — and only if routingFallback is MEMBERS (with EXIT_QUEUE the caller leaves the queue instead; members are rung only if the caller is still waiting long after that). routingConfigured false means no endpoint and secret are stored yet, so every call takes the fallback. numberSource, the endpoint, its secret and the fallback are set in the queue's page in the Mediflo Portal (Queues → the queue → Settings → Number source); no MCP tool configures them. INTERNAL queues offer the call to members signed in to Mediflo and to desk phones. A new queue receives no calls until an IVR flow routes to it. Then add members with add_queue_member and, for EXTERNAL queues, windows with set_queue_member_schedules. Names are unique per tenant. The same validation as the Portal queue editor applies.",
      "descriptionPl": "Tworzy kolejkę połączeń — wewnętrzną albo zewnętrzną (dzwoni na prywatne telefony pracowników). Tylko dla administratorów.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 100
          },
          "queueType": {
            "type": "string",
            "enum": [
              "INTERNAL",
              "EXTERNAL"
            ]
          },
          "description": {
            "description": "Free-text description; null clears it.",
            "anyOf": [
              {
                "type": "string",
                "maxLength": 2000
              },
              {
                "type": "null"
              }
            ]
          },
          "isDefault": {
            "description": "Make this the tenant default queue. Setting true unsets the previous default.",
            "type": "boolean"
          },
          "isActive": {
            "type": "boolean"
          },
          "maxWaitTime": {
            "description": "Maximum time a caller waits in the queue, in seconds.",
            "type": "integer",
            "minimum": 1,
            "maximum": 86400
          },
          "holdMusicAssetId": {
            "description": "Audio asset played while waiting; null clears it.",
            "anyOf": [
              {
                "type": "string",
                "format": "uuid",
                "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
              },
              {
                "type": "null"
              }
            ]
          },
          "ringTimeoutSeconds": {
            "description": "How long each member's phone rings before the next one is tried (5-120 s).",
            "type": "integer",
            "minimum": 5,
            "maximum": 120
          },
          "dtmfTimeoutSeconds": {
            "description": "EXTERNAL queues: how long an answering member has to press a key to accept the call (5-60 s).",
            "type": "integer",
            "minimum": 5,
            "maximum": 60
          }
        },
        "required": [
          "name"
        ]
      }
    },
    {
      "name": "update_queue",
      "requiredScope": "queues.write",
      "description": "Change a queue: name, description, isDefault, isActive, maxWaitTime, hold music, ring and DTMF timeouts. Send only the fields you change. `queueType` is immutable — create a new queue to switch type. `expectedUpdatedAt` is REQUIRED: the `updatedAt` you last read from list_queues / get_queue. If the queue changed since, nothing is written and you get the latest `updatedAt` — re-read and retry. Changes apply to the next call routed to the queue. Same validation as the Portal queue editor.",
      "descriptionPl": "Zmienia ustawienia kolejki (nazwę, limity czasu, muzykę oczekiwania, domyślność, aktywność). Tylko dla administratorów.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "queueId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
            "description": "Queue id, from list_queues."
          },
          "expectedUpdatedAt": {
            "type": "string",
            "minLength": 1,
            "description": "`updatedAt` of the queue as you last read it."
          },
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 100
          },
          "description": {
            "description": "Free-text description; null clears it.",
            "anyOf": [
              {
                "type": "string",
                "maxLength": 2000
              },
              {
                "type": "null"
              }
            ]
          },
          "isDefault": {
            "description": "Make this the tenant default queue. Setting true unsets the previous default.",
            "type": "boolean"
          },
          "isActive": {
            "type": "boolean"
          },
          "maxWaitTime": {
            "description": "Maximum time a caller waits in the queue, in seconds.",
            "type": "integer",
            "minimum": 1,
            "maximum": 86400
          },
          "holdMusicAssetId": {
            "description": "Audio asset played while waiting; null clears it.",
            "anyOf": [
              {
                "type": "string",
                "format": "uuid",
                "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
              },
              {
                "type": "null"
              }
            ]
          },
          "ringTimeoutSeconds": {
            "description": "How long each member's phone rings before the next one is tried (5-120 s).",
            "type": "integer",
            "minimum": 5,
            "maximum": 120
          },
          "dtmfTimeoutSeconds": {
            "description": "EXTERNAL queues: how long an answering member has to press a key to accept the call (5-60 s).",
            "type": "integer",
            "minimum": 5,
            "maximum": 60
          }
        },
        "required": [
          "queueId",
          "expectedUpdatedAt"
        ]
      }
    },
    {
      "name": "add_queue_member",
      "requiredScope": "queues.write",
      "description": "Add ONE member to a queue: either a user (`userId`) or a desk phone (`pbxExtensionId`) — exactly one of the two (find them with list_queue_member_candidates). `priority` 1-10 (default 1, higher rings first), `isActive` default true. EXTERNAL queues with numberSource MEMBERS (the default) ring members ONE AT A TIME on their personal phone (User.externalPhoneNumber), and only USER members that have such a number AND have at least one availability window covering the current time — a member with no windows is never rung, and a PBX extension member is never dialled by an EXTERNAL queue. An EXTERNAL queue with numberSource API works differently: for each call it dials the phone numbers returned by the organization's own routing endpoint, and its members, their availability windows and their personal numbers are used ONLY AS THE FALLBACK when that endpoint fails — and only if routingFallback is MEMBERS (with EXIT_QUEUE the caller leaves the queue instead; members are rung only if the caller is still waiting long after that). routingConfigured false means no endpoint and secret are stored yet, so every call takes the fallback. numberSource, the endpoint, its secret and the fallback are set in the queue's page in the Mediflo Portal (Queues → the queue → Settings → Number source); no MCP tool configures them. INTERNAL queues offer the call to members signed in to Mediflo and to desk phones. The result carries `warnings` when the member could never be rung by this queue (no personal number, no windows, extension in an EXTERNAL queue) — and, on an API-sourced EXTERNAL queue, a first warning that the member is only the fallback. Takes effect on the next call routed to the queue.",
      "descriptionPl": "Dodaje użytkownika albo telefon stacjonarny do kolejki. Tylko dla administratorów.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "queueId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
            "description": "Queue id, from list_queues."
          },
          "userId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
          },
          "pbxExtensionId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
          },
          "priority": {
            "type": "integer",
            "minimum": 1,
            "maximum": 10
          },
          "isActive": {
            "type": "boolean"
          }
        },
        "required": [
          "queueId"
        ]
      }
    },
    {
      "name": "update_queue_member",
      "requiredScope": "queues.write",
      "description": "Change a queue member's `priority` (1-10, higher rings first) and/or `isActive` (an inactive member is never rung but keeps its windows). Takes effect on the next call routed to the queue.",
      "descriptionPl": "Zmienia priorytet członka kolejki albo wstrzymuje go. Tylko dla administratorów.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "membershipId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
            "description": "Membership id (`membershipId`), from get_queue."
          },
          "priority": {
            "type": "integer",
            "minimum": 1,
            "maximum": 10
          },
          "isActive": {
            "type": "boolean"
          }
        },
        "required": [
          "membershipId"
        ]
      }
    },
    {
      "name": "remove_queue_member",
      "requiredScope": "queues.write",
      "description": "Remove a member from a queue. PERMANENT, as in the Portal: the membership and that member's availability windows for this queue are deleted, not archived — re-adding them later starts with no windows. To pause a member instead, use update_queue_member with isActive: false. Takes effect on the next call routed to the queue.",
      "descriptionPl": "Usuwa członka z kolejki razem z jego godzinami dostępności. Tylko dla administratorów.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "membershipId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
            "description": "Membership id (`membershipId`), from get_queue."
          }
        },
        "required": [
          "membershipId"
        ]
      }
    },
    {
      "name": "set_queue_member_schedules",
      "requiredScope": "queues.write",
      "description": "REPLACE one queue member's availability windows with `windows` — the whole set, atomically; windows not in the list are removed, and an empty list clears them. Each window: dayOfWeek 0 = Sunday … 6 = Saturday, startTime < endTime as HH:MM (24 h), and a timezone from Europe/Warsaw, Europe/London, Europe/Berlin, Europe/Paris, Europe/Prague, Europe/Bratislava, Europe/Kyiv, Europe/Moscow, UTC. Windows on the same day must not overlap; at most 50 per member. An EXTERNAL queue with numberSource MEMBERS rings a member only inside these windows — a member with none is never rung. On an EXTERNAL queue with numberSource API the windows apply ONLY to the members fallback (the organization's routing endpoint failed and routingFallback is MEMBERS); the answer then leads its `warnings` with that note. Routing itself is configured in the queue's page in the Mediflo Portal (Queues → the queue → Settings → Number source), not over MCP. Same rules as the Portal schedule editor; takes effect on the next dial attempt.",
      "descriptionPl": "Zastępuje godziny, w których członek kolejki może odbierać połączenia. Tylko dla administratorów.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "membershipId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
            "description": "Membership id (`membershipId`), from get_queue."
          },
          "windows": {
            "maxItems": 50,
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "dayOfWeek": {
                  "type": "integer",
                  "minimum": 0,
                  "maximum": 6
                },
                "startTime": {
                  "type": "string",
                  "description": "HH:MM, 24 h"
                },
                "endTime": {
                  "type": "string",
                  "description": "HH:MM, 24 h, after startTime"
                },
                "timezone": {
                  "type": "string",
                  "enum": [
                    "Europe/Warsaw",
                    "Europe/London",
                    "Europe/Berlin",
                    "Europe/Paris",
                    "Europe/Prague",
                    "Europe/Bratislava",
                    "Europe/Kyiv",
                    "Europe/Moscow",
                    "UTC"
                  ]
                }
              },
              "required": [
                "dayOfWeek",
                "startTime",
                "endTime",
                "timezone"
              ]
            }
          }
        },
        "required": [
          "membershipId",
          "windows"
        ]
      }
    },
    {
      "name": "set_user_external_phone_number",
      "requiredScope": "queues.write",
      "description": "Set or clear ONE user's external phone number — the staff member's own, real phone that EXTERNAL queues ring. Changing it changes which real phone rings for this person in EVERY external queue they belong to, from the next dial attempt; clearing it (null) means external queues skip them. Only this one field of the user changes. The number must be E.164 (e.g. +48600123456) and dialable under this tenant's calling policy (allowed countries, no premium-rate numbers) — the same check applied when the queue dials. The answer shows only the masked number (last 3 digits), plus `externalQueues` — the EXTERNAL queues this person is in — and `notes` for any of them with numberSource API: those dial the organization's routing endpoint's numbers and ring this personal number ONLY as the fallback (routingFallback MEMBERS); routing is configured in the queue's page in the Mediflo Portal (Queues → the queue → Settings → Number source), not over MCP. Confirm the number with the person it belongs to before setting it.",
      "descriptionPl": "Ustawia lub usuwa prywatny numer telefonu pracownika, na który dzwonią kolejki zewnętrzne. Tylko dla administratorów.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "userId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
            "description": "User id, from list_queue_member_candidates or get_queue."
          },
          "externalPhoneNumber": {
            "anyOf": [
              {
                "type": "string",
                "pattern": "^\\+[1-9]\\d{6,14}$"
              },
              {
                "type": "null"
              }
            ],
            "description": "E.164 number, or null to clear it."
          }
        },
        "required": [
          "userId",
          "externalPhoneNumber"
        ]
      }
    },
    {
      "name": "list_ivr_flows",
      "requiredScope": "ivr-flows.read",
      "description": "List the tenant IVR flows, one row per flow FAMILY (a family is a flow and all its versions; `familyId` is the id phone numbers point at). Each row carries the ACTIVE version — the one callers hear — or null when the family has never been activated (a number pointed at such a family would drop every call), every DRAFT version with its `updatedAt` (the concurrency token save_ivr_flow_draft needs), the phone numbers the family answers, and whether a running A/B experiment pins it. Read a version with get_ivr_flow.",
      "descriptionPl": "Lista przepływów IVR organizacji: aktywna wersja każdego z nich, wersje robocze i numery telefonów, które obsługuje.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {}
      }
    },
    {
      "name": "get_ivr_flow",
      "requiredScope": "ivr-flows.read",
      "description": "Read ONE IVR flow version by id (any version id, including the family root): its `flowDefinition`, state (draft | active), `updatedAt` — the token save_ivr_flow_draft requires as `expectedUpdatedAt` — the family's version list, the phone numbers the family answers, and every audio asset, queue, bot and contact-tier slug the definition names, resolved in this tenant (`exists: false` means deleted or not yours). EXTERNAL queues ring their members’ personal phone numbers. To change an ACTIVE version, fork it with create_ivr_flow_version first.",
      "descriptionPl": "Odczytuje jedną wersję przepływu IVR wraz z definicją, listą wersji, numerami telefonów oraz używanymi nagraniami, kolejkami i botami.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "flowId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
          }
        },
        "required": [
          "flowId"
        ]
      }
    },
    {
      "name": "get_ivr_flow_schema",
      "requiredScope": "ivr-flows.read",
      "description": "Everything needed to AUTHOR an IVR flow definition: each node type with a one-line description, its routing fields and its config as JSON Schema; the connection schema; how routing works; the rules the validator enforces (errors block a save, warnings do not); and a minimal example. Read this before create_ivr_flow or save_ivr_flow_draft, and check a definition with validate_ivr_flow.",
      "descriptionPl": "Opisuje elementy IVR i reguły, które musi spełniać przepływ, aby asystent mógł go przygotować.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {}
      }
    },
    {
      "name": "list_ivr_audio_assets",
      "requiredScope": "ivr-flows.read",
      "description": "List the tenant audio library an IVR flow can play: id, name, kind (uploaded | tts_generated) and duration in seconds. Use the `id` as `audioFile` / `promptAudioFile` in a node config. Audio cannot be uploaded or generated over MCP — that is done in the Portal.",
      "descriptionPl": "Lista nagrań, które może odtwarzać przepływ IVR (tylko nazwa, typ i długość).",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "page": {
            "default": 1,
            "type": "integer",
            "minimum": 1,
            "maximum": 9007199254740991
          },
          "limit": {
            "default": 100,
            "type": "integer",
            "minimum": 1,
            "maximum": 200
          }
        }
      }
    },
    {
      "name": "validate_ivr_flow",
      "requiredScope": "ivr-flows.read",
      "description": "Dry-run an IVR flow definition WITHOUT saving: the same schema check and tenant validator a save runs (audio assets, queues and bots must exist in this tenant). Returns `{ valid, schemaIssues, issues, references }` — `issues` carry severity, code, nodeId and field; only errors make it invalid. `references` lists every audio asset, queue, bot and contact-tier slug the definition names, with `exists`. An invalid definition is a normal result here, not an error. Call get_ivr_flow_schema for the node types, their config fields and the validator rules.",
      "descriptionPl": "Sprawdza przepływ IVR pod kątem błędów, niczego nie zapisując.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "flowDefinition": {
            "type": "object",
            "propertyNames": {
              "type": "string"
            },
            "additionalProperties": {
              "$ref": "#/definitions/__schema0"
            }
          }
        },
        "required": [
          "flowDefinition"
        ],
        "definitions": {
          "__schema0": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "number"
              },
              {
                "type": "boolean"
              },
              {
                "type": "null"
              },
              {
                "type": "array",
                "items": {
                  "$ref": "#/definitions/__schema0"
                }
              },
              {
                "type": "object",
                "propertyNames": {
                  "type": "string"
                },
                "additionalProperties": {
                  "$ref": "#/definitions/__schema0"
                }
              }
            ]
          }
        }
      }
    },
    {
      "name": "create_ivr_flow",
      "requiredScope": "ivr-flows.write",
      "description": "Create a NEW IVR flow family as a DRAFT version 1. Nothing answers calls with it until a version is activated (activate_ivr_flow_version) AND a phone number is assigned (assign_ivr_flow_to_phone_number). The definition must pass the same validation as the Portal editor — run validate_ivr_flow first. Call get_ivr_flow_schema for the node types, their config fields and the validator rules.",
      "descriptionPl": "Tworzy nowy przepływ IVR jako wersję roboczą. Dzwoniący nic nie usłyszą, dopóki nie zostanie aktywowany i przypisany do numeru.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 255
          },
          "description": {
            "type": "string",
            "maxLength": 2000
          },
          "flowDefinition": {
            "type": "object",
            "propertyNames": {
              "type": "string"
            },
            "additionalProperties": {
              "$ref": "#/definitions/__schema0"
            }
          }
        },
        "required": [
          "name",
          "flowDefinition"
        ],
        "definitions": {
          "__schema0": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "number"
              },
              {
                "type": "boolean"
              },
              {
                "type": "null"
              },
              {
                "type": "array",
                "items": {
                  "$ref": "#/definitions/__schema0"
                }
              },
              {
                "type": "object",
                "propertyNames": {
                  "type": "string"
                },
                "additionalProperties": {
                  "$ref": "#/definitions/__schema0"
                }
              }
            ]
          }
        }
      }
    },
    {
      "name": "create_ivr_flow_version",
      "requiredScope": "ivr-flows.write",
      "description": "Copy an existing IVR flow version (draft or active) into a NEW DRAFT version of the same family, with the next version number — the same as \"new version\" in the Portal. This is how you change a live flow: fork it, edit the copy with save_ivr_flow_draft, then activate it. The copy is not validated; validation happens on save and on activation.",
      "descriptionPl": "Kopiuje wersję przepływu IVR do nowej wersji roboczej — tak zmienia się działający przepływ.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "sourceFlowId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
          },
          "versionNote": {
            "type": "string",
            "maxLength": 1000
          }
        },
        "required": [
          "sourceFlowId"
        ]
      }
    },
    {
      "name": "save_ivr_flow_draft",
      "requiredScope": "ivr-flows.write",
      "description": "Update a DRAFT IVR flow version: its flowDefinition, name, description and/or versionNote. Refused for the ACTIVE version and for any version that is an A/B experiment arm — fork it with create_ivr_flow_version and edit the new draft instead. `expectedUpdatedAt` (from get_ivr_flow or list_ivr_flows) is required: a save against a version that changed since you read it is refused rather than overwriting it. A definition is validated exactly like the Portal editor’s save. Saving a draft changes nothing callers hear until it is activated. Call get_ivr_flow_schema for the node types, their config fields and the validator rules.",
      "descriptionPl": "Edytuje roboczą wersję przepływu IVR. Aktywnej wersji nie można edytować bezpośrednio.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "flowId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
          },
          "expectedUpdatedAt": {
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"
          },
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 255
          },
          "description": {
            "anyOf": [
              {
                "type": "string",
                "maxLength": 2000
              },
              {
                "type": "null"
              }
            ]
          },
          "versionNote": {
            "anyOf": [
              {
                "type": "string",
                "maxLength": 1000
              },
              {
                "type": "null"
              }
            ]
          },
          "flowDefinition": {
            "type": "object",
            "propertyNames": {
              "type": "string"
            },
            "additionalProperties": {
              "$ref": "#/definitions/__schema0"
            }
          }
        },
        "required": [
          "flowId",
          "expectedUpdatedAt"
        ],
        "definitions": {
          "__schema0": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "number"
              },
              {
                "type": "boolean"
              },
              {
                "type": "null"
              },
              {
                "type": "array",
                "items": {
                  "$ref": "#/definitions/__schema0"
                }
              },
              {
                "type": "object",
                "propertyNames": {
                  "type": "string"
                },
                "additionalProperties": {
                  "$ref": "#/definitions/__schema0"
                }
              }
            ]
          }
        }
      }
    },
    {
      "name": "activate_ivr_flow_version",
      "requiredScope": "ivr-flows.write",
      "description": "⚠️ Changes LIVE call routing: from the NEXT call on, every phone number assigned to this flow's family is answered by this version. Makes a DRAFT version the active one and demotes the previous active version to draft. The stored definition is RE-VALIDATED first against the tenant’s current audio, queues and bots. `expectedActiveVersionId` is required: the id of the version you believe is live now (from list_ivr_flows), or null for a family that has never been activated — a mismatch is refused, so you never replace a version you have not seen. Returns `previousActiveVersionId`: to roll back, activate that id with expectedActiveVersionId set to the version you just activated.",
      "descriptionPl": "Ustawia wersję roboczą jako aktywną wersję IVR. Zmienia to, co słyszą dzwoniący, od następnego połączenia; można to cofnąć.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "flowId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
          },
          "expectedActiveVersionId": {
            "anyOf": [
              {
                "type": "string",
                "format": "uuid",
                "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
              },
              {
                "type": "null"
              }
            ]
          }
        },
        "required": [
          "flowId",
          "expectedActiveVersionId"
        ]
      }
    },
    {
      "name": "assign_ivr_flow_to_phone_number",
      "requiredScope": "ivr-flows.write",
      "description": "⚠️ Changes LIVE call routing: from the next call on, the inbound phone number is answered by this flow family’s ACTIVE version (`flowId` may be the family id or any of its version ids). Refused when the family has no active version (it would drop every call), for outbound-only numbers, and while a running A/B experiment pins the number. A number already answered by ANOTHER flow is only moved with `confirmMove: true` plus `expectedPreviousFlowId` set to the family that owns it now (the refusal names it); if ownership changed meanwhile the move is refused instead of taking it from a flow you did not confirm. The Portal’s \"attach number\" uses this same service.",
      "descriptionPl": "Kieruje przychodzący numer telefonu do przepływu IVR. Działa od następnego połączenia.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "flowId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
          },
          "phoneNumberId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
          },
          "confirmMove": {
            "type": "boolean"
          },
          "expectedPreviousFlowId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
          }
        },
        "required": [
          "flowId",
          "phoneNumberId"
        ]
      }
    },
    {
      "name": "detach_ivr_flow_from_phone_number",
      "requiredScope": "ivr-flows.write",
      "description": "⚠️ Changes LIVE call routing: removes this flow family from the phone number, leaving the number with NO IVR flow — inbound calls to it are not answered by any flow until another one is assigned. The number itself stays as it is. Refused when the family does not own the number, while a running A/B experiment pins it, or when the assignment changed concurrently. To move a number to another flow, use assign_ivr_flow_to_phone_number with confirmMove instead.",
      "descriptionPl": "Odłącza przepływ IVR od numeru telefonu, zostawiając numer bez przepływu.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "flowId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
          },
          "phoneNumberId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
          }
        },
        "required": [
          "flowId",
          "phoneNumberId"
        ]
      }
    },
    {
      "name": "list_calls",
      "requiredScope": "calls.read",
      "description": "List telephony calls for the tenant, newest first, with pagination and optional phone-number and date-window filters. Mirrors GET /api/external/v1/calls. This tool returns individual call records — use it to look at specific calls. To COUNT or summarise calls (totals, answer rate, per month / day / hour / queue / agent, durations, wait times) do NOT page through it: call aggregate_calls or get_call_statistics, which compute the answer server-side in one request. Those tools need the analytics.read scope; if they are not in your tool list, call get_server_info and read `hints`. IVR announcement text and durations are not on this list — call get_call with a call id.",
      "descriptionPl": "Lista rozmów telefonicznych organizacji, od najnowszych, z filtrami po numerze i zakresie dat.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "phoneNumber": {
            "description": "E.164 number, e.g. +48123456789. Matches either call leg.",
            "type": "string",
            "pattern": "^\\+[1-9]\\d{1,14}$"
          },
          "dateFrom": {
            "description": "ISO-8601 timestamp; inclusive lower bound on startedAt.",
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
          },
          "dateTo": {
            "description": "ISO-8601 timestamp; inclusive upper bound on startedAt.",
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
          },
          "page": {
            "default": 1,
            "type": "integer",
            "minimum": 1,
            "maximum": 9007199254740991
          },
          "limit": {
            "default": 50,
            "type": "integer",
            "minimum": 1,
            "maximum": 100
          }
        }
      }
    },
    {
      "name": "get_call",
      "requiredScope": "calls.read",
      "description": "Read one telephony call by `callId` (the id from list_calls). Same fields as one list_calls row, plus `announcements`: a best-effort reconstruction from the call's IVR timeline, the pinned flow version's current `flowDefinition`, and `audio_assets`. Entering a node is not proof the caller heard that asset. `ivr.node.entered` is recorded before audio resolves or plays. The pinned version row can still be edited in place, so a later edit of that version can change what this tool returns for an older call. v1 accepts that best-effort join. The durable fix is stamping `audioAssetId` and `durationSeconds` onto the timeline event when it is emitted; this tool does not do that. Included node types are audio, menu, hangup (only when that node has an audio file) and intent. Each item has `nodeId`, `nodeType`, `nodeLabel`, `audioAssetId`, `content`, `contentSource` (`tts_text` | `display_name` | `node_label`, or null), `durationSeconds` and `enteredAt`. `enteredAt` is the `ivr.node.entered` wall-clock time, not a confirmed playback start or end. `content` is the audio asset TTS script (`text`) when that column is set; otherwise the asset display name (or its `name`); otherwise an intent node `promptText` when the node has no asset text or name; otherwise the node label; otherwise null. `contentSource` names which of those was used. This is not speech-to-text of what the caller said. `durationSeconds` is `audio_assets.duration` when the join to that asset succeeds; otherwise null. A call with no `ivr_flow_version_id`, a removed flow version, or a node missing from that version still lists the timeline nodes, with content taken from the node label when the asset cannot be joined. An unknown id and another tenant's id both come back as not found. There is no REST equivalent; list_calls stays the thin list and does not include announcements.",
      "descriptionPl": "Zwraca jedną rozmowę telefoniczną wraz z przybliżoną rekonstrukcją komunikatów IVR z osi czasu, aktualnej definicji przypiętej wersji przepływu i zasobów audio: treścią lub nazwą, źródłem tej treści, czasem trwania nagrania i chwilą wejścia w węzeł. Wejście w węzeł nie oznacza, że rozmówca usłyszał nagranie, a późniejsza edycja tej wersji przepływu może zmienić wynik.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "callId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
            "description": "Telephony call id returned by list_calls."
          }
        },
        "required": [
          "callId"
        ]
      }
    },
    {
      "name": "list_tasks",
      "requiredScope": "tasks.read",
      "description": "List tasks for the tenant, newest first by default, with pagination and optional status/priority/assignee/text filters. Mirrors GET /api/external/v1/tasks.",
      "descriptionPl": "Lista zadań organizacji z filtrami po statusie, priorytecie, osobie i treści.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "SUGGESTED",
              "TODO",
              "IN_PROGRESS",
              "ON_HOLD",
              "COMPLETED",
              "CANCELLED"
            ]
          },
          "priority": {
            "type": "string",
            "enum": [
              "LOW",
              "MEDIUM",
              "HIGH",
              "URGENT"
            ]
          },
          "assignedToUserId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
          },
          "search": {
            "type": "string"
          },
          "page": {
            "default": 1,
            "type": "integer",
            "minimum": 1,
            "maximum": 9007199254740991
          },
          "limit": {
            "default": 50,
            "type": "integer",
            "minimum": 1,
            "maximum": 100
          },
          "orderBy": {
            "default": "createdAt",
            "type": "string",
            "enum": [
              "createdAt",
              "updatedAt",
              "dueDate",
              "priority",
              "title"
            ]
          },
          "orderDir": {
            "default": "desc",
            "type": "string",
            "enum": [
              "asc",
              "desc"
            ]
          }
        }
      }
    },
    {
      "name": "create_task",
      "requiredScope": "tasks.write",
      "description": "Create a task for the tenant. Mirrors POST /api/external/v1/tasks. Returns the created task.",
      "descriptionPl": "Tworzy nowe zadanie w organizacji.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "title": {
            "type": "string",
            "minLength": 1,
            "maxLength": 500
          },
          "description": {
            "type": "string"
          },
          "priority": {
            "default": "MEDIUM",
            "type": "string",
            "enum": [
              "LOW",
              "MEDIUM",
              "HIGH",
              "URGENT"
            ]
          },
          "dueDate": {
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
          },
          "assignedToUserId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
          },
          "tagIds": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid",
              "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
            }
          },
          "companyId": {
            "description": "Link the task to a company (B2B tenants only). Rejected for tenants without companies, and for an unknown or deleted company.",
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
          }
        },
        "required": [
          "title"
        ]
      }
    },
    {
      "name": "list_cases",
      "requiredScope": "cases.read",
      "description": "List the tenant's customer cases, newest number first (or most recently updated with sort \"updatedAt\"), with pagination and optional status, open-only, category, assignee, contact, company, linked-record and title/number filters. Mirrors GET /api/external/v1/cases.",
      "descriptionPl": "Lista spraw klientów z filtrami po statusie, kategorii, osobie, kontakcie, firmie i treści.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "status": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "NEW",
                "IN_PROGRESS",
                "WAITING_CUSTOMER",
                "WAITING_THIRD_PARTY",
                "RESOLVED",
                "CLOSED",
                "CANCELLED"
              ]
            }
          },
          "open": {
            "description": "true: only open cases (NEW, IN_PROGRESS, WAITING_*).",
            "type": "boolean"
          },
          "category": {
            "maxItems": 20,
            "type": "array",
            "items": {
              "type": "string",
              "maxLength": 64,
              "pattern": "^[a-z][a-z0-9_]*$"
            }
          },
          "assignedTo": {
            "description": "A user id, or \"unassigned\".",
            "anyOf": [
              {
                "type": "string",
                "format": "uuid",
                "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
              },
              {
                "type": "string",
                "const": "unassigned"
              }
            ]
          },
          "contactId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
          },
          "companyId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
          },
          "search": {
            "description": "Words of the title, or a case number such as 1042.",
            "type": "string",
            "maxLength": 200
          },
          "resourceType": {
            "description": "With resourceId: only cases linked to that record.",
            "type": "string",
            "enum": [
              "TELEPHONY_CALL",
              "BOT_SESSION",
              "EMAIL_CONVERSATION",
              "CHAT_CONVERSATION",
              "MESSAGING_CONVERSATION",
              "CONTACT_REQUEST"
            ]
          },
          "resourceId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
          },
          "sort": {
            "default": "number",
            "type": "string",
            "enum": [
              "number",
              "updatedAt"
            ]
          },
          "page": {
            "default": 1,
            "type": "integer",
            "minimum": 1,
            "maximum": 10000
          },
          "limit": {
            "default": 20,
            "type": "integer",
            "minimum": 1,
            "maximum": 100
          }
        }
      }
    },
    {
      "name": "get_case",
      "requiredScope": "cases.read",
      "description": "Get one customer case with its links (and its tasks when the key also holds tasks.read). Mirrors GET /api/external/v1/cases/{caseId}.",
      "descriptionPl": "Szczegóły jednej sprawy razem z powiązanymi połączeniami i rozmowami.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "caseId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
          }
        },
        "required": [
          "caseId"
        ]
      }
    },
    {
      "name": "create_case",
      "requiredScope": "cases.write",
      "description": "Create a customer case: a short title, a category key, and optionally a description, priority, the contact or company it concerns, an assignee, a location, your own reference and links to the calls or conversations it came from. Returns the case with its number. Mirrors POST /api/external/v1/cases.",
      "descriptionPl": "Zakłada nową sprawę i zwraca jej numer.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "title": {
            "type": "string",
            "minLength": 1,
            "maxLength": 500
          },
          "categoryKey": {
            "type": "string",
            "minLength": 1,
            "maxLength": 64,
            "description": "A case category key: water_leak, heating, electricity, elevator, damage, common_areas, noise_neighbours, other, bug, how_to_training, configuration_request, feature_request, outage, question, complaint, service_request. Use \"other\" when none fits."
          },
          "description": {
            "anyOf": [
              {
                "type": "string",
                "maxLength": 20000
              },
              {
                "type": "null"
              }
            ]
          },
          "priority": {
            "type": "string",
            "enum": [
              "LOW",
              "MEDIUM",
              "HIGH",
              "URGENT"
            ]
          },
          "topicId": {
            "description": "A call topic id of this tenant (the product or service area).",
            "anyOf": [
              {
                "type": "string",
                "format": "uuid",
                "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
              },
              {
                "type": "null"
              }
            ]
          },
          "contactId": {
            "anyOf": [
              {
                "type": "string",
                "format": "uuid",
                "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
              },
              {
                "type": "null"
              }
            ]
          },
          "companyId": {
            "description": "Link the case to a company (only for tenants that keep companies).",
            "anyOf": [
              {
                "type": "string",
                "format": "uuid",
                "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
              },
              {
                "type": "null"
              }
            ]
          },
          "reporterName": {
            "anyOf": [
              {
                "type": "string",
                "maxLength": 255
              },
              {
                "type": "null"
              }
            ]
          },
          "reporterPhone": {
            "anyOf": [
              {
                "type": "string",
                "maxLength": 50
              },
              {
                "type": "null"
              }
            ]
          },
          "assignedToUserId": {
            "anyOf": [
              {
                "type": "string",
                "format": "uuid",
                "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
              },
              {
                "type": "null"
              }
            ]
          },
          "locationText": {
            "description": "Where the issue is, e.g. an address or a room.",
            "anyOf": [
              {
                "type": "string",
                "maxLength": 500
              },
              {
                "type": "null"
              }
            ]
          },
          "externalSystem": {
            "anyOf": [
              {
                "type": "string",
                "maxLength": 100
              },
              {
                "type": "null"
              }
            ]
          },
          "externalRef": {
            "description": "The case's id in your own system.",
            "anyOf": [
              {
                "type": "string",
                "maxLength": 255
              },
              {
                "type": "null"
              }
            ]
          },
          "externalUrl": {
            "description": "An http(s) link to it.",
            "anyOf": [
              {
                "type": "string",
                "maxLength": 2048
              },
              {
                "type": "null"
              }
            ]
          },
          "dueAt": {
            "description": "ISO 8601 date-time with an offset, e.g. 2026-10-20T12:00:00Z.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "customFields": {
            "description": "Extra fields of the tenant's case form (the full set; an omitted field is cleared).",
            "type": "object",
            "propertyNames": {
              "type": "string"
            },
            "additionalProperties": {
              "type": "string"
            }
          },
          "tagIds": {
            "maxItems": 20,
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid",
              "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
            }
          },
          "links": {
            "description": "Records the case is about, linked in the same step.",
            "maxItems": 10,
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "resourceType": {
                  "type": "string",
                  "enum": [
                    "TELEPHONY_CALL",
                    "BOT_SESSION",
                    "EMAIL_CONVERSATION",
                    "CHAT_CONVERSATION",
                    "MESSAGING_CONVERSATION",
                    "CONTACT_REQUEST"
                  ]
                },
                "resourceId": {
                  "type": "string",
                  "format": "uuid",
                  "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
                }
              },
              "required": [
                "resourceType",
                "resourceId"
              ]
            }
          }
        },
        "required": [
          "title",
          "categoryKey"
        ]
      }
    },
    {
      "name": "update_case",
      "requiredScope": "cases.write",
      "description": "Update a customer case: only the fields you send change, null clears an optional field. Changing status to RESOLVED or CLOSED stamps the matching time. Mirrors PATCH /api/external/v1/cases/{caseId}.",
      "descriptionPl": "Zmienia status, przypisanie lub inne pola sprawy.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "caseId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
          },
          "title": {
            "type": "string",
            "minLength": 1,
            "maxLength": 500
          },
          "categoryKey": {
            "description": "A case category key: water_leak, heating, electricity, elevator, damage, common_areas, noise_neighbours, other, bug, how_to_training, configuration_request, feature_request, outage, question, complaint, service_request. Use \"other\" when none fits.",
            "type": "string",
            "minLength": 1,
            "maxLength": 64
          },
          "status": {
            "type": "string",
            "enum": [
              "NEW",
              "IN_PROGRESS",
              "WAITING_CUSTOMER",
              "WAITING_THIRD_PARTY",
              "RESOLVED",
              "CLOSED",
              "CANCELLED"
            ]
          },
          "description": {
            "anyOf": [
              {
                "type": "string",
                "maxLength": 20000
              },
              {
                "type": "null"
              }
            ]
          },
          "priority": {
            "type": "string",
            "enum": [
              "LOW",
              "MEDIUM",
              "HIGH",
              "URGENT"
            ]
          },
          "topicId": {
            "description": "A call topic id of this tenant (the product or service area).",
            "anyOf": [
              {
                "type": "string",
                "format": "uuid",
                "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
              },
              {
                "type": "null"
              }
            ]
          },
          "contactId": {
            "anyOf": [
              {
                "type": "string",
                "format": "uuid",
                "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
              },
              {
                "type": "null"
              }
            ]
          },
          "companyId": {
            "description": "Link the case to a company (only for tenants that keep companies).",
            "anyOf": [
              {
                "type": "string",
                "format": "uuid",
                "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
              },
              {
                "type": "null"
              }
            ]
          },
          "reporterName": {
            "anyOf": [
              {
                "type": "string",
                "maxLength": 255
              },
              {
                "type": "null"
              }
            ]
          },
          "reporterPhone": {
            "anyOf": [
              {
                "type": "string",
                "maxLength": 50
              },
              {
                "type": "null"
              }
            ]
          },
          "assignedToUserId": {
            "anyOf": [
              {
                "type": "string",
                "format": "uuid",
                "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
              },
              {
                "type": "null"
              }
            ]
          },
          "locationText": {
            "description": "Where the issue is, e.g. an address or a room.",
            "anyOf": [
              {
                "type": "string",
                "maxLength": 500
              },
              {
                "type": "null"
              }
            ]
          },
          "externalSystem": {
            "anyOf": [
              {
                "type": "string",
                "maxLength": 100
              },
              {
                "type": "null"
              }
            ]
          },
          "externalRef": {
            "description": "The case's id in your own system.",
            "anyOf": [
              {
                "type": "string",
                "maxLength": 255
              },
              {
                "type": "null"
              }
            ]
          },
          "externalUrl": {
            "description": "An http(s) link to it.",
            "anyOf": [
              {
                "type": "string",
                "maxLength": 2048
              },
              {
                "type": "null"
              }
            ]
          },
          "dueAt": {
            "description": "ISO 8601 date-time with an offset, e.g. 2026-10-20T12:00:00Z.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "customFields": {
            "description": "Extra fields of the tenant's case form (the full set; an omitted field is cleared).",
            "type": "object",
            "propertyNames": {
              "type": "string"
            },
            "additionalProperties": {
              "type": "string"
            }
          },
          "tagIds": {
            "maxItems": 20,
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid",
              "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
            }
          }
        },
        "required": [
          "caseId"
        ]
      }
    },
    {
      "name": "create_caller_case",
      "requiredScope": "cases.bot-create",
      "description": "Open a case for the caller's matter so the team can take it from here: a fault to fix, a request to handle, a complaint to look into. Call it once you know what the matter is — not for a question you can answer yourself. Pass a short summary, the details in the caller’s own words, the category, whether it is urgent, and — only when the caller says them — where the problem is and their own reference number. Do not ask for the caller’s name or number: the platform links the case to them. Returns `caseNumber` (e.g. \"S-1042\"): read it out to the caller so they can refer to it — the letter S, then each digit on its own. Calling again with the same summary in the same conversation returns the same case — it never opens a duplicate. If the tool fails, do not invent a number.",
      "descriptionPl": "Wbudowane narzędzie bota (tylko klucz zarządzany bota): zakłada sprawę dla dzwoniącego, łączy ją z rozmową i z kontaktem dzwoniącego i zwraca numer sprawy do przeczytania na głos. Ponowione wywołanie zwraca tę samą sprawę.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "title": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200,
            "description": "A short summary of the matter in one line, in the language of the conversation."
          },
          "description": {
            "description": "The details the team needs to act without listening to the call: what happened, since when, what the caller already tried.",
            "type": "string",
            "maxLength": 4000
          },
          "categoryKey": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200,
            "description": "A category key from the list in your instructions. Use \"other\" when none fits."
          },
          "urgent": {
            "description": "true only when the matter cannot wait: a safety risk, damage spreading, a service completely down.",
            "type": "boolean"
          },
          "location": {
            "description": "Where the problem is, as the caller said it (an address, a flat, a room). Omit when not given.",
            "type": "string",
            "maxLength": 500
          },
          "externalRef": {
            "description": "The caller’s own reference or ticket number, when they give one. Omit otherwise.",
            "type": "string",
            "maxLength": 255
          },
          "contactPhone": {
            "description": "The caller's own number. Supplied by the platform — do NOT fill it in yourself.",
            "type": "string"
          }
        },
        "required": [
          "title",
          "categoryKey"
        ]
      }
    },
    {
      "name": "aggregate_calls",
      "requiredScope": "analytics.read",
      "description": "Generic call statistics: pick how to slice the calls (`groupBy`) and what to measure (`metrics`) and get the numbers in ONE call. Use this instead of paging list_calls to count anything — list_calls returns individual call records and is the wrong tool for \"how many\", \"what share\", \"per month\", \"per queue\", \"busiest hour\". It answers questions the fixed analytics cards cannot, e.g. calls and answer rate per month, answer rate per queue per weekday, median wait per hour per line. Each row is FLAT: first the key(s) of every requested dimension, then one key per requested metric. KEY NAMING: you ask in snake_case and the answer comes back in camelCase — each requested dimension/metric appears under its camelCase key (`answer_rate` → `answerRate`, `median_wait_seconds` → `medianWaitSeconds`, `hour_of_day` → `hourOfDay`), in `rows` and in `totals` alike. Dimension keys are `month`, `week`, `day`, `hourOfDay`, `dayOfWeek`, `direction`, `outcome`, `status`, `disconnectReason`, `origin`, `queueId`+`queueName`, `phoneNumberId`+`phoneNumberName`, `operatorRef` (+`name` only with the users.read scope), `topicId`+`topicName`, `tagId`+`tagName`; the id half is null when the call had no queue / line / agent / topic. OUTCOME: `ANSWERED` = completed with an answer timestamp; `MISSED` = inbound and ended NO_ANSWER / CANCELLED / BUSY / FAILED, or completed with no answer timestamp (the caller hung up while it rang); `OTHER` = everything else (still in progress, or an outbound call that never connected). Note the `answered_calls` METRIC is narrower than the ANSWERED outcome: it counts inbound answered calls only, so it reconciles exactly with get_call_statistics. ROWS ARE NOT ZERO-FILLED: a month, queue or hour with no matching call simply does not appear — do not read a missing row as a zero you can quote without saying so. `totals` repeats the same metrics over the WHOLE filtered range as a second, independent measurement; use it for shares of a whole, and always for `active_operators` / `unique_callers`, which are distinct counts and must never be summed across rows. GROUPING BY `tag` is the one exception to \"one row per call\": tags are many-to-many, so a call with three tags is counted once under EACH tag and untagged calls drop out entirely. Per-tag counts can therefore sum to more than the call total — `totals` never joins tags and stays per call. `totalGroups` is how many groups matched BEFORE `limit`; when `truncated` is true you are seeing only the first `limit` of them, so coarsen `groupBy`, narrow the range or add a filter rather than reporting the visible rows as complete. Days, weeks, months and hours are bucketed in the tenant timezone (override with `timezone`), NOT in UTC, and `day_of_week` runs 0 = Sunday … 6 = Saturday. A range may span at most 1100 days. Aggregates only: counts, rates and durations — never a caller, phone number, transcript or user id. Phone-number rows carry the line label, never its digits.",
      "descriptionPl": "Elastyczne agregacje połączeń: dowolne zestawienie liczby połączeń, odbieralności, czasu rozmów i oczekiwania w podziale na miesiąc, tydzień, dzień, godzinę, dzień tygodnia, kolejkę, linię, konsultanta, temat lub tag — do trzech lat w jednym zapytaniu.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "from": {
            "type": "string",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
            "description": "First calendar day of the range, inclusive (YYYY-MM-DD)."
          },
          "to": {
            "type": "string",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
            "description": "Last calendar day of the range, inclusive (YYYY-MM-DD)."
          },
          "timezone": {
            "description": "IANA timezone used to bucket days and hours. Defaults to the tenant timezone (usually Europe/Warsaw).",
            "type": "string",
            "minLength": 1
          },
          "groupBy": {
            "description": "Up to 3 distinct dimensions to break the numbers down by, outermost first. Omit (or pass []) for a single totals row. Time buckets, in the bucketing timezone: `month` (\"YYYY-MM\"), `week` (the ISO week's Monday, \"YYYY-MM-DD\"), `day` (\"YYYY-MM-DD\"), `hour_of_day` (0-23), `day_of_week` (0 = Sunday … 6 = Saturday). Call attributes: `direction` (INBOUND/OUTBOUND), `outcome` (see below), `status` (the raw call status), `disconnect_reason` (who or what ended the call; endings on the platform side are grouped as SYSTEM_ENDED), `origin` (STANDARD, or BOT_TRANSFER for the internal leg a bot creates when it hands a caller to a human). Entities: `queue`, `phone_number` (the phone line the call arrived on), `operator` (the agent who handled it), `topic` (what the call was about), `tag`. Name them in snake_case here; the row keys are camelCase (`hour_of_day` → `hourOfDay`).",
            "maxItems": 3,
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "month",
                "week",
                "day",
                "hour_of_day",
                "day_of_week",
                "direction",
                "outcome",
                "status",
                "disconnect_reason",
                "origin",
                "queue",
                "phone_number",
                "operator",
                "topic",
                "tag"
              ]
            }
          },
          "metrics": {
            "minItems": 1,
            "maxItems": 12,
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "calls",
                "inbound_calls",
                "outbound_calls",
                "answered_calls",
                "missed_calls",
                "answer_rate",
                "queue_abandoned_calls",
                "ivr_abandoned_calls",
                "outbound_connected_calls",
                "total_talk_seconds",
                "avg_talk_seconds",
                "median_talk_seconds",
                "max_talk_seconds",
                "avg_wait_seconds",
                "median_wait_seconds",
                "p90_wait_seconds",
                "max_wait_seconds",
                "active_operators",
                "unique_callers"
              ]
            },
            "description": "Between 1 and 12 distinct metrics. Counts: `calls` (every call in the group), `inbound_calls`, `outbound_calls`, `answered_calls` (INBOUND, completed, with an answer timestamp), `missed_calls` (INBOUND and ended NO_ANSWER / CANCELLED / BUSY / FAILED, or completed with no answer timestamp), `queue_abandoned_calls` (missed after reaching a queue), `ivr_abandoned_calls` (missed inside the IVR, before any queue), `outbound_connected_calls`. Rate: `answer_rate` = answered ÷ (answered + missed) in percent, inbound only, null when the group has no inbound traffic. Talk time in SECONDS over ANSWERED calls only: `total_talk_seconds`, `avg_talk_seconds`, `median_talk_seconds`, `max_talk_seconds`. Queue wait in SECONDS over calls that both entered and left a queue: `avg_wait_seconds`, `median_wait_seconds`, `p90_wait_seconds`, `max_wait_seconds`. Distinct counts, which CANNOT be summed across rows (read them from `totals` instead): `active_operators` (distinct agents who answered a call) and `unique_callers` (distinct inbound caller numbers — a COUNT, never a number). Name them in snake_case here; each comes back under its camelCase key (`answer_rate` → `answerRate`)."
          },
          "orderBy": {
            "description": "How to sort the rows before `limit` is applied. Default: if any time dimension was requested, those ascending in the order given; otherwise the first metric descending. Nulls always sort last.",
            "type": "object",
            "properties": {
              "by": {
                "type": "string",
                "enum": [
                  "month",
                  "week",
                  "day",
                  "hour_of_day",
                  "day_of_week",
                  "direction",
                  "outcome",
                  "status",
                  "disconnect_reason",
                  "origin",
                  "queue",
                  "phone_number",
                  "operator",
                  "topic",
                  "tag",
                  "calls",
                  "inbound_calls",
                  "outbound_calls",
                  "answered_calls",
                  "missed_calls",
                  "answer_rate",
                  "queue_abandoned_calls",
                  "ivr_abandoned_calls",
                  "outbound_connected_calls",
                  "total_talk_seconds",
                  "avg_talk_seconds",
                  "median_talk_seconds",
                  "max_talk_seconds",
                  "avg_wait_seconds",
                  "median_wait_seconds",
                  "p90_wait_seconds",
                  "max_wait_seconds",
                  "active_operators",
                  "unique_callers"
                ],
                "description": "Must be one of the dimensions or metrics THIS call requested; anything else is refused."
              },
              "direction": {
                "type": "string",
                "enum": [
                  "asc",
                  "desc"
                ]
              }
            },
            "required": [
              "by",
              "direction"
            ]
          },
          "limit": {
            "description": "Maximum number of groups to return. Default 500, maximum 2000.",
            "type": "integer",
            "minimum": 1,
            "maximum": 2000
          },
          "direction": {
            "description": "Restrict to inbound or outbound calls.",
            "type": "string",
            "enum": [
              "INBOUND",
              "OUTBOUND"
            ]
          },
          "outcome": {
            "description": "Restrict to one outcome bucket (ANSWERED / MISSED / OTHER).",
            "type": "string",
            "enum": [
              "ANSWERED",
              "MISSED",
              "OTHER"
            ]
          },
          "operatorIds": {
            "description": "Restrict to calls handled by these agents (user ids). Omit for every agent.",
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid",
              "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
            }
          },
          "queueIds": {
            "description": "Restrict to calls that went through these queues.",
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid",
              "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
            }
          },
          "phoneNumberIds": {
            "description": "Restrict to calls that arrived on these inbound phone numbers (lines).",
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid",
              "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
            }
          },
          "tagIds": {
            "description": "Restrict to calls carrying ANY of these tags.",
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid",
              "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
            }
          },
          "topicIds": {
            "description": "Restrict to calls filed under these call topics.",
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid",
              "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
            }
          }
        },
        "required": [
          "from",
          "to",
          "metrics"
        ]
      }
    },
    {
      "name": "aggregate_bot_sessions",
      "requiredScope": "analytics.read",
      "description": "Group-by/metric aggregation over voice-bot sessions (dispatches). USE THIS INSTEAD OF PAGING list_bot_sessions TO COUNT ANYTHING — one call answers \"sessions per month\", \"connect rate per bot per weekday\", \"median talk time by end reason\", and it returns no caller data at all. Pick 0–3 `groupBy` dimensions and 1+ `metrics`. DIMENSIONS — `month` (YYYY-MM), `week` (ISO week start, the Monday, as YYYY-MM-DD), `day` (YYYY-MM-DD), `hour_of_day` (0–23), `day_of_week` (0 = Sunday, 1 = Monday … 6 = Saturday), `bot` (emits `botId` + `botName`), `result` (the bot-reported call result from the structured remark field, e.g. CONFIRMED / RESCHEDULE / CANCELLED; a session with no result is `NONE`, and any value that is not an UPPER_SNAKE token is folded into `OTHER`, so free text a bot wrote can never appear as a key), `end_reason` (why the call ended, as the runtime recorded it — lower_snake tokens such as `completed`, `no_answer`, `customer_hangup`, `voicemail`, `call_failed`, `room_disconnected`; `NONE` when absent, `OTHER` for anything that is not a single-case snake token). METRICS — `sessions` (dispatches), `connected_sessions` (call duration > 0, i.e. the call actually carried audio), `connect_rate` (connected ÷ sessions as a PERCENT rounded to 1 decimal, null when there were no sessions), `total_seconds`, `total_minutes` (rounded from the group's total seconds, never from pre-rounded sub-buckets), `avg_duration_seconds`, `median_duration_seconds`, `max_duration_seconds` — the three duration metrics are computed over CONNECTED sessions only and are null when the group has none. RESPONSE — `rows` is flat (dimension keys then metric keys) and is NOT zero-filled: a month/bot/result combination with no sessions is simply absent, so do not read a missing row as a zero you can rely on. `totalGroups` is how many groups matched before `limit`, and `truncated` is true when `limit` cut the tail off — coarsen `groupBy` or narrow the range rather than paging. `totals` carries the SAME metrics over the whole filtered range with no grouping, so a share-of-total needs no second call (and must not be summed from `rows`, which may be truncated). RANGE — `from`/`to` are inclusive calendar days; a session is bucketed by when the dispatch was CREATED, in the timezone you pass (default: the tenant timezone, usually Europe/Warsaw). Up to 1100 days (about three years) — the answer is bounded by `limit`, not by the range, so ask for the whole period at once. Aggregates only: no phone number, caller, transcript, recording or remark text is returned, and bot NAMES are configuration, not people.",
      "descriptionPl": "Elastyczne agregacje rozmów botów: liczba sesji, skuteczność połączeń i minuty w podziale na miesiąc, tydzień, dzień, godzinę, dzień tygodnia, bota, wynik lub powód zakończenia — do trzech lat w jednym zapytaniu.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "from": {
            "type": "string",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
            "description": "First calendar day of the range, inclusive (YYYY-MM-DD)."
          },
          "to": {
            "type": "string",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
            "description": "Last calendar day of the range, inclusive (YYYY-MM-DD)."
          },
          "timezone": {
            "description": "IANA timezone used to bucket days and hours. Defaults to the tenant timezone (usually Europe/Warsaw).",
            "type": "string",
            "minLength": 1
          },
          "groupBy": {
            "description": "Up to 3 distinct dimensions to group by, outermost first. Omit (or pass an empty array) for a single all-up row over the whole range.",
            "maxItems": 3,
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "month",
                "week",
                "day",
                "hour_of_day",
                "day_of_week",
                "bot",
                "result",
                "end_reason"
              ]
            }
          },
          "metrics": {
            "minItems": 1,
            "maxItems": 8,
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "sessions",
                "connected_sessions",
                "connect_rate",
                "total_seconds",
                "total_minutes",
                "avg_duration_seconds",
                "median_duration_seconds",
                "max_duration_seconds"
              ]
            },
            "description": "One or more distinct metrics to compute for every group and for `totals`. See the tool description for what each one means."
          },
          "botIds": {
            "description": "Restrict to these bot ids (from list_bots). Omit for every bot in the organisation — an empty list is refused, because it would match nothing and read as a quiet month.",
            "minItems": 1,
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid",
              "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
            }
          },
          "results": {
            "description": "Restrict to sessions whose bucketed result is one of these tokens. Values must be UPPER_SNAKE (e.g. CONFIRMED); `NONE` selects sessions with no result and `OTHER` the ones whose value was not a token. Omit for every session.",
            "minItems": 1,
            "type": "array",
            "items": {
              "type": "string",
              "pattern": "^[A-Z][A-Z0-9_]{0,39}$"
            }
          },
          "connectedOnly": {
            "description": "When true, count only sessions whose call actually connected (call duration > 0). Affects `totals` as well as `rows`.",
            "type": "boolean"
          },
          "orderBy": {
            "description": "How to sort `rows`. Default: chronologically ascending when a time dimension is grouped by, otherwise the FIRST metric descending. Ties are broken by the grouped dimensions so pages are stable.",
            "type": "object",
            "properties": {
              "by": {
                "type": "string",
                "enum": [
                  "month",
                  "week",
                  "day",
                  "hour_of_day",
                  "day_of_week",
                  "bot",
                  "result",
                  "end_reason",
                  "sessions",
                  "connected_sessions",
                  "connect_rate",
                  "total_seconds",
                  "total_minutes",
                  "avg_duration_seconds",
                  "median_duration_seconds",
                  "max_duration_seconds"
                ],
                "description": "Must be one of the dimensions or metrics you requested in this same call; anything else is refused."
              },
              "direction": {
                "type": "string",
                "enum": [
                  "asc",
                  "desc"
                ],
                "description": "Sort direction (required, like the sibling tools)."
              }
            },
            "required": [
              "by",
              "direction"
            ]
          },
          "limit": {
            "default": 500,
            "description": "Maximum number of rows to return (1–2000, default 500). `totalGroups` and `truncated` tell you whether it bit.",
            "type": "integer",
            "minimum": 1,
            "maximum": 2000
          }
        },
        "required": [
          "from",
          "to",
          "metrics"
        ]
      }
    },
    {
      "name": "aggregate_agent_status",
      "requiredScope": "analytics.read",
      "description": "How much time agents spent in each availability status, and how many agents that was — sliced by `groupBy`, measured by `metrics`, in ONE call. This is the ONLY way to answer \"how many agents were active / logged in / on break at a given time\": the call log cannot, because an agent who was on shift but took no call in that hour leaves no trace in it. Typical question: agents in status \"aktywny\" per hour of each day. DISCOVER THE STATUS NAMES FIRST. Statuses are tenant configuration, so the names differ per organization. Call once with `groupBy: [\"status\"]` and no filter to see this tenant's `statusName` values (with their `statusDisplayName` and `canAcceptCall`), pick the ones that mean what you were asked about, then call again with `statusNames`. Never guess a status name. `canAcceptCall: true` is the name-free way to say \"reachable for calls\". YOU ASK IN snake_case, KEYS COME BACK IN camelCase. The `groupBy` / `metrics` / `orderBy.by` tokens are snake_case (`hour_of_day`, `agent_hours`); the keys on each row and in `totals` are camelCase (`hourOfDay`, `agentHours`), matching the rest of this API. Only these four differ: `hour_of_day` → `hourOfDay`, `day_of_week` → `dayOfWeek`, `status_seconds` → `statusSeconds`, `agent_hours` → `agentHours`. DIMENSIONS (up to 3, outermost first; omit for one totals row). Time, bucketed in the tenant timezone: `month` → key `month` (\"YYYY-MM\"), `week` → key `week` (the ISO week's Monday, \"YYYY-MM-DD\"), `day` → key `day` (\"YYYY-MM-DD\"), `hour_of_day` → key `hourOfDay` (0-23), `day_of_week` → key `dayOfWeek` (0 = Sunday … 6 = Saturday). Plus `status` → the keys `statusName`, `statusDisplayName`, `canAcceptCall`; and `operator` → `operatorRef`, an opaque tenant-scoped reference that is stable across calls and not reversible to a user id, plus `name` only when the key also holds the users.read scope. A user id or e-mail is never returned. METRICS. `status_seconds` (key `statusSeconds`) = total seconds in the status inside the group, summed over agents. `agent_hours` (key `agentHours`) = the same in hours; for a group that pins down ONE clock hour (`day` + `hour_of_day`, or `day_of_week` + `hour_of_day` over a single week) this number IS the average number of agents concurrently in that status during the hour — 3.0 means three agents all hour, 1.5 means three agents for half of it. `agents` (key `agents`) = how many DISTINCT agents were in the status for at least `minSecondsPerAgent` seconds (default 60) inside the group; it is a headcount and must NEVER be summed across rows — take the window-wide headcount from `totals`, which is a second, independent measurement over the whole filtered range. HOW THE TIME IS COUNTED. A status interval is split across the hours it spans, so a shift from 08:40 to 11:10 contributes 20 min to the 08:00 hour, 60 to each of 09:00 and 10:00, and 10 to the 11:00 hour. An interval still open right now is counted up to NOW, never to the end of the range, so hours that have not happened yet stay empty. OFFLINE TIME: when the organisation has configured which status means \"offline\", that status is excluded and the numbers describe time ON SHIFT — the response says so with `offlineStatusExcluded: true`. When it is `false` the organisation never configured one, so EVERY status is counted, logged-out time included: do not present those numbers as availability. In that case (and whenever you mean \"reachable\") filter with `canAcceptCall: true` or with explicit `statusNames`. Agents deactivated in the Portal are excluded. ROWS ARE NOT ZERO-FILLED: an hour, day or status with nobody on shift simply does not appear — do not read a missing row as a zero you can quote without saying so. `totalGroups` is how many groups matched BEFORE `limit`; when `truncated` is true you are seeing only the first `limit` of them, so coarsen `groupBy` or narrow the range rather than reporting the visible rows as complete. Days, weeks, months and hours are bucketed in the tenant timezone (override with `timezone`), NOT in UTC. A range may span at most 366 days; split longer questions. Aggregates only: seconds, hours and headcounts — never an individual status change, a timeline, a caller or a phone number.",
      "descriptionPl": "Dostępność konsultantów w czasie: ilu konsultantów było w danym statusie i jak długo — w podziale na godzinę, dzień, tydzień, miesiąc, status lub konsultanta (na podstawie historii statusów, nie połączeń).",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "from": {
            "type": "string",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
            "description": "First calendar day of the range, inclusive (YYYY-MM-DD)."
          },
          "to": {
            "type": "string",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
            "description": "Last calendar day of the range, inclusive (YYYY-MM-DD)."
          },
          "timezone": {
            "description": "IANA timezone used to bucket days and hours. Defaults to the tenant timezone (usually Europe/Warsaw).",
            "type": "string",
            "minLength": 1
          },
          "groupBy": {
            "description": "Up to 3 distinct dimensions to break the time down by, outermost first. Omit (or pass []) for a single totals row. Ask in snake_case; the ROW KEY is camelCase. Time buckets, in the bucketing timezone: `month` → key `month` (\"YYYY-MM\"), `week` → `week` (the ISO week's Monday, \"YYYY-MM-DD\"), `day` → `day` (\"YYYY-MM-DD\"), `hour_of_day` → key `hourOfDay` (0-23), `day_of_week` → key `dayOfWeek` (0 = Sunday … 6 = Saturday). Other: `status` (emits `statusName`, `statusDisplayName` and `canAcceptCall`) and `operator` (emits `operatorRef`, plus `name` only with the users.read scope — never a user id or e-mail).",
            "maxItems": 3,
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "month",
                "week",
                "day",
                "hour_of_day",
                "day_of_week",
                "status",
                "operator"
              ]
            }
          },
          "metrics": {
            "minItems": 1,
            "maxItems": 3,
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "status_seconds",
                "agent_hours",
                "agents"
              ]
            },
            "description": "Between 1 and 3 distinct metrics. Ask in snake_case; the ROW KEY is camelCase. `status_seconds` → key `statusSeconds`: total seconds agents spent in the status inside the group, summed over agents. `agent_hours` → key `agentHours`: the same figure in hours (2 decimals) — for a group that pins down ONE clock hour (e.g. `day` + `hour_of_day`) this is the AVERAGE NUMBER OF AGENTS concurrently in the status during that hour, so 2.5 means two and a half agents on average. `agents` → key `agents`: how many DISTINCT agents were in the status for at least `minSecondsPerAgent` seconds inside the group — a headcount, so it must never be summed across rows (read `totals` for the window-wide headcount)."
          },
          "orderBy": {
            "description": "How to sort the rows before `limit` is applied. Default: if any time dimension was requested, those ascending in the order given; otherwise the first metric descending. Nulls always sort last.",
            "type": "object",
            "properties": {
              "by": {
                "type": "string",
                "enum": [
                  "month",
                  "week",
                  "day",
                  "hour_of_day",
                  "day_of_week",
                  "status",
                  "operator",
                  "status_seconds",
                  "agent_hours",
                  "agents"
                ],
                "description": "Must be one of the dimensions or metrics THIS call requested; anything else is refused."
              },
              "direction": {
                "type": "string",
                "enum": [
                  "asc",
                  "desc"
                ]
              }
            },
            "required": [
              "by",
              "direction"
            ]
          },
          "limit": {
            "description": "Maximum number of groups to return. Default 500, maximum 2000.",
            "type": "integer",
            "minimum": 1,
            "maximum": 2000
          },
          "statusNames": {
            "description": "Restrict to these exact status names (the `statusName` values, not the display names). Statuses are tenant configuration and differ per organization — call this tool once with `groupBy: [\"status\"]` to list the tenant's own names before filtering by them.",
            "maxItems": 20,
            "type": "array",
            "items": {
              "type": "string",
              "minLength": 1,
              "maxLength": 100
            }
          },
          "canAcceptCall": {
            "description": "Restrict to statuses that made the agent reachable for calls (true) or unreachable (false). Use this when you want \"available\" without knowing the tenant's status names.",
            "type": "boolean"
          },
          "operatorIds": {
            "description": "Restrict to these agents (user ids). Omit for every agent.",
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid",
              "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
            }
          },
          "minSecondsPerAgent": {
            "description": "How many seconds an agent must have spent in the status inside a group before the `agents` metric counts them. Defaults to 60, so a status that merely flickered across an hour boundary does not read as an agent on shift. Affects `agents` only, never `status_seconds` / `agent_hours`.",
            "type": "integer",
            "minimum": 0,
            "maximum": 3600
          }
        },
        "required": [
          "from",
          "to",
          "metrics"
        ]
      }
    },
    {
      "name": "get_call_statistics",
      "requiredScope": "analytics.read",
      "description": "Call volume and handling for a date range: total / inbound / outbound calls, a per-day time series, answered vs missed inbound calls with the reason breakdown, and the outcome of the automatic callbacks placed for missed calls. Mirrors the \"Call volume\" and \"Call handling\" cards of the Portal analytics page. A call counts as MISSED when it is inbound and ended as NO_ANSWER, CANCELLED, BUSY or FAILED, or completed with no answer timestamp (the caller hung up while it rang). Everything else inbound that has an answer timestamp is ANSWERED. `answerRates.overall` is answered ÷ (answered + missed); `answerRates.queueEligible` excludes callers who hung up inside the IVR before ever reaching a queue, so it measures the staffed part of the funnel. `missedBreakdown` splits missed calls into queueAbandoned (gave up waiting in a queue), ivrAbandoned (gave up in the IVR, pre-queue) and other. Callback `successRate` is connected ÷ all attempts with a result, in percent. Every day in the range appears in `data`, zero-filled. A missed call is never attributed to an agent, so with `operatorIds` set the answered/missed split is ~100% answered by construction; `operatorOffers` is the number to use then: `callsOffered` = distinct calls that rang at ANY selected agent (an offer cancelled because a colleague accepted the call first, because it never reached the agent, because the call ended at the moment of acceptance, or because the agent was already on, had just accepted, or was dialing or on an outbound call (a different call) is not counted, and neither is an offer that expired while she was already handling another call or an expired duplicate of a call she had already accepted), `callsAnswered` = those a selected agent accepted, `rate` = round(callsAnswered ÷ callsOffered × 100), null when nothing was offered. `operatorOffers` is null unless `operatorIds` is passed. Aggregates only: counts, rates, durations and time series — never a caller, phone number or transcript. Days and hours are bucketed in the tenant timezone (override with `timezone`). A range may span at most 366 days; split longer questions.",
      "descriptionPl": "Liczba połączeń dziennie oraz odsetek odebranych/nieodebranych i wyniki oddzwonień w zakresie dat (Analityka → Przegląd).",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "from": {
            "type": "string",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
            "description": "First calendar day of the range, inclusive (YYYY-MM-DD)."
          },
          "to": {
            "type": "string",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
            "description": "Last calendar day of the range, inclusive (YYYY-MM-DD)."
          },
          "timezone": {
            "description": "IANA timezone used to bucket days and hours. Defaults to the tenant timezone (usually Europe/Warsaw).",
            "type": "string",
            "minLength": 1
          },
          "operatorIds": {
            "description": "Restrict to calls handled by these agents (user ids). Omit for every agent.",
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid",
              "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
            }
          },
          "queueIds": {
            "description": "Restrict to calls that went through these queues.",
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid",
              "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
            }
          },
          "phoneNumberIds": {
            "description": "Restrict to calls that arrived on these inbound phone numbers (lines).",
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid",
              "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
            }
          },
          "tagIds": {
            "description": "Restrict to calls carrying ANY of these tags. Requires the analytics-tags feature for the tenant to be meaningful.",
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid",
              "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
            }
          }
        },
        "required": [
          "from",
          "to"
        ]
      }
    },
    {
      "name": "get_call_time_distribution",
      "requiredScope": "analytics.read",
      "description": "When calls arrive: a weekday x hour heatmap of call counts for the range. Mirrors the \"Call distribution\" heatmap of the Portal analytics page. `dayOfWeek`: 0 = Sunday … 6 = Saturday. `hour` is 0-23. Both are taken in the bucketing timezone, NOT in UTC, so the answer already reflects the organization's local clock and daylight-saving changes. Only weekday/hour pairs with at least one call are returned; `maxCount` is the busiest single cell and `totalCalls` the sum over the grid. Aggregates only: counts, rates, durations and time series — never a caller, phone number or transcript. Days and hours are bucketed in the tenant timezone (override with `timezone`). A range may span at most 366 days; split longer questions.",
      "descriptionPl": "Ile połączeń przyszło w każdej komórce dzień tygodnia × godzina (Analityka → mapa cieplna).",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "from": {
            "type": "string",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
            "description": "First calendar day of the range, inclusive (YYYY-MM-DD)."
          },
          "to": {
            "type": "string",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
            "description": "Last calendar day of the range, inclusive (YYYY-MM-DD)."
          },
          "timezone": {
            "description": "IANA timezone used to bucket days and hours. Defaults to the tenant timezone (usually Europe/Warsaw).",
            "type": "string",
            "minLength": 1
          },
          "operatorIds": {
            "description": "Restrict to calls handled by these agents (user ids). Omit for every agent.",
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid",
              "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
            }
          },
          "queueIds": {
            "description": "Restrict to calls that went through these queues.",
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid",
              "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
            }
          },
          "phoneNumberIds": {
            "description": "Restrict to calls that arrived on these inbound phone numbers (lines).",
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid",
              "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
            }
          },
          "tagIds": {
            "description": "Restrict to calls carrying ANY of these tags. Requires the analytics-tags feature for the tenant to be meaningful.",
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid",
              "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
            }
          }
        },
        "required": [
          "from",
          "to"
        ]
      }
    },
    {
      "name": "get_wait_time_statistics",
      "requiredScope": "analytics.read",
      "description": "How long callers wait in queue, in SECONDS, two ways: per queue (`queues`, with average / median / p95 / max wait and call count) and per hour of the day (`byHour`). Mirrors the \"Queue times\" and \"Wait time by hour\" cards of the Portal analytics page. Only calls that both entered AND left a queue are measured, so abandoned-in-IVR calls are absent. `overallAvg` is weighted by call count, i.e. the average wait per CALL, not the unweighted average across queues. `byHour.data` buckets by the hour the call ENTERED the queue, in the bucketing timezone; `byHour.summary.peakWaitHour` is the hour with the highest median wait, not the busiest hour, and its wait fields are null when no call queued in the range. `tagIds` filters the per-queue half only — the hourly half has no tag filter. Aggregates only: counts, rates, durations and time series — never a caller, phone number or transcript. Days and hours are bucketed in the tenant timezone (override with `timezone`). A range may span at most 366 days; split longer questions.",
      "descriptionPl": "Czasy oczekiwania w kolejkach — per kolejka i per godzina: średnia, mediana, p95 (Analityka → czasy w kolejce).",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "from": {
            "type": "string",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
            "description": "First calendar day of the range, inclusive (YYYY-MM-DD)."
          },
          "to": {
            "type": "string",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
            "description": "Last calendar day of the range, inclusive (YYYY-MM-DD)."
          },
          "timezone": {
            "description": "IANA timezone used to bucket days and hours. Defaults to the tenant timezone (usually Europe/Warsaw).",
            "type": "string",
            "minLength": 1
          },
          "operatorIds": {
            "description": "Restrict to calls handled by these agents (user ids). Omit for every agent.",
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid",
              "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
            }
          },
          "queueIds": {
            "description": "Restrict to calls that went through these queues.",
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid",
              "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
            }
          },
          "phoneNumberIds": {
            "description": "Restrict to calls that arrived on these inbound phone numbers (lines).",
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid",
              "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
            }
          },
          "tagIds": {
            "description": "Restrict to calls carrying ANY of these tags. Requires the analytics-tags feature for the tenant to be meaningful.",
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid",
              "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
            }
          }
        },
        "required": [
          "from",
          "to"
        ]
      }
    },
    {
      "name": "get_abandonment_statistics",
      "requiredScope": "analytics.read",
      "description": "Where and when callers give up: a weekday x hour heatmap of abandoned inbound calls, split into queueAbandonments (reached a queue, hung up while waiting) and ivrAbandonments (hung up inside the IVR, before any queue). Mirrors the \"Abandonment heatmap\" card of the Portal analytics page. A call counts as MISSED when it is inbound and ended as NO_ANSWER, CANCELLED, BUSY or FAILED, or completed with no answer timestamp (the caller hung up while it rang). Everything else inbound that has an answer timestamp is ANSWERED. `dayOfWeek`: 0 = Sunday … 6 = Saturday; `hour` is 0-23; both are taken in the bucketing timezone. Wait fields are SECONDS spent in queue before abandoning and cover the queue half only (null when nobody abandoned a queue). `summary.peakHour` / `summary.peakDayOfWeek` point at the worst single cell. This card has no tag filter. Aggregates only: counts, rates, durations and time series — never a caller, phone number or transcript. Days and hours are bucketed in the tenant timezone (override with `timezone`). A range may span at most 366 days; split longer questions.",
      "descriptionPl": "Porzucone połączenia wg dnia tygodnia × godziny, z podziałem na kolejkę i IVR (Analityka → Obsada).",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "from": {
            "type": "string",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
            "description": "First calendar day of the range, inclusive (YYYY-MM-DD)."
          },
          "to": {
            "type": "string",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
            "description": "Last calendar day of the range, inclusive (YYYY-MM-DD)."
          },
          "timezone": {
            "description": "IANA timezone used to bucket days and hours. Defaults to the tenant timezone (usually Europe/Warsaw).",
            "type": "string",
            "minLength": 1
          },
          "operatorIds": {
            "description": "Restrict to calls handled by these agents (user ids). Omit for every agent.",
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid",
              "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
            }
          },
          "queueIds": {
            "description": "Restrict to calls that went through these queues.",
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid",
              "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
            }
          },
          "phoneNumberIds": {
            "description": "Restrict to calls that arrived on these inbound phone numbers (lines).",
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid",
              "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
            }
          }
        },
        "required": [
          "from",
          "to"
        ]
      }
    },
    {
      "name": "get_staffing_analysis",
      "requiredScope": "analytics.read",
      "description": "Erlang-C staffing comparison per hour of the day: offered calls, agents actually logged in, agents REQUIRED to hit the service-level target, and the resulting gap. Mirrors the \"Staffing gaps\" card of the Portal analytics page. All 24 hours are returned, including hours with no traffic; `label` is \"HH:00\" in the bucketing timezone. `callsCount` is the total over the range while the model uses calls per DAY at that hour, so a wide range does not inflate the requirement. `staffingGap` is requiredAgents − actualAgents (0 when adequately staffed); `currentServiceLevel` and `targetServiceLevel` are percentages of calls answered within the SLA threshold. When `slaThresholdSeconds` is omitted the threshold is derived from the median wait of callers who abandoned in the range (`summary.autoCalculatedThreshold` reports this). Agents with no handle-time data are modelled at 180 s. This card has no tag filter. Aggregates only: counts, rates, durations and time series — never a caller, phone number or transcript. Days and hours are bucketed in the tenant timezone (override with `timezone`). A range may span at most 366 days; split longer questions.",
      "descriptionPl": "Luki w obsadzie wg Erlanga C: wymagana vs. faktyczna liczba agentów i poziom obsługi per godzina (Analityka → Obsada).",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "from": {
            "type": "string",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
            "description": "First calendar day of the range, inclusive (YYYY-MM-DD)."
          },
          "to": {
            "type": "string",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
            "description": "Last calendar day of the range, inclusive (YYYY-MM-DD)."
          },
          "timezone": {
            "description": "IANA timezone used to bucket days and hours. Defaults to the tenant timezone (usually Europe/Warsaw).",
            "type": "string",
            "minLength": 1
          },
          "operatorIds": {
            "description": "Restrict to calls handled by these agents (user ids). Omit for every agent.",
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid",
              "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
            }
          },
          "queueIds": {
            "description": "Restrict to calls that went through these queues.",
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid",
              "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
            }
          },
          "phoneNumberIds": {
            "description": "Restrict to calls that arrived on these inbound phone numbers (lines).",
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid",
              "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
            }
          },
          "dayOfWeek": {
            "description": "Restrict to one weekday, Monday = 0 … Sunday = 6. Omit to average over every day in the range.",
            "type": "integer",
            "minimum": 0,
            "maximum": 6
          },
          "slaThresholdSeconds": {
            "description": "Answer-within target in seconds (e.g. 20). Omit to derive it from the median abandon wait in the range.",
            "type": "integer",
            "minimum": 1,
            "maximum": 9007199254740991
          },
          "slaTargetPercent": {
            "description": "Share of calls that must be answered within the threshold, in percent. Defaults to 80.",
            "type": "integer",
            "minimum": 1,
            "maximum": 100
          }
        },
        "required": [
          "from",
          "to"
        ]
      }
    },
    {
      "name": "get_operator_statistics",
      "requiredScope": "analytics.read",
      "description": "Per-agent call performance plus how the team spent its time. Mirrors the \"Operator statistics\" table and the status-time card of the Portal analytics page. Each row in `operators` is keyed by `operatorRef`, an opaque tenant-scoped reference that is stable across calls and not reversible to a user id; the agent's display `name` is included only when the key also holds the users.read scope, and the user id, email and avatar are never returned. `totalCalls` = answered inbound + outbound calls the agent handled; `avgDuration` is seconds of call time. Offer counts (all within the range, filters applied to the offered call): `callsOffered` = distinct calls that rang at the agent (a call that rang twice counts once; an offer cancelled because a colleague accepted the same call first, because it never reached the agent (delivery failure), because the call ended at the moment she accepted, or because she was already on, had just accepted, or was dialing or on an outbound call (a different call) is not counted, and neither is an offer that expired while she was already handling another call or an expired duplicate of a call she had already accepted), `callsAnswered` = offered calls the agent accepted (`acceptedOffers` is the same number, legacy name), `callsUnanswered` = callsOffered − callsAnswered, `callersOffered` / `callersUnanswered` = how many distinct caller numbers those offered / not-accepted calls came from (COUNTS only — no number is ever returned). Per-OFFER counts, over offers on calls she did not answer (they break down `callsUnanswered`; an offer on a call she later answered herself is not included): `expiredOffers` (\"wygasłe\") = offers that rang for the offer timeout without being picked up, after which the queue moved on (an expire while she was already on another call, or a duplicate expire after she accepted that same call, is not included); `rejectedOffers` = offers the agent declined; `cancelledOffers` = offers interrupted because the agent changed status or the caller hung up while it rang (the not-counted cancels above are excluded). `answerRate` = round(callsAnswered ÷ callsOffered × 100), null for an agent who was never offered a call — it is NOT the tenant-wide answered/missed rate, which get_call_statistics reports. `totals` sums callsOffered / callsAnswered over the agents (a call offered to two agents counts twice) and gives `offerAnswerRate` from those sums. `statusSummary` is the tenant-wide time in seconds per availability status over the range, excluding the configured offline status, so it describes time on shift; `canAcceptCall` marks the statuses that made an agent reachable. Aggregates only: counts, rates, durations and time series — never a caller, phone number or transcript. Days and hours are bucketed in the tenant timezone (override with `timezone`). A range may span at most 366 days; split longer questions.",
      "descriptionPl": "Zespołowe sumy obsługi połączeń i czasu w statusach; wiersze per operator są zanonimizowane, chyba że klucz może czytać użytkowników.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "from": {
            "type": "string",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
            "description": "First calendar day of the range, inclusive (YYYY-MM-DD)."
          },
          "to": {
            "type": "string",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
            "description": "Last calendar day of the range, inclusive (YYYY-MM-DD)."
          },
          "timezone": {
            "description": "IANA timezone used to bucket days and hours. Defaults to the tenant timezone (usually Europe/Warsaw).",
            "type": "string",
            "minLength": 1
          },
          "operatorIds": {
            "description": "Restrict to calls handled by these agents (user ids). Omit for every agent.",
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid",
              "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
            }
          },
          "queueIds": {
            "description": "Restrict to calls that went through these queues.",
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid",
              "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
            }
          },
          "phoneNumberIds": {
            "description": "Restrict to calls that arrived on these inbound phone numbers (lines).",
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid",
              "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
            }
          }
        },
        "required": [
          "from",
          "to"
        ]
      }
    },
    {
      "name": "get_ivr_statistics",
      "requiredScope": "analytics.read",
      "description": "Where inbound callers drop out of the IVR: the node each abandoned call was last on, how many dropped there, and the disconnect reasons (endings on the platform side are grouped as SYSTEM_ENDED). Mirrors the \"IVR drop-off\" card of the Portal analytics page. Only calls that reached a menu node and were never answered are counted. `totalDropoffsOverall` sums the per-node counts (a caller who hung up in the IVR after queueing still appears); `totalDropoffsPreQueue` counts only callers who gave up BEFORE any queue, which is the number to quote for \"lost in the IVR\". `totalDropoffs` is a legacy alias of `totalDropoffsOverall`. `byVersion` compares flow versions (completionRate = entered queue or answered ÷ entered IVR, 0-1) and is populated only for tenants with the IVR experiments feature AND exactly one phoneNumberIds entry, because versions are comparable only inside one phone number's flow family; it is an empty array otherwise. Aggregates only: counts, rates, durations and time series — never a caller, phone number or transcript. Days and hours are bucketed in the tenant timezone (override with `timezone`). A range may span at most 366 days; split longer questions.",
      "descriptionPl": "Gdzie dzwoniący porzucają IVR — per węzeł i per wersja przepływu (Analityka → IVR).",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "from": {
            "type": "string",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
            "description": "First calendar day of the range, inclusive (YYYY-MM-DD)."
          },
          "to": {
            "type": "string",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
            "description": "Last calendar day of the range, inclusive (YYYY-MM-DD)."
          },
          "timezone": {
            "description": "IANA timezone used to bucket days and hours. Defaults to the tenant timezone (usually Europe/Warsaw).",
            "type": "string",
            "minLength": 1
          },
          "operatorIds": {
            "description": "Restrict to calls handled by these agents (user ids). Omit for every agent.",
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid",
              "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
            }
          },
          "queueIds": {
            "description": "Restrict to calls that went through these queues.",
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid",
              "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
            }
          },
          "phoneNumberIds": {
            "description": "Restrict to calls that arrived on these inbound phone numbers (lines).",
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid",
              "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
            }
          },
          "tagIds": {
            "description": "Restrict to calls carrying ANY of these tags. Requires the analytics-tags feature for the tenant to be meaningful.",
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid",
              "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
            }
          },
          "flowVersionIds": {
            "description": "Restrict to these IVR flow versions. Ignored unless the tenant has the IVR experiments feature and exactly one phoneNumberIds entry is given.",
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid",
              "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
            }
          }
        },
        "required": [
          "from",
          "to"
        ]
      }
    },
    {
      "name": "get_call_topic_statistics",
      "requiredScope": "analytics.read",
      "description": "What calls were ABOUT: the per-topic call counts, and — for tenants with the analytics-tags feature — the per-tag counts. Mirrors the \"Topics\" and \"Calls by tag\" cards of the Portal analytics page. A topic's `source` says whether a human set it (USER) or call analysis inferred it (AI). `uncategorized` counts calls with no live topic. Percentages are of ALL calls in the range, so topics plus uncategorized need not sum to 100. A call may carry several tags and is counted once per tag, so tag percentages can legitimately exceed 100 in total — the denominator is the independent call total, never the sum of the per-tag counts. When the tenant does not have the tags feature, `tags` is null and `tagsAvailable` is false; the topic half is unaffected. Tag names are tenant configuration. Topic names are labels: `source: \"USER\"` topics are tenant-authored, `source: \"AI\"` topics were named by the call-topic extractor from call content — short category labels, never a transcript, but not hand-configured either. Aggregates only: counts, rates, durations and time series — never a caller, phone number or transcript. Days and hours are bucketed in the tenant timezone (override with `timezone`). A range may span at most 366 days; split longer questions.",
      "descriptionPl": "Liczba połączeń wg tematu i wg tagu (Analityka → tematy i tagi).",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "from": {
            "type": "string",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
            "description": "First calendar day of the range, inclusive (YYYY-MM-DD)."
          },
          "to": {
            "type": "string",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
            "description": "Last calendar day of the range, inclusive (YYYY-MM-DD)."
          },
          "timezone": {
            "description": "IANA timezone used to bucket days and hours. Defaults to the tenant timezone (usually Europe/Warsaw).",
            "type": "string",
            "minLength": 1
          },
          "operatorIds": {
            "description": "Restrict to calls handled by these agents (user ids). Omit for every agent.",
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid",
              "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
            }
          },
          "queueIds": {
            "description": "Restrict to calls that went through these queues.",
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid",
              "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
            }
          },
          "phoneNumberIds": {
            "description": "Restrict to calls that arrived on these inbound phone numbers (lines).",
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid",
              "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
            }
          },
          "tagIds": {
            "description": "Restrict to calls carrying ANY of these tags. Requires the analytics-tags feature for the tenant to be meaningful.",
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid",
              "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
            }
          }
        },
        "required": [
          "from",
          "to"
        ]
      }
    },
    {
      "name": "get_contact_request_statistics",
      "requiredScope": "analytics.read",
      "description": "Aggregated contact-request (callback request) statistics — the numbers on the Portal \"Prośby o kontakt\" analytics card. Cohort: every non-deleted contact request CREATED inside the range, so duration metrics describe that cohort even when the request was resolved after `to`. Returns: `summary` (total, count per status, mean callback attempts across COMPLETED requests), `volume` (one point per day: total + completed), `bySource` and `byPriority` breakdowns, `reactionTime` (createdAt → first callback attempt, falling back to handledAt) and `completionTime` (createdAt → handledAt, still-open requests excluded) as average/median/count in SECONDS. Days are bucketed in the timezone you pass, defaulting to the tenant timezone. Range is inclusive on both ends and capped at 366 days. Aggregates only: no contact, phone number, note or staff identity is returned.",
      "descriptionPl": "Wolumen próśb o kontakt, podział wg statusu/źródła/priorytetu oraz czasy reakcji i realizacji (Analityka → prośby o kontakt).",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "from": {
            "type": "string",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
            "description": "First calendar day of the range, inclusive (YYYY-MM-DD)."
          },
          "to": {
            "type": "string",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
            "description": "Last calendar day of the range, inclusive (YYYY-MM-DD)."
          },
          "timezone": {
            "description": "IANA timezone used to bucket days and hours. Defaults to the tenant timezone (usually Europe/Warsaw).",
            "type": "string",
            "minLength": 1
          }
        },
        "required": [
          "from",
          "to"
        ]
      }
    },
    {
      "name": "get_visit_confirmation_statistics",
      "requiredScope": "analytics.read",
      "description": "Aggregated visit-confirmation bot statistics — the Portal \"Wizyty → Statystyki\" dashboard. Returns `kpis` (each with value, previous-period value and delta), `daily` volume, `byLocation` rows (location and integration NAMES plus counts), the outcome `funnel`, `skipReasons`, `issueReasons`, an `attempts` histogram (1/2/3/4+/SMS buckets showing how many attempts it took to finalise) and `answerability` (pick-up rate by hour and weekday plus the configured call window, for \"are my calling hours right?\"). ⚠️ This dashboard ALWAYS buckets days and hours in Europe/Warsaw — the `timezone` argument is ignored here and `range.timezone` always echoes Europe/Warsaw. Range is inclusive on both ends and capped at 366 days. Aggregates only: no patient, phone number or transcript.",
      "descriptionPl": "KPI bota potwierdzającego wizyty: połączenia dziennie, lejek wyników, wyniki per lokalizacja, próby i odbieralność (Terminarz → statystyki potwierdzeń).",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "from": {
            "type": "string",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
            "description": "First calendar day of the range, inclusive (YYYY-MM-DD)."
          },
          "to": {
            "type": "string",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
            "description": "Last calendar day of the range, inclusive (YYYY-MM-DD)."
          },
          "timezone": {
            "description": "IGNORED by this tool — the visit-confirmation dashboard buckets in Europe/Warsaw regardless. Accepted only so the argument shape matches the other analytics tools.",
            "type": "string",
            "minLength": 1
          },
          "locationIds": {
            "description": "Restrict to these location ids. Omit for every location, including appointments with no location assigned.",
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid",
              "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
            }
          }
        },
        "required": [
          "from",
          "to"
        ]
      }
    },
    {
      "name": "get_bot_statistics",
      "requiredScope": "analytics.read",
      "description": "Aggregated voice-bot dispatch statistics — the Portal \"Boty\" dashboard. Returns `totalBots` / `activeBots` (current configuration, not range-bound), `dispatches` (`total` = every dispatch ever recorded for the selected bots, `period` = dispatches inside the range, `monthly` = an alias of `period`, `today` = dispatches whose local day is today and 0 when today falls outside the range), `minutes` (whole minutes of call time over the range, rounded from total seconds), `results.breakdown` (count per bot-reported call result such as CONFIRMED / RESCHEDULE / CANCELLED, taken from the structured remark field; any non-token value is folded into `other`, so no free text is ever a key), `daily` (dispatches and rounded minutes per day) and `byBot` (per-bot dispatches and minutes, bot NAME only, busiest first). Days are bucketed in the timezone you pass, defaulting to the tenant timezone — note the Portal page itself buckets in UTC, so daily rows can differ by one day from the screen. Range is inclusive on both ends and capped at 366 days. Aggregates only: no phone number, transcript or recording.",
      "descriptionPl": "Zlecenia i minuty botów dziennie i per bot, z podziałem na wyniki (pulpit Botów).",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "from": {
            "type": "string",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
            "description": "First calendar day of the range, inclusive (YYYY-MM-DD)."
          },
          "to": {
            "type": "string",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
            "description": "Last calendar day of the range, inclusive (YYYY-MM-DD)."
          },
          "timezone": {
            "description": "IANA timezone used to bucket days and hours. Defaults to the tenant timezone (usually Europe/Warsaw).",
            "type": "string",
            "minLength": 1
          },
          "botIds": {
            "description": "Restrict to these bot ids (from list_bots). Omit for every bot in the organisation.",
            "minItems": 1,
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid",
              "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
            }
          }
        },
        "required": [
          "from",
          "to"
        ]
      }
    },
    {
      "name": "get_bot_knowledge_statistics",
      "requiredScope": "analytics.read",
      "description": "Aggregated knowledge-base retrieval statistics for ONE bot — the Portal bot \"Baza wiedzy → Analityka\" tab. Returns `totals` (searches, hits, noAnswer, timeouts, errors, skipped, and `hitRate` = hits ÷ (searches − skipped), i.e. skipped retrievals are excluded from the denominator because nothing was actually searched), `latency` (avg / p50 / p95 in milliseconds, over non-skipped retrievals), `daily` (searches, hits and noAnswer per day) and `topSources` (the 20 most-used knowledge sources by article TITLE, with how many retrievals involved each and when it was last used). ⚠️ The window is a ROLLING 7/30/90×24 h period ending now, not a calendar range — there is no from/to here; `range` echoes the calendar days that window touches in the tenant timezone, and `daily` buckets are UTC days. Aggregates only: the raw caller questions behind these searches (recent searches) are NEVER returned by this tool — read them in the Portal.",
      "descriptionPl": "Sumy wyszukiwań w bazie wiedzy, trafność, opóźnienia i aktywność dzienna jednego bota — bez treści pytań.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "botId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
            "description": "Bot whose knowledge retrievals to summarise (from list_bots)."
          },
          "days": {
            "default": 30,
            "description": "Length of the rolling window ending now, in days. One of 7, 30 or 90.",
            "anyOf": [
              {
                "type": "number",
                "const": 7
              },
              {
                "type": "number",
                "const": 30
              },
              {
                "type": "number",
                "const": 90
              }
            ]
          }
        },
        "required": [
          "botId"
        ]
      }
    },
    {
      "name": "get_chat_statistics",
      "requiredScope": "analytics.read",
      "description": "Aggregated website-chat statistics — the Portal \"Czat → Analityka\" dashboard. Returns `overview` (total conversations, count per status, resolution and abandonment rates, average first-response and resolution times in SECONDS, average CSAT with its response count, reopen rate), `topics` (conversation tags with count and share of the period), `queue` (LIVE, not range-bound: current queue depth, average wait time, abandonment rate, and `longestWaitingSince` = when the longest-waiting conversation entered the queue) and `operators` (per-agent conversations handled, average response and resolution time, resolution rate, CSAT). Each operator row is an opaque `operatorRef` that is stable across calls within this organisation; a human-readable `name` appears only if the key also holds the `users.read` scope. Days are bucketed in the timezone you pass, defaulting to the tenant timezone. Range is inclusive on both ends and capped at 366 days. Aggregates only: no message text, visitor identity or conversation id. Only available to organisations with the website-chat feature enabled.",
      "descriptionPl": "Przegląd czatu WWW: rozmowy, odsetek rozwiązanych i porzuconych, czasy odpowiedzi, CSAT, tematy i bieżąca kolejka.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "from": {
            "type": "string",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
            "description": "First calendar day of the range, inclusive (YYYY-MM-DD)."
          },
          "to": {
            "type": "string",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
            "description": "Last calendar day of the range, inclusive (YYYY-MM-DD)."
          },
          "timezone": {
            "description": "IANA timezone used to bucket days and hours. Defaults to the tenant timezone (usually Europe/Warsaw).",
            "type": "string",
            "minLength": 1
          }
        },
        "required": [
          "from",
          "to"
        ]
      }
    },
    {
      "name": "get_task_statistics",
      "requiredScope": "analytics.read",
      "description": "Aggregated task statistics — the Portal supervisor task summary. Returns `totals` (open, overdue, due today, completed in range, unassigned open, and completion rate = completed ÷ (completed + open)), `statusBreakdown` (every task by status, CANCELLED included), `priorityBreakdown` (OPEN tasks by priority) and `perAssignee` rows. Open means SUGGESTED / TODO / IN_PROGRESS / ON_HOLD. Only the \"completed in range\" buckets are range-bound — open, overdue and due-today counts are always live. Per-assignee rows also carry AI-suggestion triage quality: decisions made, acceptance rate, reason-capture rate, median decision latency in ms, and a `cohort` label (evaluator vs bulk dismisser). Each row is an opaque `operatorRef` stable across calls within this organisation, with a human-readable `name` only if the key also holds the `users.read` scope; the unassigned row has `operatorRef: null`. ⚠️ This tool takes `rangeDays`, NOT from/to — the underlying view has no calendar range. `rangeDays: 1` means the tenant-local calendar day; anything larger is a rolling N×24 h window ending now. Day boundaries (overdue, due today) are computed in the tenant timezone. Aggregates only: no task title, description, comment, contact or user id.",
      "descriptionPl": "Sumy zadań, podział wg statusu i priorytetu oraz odsetek ukończonych za ostatnie N dni (Zadania → widok przełożonego).",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "rangeDays": {
            "default": 7,
            "description": "Length of the completion window ending now, in days (1–180). 1 = the tenant-local calendar day; larger values are a rolling N×24 h window.",
            "type": "integer",
            "minimum": 1,
            "maximum": 180
          }
        }
      }
    },
    {
      "name": "get_quality_control_summary",
      "requiredScope": "analytics.read",
      "description": "Quality control summary / call evaluation results for the team — the same numbers as the Portal \"Kontrola jakości\" → \"Wyniki\" view. Use this when asked how the team or an employee is scoring, which rules fail most often, or what share of calls had a critical error. Do not page list_calls (or any call list) looking for scores. Returns aggregates only, computed by the same service as the page. `summary.current` and `summary.previous` are this period and the previous one of the same length: evaluatedCalls (calls with a completed analysis; the latest run per call), eligibleCalls (answered human calls of at least 30 seconds that could have been analysed; null when filtered to one conversation type), coverage (evaluated ÷ eligible, capped at 1), callsWithoutCritical (calls with no failed critical rule, over calls that had at least one applicable rule), standardsMet (met checks ÷ applicable checks; \"not applicable\" checks are excluded from every denominator) and averageCallScore (mean per-call score, 0–100, over calls with an applicable rule). Each share is { x, n, rate, low, high, confidence }: x of n, the Wilson 95% interval, and a confidence class. For standardsMet the confidence counts scored CALLS, not rule checks. `rules` is every evaluation rule, most failures first — applicable, met, failed (not met), notApplicable and passRate. The Wyniki \"most frequent problems\" card is the first 8 entries with failed > 0. When `userId` is set, each rule also carries teamPassRate and `summary.team` is the whole team's totals. `employees` is one row per employee who had an evaluated call, plus a final row with operatorRef null for calls with no attributed employee, in the portal's order. Each row has the same rates and at most one `improvement` (the rule whose shrunk pass rate is furthest below the team, needing at least 3 applicable checks). An employee is an opaque `operatorRef` — stable across calls for this tenant, not reversible to a user id. A display `name` is included only when the key also holds users.read. `userId` may be \"me\" on an OAuth connection (the signed-in user); an API key has no caller, so \"me\" is refused there. Emails, user ids, phone numbers, transcripts, recordings, AI quotes, reasoning and individual calls are never returned. Days are Europe/Warsaw calendar days, the same clock the Wyniki page uses; there is no timezone argument. The default period is the last 30 days. A custom period may span at most 366 days. Requires analytics.read, which an OAuth sign-in grants only from the Portal permission analytics.view — the permission the Wyniki team view and its per-employee list enforce.",
      "descriptionPl": "Wyniki kontroli jakości: oceny zespołu i pracowników, udział rozmów bez błędu krytycznego i spełnionych standardów oraz reguły, które najczęściej nie wychodzą (Kontrola jakości → Wyniki). Same zestawienia.",
      "inputSchema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "period": {
            "default": "30d",
            "description": "Reporting window, the same presets as the Wyniki page. \"30d\" (the default) is the last 30 Europe/Warsaw calendar days including today; \"7d\" and \"90d\" likewise. \"custom\" requires `from` and `to`.",
            "type": "string",
            "enum": [
              "7d",
              "30d",
              "90d",
              "custom"
            ]
          },
          "from": {
            "description": "First calendar day of a custom period, inclusive (YYYY-MM-DD, Europe/Warsaw). Required when period is \"custom\"; ignored otherwise.",
            "type": "string",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}$"
          },
          "to": {
            "description": "Last calendar day of a custom period, inclusive (YYYY-MM-DD, Europe/Warsaw). Required when period is \"custom\"; ignored otherwise. At most 366 days.",
            "type": "string",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}$"
          },
          "conversationTypeId": {
            "description": "Restrict to one conversation type — the Wyniki type filter. This is the analysis protocol id. Omit for every type. Coverage is null when this is set, because which calls belong to a type is only known once they are analysed.",
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
          },
          "userId": {
            "description": "Scope the summary, trend and rules to this one employee. An internal user id — the same id `operatorIds` takes on the other analytics tools — or \"me\" for the signed-in user on an OAuth connection. An API key has no caller, so \"me\" is refused there. Omit for the whole team. The `employees` list is always the whole team. The response identifies the employee by `operatorRef`, never by this id.",
            "anyOf": [
              {
                "type": "string",
                "format": "uuid",
                "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
              },
              {
                "type": "string",
                "const": "me"
              }
            ]
          }
        }
      }
    }
  ]
}
