Zooza MCP Server vs Content Distribution MCP

A side-by-side look at the Zooza MCP Server and Content Distribution MCP MCP servers: transport, auth, tools, quality signals, and the config to install either one in Claude, Cursor, Windsurf or VS Code.

Zooza MCP Server logoZooza MCP ServerCommunication · Remote HTTP/SSEMCP server for Zooza — class scheduling, attendance, and booking for activity businesses.59GoodAuth: not declaredContent Distribution MCP logoContent Distribution MCPCommunication · Local subprocess (stdio)Publish one piece of content to DEV.to, Hashnode, GitHub Discussions, Reddit, Bluesky, LinkedIn, Medium and Twitter with idempotent state and per-platform adaptation.60GoodAuth: api key required

Verdict: choose Zooza MCP Server for communication work over a hosted endpoint; choose Content Distribution MCP for communication as a local process. You can also run both side by side in one mcpServers block.

Which MCP server should you choose?

Zooza MCP Server logoChoose Zooza MCP Server when

  • You need dedicated capabilities in the Communication domain.
  • You prefer a remote HTTP/SSE transport.
  • You want tools like get_terminology, whoami, classes_find_courses.
Explore Zooza MCP Server

Content Distribution MCP logoChoose Content Distribution MCP when

  • You need dedicated capabilities in the Communication domain.
  • You prefer a local stdio subprocess transport.
  • Its auth model fits you: API key required.
  • You want tools like post_publish, post_schedule, post_drain.
Explore Content Distribution MCP

Feature & specification comparison

Rows where the two servers differ are marked.

SpecZooza MCP Server logoZooza MCP ServerContent Distribution MCP logoContent Distribution MCP
CategoryCommunicationCommunication
Quality signal59/100 · Good60/100 · Good
Install commandConfirmedConfirmed
TransportRemote HTTP/SSELocal subprocess (stdio)
AuthNot declaredAPI key required
PricingNot declaredFree
Required env varsNone listedNone listed
Verified ownerCommunity listingCommunity listing
GitHub stars65
Directory activity 0 views 0 installs 0 upvotes 3 views 0 installs 0 upvotes
Tools188

Tools & capabilities

Zooza MCP Server tools 18

  • get_terminologySearch 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.
  • whoamiReturns 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.8]
  • classes_find_coursesSearch 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.
  • classes_find_resourceResolve 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.
  • classes_find_classesSearch 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`).
  • bookings_findFind 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.
  • sessions_find_eventsList **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 `{unit_amount, amount}` mirrors the Zooza trainer report: `amount = unit_amount × payout_percentage × minutes` (`fixed` rate type: no × minutes), where the RATE TYPE is the session's and `unit_amount` is that trainer's own rate for it (`null` → no rate set, amount 0). `payout_percentage` is a 0–1 fraction (cancelled sessions are 0). `pay` is null when the session has no rate type or rates are not readable for this caller. 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.)
  • sessions_get_attendanceRead 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.
  • sessions_mark_attendanceRecord 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.
  • classes_updateEdit 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.
  • sessions_updateEdit 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.
  • sessions_cancelCancel 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.
  • +6 more on the listing page

Content Distribution MCP tools 8

  • post_publishPublish one or more channel variants immediately. Side effects: makes external HTTP requests to each channel platform; writes publish state to the local YAML backend; requires valid credentials in the named profile. Idempotent on (content.id, channel) — re-running with the same IDs returns cached state without re-posting. Use post_publish for immediate-only delivery; use post_schedule when any variant needs a future schedule_at; use post_drain to flush a previously built queue.
  • post_scheduleEnqueue channel variants with schedule_at for future publishing; variants without schedule_at are published immediately. Side effects: writes entries to the local YAML schedule store; makes external HTTP requests for any immediately-published variants; requires credentials in the named profile. Idempotent on (content.id, channel). Use post_schedule when any variant needs a future publish time; use post_publish for all-immediate delivery; use post_drain to process the scheduled queue later.
  • post_drainFire all scheduled posts due at or before the given time boundary. Side effects: makes external HTTP requests for each due entry; writes results to the YAML backend. Idempotent — already-published (content.id, channel) pairs are skipped; no-op when no entries are due. Safe to call from cron. Use post_drain on a recurring schedule to flush the queue; use post_publish or post_schedule to add new content; use post_status to inspect results after drain runs.
  • post_statusReturn publish state for content pieces. Filters by content_id, channel, or both; returns all entries when neither is given. Side effects: read-only; no external HTTP calls; no auth needed. Deterministic given unchanged backend state. Use post_status to inspect what has been published, what is queued, or what errored; use post_publish, post_schedule, or post_drain to change state.
  • post_unpublishBest-effort delete of a published post on the target platform. Side effects: makes an external HTTP DELETE or update request; DEV.to sets published=false (soft delete); platforms without a delete API return success=false without error. Non-idempotent — calling on an already-deleted URL may return a platform 404. Use post_unpublish to retract a live post; use post_status first to obtain the live_url; use post_publish to re-publish after an unpublish.
  • channel_hintsReturn static per-channel metadata: character limits, Markdown support flags, tag vocabulary, and CTA placement rules. Side effects: read-only; no external HTTP calls; no auth needed. Fully deterministic — returns compile-time adapter constants. Use channel_hints before composing a variant body to understand channel constraints; use post_publish or post_schedule once you have a valid variant.
  • profile_listReturn all distribution profile names configured in the YAML backend. Side effects: read-only; no external HTTP calls. Deterministic given backend state. Use profile_list to discover available profiles before calling post_publish, post_schedule, or subreddit_list; then pass the chosen name as profile_name.
  • subreddit_listReturn all subreddits in the Subreddit Catalog with cooldown windows, flair vocabulary, and last-posted metadata. Optionally filtered to subreddits allowed by the named profile. Side effects: read-only; no external HTTP calls. Deterministic given backend state. Use subreddit_list to select a subreddit and obtain flair IDs before composing a reddit: channel variant; pass flair in variant.extras.flair.

Ready-to-paste client configs

Paste either block (or both) into your client config, e.g. claude_desktop_config.json or ~/.cursor/mcp.json. We only show a config when the install command is confirmed.

Zooza MCP Server

mcpServers (Claude Desktop / Cursor)
{
  "mcpServers": {
    "zooza-mcp-server": {
      "url": "https://mcp.zooza.app/mcp"
    }
  }
}

Content Distribution MCP

mcpServers (Claude Desktop / Cursor)
{
  "mcpServers": {
    "automatelab-tech-content-distribution-mcp": {
      "command": "npx",
      "args": [
        "-y",
        "@automatelab/content-distribution-mcp"
      ]
    }
  }
}

Frequently asked questions

Zooza MCP Server is categorized under Communication and uses a remote HTTP/SSE transport. Content Distribution MCP belongs to Communication and uses a local stdio subprocess. Pick Zooza MCP Server for communication work and Content Distribution MCP for communication.

Copy the mcpServers block from the config section on this page into your client's config (for example claude_desktop_config.json or ~/.cursor/mcp.json), then fully restart the client.

Zooza MCP Server has no declared pricing — check the project's own site. Content Distribution MCP is listed as free. Auth: api key required. If a server needs environment variables such as API keys, define them under the env key in your client's MCP config.

Yes. MCP clients support multiple servers: add zooza-mcp-server and automatelab-tech-content-distribution-mcp as separate keys in the same mcpServers object, and your assistant will call tools from either one.

On AllMCPs, Zooza MCP Server has a quality signal of 59/100 (Good) with 0 views, 0 installs and 6 GitHub stars. Content Distribution MCP has 60/100 (Good) with 3 views, 0 installs and 5 GitHub stars.