MCP HeartbeatTHE MCP OBSERVATORY
Public workspace

Explore

Evidence for your next connection.

Independent measurements of remote MCP servers. Every observation has a timestamp.

Read-only probes

MCP server · Code & DevOps

Zooza MCP Server

MCP server for Zooza — class scheduling, attendance, and booking for activity businesses.

Checks passed
13 / 22

1 failed · 8 not measured

Median response
183 ms

Median of successful probes over 7 days

30-day uptime
Not measured

Share of countable probes over 30 days, including daily rollups. Auth challenges are excluded.

Discovered tools
18

Tool definitions returned by this server

Independent monitoring

Health & checks

Status history

Responded

The server completed its last measured handshake. This is an observation, not a guarantee.

Last checked
Check summary by test group
Test areaPassedFailedUnmeasured
ConnectionCan a client connect and discover tools?
500
Reliability & speedDoes it respond consistently and quickly?
004
Access & authenticationWhat is required to use this server?
102
Tool qualityAre the tool definitions usable by an agent?
400
Publisher identityCan the publisher be traced to its source?
312

Unmeasured means there is not enough evidence to decide. How these checks work

Inspect individual checks 22 recorded

Core

  • Pass
    Reachable

    Latest probe returned HTTP 200 in 183 ms.

  • Pass
    Valid handshake

    protocolVersion 2025-11-25, serverInfo.name zooza-mcp.

  • Pass
    Lists tools

    tools/list returned 18 tools.

  • Pass
    Current protocol

    Negotiated 2025-11-25. The bar is 2025-06-18 or newer.

  • Pass
    HTTPS

    The endpoint URL is https.

Performance

  • Not measured
    Fast (p50)

    Median latency 183 ms over 1 samples in 7 days. The bar is 1000 ms. Need at least 5 samples.

  • Not measured
    Consistent (p95)

    95th percentile 183 ms over 1 samples in 7 days. The bar is 3000 ms. Need at least 20 samples.

  • Not measured
    Reliable (uptime)

    1 of 1 attempts returned a tool list in 30 days (100%). The bar is 99%. Need at least 30 samples.

  • Not measured
    Stable tool list

    Fewer than two successful probes in 30 days, so tool-list stability is not measured.

Access

  • Pass
    Open access

    tools/list succeeded with no credential, and the record declares none.

  • Not measured
    Standard auth

    Auth is not required, so the OAuth metadata check does not apply.

  • Not measured
    Free tier cited

    No publisher quote has been checked.

Tools

  • Pass
    Tools described

    Every tool description is at least 20 characters.

  • Pass
    Valid input schemas

    Every inputSchema is a JSON Schema object.

  • Pass
    Parameters documented

    180 of 180 parameters have a description (100%). The bar is 80%.

  • Pass
    Safety hints

    Every tool sets readOnlyHint or destructiveHint.

Publisher

  • Pass
    Verified namespace

    Listed in the Official MCP Registry.

  • Fail
    Website matches publisher

    The website host does not match the reverse-domain namespace.

  • Pass
    Source repo linked

    A repository URL is on file.

  • Not measured
    Repo owner matches

    This namespace is not an io.github login, so the owner comparison does not apply.

  • Pass
    Recently updated

    Updated 2026-06-10T19:47:30.716616Z, within 180 days.

  • Not measured
    Open-source licence

    The GitHub licence lookup has not returned an OSI result yet.

Start using Zooza MCP Server

Connect to this server

Choose your client, copy the configuration, then enable the server in your client’s MCP settings.

Run this command in your terminal, then restart Claude Code.

Claude Code

claude mcp add --transport http zooza-mcp-server https://mcp.zooza.app/mcp

Capabilities

Tools 18

bookings_copy_booking

Copy or move a client's existing booking into a different class. COPY creates a second booking and leaves the original in place — use it when the client is continuing into a new term or adding a class alongside their current one. MOVE relocates the booking itself, carrying its payment history and debt, and the client is no longer in the old class — use it when they are switching. TWO CALLS. First WITHOUT `token`: returns both class prices, what the client currently owes, free places in the target class, and any blockers, plus a single-use token. Show that to the operator — especially the `money` line — and get explicit approval. Then call again with `token` + `confirmed: true` to apply; send nothing else, the plan is frozen. `payments` has no default on purpose: pricing the booking from the target class is what operators most often get wrong, so ask rather than assume. MONEY FIGURES ARE TOTALS. Every amount in the `money` block is for the WHOLE booking, never per session — Zooza's per-session figure is `target_unit_price_per_session`, shown separately, and quoting it as the price is how a booking ends up quoted ~20x too cheap. Read `target_will_owe_total` to the operator: it is the class price PLUS the target class's registration fee, which Zooza charges on top of it. ANOTHER PROGRAMME IS ALLOWED. The target class may belong to a different programme than the booking (e.g. a free trial moving into a paid course). The preview then carries a `different_programme` warning: the booking is priced by the TARGET programme, no instalment plan is applied (payments_add_plan afterwards), and the old programme's booking-form answers are carried over — the target programme's own required questions are NOT asked. SIMPLE CASE ONLY. This tool cannot set up instalment plans, pick specific term blocks, or set a custom amount owed. If the operator needs any of those, do not improvise with other tools — tell them to use the Copy/Transfer wizard in the Zooza admin app (open the booking, then Transfer or Copy booking).

Discovered
bookings_find

Find this company's bookings — a client's enrolment in a class (registration; "prihláška"/"Buchung") — and resolve them to a `registration_id`, or a client to a `user_id`. Use for "is X enrolled?", "who's in this class?", "who hasn't paid?" (set `payment_status:["unpaid","partially_paid"]`), and "find client X". Filter by `search` (loose: name/email/phone) or `name`, by `course_id`/`schedule_id` (resolve via classes_find_courses / classes_find_classes), `billing_period_id` (a term/season), `user_id`, `registration_id` (one exact booking by its id), `place_id` (venue), `status`, `payment_status`, or booking date with `created_from`/`created_to` (the "new registrations this week" lever). `distinct:true` returns one row per client (→ `user_id`) for person lookups. Chain a result's `registration_id` or `user_id` straight into comms_send_message (`audience.registration_id` / `audience.user_id`). Each booking row carries when its class meets — `class_name`, `class_weekday` (day of the first session), `class_time` (HH:MM) — so "his Tuesday class" needs no classes_find_classes call — and `created` (when the booking was made, for "most recent"); programme names are not returned (classes_find_courses). Defaults to active enrolments; guest, waitlist, canceled and deleted are excluded unless you pass `status`. A row's `status` may read `pre_*` (e.g. pre_registered = not yet confirmed); its group filter (`registered`) already includes it. `include_extra_fields:true` adds each booking's custom booking-form answers (allergies, "how did you hear about us"…). Money per booking: `payment_debt` = total CHARGED (negative), NOT what is still owed; `payment_paid` = amount received; `payment_balance` = paid + debt (negative = still owed, positive = overpaid). Sum `payment_paid` for income, `payment_balance` for outstanding debt. Read-only — does not create or change bookings.

Discovered
call_tool

Runs a Zooza tool by name. Use it for every Zooza tool that is NOT in your tool list — these exist and work; never send the user to another app because a Zooza tool isn't listed. Call get_tool_schema(name) first for the arguments. Write tools behave exactly as when called directly: the first call previews and returns a token, the second (token + confirmed:true) applies. Tools reachable this way: - bookings_add_lead: Add a lead (minimal registration on a lead-collection schedule) - classes_add_course: Create a new programme (course) - classes_add_share_link: Create a private registration link for a class - classes_commit_class: Commit a class (schedule + events) - classes_list_schedule_patterns: List valid Zooza schedule patterns - classes_list_share_links: List a class's private registration links - classes_preview_events: Preview class session dates - classes_preview_schedule: Preview a class schedule shell - classes_update_course_settings: Change programme settings (preview, then apply) - comms_find_messages: Read a client's message history (sent emails, replies, SMS) - comms_find_replies: Read inbound customer replies and set a reply's state - comms_list_merge_vars: List Zooza message merge variables - comms_list_templates: List the company's automated email templates - explain_data_model: Explain Zooza data model - labels_mark: Attach or detach a label on a course, schedule, or registration - negotiate_terminology: Negotiate terminology — build a personalised vocabulary profile - payments_add_plan: Apply a payment plan to a booking - payments_find_orders: Find a client's product orders - payments_send_invoice: Issue an invoice (payment confirmation) for bookings and email it - sessions_add_summary: Add a session summary - setup_add_payment_template: Create a payment plan template - setup_update_course_templates: Set a programme's payment plan templates - submit_feedback: Submit feedback to engineering - todos_add: Create an operator to-do item - todos_mark: Change the status of a to-do item - trainers_add_helpers: Add additional lecturers to classes and sessions

Discovered
classes_find_classes

Search this company's CLASSES — the scheduled groups inside a programme (a "class" / "group" / "skupina"; internally a *schedule*) — by name (substring) and resolve them to a `schedule_id`. Reach for this whenever the user names a specific group rather than a whole programme ("the Nejaké class", "the Monday 5pm group", "her Wednesday ballet class"), or whenever a downstream tool needs a `schedule_id` — most importantly `comms_send_message` targeting everyone in one class (`audience.schedule_id`). This is the missing middle rung between `classes_find_courses` (finds the PROGRAMME → `course_id`) and `sessions_find_events` (finds individual dated SESSIONS → `event_id`): a class is one recurring group within a programme, made of many sessions. Optionally narrow by `course_id` (classes inside one programme), `trainer_id`, `place_id`, `billing_period_id` (term block; one id or up to 10), `day` of week, `registration_type`, `in_trial: true` (only classes currently offering a TRIAL), `active_only: true` (exclude classes whose schedule has ENDED), or `lead_only: true` (only lead-collection pipelines). To answer "the latest classes that actually have sessions" in ONE call, combine `with_sessions: true` (only classes whose schedule has ≥1 session) with `sort: "created_desc"` and a `page_size` — no need to scan `sessions_find_events`. `sort` also takes created_asc / date_asc / date_desc / name_asc / registrations_desc. Returns a slim list — `{schedule_id, name, course_id, start, end, time, trainer_id, trainer_name, place_id, place_name, room_id, duration_minutes, trainer_rate_type_id, capacity, registrations_count, sessions_count, status, in_trial, registration_url, billing_period_id, schedule_type}` — enough to disambiguate when several classes share a name, never enough to mutate. When COPYING an existing class, pass its `room_id`, `duration_minutes` and `trainer_rate_type_id` into `classes_preview_schedule` explicitly — otherwise the preview falls back to no room, the 60-minute default and no pay rate. `room_id` 0 = no specific room; `trainer_rate_type_id` 0 = none set (or hidden from your role). `sessions_count` is the class's number of sessions (a stored/materialised count — fine for overview and "how many", may lag a very recent edit; chain `sessions_find_events` for an exact live count). `schedule_type` tells a real class (`fixed_period`) from a lead pipeline (`lead_collection`) — use `lead_only: true` to find the pipeline `bookings_add_lead` needs. `registration_url` is the public link a prospect clicks to book that specific class (empty when the class isn't publicly bookable or the company has no registration widget); its programme/venue/room params are rebuilt from the class's live values, so it stays correct after a class is moved. `billing_period_id` 0 = no billing period. Combine filters in ONE call — e.g. `{place_id, in_trial: true, active_only: true}` returns the bookable trial classes at a venue in a single query; do not split them across separate calls. `course_id` is returned but not the course name (resolve it with `classes_find_courses` if you need it). `additional_trainers` lists the class's ADDITIONAL lecturers — people who work it alongside the main instructor in `trainer_id`/`trainer_name` — as `{trainer_id, trainer_name, role}`, `[]` when there are none. At class level this is the eligibility roster: it says who may work the class, NOT which sessions each one actually works — for that call `sessions_find_events` with the `schedule_id` and read each session's own `additional_trainers`. Roles render as `secondary` = "Secondary instructor", `assistant` = "Assistant", `helper` = "Assistant instructor", `trainer` = "Instructor". To change the roster use `trainers_add_helpers`. By default returns active + paused (inactive) classes; pass `include_archived: true` to search archived classes instead. Does NOT create or change classes (that is `classes_preview_schedule` → `classes_commit_class`) and does NOT list a class's sessions (use `sessions_find_events` with the `schedule_id`).

Discovered
classes_find_courses

Search the company's courses by name (substring match) and optionally by registration_type. Returns a slim list of matches — `{id, name, registration_type, target_audience, price, schedules_count, ...}` — enough to disambiguate, not enough to act. Use this whenever the user names a course in natural language; never demand a raw course_id. Archived courses are excluded by default (pass `include_archived: true` to opt in). Pagination defaults to page 0, page_size 25 (max 200); `truncated: true` is returned when more matches exist than the current page reveals. `registration_type` business meanings (when filtering, AND when surfacing results to the user — always translate to these terms, never show the raw enum value): - `single` — drop-in / per-session: customer books one event at a time. - `full2` — full-course enrollment: customer signs up for the entire course/schedule in one go. - `open` — open-ended / membership: no fixed enrollment window; customer joins and stays.

Discovered
classes_find_resource

Resolve a NAME the operator said into an id, for four kinds of company-level records. Pick `kind`: - `place` — venues. Returns `{id, name, city, street, rooms: [{id, name, capacity}]}`. Rooms are inlined because picking a venue is usually followed by picking a room. Filters: name, city. - `billing_period` — term blocks (e.g. "Autumn 2026"). Returns `{id, name, active, period_start, period_end}`; either date may be null, an open-ended period is valid. Filter: name. Match on the DATES, not just the name — a period covering the term you want may be named anything. - `trainer` — team members assignable to classes. Returns `{id, full_name, email, active, virtual}`. Filters: name, place_id, course_id. **Virtual trainers** are always included regardless of place/course filters — they are system-wide placeholders with `virtual: true`, a synthetic id (>= 9000000000000) and no email. Pick one when the operator says "we'll decide later", "no trainer yet", "TBD", "unassigned", "guest", "external speaker". Three ship by default: 'To be decided', 'Trainer unassigned', 'Guest trainer'. - `trainer_rate_type` — named pay rates (e.g. "Hourly", "Per class"). Returns `{id, name, minutes, type}`. This is the ONLY way to turn a rate the operator names into the `trainer_rate_type_id` that classes_update and sessions_update need — NEVER guess that id. `include_inactive` (place: n/a) defaults false. Sending a filter that does not apply to the chosen kind returns the list of filters that do. For PROGRAMMES use classes_find_courses, for CLASSES classes_find_classes, for people enrolled use bookings_find — those are separate, richer tools.

Discovered
classes_update

Edit one or more existing classes (a "class"/"timetable" is the recurring group within a programme) — name, price, registration fee, capacity, make-up/replacement extra capacity ("počet miest navyše pre náhradné hodiny" → extra_capacity/extra_capacity_usage, NOT registrations_cap, which caps the NUMBER OF REGISTRATIONS), registration-count cap, billing period, online-registration, trial lessons, status — and/or instructor, venue, or session duration. TWO CALLS. First WITHOUT `token`: returns a preview of exactly what changes and how many sessions are affected, plus a single-use token. Show it to the operator and get explicit approval. Then call again with `token` + `confirmed: true` to apply — send nothing else, the change is frozen in the plan. To alter anything, run the first call again. Changing instructor/venue/duration forces a `session_scope` choice about existing sessions: "upcoming" (this class + its future sessions — usually what people mean), "all" (every session incl. past), or "class_only" (re-advertise the class but leave existing sessions on their old value — the #1 cause of "I changed it but the sessions still show the old value", so only pick it deliberately). This scope is the OPERATOR's call, not yours: when they haven't stated one, PRESENT the three options and their consequences and let them choose — do NOT silently pick a scope, and do NOT stall. If the operator asks what a cascade edit will do before applying, explain this same taxonomy. Handles one class or many at once. To edit specific individual sessions (move one date, change one session's room), use sessions_update instead. To cancel sessions, use the cancellation tools.

Discovered
comms_send_message

Email clients of this company. Describe the audience (a course/programme, a class schedule, a specific booking, one client, a saved segment, an ad-hoc cohort, or course-level labels) and the content (an existing template `type` from comms_list_templates, or a custom subject + body which may use *|MERGE_VAR|* tags from comms_list_merge_vars). TWO CALLS. First WITHOUT `token`: sends NOTHING. Returns the estimated recipient count, a sample of recipients, the content as it will be sent, warnings (unknown merge tags, zero recipients), and a single-use token. Show that plan to the operator and get explicit confirmation. Then call again with `token` + `confirmed: true` to actually send. Calling the first form again with adjusted filters is free and repeatable — refine the audience that way rather than guessing. LARGE SENDS need a SECOND confirmation. If the recipient count exceeds the approval threshold, the sending call returns `requires_second_confirmation: true` with the count and job id and sends NOTHING yet. Show the operator the exact recipient count and ask again (e.g. "Send to all 105 clients?"). Only after they explicitly agree, call once more with the SAME token, `confirmed: true`, and `confirm_large_send: true`. If they decline, send nothing. Resolve names to ids first: classes_find_courses for a course/programme → course_id, classes_find_classes for a class/group by name → schedule_id, sessions_find_events for a single session → event_id; never guess ids. When the operator names an ad-hoc cohort rather than the whole company — "everyone who hasn't paid", the unpaid roster, the waitlist, this week's sign-ups — resolve it with bookings_find and pass the resulting registration_id LIST as audience.registration_id. Reserve audience.whole_company for genuinely company-wide sends; do NOT use it as a shortcut for a named subset, or you email far more people than the operator asked for.

Discovered
get_skill

Returns the full markdown playbook for one of the registered skills. Call this BEFORE starting a flow named in the server's instructions — the playbook contains the interview steps, mapping rules, and confirmation pattern for that scenario. Available skills: business-model-validator, class-management, communication, feedback-nudge, negotiate-terminology, report-compose, report-discovery, report-page-new, schedule-optimization.

Discovered
get_terminology

Search the Zooza domain glossary. Returns canonical term names, definitions, cross-language synonyms, disambiguation rules, and AI guidance notes. No Zooza API call is made — purely local lookup against the compiled glossary. Use this to resolve ambiguous user input before calling operational tools. Examples: query="hodina" → Session; query="kurz", language="sk" → Programme; category="product-hierarchy" → all hierarchy terms; empty call → full index.

Discovered
get_tool_schema

Returns the description and exact input schema of a Zooza tool, including tools not in your tool list (call_tool lists them). Call it once per tool before that tool's first call_tool.

Discovered
reports_get_data

Return the REAL, pre-aggregated numbers for ONE business question about an activity brand — and the basis for SHOWING it. This is how you show an operator a report / dashboard / chart of their business numbers (occupancy, unpaid, churn, attendance, trials, retention, revenue, "how are we doing", per programme / venue / instructor): call this, then COMPOSE a focused report as an ARTIFACT in the conversation that renders in the side panel — do NOT hand the user a link or open a browser page. Views: occupancy, unpaid, churn, attendance, trials, retention, clients_by_location, replacements, summary. Use view="replacements" for ANY question about make-up / replacement credits — "unused make-ups", "expiring make-ups", "credits", "náhrady / náhradné hodiny", "are we overloaded on make-ups", make-up demand vs available slots per programme (this IS the credits report; Zooza HAS make-up credits even though they are not in the business_dashboard views). The result has `headline` (computed key figures), `rows` (chart/table-ready, named, capped), `note` (a data-aware caption), `currency`, and `period`. RULES: every number you show the user MUST come from this result verbatim — never invent, estimate, or recompute figures, and never draw a chart without calling this first. Render charts with inline SVG/CSS — no external CDN or chart library (the artifact sandbox blocks them). If a view returns no rows, say so plainly. Follow get_skill("report-compose").

Discovered
sessions_cancel

Cancel one or more scheduled SESSIONS of a class so they do not take place — "cancel Tuesday's sessions", "the pool is closed on Friday", "Martina is ill all week, cancel her classes". A cancelled session stays visible in Zooza with a cancelled status; it is NOT deleted. Scope the call to exactly ONE entity: `event_ids`, a single `date`, a single `trainer_id` with `from`/`to`, a single `schedule_id` with `from`/`to`, or a single `place_id` with a `date`. Two instructors or two classes in one call are refused by design — cancel them one at a time so each blast radius is reviewed on its own. At most 7 days, 20 sessions and 150 affected clients per call. TWO CALLS. First WITHOUT `token`: returns every affected session, the client count, how many emails would be sent, and what will NOT happen — plus a single-use token. Show that to the operator and get explicit approval. Then call again with `token` + `confirmed: true` to apply; send nothing else, the plan is frozen. `reason` is required and is recorded for staff only — it must be the operator's own words: if they did not say why, ASK before the first call; never invent a placeholder such as "cancelled by operator". `public_reason` is what clients read and is required when `notify` is true — one email per session per client, so five sessions in a class of 25 is 125 messages; the preview states the exact count either way. Optionally create a make-up session with `replacement_date`, which moves the attendees onto the new date. Allowed ONLY when exactly one session is in scope, because a make-up date belongs to one session. Attendees who already have attendance marked (attended / no-show) and anyone waitlisted do NOT move — the preview counts them. Sessions that already happened are left out unless you pass `include_past: true`. Only owners and assistants can cancel; other roles are refused with an explanation. This tool does NOT un-cancel a session, does NOT delete sessions (cancelling and deleting are different verbs in Zooza), does NOT cancel ONE client's booking on a session — that is `sessions_mark_attendance` with `attendance: 'canceled'`, and that is the path that issues make-up credits — and does NOT reschedule anything (`sessions_update`). Cancelling a session issues no make-up credits to anyone.

Discovered
sessions_find_events

List **events** (scheduled sessions of classes) in the caller's company. Use this whenever you need to resolve an `event_id` from natural language ("my next class," "Monday's ballet," "all swim sessions this week," "Sarah's classes tomorrow") before chaining into another tool like `sessions_get_attendance` or `sessions_mark_attendance`. With no filters at all, returns the company's **upcoming** scheduled sessions (from today onward, earliest first) — not just the caller's — so a bare call stays near-term instead of dumping years of history. **Any** filter you add returns the FULL matching set, including PAST sessions: pass a `schedule_id` to get a class's entire history (past + future), or use `from`/`to` for an explicit window. There is no `past` flag — past sessions are just a range with `from` set early (or omitted alongside another scope). Filters cover date window, course, schedule, trainer, place, room, segment, billing period, status, and event-type (over-capacity, substituted, cancelled, etc.). Each returned row includes denormalised names (trainer, place, event-number), the event's date and duration, `capacity`, `free_spots` (remaining places = capacity − going, or null for open/unlimited events — use this to answer "which sessions still have space"), and an `attendance_counts` object (`going`, `attended`, `noshow`, `canceled`, `canceled_late`, `waitlist`). Read-only — does not modify events. **Additional lecturers.** Two separate fields, and they mean different things. `additional_trainers` = who is actually working THAT session alongside the main instructor. `class_additional_trainers` = the parent class's roster of people ELIGIBLE to work it, who are not necessarily on that session. Answer "who is helping on Wednesday?" from `additional_trainers`, never from the roster. Both are always arrays (`[]` = nobody), and neither includes the main instructor, who stays in `trainer_id`/`trainer_name`. Each entry is `{trainer_id, trainer_name, role}`; `role` is the raw enum — show it to operators as `secondary` = "Secondary instructor", `assistant` = "Assistant", `helper` = "Assistant instructor", `trainer` = "Instructor". `trainer_name` can be null if the lookup failed — that is not proof the trainer is gone. To CHANGE any of this, use `trainers_add_helpers`. **Trainer pay (payroll).** `pay` = `{rate_type_id, rate_type, payout_percentage, minutes, main_trainer}`; each `additional_trainers[]` entry has its own `pay`. Each trainer's `{amount}` is calculated by Zooza and equals the trainer report — use it as-is, never recompute it from rates. `amount: 0` = that trainer has no rate set for the session's rate type; `amount: null` = hidden from this caller (a trainer sees only their own). `payout_percentage` is a 0–1 fraction (cancelled sessions are 0). `pay` is null when the session has no rate type or this caller may not see financial data — never report that as 0. `main_trainer` is null when the session has no main instructor. For one trainer's monthly cost, sum `pay.main_trainer.amount` ONLY on rows whose `trainer_id` is that trainer, plus `pay.amount` of THEIR entry in `additional_trainers[]` — the `trainer_id` filter also returns sessions they are merely linked to (substituted away, class roster), which pay them nothing. The roster (`class_additional_trainers`) carries no pay. **Critical: "my sessions" / "what am I teaching" / "my classes today".** When the user is asking for THEIR OWN sessions (any first-person framing), you MUST pass `trainer_id` matching `whoami.identity.user_id`. Without it, this tool returns every trainer's events in the company — which is almost never what the user meant when they said "my." The only exception: when the caller's role is `member` or `external_member`, the server silently auto-scopes to their assignments anyway; `meta.scoped_to` in the response flags when this has happened. Filter notes: - `trainer_id` matches across FIVE trainer relationships including pre-substitution and schedule-level extras. Treat it as "events trainer X is connected to," not strictly "events trainer X currently teaches." - `status` uses raw db terms: `scheduled` (default — only state attendance can be tracked on), `unplanned` (includes cancelled events), `finished`, or `any`. - `segment_id=[0]` is a sentinel matching events with NO segment assignment. - Counters in `attendance_counts` may be sub-second-stale; for real-time counts on one event, chain into `sessions_get_attendance`. DISPLAYING A CLASS'S TIMETABLE: when the user wants to SEE a class's sessions (e.g. viewing or COPYING a class), render them as a weekly GRID — days across the top (Mon–Sun), time down the left, like the Zooza app calendar — collapsed to the weekday+time pattern with the run range + session count in a one-line caption; list individual dates only if the user explicitly asks. (Display only — ignore when you are merely resolving an event_id to chain into another tool.)

Discovered
sessions_get_attendance

Read who's enrolled in **one event** (a single session of a class) and their current attendance, so you can show the list and then mark it. Pass an `event_id`; the tool returns each enrolled attendee, their current attendance value (if already marked), and per-row context the LLM needs to mark attendance correctly: `allowed_statuses[]` (the statuses the **current caller** is permitted to set for THIS attendee), `is_trial` / `is_last_trial_session` flags, warnings about cross-company or cascade-sensitive (full2) cases, and — for open-type registrations only — `entrance_voucher` info (how many unused vouchers the attendee has, and whether one is already spent on this event). Use this **before** `sessions_mark_attendance` whenever the user has not already dictated the full list of attendees and marks — typically: "open attendance for X," "who's enrolled in tomorrow's class," "show me Monday's attendance." If the event's course has attendance tracking disabled, the tool returns an `attendance_tracking_disabled` error rather than an empty list. This tool is read-only — it never writes attendance, notes, or summaries. **Talking to the user — vocabulary.** Zooza's customers are activity brands — dance, swim, language, sport, STEAM schools. Call this **"attendance," "the attendance list," "the class list," or "who's coming."** Don't expose the tool name or use sports/HR jargon ("roster") — it reads as foreign to these businesses. When the user asks to "see attendance" / "open the register" / "who's in Monday's class," just call this tool and render the list directly. **Attendee vs client (critical for children's-class programmes).** Each row carries TWO people: - `attendee` — who actually shows up to the session. Often a child (Zooza data-model name: `customer`). May have `user_id: 0` when they aren't a registered account holder, which is normal for children. `attendee.date_of_birth` is available. - `client` — the account holder / payer (Zooza data-model name: `buyer`). Usually the parent. Has a real `user_id`. Contact info (`email`, `phone`) lives on the client when the attendee is a child; copy from client when speaking to / messaging the family. - `display_name` — a pre-formatted one-line label. When attendee == client (adult attending themselves), just the one name. When they differ, `attendee_name (client_name)` — e.g. `"Jozko Jozko (Martin Rapavy)"`. Use this when listing attendees; the LLM doesn't need to compose it from scratch. Response shape notes: - `allowed_statuses[]` already factors in the caller's role, `company.trainer_attendance_management`, and the row's cross-company state. Do not propose a status not in this array — refuse locally and explain instead of calling `sessions_mark_attendance` to discover the constraint. - `is_last_trial_session` is currently `null` in V1 (derivation requires either a new api-v1 field or extra per-row lookups; deferred). Treat `is_trial=true` as the trigger for caution — a future enrichment will tighten this. - `entrance_voucher` is non-null only when `course.registration_type="open"`. Check it before setting `sessions_mark_attendance`'s `use_voucher=true` on a `going` write. - `additional_trainers` — who works THIS session alongside the main instructor, as `{trainer_id, trainer_name, role}` (`secondary` = "Secondary instructor", `assistant` = "Assistant", `helper` = "Assistant instructor", `trainer` = "Instructor"); `[]` = nobody. `trainer_name` null = lookup failed, not a missing trainer. For pay, use `sessions_find_events`. - `summary` block at the top level surfaces whether this event already has a public / internal session summary (`public_set` / `internal_set`), whether the public one is locked, and whether the caller's role is permitted to write summaries (`writable_by_caller`). After the user has marked attendance, the LLM can use this to offer `sessions_add_summary` as a follow-up when appropriate.

Discovered
sessions_mark_attendance

Record per-attendee attendance for **one event** (a single session of a class — e.g. "Monday Ballet on 2026-06-03 at 09:00"). Pass an `event_id` and a list of attendees, each with their own attendance value (`attended`, `noshow`, `canceled`, `going`, `ignore`). Each value is set on **that one attendee for that one event**, never on the event as a whole. The tool writes each attendee individually and returns a per-row outcome. Use this **after** you already know the event and the attendees you want to mark — typically because the user dictated them or because you previously called `sessions_get_attendance`. If you don't yet know which event or who's enrolled, call `sessions_find_events` or `sessions_get_attendance` first. This tool does **not** cancel or reschedule the event itself or handle trialist follow-ups — those are separate tools. **Follow-up chaining.** The response includes a top-level `summary` block with `public_set` / `internal_set` / `writable_by_caller` flags. After a successful mark, if `summary.public_set=false` AND `summary.writable_by_caller=true`, proactively offer the user the option to write a parent-visible recap via `sessions_add_summary`. If `writable_by_caller=false`, don't offer (the caller's role can't write summaries). If `public_set=true`, don't volunteer an update unless asked. **Trial follow-ups.** A per-row `pending_action: "trial_followup"` (with `todo_id`) means that attendee just completed their trial by being marked `attended` — a follow-up (parent feedback + continuing-class recommendation) is now pending. Tell the user it's waiting and offer to handle it; the attendance skill resolves it against the todo. This tool only surfaces the hint — it does not orchestrate the follow-up. If the field is absent, there's nothing pending. Attendance value semantics: - `attended` — attendee was present. - `noshow` — attendee did not show up and did not warn. - `canceled` — attendee cancelled (admin-recorded). Triggers server-side make-up credit creation automatically when the programme allows it; do not call any other tool to issue credits. - `going` — pre-event RSVP / "planning to attend." Restricted for member/receptionist roles under `trainer_attendance_management="limited"`. - `ignore` — hide this event from the attendee's history (Zooza-specific; rare). `use_voucher` is a tentative V1 design: only meaningful when `attendance="going"` AND `course.registration_type="open"`. Check the attendee's `entrance_voucher.unused_entrance_vouchers > 0` (from `sessions_get_attendance`) before setting it to true; the server silently downgrades to cash debt when no voucher is available.

Discovered
sessions_update

Edit specific individual sessions (events) of a class, OR add new sessions to a class. Two modes, one tool. NOT for a change that holds from a date onward or for every session ("from next week Jana teaches the Monday class", "the class moves to Room B") — that is ONE classes_update call with session_scope ('upcoming' or 'all'), never a per-session edit of each date. EDIT-MODE — pass `event_ids` + `changes`: reschedule a session's date/time, or change a hand-picked session's instructor, venue/room, block, or duration. Works on one session or a chosen set. ADD-MODE — pass `schedule_id` + `sessions`: CREATE one or more new sessions on an existing class (e.g. "add one more session at the end", "add a make-up class on 2026-05-04"). Each new session needs a `date`; its time, duration, trainer, venue and room default from the class. To append after the last session, first resolve the class's latest session with sessions_find_events, then pass the next date. New sessions are created billable so a priced class keeps charging. The two modes are mutually exclusive — send event_ids/changes OR schedule_id/sessions, never both. TWO CALLS either way. First WITHOUT `token`: returns a preview (per-session before→after for edits, or the list of sessions to be created for adds) plus a single-use token. Show it to the operator and get explicit approval (and, if `notify` is set, confirm that clients will be emailed). Then call again with `token` + `confirmed: true` to apply — send nothing else, the plan is frozen. Use EDIT-MODE when the user points at particular sessions ("move next Tuesday's class to Wednesday 5pm", "give Friday's session to Jana"). To cancel sessions, use the cancellation tools — this tool does not cancel.

Discovered
whoami

Returns the connected user's identity, the companies they can operate on, regional context, and the session's token state. Call ONCE at the start of every conversation. How to interpret the response: - 'status: "ok"' — authenticated, at least one company available. Pick a 'company_id' from 'companies' for follow-up calls (ask the user if more than one exists). - 'status: "no_companies"' — authenticated but no companies linked. Surface 'status_message' verbatim. No other tool will work. - 'status: "invalid_user"' — account rejected by api-v1. Surface 'status_message' verbatim. - 'status: "api_error"' — api-v1 unreachable. Surface 'status_message' and suggest retry. Identity fields (use these to scope follow-up calls to the calling user): - 'identity.user_id' — the caller's Zooza user id. Pass this as 'trainer_id' to filter sessions_find_events / sessions_get_attendance / etc. to the user's OWN data whenever the user says "my sessions," "my classes today," "what am I teaching tomorrow," etc. Without it, sessions_find_events returns ALL company events, not just the caller's. - 'identity.email', 'identity.name' — for display only. Regional context fields (use these to adapt behaviour): - 'server_region' — which Zooza installation this token routes to, taken from the JWT 'region' claim: "eu" (SK/CZ/DE/RO/HU/IT/PL), "uk", "us", "asia". This is the API-instance region, NOT the company's market region. Null in dev-fallback (no JWT). - 'company.region' — the company's MARKET region code (e.g. "sk", "cz", "de", "en"). Different axis from 'server_region': one EU instance ('server_region: "eu"') serves Slovak, Czech, German, … companies. - 'company.locale' — BCP-47 locale for date/number/currency formatting (e.g. "sk-SK", "cs-CZ", "en-GB"). - 'company.language' — the company's primary language code. - 'company.currency' — the company's currency (e.g. "EUR", "CZK", "GBP"). Use 'company.region' and 'company.language' to resolve terminology: a Slovak company saying "kurz" means Programme; a Czech company saying "lekce" means Session. When region context is available, skip asking the user to clarify language. Branding (per company in 'companies[].branding'): - 'branding.logo' (boolean) and 'branding.primary_color' (#hex or null) report what brand assets were cached for this company. Use 'primary_color' to brand a report artifact you compose for the client; the actual logo image is held server-side and never passes through you. Null branding = fetch failed or nothing configured; default to Zooza styling. Feedback context (used by the 'feedback-nudge' skill): - 'last_feedback_at' — ISO timestamp of the user's last MCP feedback submission, or null if never. Drives the skill's 7-day cool-off on proactive feedback nudges. - 'feedback_count' — total submissions to date (0 if never). Tool-list freshness (Zooza has no marketplace auto-update, so proactively spot an out-of-date connector). There are TWO independent staleness signals — check BOTH: 1. NEW or REMOVED tools — compare 'tool_surface.names' (every tool the live server exposes) against the tools you actually have available in THIS session. Any name in that list you do NOT have = the client is missing newly-added tools. 2. UPDATED tools (same names, changed behaviour) — compare 'server_version' in this response against the [SURFACE_BUILD: …] marker at the very end of THIS tool description. If they differ, the tool DEFINITIONS you currently hold are from an OLDER build even when every tool name still matches — an existing tool's description, inputs, or behaviour changed after the client last connected. (This is why a deploy that only tweaks existing tools is still detectable.) If EITHER signal fires, tell the user once — briefly, not naggingly — that their Zooza tools are out of date, and how to refresh: in Claude Desktop or Claude Code toggle the Zooza connector off then on (or reconnect / reinstall it); a browser client may just need a reload. Do NOT prompt when neither signal fires. Never surface 'sub' or 'scopes' to the user — diagnostic only. [SURFACE_BUILD: 0.20.10]

Discovered

Recent measurements

Probe history

View status
Latest stored probes. Raw probe rows are kept for 14 days.
CheckedResultResponseTools
Connected183 ms18