Zulip MCP Server — 111 tools via DADL
contains code
The Zulip DADL turns Zulip's API into an MCP server that Claude, GPT or any MCP-compatible agent can consume directly. One YAML file declares all 111 tools — user, message, realm, custom, stream, draft, and more — and ToolMesh serves them at runtime. No Python boilerplate, no per-endpoint code, no separate MCP server process.
Below: the endpoint coverage matrix, a two-block ToolMesh setup, the full tool reference grouped by Zulip feature area, required credential scopes.
Source: Zulip REST API
Which Zulip endpoints are covered?
67% (111 of ~165 endpoints)
Focus: messages (send + get + get-one + update + delete + render + history + reactions + flags + flags-for-narrow + mark-as-read + read-receipts + typing + matches-narrow), channels/streams (list + get-by-id + get-id-by-name + update + archive + topics + subscribers + delete-topic + subscription-status + email-address), subscriptions (list + subscribe-or-create + unsubscribe + update-settings), users (list + get + by-email + own + create + update + deactivate + reactivate + presence + status + regenerate-own-api-key + update-own-settings), user groups (list + create + update + deactivate + members + subgroups), personal preferences (mute/unmute user, topic visibility policy follow/mute/unmute, alert words CRUD), drafts (CRUD), saved snippets (CRUD), scheduled messages (CRUD), real-time events (register + get + delete), invitations (send + list + resend + revoke + multi-use), attachments (list + delete), files (upload + custom emoji upload), org admin (update_realm + user_settings_defaults + linkifiers full CRUD + reorder + custom profile fields full CRUD + reorder + code playgrounds + realm domains + default streams + bot CRUD + regenerate bot api key), server/realm (server settings + linkifiers list + custom emoji list + deactivate)
Missing: OAuth/SAML/external auth flows, mobile push device registration (APNS/FCM tokens), email confirmation flows, password reset flows, video call provider connection setup (the realm setting is covered, the per-call plumbing isn't), navigation views, message reminders (3 endpoints), realm export/import requests, SCIM provisioning, in-app hotspots & tutorial step tracking, BotConfig per-bot key-value storage (3 endpoints), some dev-only endpoints, billing/plan management
How do you configure the Zulip DADL?
- Decide whether you want to use a human user account or a dedicated bot (recommended for production).
- For a bot: log into Zulip → click your avatar → Personal Settings → Bots → Add a new bot.
- Choose type 'Generic bot' (most flexible). Give it a name and email. Click Create.
- Click the download icon next to the new bot to view its API key, or click the copy icon.
- For a human account: Personal Settings → Account & privacy → API key → Manage your API key → enter your password → copy the key.
- Store BOTH the email address and the API key — they are used together as HTTP Basic credentials.
- Subscribe the bot to any channels it needs to read or post in (channels → settings → Add subscribers).
- Note your organization URL: https://YOUR-ORG.zulipchat.com (Zulip Cloud) or your self-hosted domain.
- Set BOTH env vars in your ToolMesh deployment:
- CREDENTIAL_ZULIP_EMAIL=<bot or user email address> (used as HTTP Basic username)
- CREDENTIAL_ZULIP_API_KEY=<api key> (used as HTTP Basic password)
Environment variable: CREDENTIAL_ZULIP_API_KEY
Two credentials are needed: zulip_email (the bot's or user's email) and zulip_api_key (the API key string). Both are sent as HTTP Basic auth — ToolMesh handles the base64 encoding. The url in backends.yaml is the bare org URL WITHOUT the /api/v1 suffix (the DADL adds it). For Zulip Cloud orgs the URL pattern is https://<slug>.zulipchat.com; for self-hosted it is whatever the admin configured. Anyone with the API key can impersonate the bot — treat it as a secret.
How do you install the Zulip MCP server with ToolMesh?
Add to your backends.yaml:
- name: zulip
transport: rest
dadl: zulip.dadl
url: "https://your-org.zulipchat.com"
Set the credential:
CREDENTIAL_ZULIP_API_KEY=your-token-here What 111 tools does the Zulip DADL expose?
POST send_message Send a message to a channel topic or as a direct message. For channel messages set type=channel (or 'stream'), to=<channel name or stream_id>, topic=<topic name>, content=<markdown>. For DMs set type=direct (or 'private'), to=<user email/id or JSON array of user IDs>, content=<markdown>. Returns {id, automatic_new_visibility_policy?}.
GET get_messages Fetch a range of messages around an anchor. Use anchor + num_before + num_after to page; anchor can be a message ID or one of: 'newest', 'oldest', 'first_unread'. narrow is a JSON-encoded array of filter objects, e.g. '[{"operator":"channel","operand":"general"},{"operator":"topic","operand":"deploys"}]'. Operators: channel, topic, sender, search, is, has, dm, dm-including, id, with. Max 5000 messages per request (recommend ≤1000). Response includes {messages, anchor, found_oldest, found_newest, found_anchor, history_limited}.
GET get_message Fetch a single message by ID. Returns the message object with content, sender info, topic, channel, reactions, flags, and timestamps. PATCH update_message Edit a message's content, topic, or move it to a different channel. propagate_mode controls scope: change_one (default, this message), change_later (this + later in same topic), change_all (entire topic). Only the sender or users with appropriate permissions can edit.
DELETE delete_message Permanently delete a message. Only the sender (within edit window) or organization administrators can delete. Cannot be undone. POST render_message Render a Markdown string to HTML using Zulip-flavored Markdown, without sending it. Useful for previewing how a message will look. GET get_message_history Get the edit history of a message. Returns array of snapshots (oldest first) including topic, content, rendered_content, timestamp, user_id, and prev_* fields where changed. POST add_reaction Add an emoji reaction to a message. emoji_name is the Zulip emoji name (e.g. 'thumbs_up', 'octopus'). DELETE remove_reaction Remove your own emoji reaction from a message. POST update_message_flags Add or remove a flag on a batch of messages by ID. Flags: read, starred, collapsed. Some flags are read-only and change automatically (mentioned, has_alert_word, etc.). op is 'add' or 'remove'.
POST update_message_flags_for_narrow Add or remove a flag across all messages matching a narrow. Useful for 'mark all unread in this channel as read'. anchor + num_before + num_after define the scope (same semantics as get_messages). Returns processed_count, updated_count, first/last_processed_id, found_oldest/found_newest.
POST mark_all_as_read Mark every unread message visible to the current user as read. Deprecated — prefer update_message_flags_for_narrow with anchor=oldest, num_after=large. POST mark_stream_as_read Mark all unread messages in a channel as read. Deprecated — prefer update_message_flags_for_narrow with a channel narrow. POST mark_topic_as_read Mark all unread messages in a topic as read. Deprecated — prefer update_message_flags_for_narrow with channel + topic narrow. GET get_read_receipts Get the list of user IDs who have read a message. Excludes the sender, users with send_read_receipts disabled, and muted users. POST set_typing_status Send a "user is typing" notification. Clients call this every few seconds while typing, then send op=stop when done. type=direct needs `to` (JSON array of user IDs); type=channel needs stream_id + topic.
GET check_messages_match_narrow Test which of a given set of message IDs match a narrow. Useful for selectively highlighting search hits in a list. Returns a dict keyed by message ID with match_content + match_subject (HTML, with search keyword highlighting). Messages that don't match are omitted.
GET get_streams List channels visible to the authenticated user. Filters control which categories are included (public, subscribed, default, archived). Returns array of stream objects with stream_id, name, description, invite_only, is_web_public, history_public_to_subscribers, message_retention_days, and permission groups.
GET get_stream Get full details of a channel by its numeric ID. Returns stream object with name, description, privacy, retention, permission groups, subscriber_count, stream_weekly_traffic. GET get_stream_id Look up a channel's numeric ID by its exact name. Returns {stream_id}. Returns code=BAD_REQUEST if the channel does not exist or is not accessible. PATCH update_stream Update a channel's settings. Requires channel admin permission. All body params optional — only fields you pass are changed. Permission group settings take a JSON-encoded object like {"new":"role:everyone"} or a group ID.
DELETE archive_stream Archive (delete) a channel. Messages remain in the database but the channel is hidden. Organization admins can unarchive via update_stream. Irreversible from the API side without admin recovery. GET get_stream_topics List topics in a channel, ordered by recency (most recent first). Returns array of {name, max_id (last message ID)}. The user must have access to the channel. GET get_subscribers List user IDs subscribed to a channel. Returns array of integers. POST delete_topic Delete an entire topic and all its messages. Admin-only. Processes in batches — the response contains 'complete' (boolean); if false, RETRY the same call until complete=true.
GET get_subscription_status Check whether a specific user is subscribed to a specific channel. Returns {is_subscribed}. GET get_stream_email_address Get the email address that, when emailed, posts to a channel. Used for email integrations. GET get_subscriptions List channels the current user/bot is subscribed to, with all subscription-specific settings (notifications, color, pin, mute). include_subscribers='partial' returns subscribers only for bots and recently-active users (faster for big orgs).
POST create_or_subscribe Subscribe one or more users to one or more channels. CREATES channels that don't yet exist (this is the only way to create a channel via the API). principals is a JSON array of user IDs or emails — omit to subscribe yourself.
DELETE unsubscribe Unsubscribe one or more users from channels. principals defaults to the current user. Cannot remove admins from public channels they administer. POST update_subscription_settings Update per-channel subscription preferences (color, pin_to_top, audible_notifications, desktop_notifications, push_notifications, email_notifications, is_muted, in_home_view). subscription_data is a JSON array of {stream_id, property, value} objects.
GET get_users List all users in the organization. Returns array of user objects with id, email, full_name, is_bot, is_active, role, avatar_url, timezone, date_joined. GET get_user Get one user by numeric ID. Preferred over get_user_by_email because email can change. GET get_user_by_email Get one user by email address. Email can be the real address or the dummy user{id}@{host} form. GET get_own_user Get the currently authenticated user (or bot). Returns identity, role, custom profile data, and account flags. The simplest test that auth is configured correctly. POST create_user Create a new user. Admin-only. The new user receives an email to set their password unless full_name and password are pre-set. PATCH update_user Update a user's profile or role. Admins can change role and custom fields; users can update their own profile via this endpoint too (some fields). DELETE deactivate_user Deactivate a user. They can no longer log in or use the API. Their messages remain. Reversible via reactivate_user. POST reactivate_user Reactivate a previously deactivated user. Admin only. GET get_user_presence Get a single user's current presence (active/idle, last update timestamp). Path accepts either user ID or email. GET get_realm_presence Get presence for all users the current user can access. Returns server_timestamp and presences keyed by email. POST update_presence Update the current user's presence (active/idle/offline). Most clients call this every minute while open. GET get_user_status Get a user's custom status (status_text, emoji, away flag). POST update_status Set your own custom status — a short text + optional emoji. Pass empty status_text to clear. POST regenerate_own_api_key Generate a new API key for the authenticated user. Returns {api_key}. WARNING: the old key stops working immediately and all devices using it (including this connection) must be re-authenticated. Use sparingly.
PATCH update_own_settings Update the authenticated user's own preferences (display, notifications, behavior, privacy). Pass only the fields you want to change. Admins can also use target_users to bulk-update settings for others (Zulip 12.0+).
GET get_user_groups List all user groups in the organization. Returns array of group objects with id, name, description, members (user IDs), direct_subgroup_ids, deactivated. POST create_user_group Create a new user group with an initial set of members. can_*_group fields accept a JSON-encoded group ID or 'role:everyone'/'role:members' etc. PATCH update_user_group Update a user group's name, description, or permission settings. Admin-only or has_manage permission. DELETE deactivate_user_group Deactivate (soft-delete) a user group. The group remains in the database but is hidden. Cannot deactivate system groups. POST update_user_group_members Add or remove members from a user group. Pass JSON arrays of user IDs for 'add' and 'delete'. POST update_user_group_subgroups Add or remove nested subgroups from a user group. Pass JSON arrays of group IDs. GET get_user_group_members List all member user IDs of a group. Set direct_member_only=true to exclude transitively-included members from subgroups. POST mute_user Mute another user from the current user's perspective. Messages sent by muted users are automatically marked as read and hidden in the UI. Mute state is per-user — only affects the calling account. There is no dedicated "list muted users" endpoint; the current list is delivered via the muted_users key in register_queue fetch.
DELETE unmute_user Unmute a previously muted user. Returns an error if the user is not currently muted. POST update_user_topic_visibility Set the current user's visibility policy for a specific channel + topic. This is the modern replacement for the deprecated /users/me/subscriptions/muted_topics endpoint. Supports both muting (hide notifications) and following (boost notifications), plus unmute and reset.
GET get_alert_words Get the current user's configured alert words. Alert words trigger a notification when they appear in any message visible to the user. Case-insensitive, max 100 chars per word.
POST add_alert_words Add one or more alert words. alert_words is a JSON-encoded array of strings, e.g. '["deploy","incident"]'. DELETE remove_alert_words Remove one or more alert words. alert_words is a JSON-encoded array of strings to remove. Returns the remaining alert words. GET get_drafts Get all drafts saved by the current user (ordered by most recently edited). POST create_drafts Create one or more drafts. drafts is a JSON array of draft objects: {type, to, topic, content, timestamp}. PATCH edit_draft Replace an existing draft's contents. DELETE delete_draft Delete a draft permanently. GET get_saved_snippets List the current user's saved snippets — reusable text fragments (e.g. canned responses, command incantations, KB-style notes). Returns array of {id, title, content, date_created}. POST create_saved_snippet Create a saved snippet. Returns {saved_snippet_id}. PATCH edit_saved_snippet Edit a saved snippet's title or content. DELETE delete_saved_snippet Permanently delete a saved snippet. GET get_scheduled_messages List the current user's pending scheduled messages, ordered by delivery time. POST create_scheduled_message Schedule a message for later delivery. scheduled_delivery_timestamp is a UNIX timestamp in UTC seconds. PATCH update_scheduled_message Update a pending scheduled message. Can change content, topic, recipients, or scheduled time. Pass only the fields you want to change. DELETE delete_scheduled_message Cancel a pending scheduled message before it is delivered. POST register_queue Register an event queue and fetch initial state. The server returns queue_id, last_event_id, and snapshots of requested data. Use get_events to long-poll for new events. Always call delete_queue when finished — queues consume server resources. event_types and fetch_event_types are JSON-encoded arrays of strings.
GET get_events Long-poll for new events on a registered queue. queue_id from register_queue. last_event_id is the highest event ID already processed (-1 for first call). dont_block=true returns immediately (possibly with empty events). Default behavior blocks up to ~10 minutes.
DELETE delete_queue Delete a previously registered event queue. Always call this when done with register_queue to free server resources. POST send_invites Send email invitations to join the organization. invite_as: 100=owner, 200=admin, 300=moderator, 400=member (default), 600=guest. stream_ids is a JSON array of channels to auto-subscribe. Requires admin role typically.
POST create_multi_use_invite_link Create a reusable invite link that anyone can use to join the org. Returns {invite_link}. GET list_invites List pending email and multi-use invitations. POST resend_invite Resend the invitation email for a pending email invitation. DELETE revoke_invite Revoke a pending email invitation. DELETE revoke_multi_use_invite Revoke a multi-use invite link. POST upload_file Upload a file. Pass file as a file_url — ToolMesh will fetch and forward it as multipart. Response includes a relative url like /user_uploads/1/4e/.../filename. To share, embed in a message as [name](url). For files >25MB use the tus resumable upload endpoint (not modeled).
GET get_attachments List the current user's uploaded attachments. Returns {attachments: [...], upload_space_used}. DELETE delete_attachment Delete an uploaded attachment by ID. The file is removed from storage; messages referencing it will show a broken link. GET get_server_settings Get public server configuration including zulip_version, zulip_feature_level, available auth methods, and realm branding. Works WITHOUT authentication — useful for discovery. GET get_realm_linkifiers List linkifiers (regex → URL template rules) configured for the realm. Clients should process them in order. Returns array of {pattern, url_template, id, example_input, ...}. GET get_custom_emoji Get all custom emoji defined in the realm. Returns map of emoji_id → {id, name, source_url, still_url, deactivated, author_id}. DELETE deactivate_custom_emoji Deactivate (hide) a realm custom emoji. The image data remains but the emoji is no longer selectable. POST upload_custom_emoji Upload a new custom emoji to the realm. emoji_name allows letters/digits/dashes/spaces. File goes in the request body as form data. Server limits file size (default 5 MB) and accepts common image/gif formats.
PATCH update_realm Update org-wide (realm) settings. Admin/owner only. Only fields you pass are changed. Setting target_users on the related update_own_settings is for per-user defaults; this endpoint changes org-wide policy. The full realm settings surface is ~80 fields; the most commonly-tuned ones are exposed here. For uncommon fields, pass via this same endpoint.
PATCH update_realm_user_settings_defaults Update the default per-user settings applied to newly-created accounts in the realm. Does NOT change settings of existing users. Admin/owner only.
POST add_linkifier Add a new linkifier (regex → URL template). Used to auto-link ticket IDs like 'JIRA-1234', commit hashes, internal references, etc. url_template uses RFC 6570 with named variables captured from the regex (e.g. '{ticket_id}').
PATCH update_linkifier Update an existing linkifier's pattern, template, or auxiliary fields. Pass only fields to change. DELETE remove_linkifier Delete a linkifier. Existing messages keep their rendered links but new messages won't auto-link this pattern. PATCH reorder_linkifiers Reorder linkifiers. Order matters when patterns overlap — earlier wins. Pass a JSON array of all linkifier IDs in the desired order.
GET get_custom_profile_fields List custom profile fields configured for the realm. Returns array of {id, type, name, hint, field_data, order, ...}. Field types: 1=short text, 2=paragraph, 3=dropdown, 4=date, 5=link, 6=users, 7=external account, 8=pronouns.
POST create_custom_profile_field Create a new custom profile field that users can fill in their profile. field_data is type-specific JSON (e.g. choices for dropdown). PATCH update_custom_profile_field Update a custom profile field. Cannot change field_type after creation. DELETE delete_custom_profile_field Delete a custom profile field. All user values for this field are lost. Cannot be undone. PATCH reorder_custom_profile_fields Reorder custom profile fields. Pass a JSON array of field IDs in the desired display order. POST add_realm_playground Add a code playground integration — clicking the "View in playground" button on a code block of the matching language opens the code in an external editor (e.g. play.rust-lang.org). url_template must contain exactly one `{code}` variable.
DELETE remove_realm_playground Remove a code playground integration. POST add_realm_domain Add an email domain to the org's allowed-signup list (only effective when realm setting emails_restricted_to_domains=true). Use to lock self-signup to your company domain.
PATCH update_realm_domain Toggle allow_subdomains on an existing realm domain entry. DELETE remove_realm_domain Remove an email domain from the allowed-signup list. POST add_default_stream Mark a channel as a default channel — new users are auto-subscribed on signup (unless include_realm_default_subscriptions is overridden in the invite).
DELETE remove_default_stream Remove a channel from the realm's default channel list. Existing subscribers are not affected. GET get_bots List bots owned by the current user. Returns array of {user_id, username, full_name, api_key, default_sending_stream, default_events_register_stream, default_all_public_streams, avatar_url, services}. IMPORTANT: this endpoint and the other /bots endpoints are for HUMAN accounts only — bot accounts cannot list/manage other bots (Zulip returns 'This endpoint does not accept bot requests'). Connect with a human user's API key to manage bots.
POST create_bot Create a new bot owned by the current user. bot_type: 1=Generic, 2=Incoming webhook, 3=Outgoing webhook, 4=Embedded. Generic is the most flexible.
PATCH update_bot Update settings of a bot owned by the current user (or any bot, for admins). POST regenerate_bot_api_key Generate a new API key for one of your bots. Old key stops working immediately. DELETE deactivate_bot Deactivate (delete) a bot owned by the current user. Reversible by re-activating the bot user. What composite workflows does the Zulip DADL provide? ⚠ contains code
FN send_channel_message Convenience wrapper around send_message for the common case of posting to a channel topic. Pass channel_name OR stream_id (one is required) plus topic and content.
FN search_messages Search for messages by free-text query, optionally within a channel and/or topic. Returns up to `limit` matching messages, newest first. Wraps get_messages with a constructed narrow.
FN get_recent_in_topic Get the N most recent messages in a specific channel + topic. Convenience wrapper around get_messages with a channel+topic narrow anchored at 'newest'.
FN mark_all_in_channel_read Mark every unread message in a channel as read (preferred replacement for mark_stream_as_read). FN search_users Find users by a prefix match on full_name or email (Zulip has no dedicated user-search endpoint, so this fetches the org user list and filters client-side). Bots and inactive users are excluded by default.
FN daily_summary Build a digest of unread message activity for the authenticated user — total unread count, breakdown by channel, top topics, and DM senders. Uses get_messages with an is:unread narrow; for very busy accounts, raise `max_scan` (Zulip caps at 5000 per request).