MCP Server
Notifly MCP Server — this is an implementation of the Model Context Protocol (MCP) that allows AI assistants (Claude, GitHub Copilot and others) to control Notifly via a set of ready-made tools.
With the MCP server an AI assistant can:
- send notifications to channels;
- read, filter and delete messages;
- create and manage channels and clients;
- ask a human questions and wait for the answer (human-in-the-loop);
- set up heartbeats, monitors and port scans, and read the check history;
- administer server users — only with an administrator’s device token (see Users).
Quick start: copy and go
Section titled “Quick start: copy and go”Five steps to your first call:
- Pick your client. Ready-made configs for Claude Desktop, Claude Code and Cursor are below; VS Code, Windsurf, Codex, Zed and Continue are covered in the Connecting to … sections further down.
- Build the server.
notifly-mcpis a single executable, and there are no downloadable builds yet: it is built from source (see Installation) and placed somewhere like/usr/local/bin/notifly-mcp. - Grab your tokens in the admin panel: an
API token (
M…, the API tokens page) — for working with channels, monitors and messages, and a channel app token (A…, the Channels section) — so the assistant can send notifications. - Paste the config for your client and substitute your own values for
MXXXXXXXXXXXXXXXXXXXXXX(API token) andAXXXXXXXXXXXXXXXXXXXXXX(app token).NOTIFLY_URLfor cloud Notifly ishttps://api.notifly.ru; for self-hosted — your server address. - Check it with a first call — see below.
Claude Desktop — ~/Library/Application Support/Claude/claude_desktop_config.json
(macOS) or %APPDATA%\Claude\claude_desktop_config.json (Windows):
{ "mcpServers": { "notifly": { "command": "/usr/local/bin/notifly-mcp", "env": { "NOTIFLY_URL": "https://api.notifly.ru", "NOTIFLY_APP_TOKEN": "AXXXXXXXXXXXXXXXXXXXXXX", "NOTIFLY_API_TOKEN": "MXXXXXXXXXXXXXXXXXXXXXX" } } }}Claude Code — a single terminal command:
claude mcp add notifly /usr/local/bin/notifly-mcp \ -e NOTIFLY_URL=https://api.notifly.ru \ -e NOTIFLY_APP_TOKEN=AXXXXXXXXXXXXXXXXXXXXXX \ -e NOTIFLY_API_TOKEN=MXXXXXXXXXXXXXXXXXXXXXXCursor — a .cursor/mcp.json file in the project root (or ~/.cursor/mcp.json globally):
{ "mcpServers": { "notifly": { "command": "/usr/local/bin/notifly-mcp", "env": { "NOTIFLY_URL": "https://api.notifly.ru", "NOTIFLY_APP_TOKEN": "AXXXXXXXXXXXXXXXXXXXXXX", "NOTIFLY_API_TOKEN": "MXXXXXXXXXXXXXXXXXXXXXX" } } }}Checking the connection
Section titled “Checking the connection”Restart the client (Cursor picks the config up by itself) and ask your assistant:
- “Show my Notifly channels” — the assistant calls
list_applicationsand returns a JSON list of your channels. This checks the address and the API token. - “Send a Notifly notification saying ‘MCP connected’” — the
send_messagecall returns the created message with itsid, the message appears in the admin feed and — if a receiving device is connected — arrives as a notification.
If a tool returns HTTP 401, check the token; if the client does not see any
Notifly tools at all, check the path to notifly-mcp in the config.
get_health checks the server address without any tokens.
Installation
Section titled “Installation”Requirements
Section titled “Requirements”- Go 1.27 or newer
Build from source
Section titled “Build from source”There is no downloadable binary yet, and the source repository is private: the MCP server is built from source. If you don’t have access to the repository, write to support@notifly.ru. If you do have the sources, the build goes like this:
git clone https://github.com/truboter/notifly.ru.gitcd notifly.ru/mcpgo mod tidygo build -o notifly-mcp .The resulting binary notifly-mcp runs over a stdio transport — the MCP client launches it as a child process.
Configuration
Section titled “Configuration”The server is configured exclusively via environment variables:
| Variable | Required | Description |
|---|---|---|
NOTIFLY_URL | ✓ | Base URL of the Notifly server: https://api.notifly.ru for the cloud, or your own server’s address |
NOTIFLY_APP_TOKEN | Channel token (prefix A) — for send_message and the question tools: ask_question, get_ask_answer, extend_ask_question, list_pending_answers, ack_answers | |
NOTIFLY_API_TOKEN | API token (prefix M) — for everything else | |
NOTIFLY_USER | Username (fallback instead of an API token) | |
NOTIFLY_PASS | Its password |
Connecting to Claude Desktop
Section titled “Connecting to Claude Desktop”Open the Claude Desktop configuration file:
- macOS:
~/Library/Application Support/Claude/claude_desktop_config.json - Windows:
%APPDATA%\Claude\claude_desktop_config.json
Add a mcpServers section:
{ "mcpServers": { "notifly": { "command": "/usr/local/bin/notifly-mcp", "env": { "NOTIFLY_URL": "https://api.notifly.ru", "NOTIFLY_APP_TOKEN": "AXXXXXXXXXXXXXXXXXXXXXX", "NOTIFLY_API_TOKEN": "MXXXXXXXXXXXXXXXXXXXXXX" } } }}After restarting Claude Desktop, the Notifly tools will appear in the interface.
Connecting to VS Code (GitHub Copilot)
Section titled “Connecting to VS Code (GitHub Copilot)”Add to .vscode/mcp.json in the project root or to your VS Code user settings:
{ "servers": { "notifly": { "type": "stdio", "command": "/usr/local/bin/notifly-mcp", "env": { "NOTIFLY_URL": "https://api.notifly.ru", "NOTIFLY_APP_TOKEN": "AXXXXXXXXXXXXXXXXXXXXXX", "NOTIFLY_API_TOKEN": "MXXXXXXXXXXXXXXXXXXXXXX" } } }}Connecting to Cursor
Section titled “Connecting to Cursor”Add .cursor/mcp.json in the project root (project-scope) or ~/.cursor/mcp.json globally:
{ "mcpServers": { "notifly": { "command": "/usr/local/bin/notifly-mcp", "env": { "NOTIFLY_URL": "https://api.notifly.ru", "NOTIFLY_APP_TOKEN": "AXXXXXXXXXXXXXXXXXXXXXX", "NOTIFLY_API_TOKEN": "MXXXXXXXXXXXXXXXXXXXXXX" } } }}After saving, Cursor will pick up the server automatically. Make sure notifly-mcp is available in PATH.
Connecting to Windsurf
Section titled “Connecting to Windsurf”Open or create ~/.codeium/windsurf/mcp_config.json and add a mcpServers section:
{ "mcpServers": { "notifly": { "command": "/usr/local/bin/notifly-mcp", "env": { "NOTIFLY_URL": "https://api.notifly.ru", "NOTIFLY_APP_TOKEN": "AXXXXXXXXXXXXXXXXXXXXXX", "NOTIFLY_API_TOKEN": "MXXXXXXXXXXXXXXXXXXXXXX" } } }}After saving, restart Windsurf — the Notifly tools will appear in Cascade.
Connecting to Claude Code (CLI)
Section titled “Connecting to Claude Code (CLI)”Use the claude mcp add command to register the server:
claude mcp add notifly /usr/local/bin/notifly-mcp \ -e NOTIFLY_URL=https://api.notifly.ru \ -e NOTIFLY_APP_TOKEN=AXXXXXXXXXXXXXXXXXXXXXX \ -e NOTIFLY_API_TOKEN=MXXXXXXXXXXXXXXXXXXXXXXThe --scope user flag adds the server globally for all projects (by default — project-scope). To list added servers: claude mcp list.
Connecting to Codex CLI
Section titled “Connecting to Codex CLI”Open or create ~/.codex/config.toml and add the section:
[mcp_servers.notifly]command = "/usr/local/bin/notifly-mcp"env = { NOTIFLY_URL = "https://api.notifly.ru", NOTIFLY_APP_TOKEN = "AXXXXXXXXXXXXXXXXXXXXXX", NOTIFLY_API_TOKEN = "MXXXXXXXXXXXXXXXXXXXXXX" }After saving, Codex will pick up the server on next start. Make sure notifly-mcp is available in PATH.
Connecting to Zed
Section titled “Connecting to Zed”Open the Zed settings file (~/.config/zed/settings.json) and add a context_servers section:
{ "context_servers": { "notifly": { "command": { "path": "/usr/local/bin/notifly-mcp", "args": [], "env": { "NOTIFLY_URL": "https://api.notifly.ru", "NOTIFLY_APP_TOKEN": "AXXXXXXXXXXXXXXXXXXXXXX", "NOTIFLY_API_TOKEN": "MXXXXXXXXXXXXXXXXXXXXXX" } } } }}Zed will pick up the server without a restart.
Connecting to Continue
Section titled “Connecting to Continue”Add a mcpServers section to the Continue config (~/.continue/config.json):
{ "mcpServers": [ { "name": "notifly", "command": "/usr/local/bin/notifly-mcp", "env": { "NOTIFLY_URL": "https://api.notifly.ru", "NOTIFLY_APP_TOKEN": "AXXXXXXXXXXXXXXXXXXXXXX", "NOTIFLY_API_TOKEN": "MXXXXXXXXXXXXXXXXXXXXXX" } } ]}Continue is supported as an extension for VS Code and JetBrains IDEs.
Available tools
Section titled “Available tools”The server registers 117 tools in total. A few of them cover most needs:
| Task | Tools |
|---|---|
| Check the connection | get_health, list_applications |
| Send a notification | send_message |
| Ask a human and wait for the answer | ask_question → get_ask_answer (wait_sec) |
| Read and search messages | list_messages, search_messages |
| Create a channel | create_application |
The full catalog below is grouped by section.
Server information
Section titled “Server information”| Tool | Description |
|---|---|
get_health | Check server and database health |
get_version | Get version, commit and build date |
get_server_info | Get server flags (registration, OIDC) |
Messages
Section titled “Messages”| Tool | Parameters | Description |
|---|---|---|
send_message | message*, title, priority, markdown, extras, app_token | Send a notification (requires NOTIFLY_APP_TOKEN or app_token). priority 0–10, 0 — the channel’s default priority; markdown: true — render the text as Markdown; extras — a string holding a JSON object of extras, merged with markdown; app_token — another channel’s token (A…) instead of NOTIFLY_APP_TOKEN |
list_messages | limit, since | List all messages with pagination |
list_application_messages | app_id*, limit, since | Messages of a specific channel |
delete_message | id* | Delete a message by ID |
delete_all_messages | — | Delete all messages |
delete_application_messages | app_id* | Delete all messages of a channel |
search_messages | q*, app_id, recursive, limit, since | Search messages by text (min. 2 characters); across all channels or in one. recursive: true together with app_id — search child channels too |
mark_messages_read | ids* | Mark messages as read (JSON array of IDs, e.g. [1,2,3]) |
mark_application_read | app_id*, recursive | Mark all of a channel’s messages as read (recursive — including child channels). Reading stops delivery escalation for those messages |
Questions (ask) — human-in-the-loop
Section titled “Questions (ask) — human-in-the-loop”| Tool | Parameters | Description |
|---|---|---|
ask_question | question*, options, fields, next, title, timeout_sec, default_answer, allow_multi, json_format, client_message_id, app_token | Ask a human a question (push/Telegram/email with buttons); returns the question id. fields — a form of up to 10 fields, the answer comes as a {name: value} object (incompatible with options); next — branching for a closed question {"<option>": {…question…}}: only with options and without allow_multi, chain depth up to 5, the answer carries nextQuestionId. Requires NOTIFLY_APP_TOKEN or app_token. See Questions (Ask) |
get_ask_answer | id*, wait_sec, app_token | Fetch the answer; with wait_sec > 0 — wait for it: the answer arrives as a WebSocket push, falling back to polling every 3 s, up to 300 s. Returns answered+answer (for a form — a {name: value} object, with branching — also nextQuestionId), pending, expired or cancelled. Requires NOTIFLY_APP_TOKEN or app_token |
extend_ask_question | id*, timeout_sec*, app_token | Extend a question: for a pending one the deadline moves to timeout_sec (1–86400) from now; an expired one that expired less than 24 hours ago goes back to pending; for answered/cancelled — a conflict. Requires NOTIFLY_APP_TOKEN or app_token |
cancel_ask_question | id* | Cancel a pending question |
answer_question | id*, answer* | Answer a question on the user’s behalf — when the agent makes the decision itself rather than a human |
list_pending_answers | app_token | The channel’s questions that have been answered but whose answers have not been collected yet. Handy when there are many questions: get the list, process the answers, then confirm with ack_answers. Requires NOTIFLY_APP_TOKEN or app_token |
ack_answers | ids*, app_token | Confirm the answers are processed — list_pending_answers will stop showing them. ids is an array of question IDs, e.g. [1,2,3]. Requires NOTIFLY_APP_TOKEN or app_token |
The ask_question → get_ask_answer(wait_sec) pair is a ready-made approval
gate for an AI agent: ask a human for permission and continue based on the answer.
Channels
Section titled “Channels”| Tool | Parameters | Description |
|---|---|---|
list_applications | — | List all channels |
create_application | name*, description, default_priority, tags | Create a channel. tags — up to 10 tags of [a-z0-9_-]; used by the tag scope of API tokens |
update_application | id*, name, description, default_priority, tags | Update a channel; tags replace the current ones (omitted — unchanged) |
delete_application | id* | Delete a channel |
get_application_status | app_id*, recursive | The channel’s aggregate status from its heartbeats: how many in total, how many are alerting, when the last signal came; recursive — include child channels |
list_event_sources | app_id* | What sends events to the channel: monitors, heartbeats and other sources |
test_delivery | app_id*, adapter* | Send a test notification through a configured delivery method. adapter — email, sms, call, slack, telegram, webhook, discord, teams, mattermost, pagerduty or opsgenie |
Clients
Section titled “Clients”| Tool | Parameters | Description |
|---|---|---|
list_clients | — | List all clients (devices/tokens) |
create_client | name* | Create a device and get its token (C…) |
update_client | id*, name* | Rename a client |
delete_client | id* | Delete a client (revoke token) |
create_client, update_client and delete_client answer 403 with an API
token: an API token cannot manage devices, a device token (C…) is required.
| Tool | Parameters | Description |
|---|---|---|
get_current_user | — | Information about the current user |
list_users | — | List all users (admin) |
get_user | id* | Get a user by ID (admin). The current server version has no GET /user/:id route — the call returns an error |
create_user | name*, pass*, admin | Create a user. Without administrator rights the call is treated as a regular sign-up: it succeeds only if registration is open, and without admin |
delete_user | id* | Delete a user (admin) |
request_password_reset | email* | Request a password reset — a link with a token will be sent to the email (public, no auth) |
confirm_password_reset | token*, new_password* | Confirm reset: set a new password using the token from the email |
An API token never gets administrator rights, even if it belongs to an
administrator: list_users and delete_user answer 403 with it. These tools
need an administrator’s device token (C…) in NOTIFLY_API_TOKEN and, on top
of that, a fresh sudo confirmation on that device (a 15-minute window);
otherwise they also answer 403.
Heartbeats (control signals)
Section titled “Heartbeats (control signals)”More about heartbeats — see the Heartbeat page.
| Tool | Parameters | Description |
|---|---|---|
list_heartbeats | — | List heartbeat notifications (dead-man switch) |
create_heartbeat | appid*, name*, intervalSec*, alertMessage*, graceSec, alertTitle, alertPriority, recoveryTitle, recoveryMessage | Create a heartbeat. intervalSec ≥ 30; an alert is sent if a ping hasn’t arrived within intervalSec+graceSec seconds; alertPriority defaults to 8 |
update_heartbeat | id*, name*, intervalSec*, alertMessage*, graceSec, alertTitle, alertPriority, recoveryTitle, recoveryMessage | Update heartbeat settings |
delete_heartbeat | id* | Delete a heartbeat |
pause_heartbeat | id* | Pause (alerts will not be sent until resume_heartbeat) |
resume_heartbeat | id* | Resume a paused heartbeat |
ping_heartbeat | ping_token* | Send a ping using a public ping token (as an app would from cron) |
test_heartbeat_alert | id* | Send the heartbeat’s alert notification right now — to check the channel and delivery. Creates a real message in the channel |
test_heartbeat_recovery | id* | Send the heartbeat’s recovery notification right now. Creates a real message in the channel |
Monitors
Section titled “Monitors”More about active checks — see the Monitors page.
| Tool | Parameters | Description |
|---|---|---|
list_monitors | — | List active monitors (HTTP, TCP and SSL) |
create_monitor | appid*, name*, kind*, target*, intervalSec*, alertMessage*, timeoutSec, expectedStatus, certMinDays, consecutiveFails, alertTitle, alertPriority, recoveryTitle, recoveryMessage | Create a monitor. kind = http (then target is a URL), tcp (target is host:port) or ssl — TLS certificate expiry and validity (target is host or host:port, port 443 by default). intervalSec 30–86400; timeoutSec 1–60, defaults to 10; expectedStatus (http only) 0 = “any 2xx”; certMinDays (ssl only) 1–365, defaults to 14 — an alert fires when fewer days are left before the certificate expires; consecutiveFails defaults to 1 (max. 20); alertPriority defaults to 8 |
update_monitor | id*, name*, kind*, target*, intervalSec*, alertMessage*, timeoutSec, expectedStatus, certMinDays, consecutiveFails, alertTitle, alertPriority, recoveryTitle, recoveryMessage | Update monitor settings (fields as in create_monitor) |
delete_monitor | id* | Delete a monitor |
pause_monitor | id* | Pause (checks will not run until resume_monitor) |
resume_monitor | id* | Resume a paused monitor |
test_monitor | kind*, target*, timeoutSec, expectedStatus, certMinDays | Probe a target once without creating a monitor — the same probe an active monitor runs. An unreachable target returns ok: false in the body rather than a tool error |
Specialized monitors
Section titled “Specialized monitors”The five families work the same way: list_*, create_*, update_*, delete_*, pause_*, resume_*. An update replaces the settings entirely (omitted fields are reset), and the channel (appid) of an existing monitor cannot be changed.
| Family | Tools | Purpose |
|---|---|---|
http_monitor | list_http_monitors, create_http_monitor, update_http_monitor, delete/pause/resume_http_monitor | A request with the response checked by filters: status, substring, regular expression, JSONPath, duration. intervalSec 30–86400 |
port_monitor | list_port_monitors, create_port_monitor, update_port_monitor, delete/pause/resume_port_monitor | Watches that the required TCP ports are open (alertMode: closed) or, conversely, closed (open). intervalSec 60–86400 |
content_monitor | list_content_monitors, create_content_monitor, update_content_monitor, delete/pause/resume_content_monitor | A value on a page: mode = hash, json, text, css or xpath; useBrowser renders JS. intervalSec 60–86400 |
workflow_monitor | list_workflow_monitors, create_workflow_monitor, update_workflow_monitor, delete/pause/resume_workflow_monitor | A chain of HTTP requests (log in, then call) with {{name}} substitution from extracted values. intervalSec 60–86400 |
browser_workflow | list_browser_workflows, create_browser_workflow, update_browser_workflow, delete/pause/resume_browser_workflow | A real browser walks through the page steps. intervalSec from 300 — a higher minimum than the rest; pause answers with no body |
Trial runs without creating a monitor: test_http_monitor, test_port_monitor, test_content_rule, test_workflow (browser_workflow has no trial run; for a regular monitor — test_monitor above). An unreachable target returns ok: false in the body rather than a tool error.
Check history
Section titled “Check history”| Tool | Parameters | Description |
|---|---|---|
get_monitor_history | kind*, id*, bucket, from, to | Check statistics by bucket: total, successful, durations. bucket = 1m, 1h (default) or 1d; from/to are Unix seconds |
get_monitor_log | kind*, id*, limit | The latest individual checks with each one’s error text; up to 200, kept for 7 days |
kind is written in snake_case and does not match the monitor paths: monitor, heartbeat, http_monitor, content_monitor, port_monitor, workflow_monitor, browser_workflow_monitor.
Port scans and host verification
Section titled “Port scans and host verification”| Tool | Parameters | Description |
|---|---|---|
list_port_scans | — | List scans |
get_port_scan | id* | A scan with its progress and the ports found |
create_port_scan | appid*, name*, host*, profile, rangeFrom, rangeTo, timeoutSec, repeatIntervalSec, alertTitle, alertMessage, alertPriority | Start a scan. profile = quick, standard, full or custom; except for custom, the profile overwrites the range. A private host is allowed. alertTitle/alertMessage/alertPriority — the result notification (the text is optional here) |
update_port_scan | id*, the rest optional | A true patch: only the passed fields change (the same as in create_port_scan, except profile). A running scan cannot be edited; appid can be changed here |
delete_port_scan | id* | Delete a scan |
cancel_port_scan | id* | Stop a queued or running scan |
restart_port_scan | id* | Restart a finished scan |
port_scan_to_monitor | id*, name, intervalSec, timeoutSec, alertTitle, alertMessage, alertPriority | Turn the open ports found into a monitor that watches for them closing |
list_verified_hosts | — | Verified hosts (a verification lives 365 days) |
start_host_verification | host*, method* | Start verification: dns or http. A private address is verified immediately, a public one returns a token to publish |
check_host_verification | host*, method*, token* | Confirm a published challenge. While the record is not visible yet — 422, worth retrying |
delete_verified_host | id* | Revoke a verification |
Metrics
Section titled “Metrics”| Tool | Parameters | Description |
|---|---|---|
push_metric | metric_token*, name + value or points | Write values to a metric source. points — a JSON array of up to 500 points with an optional ts in Unix milliseconds. A 204 response comes back even for an unknown token |
Channel sharing (shares)
Section titled “Channel sharing (shares)”More — see the Channel sharing page.
| Tool | Parameters | Description |
|---|---|---|
list_channel_shares | app_id* | List share tokens (delegations) for a channel; available to the owner or a user with send permission |
create_channel_share | app_id*, permission*, recipient_email, note | Create a delegation. permission = view (read) or send (read + send); an empty recipient_email = an open token |
update_channel_share | id*, permission*, recipient_email, note | Change recipient email, access level or note (channel owner only) |
delete_channel_share | id* | Revoke a share token (delete the delegation) |
list_incoming_shares | — | Channels the current user has been granted access to (incoming shares by email) |
accept_share | id* | Accept an incoming delegation (status → active) |
decline_share | id* | Decline an incoming delegation |
join_share_by_token | token* | Join a channel by share token (prefix S) |
Device subscriptions
Section titled “Device subscriptions”| Tool | Parameters | Description |
|---|---|---|
get_client_subscriptions | client_id* | Channels the device is subscribed to by client ID |
set_client_subscriptions | client_id*, app_ids* | Set the full list of device subscriptions; app_ids is an array of IDs, e.g. [1,2,3] (replaces the current ones; an empty one removes all subscriptions) |
get_my_subscriptions | — | Subscriptions of the current device |
subscribe_to_channel | app_id* | Subscribe the current device to a channel |
unsubscribe_from_channel | channel_id* | Unsubscribe the current device from a channel |
get_my_subscriptions, subscribe_to_channel and unsubscribe_from_channel
work with the current device: they need a device token (C…) in
NOTIFLY_API_TOKEN; an API token has no device, and the server answers 401.
Account and billing
Section titled “Account and billing”| Tool | Parameters | Description |
|---|---|---|
get_quota_breakdown | day | Detailed breakdown of event quota usage for a day (YYYY-MM-DD, default — today in MSK) |
topup_balance | amount_rubles* | Top up balance (integer ≥ 1; admin only, others will receive payment details) |
* — required parameter
Examples
Section titled “Examples”Send a notification via Claude
Section titled “Send a notification via Claude”Send a notification to Notifly with the title "Deploy finished"and the text "Version 2.1.0 was deployed to prod successfully" with priority 7.Claude will call the send_message tool automatically.
Channel management
Section titled “Channel management”Show the list of all channels in Notifly and the last 10 messagesfrom the channel with ID 3.Claude will call list_applications and then list_application_messages sequentially.
Administration
Section titled “Administration”Create a new user in Notifly named "devops"with the password "securepass", without administrator rights.Claude will call create_user with the required parameters. Creating a user
as an administrator works with an administrator’s device token in
NOTIFLY_API_TOKEN; without administrator rights the server treats the call
as a regular sign-up (see Users).
Recipe: an approval gate for an AI agent (human-in-the-loop)
Section titled “Recipe: an approval gate for an AI agent (human-in-the-loop)”An autonomous agent should not decide on its own whether to apply a migration
to prod — it should ask. The ask_question → get_ask_answer tool pair turns
Notifly into a ready-made approval gate: the question arrives as a push
notification (and in Telegram, if connected), the human answers with a
button right in the notification, the agent receives the answer and acts.
Step 1. The agent asks the question
Section titled “Step 1. The agent asks the question”The agent calls ask_question with button options, a timeout and a safe
default answer:
{ "tool": "ask_question", "arguments": { "question": "Apply the migration to prod?", "options": ["yes", "no"], "timeout_sec": 300, "default_answer": "no", "client_message_id": "deploy-2026-07-13-migration" }}The tool result is the id of the created question:
{"id": 8260183728527568, "status": "pending"}timeout_sec: 300+default_answer: "no"— if the human hasn’t answered within 5 minutes, the question closes with a safe “no”: silence is not consent.client_message_idis an idempotency key: if the agent repeats the call (a retry after a dropped connection), no duplicate question is created — the existing one is returned.
Step 2. The human answers from the notification
Section titled “Step 2. The human answers from the notification”A push arrives on the phone — “Apply the migration to prod?” with yes / no buttons; the same question with buttons lands in Telegram if it is connected to the channel. One tap and the answer is recorded — no need to open the app.
Step 3. The agent waits for the answer
Section titled “Step 3. The agent waits for the answer”{ "tool": "get_ask_answer", "arguments": {"id": 8260183728527568, "wait_sec": 300}}get_ask_answer with wait_sec > 0 blocks until the answer arrives and
returns a terminal status. The answer comes as a WebSocket push, so the tool
returns the moment someone presses the button; when the socket is unavailable
it falls back to polling every 3 seconds:
{"id": 8260183728527568, "status": "answered", "answer": "yes"}Step 4. The agent acts on the answer
Section titled “Step 4. The agent acts on the answer”The full pseudo-dialog:
Agent: about to apply migration 0042 to prod. Asking for permission. → ask_question("Apply the migration to prod?", options=["yes","no"], timeout_sec=300, default_answer="no") ← {"id": 8260…, "status": "pending"} → get_ask_answer(id=8260…, wait_sec=300)
Human: [push on the phone] "Apply the migration to prod?" [yes] [no] — taps "yes"
Agent: ← {"status": "answered", "answer": "yes"} applying the migration… done, sending send_message("Migration 0042 applied").If the status is expired, the timeout fired and the default_answer (“no”)
was recorded; if cancelled, the question was cancelled. In both cases the
agent does not perform the action. More about the questions mechanics — on the
Questions (Ask) page.
Security: a tag scope and TTL for the agent’s token
Section titled “Security: a tag scope and TTL for the agent’s token”The agent does not need access to every channel in the account. Restrict its API token:
- Put an
aitag on the “agent” channels (the channel’s “Tags” field orcreate_application/update_applicationwithtags: ["ai"]). - Issue an API token with a tag scope and an expiry:
POST /api-token{ "name": "deploy-agent", "access": "write", "appTags": ["ai"], "expiresAt": "2026-08-01T00:00:00Z"}Such a token sees only channels tagged ai — including ones created later
(a dynamic scope, no token edits needed), and past expiresAt it stops working
by itself: a token forgotten in an agent’s config does not remain a permanent
hole. More about the scope — on the API tokens page.
Security
Section titled “Security”- The MCP server runs locally over stdio — no network port is opened.
- Tokens are stored only in the process environment variables.
- The
delete_all_messagesanddelete_usertools perform irreversible operations — the AI assistant should request confirmation before calling them.