Skip to content

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);
  • administer server users.

Three steps and your assistant is sending notifications:

  1. Build the notifly-mcp binary (see Installation) and place it somewhere like /usr/local/bin/notifly-mcp.
  2. Grab your tokens in the admin panel: a channel app token (A…, channels tab) and an MCP code (M…, the MCP page).
  3. Copy the config for your client and substitute your own values for AXXXXXXXXXXXXXX (app token) and MXXXXXXXXXXXXXXXXXXXXXX (MCP code). NOTIFLY_URL for cloud Notifly is https://api.notifly.ru; for self-hosted — your server address.

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": "AXXXXXXXXXXXXXX",
"NOTIFLY_CLIENT_TOKEN": "MXXXXXXXXXXXXXXXXXXXXXX"
}
}
}
}

Claude Code — a single terminal command:

Terminal window
claude mcp add notifly /usr/local/bin/notifly-mcp \
-e NOTIFLY_URL=https://api.notifly.ru \
-e NOTIFLY_APP_TOKEN=AXXXXXXXXXXXXXX \
-e NOTIFLY_CLIENT_TOKEN=MXXXXXXXXXXXXXXXXXXXXXX

Cursor — 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": "AXXXXXXXXXXXXXX",
"NOTIFLY_CLIENT_TOKEN": "MXXXXXXXXXXXXXXXXXXXXXX"
}
}
}
}

Restart the client (Cursor picks the config up by itself) — and ask your assistant: “Send a Notifly notification saying ‘MCP connected’”. Other clients (VS Code, Windsurf, Codex, Zed, Continue) are covered in the sections below.


  • Go 1.21 or newer
Terminal window
git clone https://github.com/notifly/mcp.git
cd mcp
go mod tidy
go build -o notifly-mcp .

The resulting binary notifly-mcp runs over a stdio transport — the MCP client launches it as a child process.


The server is configured exclusively via environment variables:

ПеременнаяОбязательнаяОписание
NOTIFLY_URLBase URL of the Notifly server, e.g. https://your-domain.com
NOTIFLY_APP_TOKENApp token (prefix A) — for the send_message tool
NOTIFLY_CLIENT_TOKENAn MCP code (prefix M) or a client token (prefix C) — for managing resources
NOTIFLY_USERLogin for Basic Auth (alternative to MCP code)
NOTIFLY_PASSPassword for Basic Auth

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://your-notifly-domain.com",
"NOTIFLY_APP_TOKEN": "AGdjfk_L.dKe8q",
"NOTIFLY_CLIENT_TOKEN": "CaQw5lL_L.yiRbN"
}
}
}
}

After restarting Claude Desktop, the Notifly tools will appear in the interface.


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://your-notifly-domain.com",
"NOTIFLY_APP_TOKEN": "AGdjfk_L.dKe8q",
"NOTIFLY_CLIENT_TOKEN": "CaQw5lL_L.yiRbN"
}
}
}
}

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://your-notifly-domain.com",
"NOTIFLY_APP_TOKEN": "AGdjfk_L.dKe8q",
"NOTIFLY_CLIENT_TOKEN": "CaQw5lL_L.yiRbN"
}
}
}
}

After saving, Cursor will pick up the server automatically. Make sure notifly-mcp is available in PATH.


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://your-notifly-domain.com",
"NOTIFLY_APP_TOKEN": "AGdjfk_L.dKe8q",
"NOTIFLY_CLIENT_TOKEN": "CaQw5lL_L.yiRbN"
}
}
}
}

After saving, restart Windsurf — the Notifly tools will appear in Cascade.


Use the claude mcp add command to register the server:

Terminal window
claude mcp add notifly /usr/local/bin/notifly-mcp \
-e NOTIFLY_URL=https://your-notifly-domain.com \
-e NOTIFLY_CLIENT_TOKEN=CaQw5lL_L.yiRbN

The --scope user flag adds the server globally for all projects (by default — project-scope). To list added servers: claude mcp list.


Open or create ~/.codex/config.toml and add the section:

[[mcp_servers]]
name = "notifly"
command = "/usr/local/bin/notifly-mcp"
env = { NOTIFLY_URL = "https://your-notifly-domain.com", NOTIFLY_CLIENT_TOKEN = "CaQw5lL_L.yiRbN" }

After saving, Codex will pick up the server on next start. Make sure notifly-mcp is available in PATH.


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://your-notifly-domain.com",
"NOTIFLY_CLIENT_TOKEN": "CaQw5lL_L.yiRbN"
}
}
}
}
}

Zed will pick up the server without a restart.


Add a mcpServers section to the Continue config (~/.continue/config.json):

{
"mcpServers": [
{
"name": "notifly",
"command": "/usr/local/bin/notifly-mcp",
"env": {
"NOTIFLY_URL": "https://your-notifly-domain.com",
"NOTIFLY_CLIENT_TOKEN": "CaQw5lL_L.yiRbN"
}
}
]
}

Continue is supported as an extension for VS Code and JetBrains IDEs.


ToolDescription
get_healthCheck server and database health
get_versionGet version, commit and build date
get_server_infoGet server flags (registration, OIDC)
ToolParametersDescription
send_messagemessage*, title, prioritySend a notification (requires NOTIFLY_APP_TOKEN)
list_messageslimit, sinceList all messages with pagination
list_application_messagesapp_id*, limit, sinceMessages of a specific channel
delete_messageid*Delete a message by ID
delete_all_messagesDelete all messages
delete_application_messagesapp_id*Delete all messages of a channel
search_messagesq*, app_id, limit, sinceSearch messages by text (min. 2 characters); across all channels or in one
mark_messages_readids*Mark messages as read (JSON array of IDs, e.g. [1,2,3])
ToolParametersDescription
ask_questionquestion*, options, title, timeout_sec, default_answer, allow_multi, json_format, client_message_idAsk a human a question (push/Telegram/email with buttons); returns the question id. Requires NOTIFLY_APP_TOKEN. See Questions (Ask)
get_ask_answerid*, wait_secFetch the answer; with wait_sec > 0 — wait for it (polls every 3 s, up to 300 s). Returns answered+answer, pending, expired or cancelled. Requires NOTIFLY_APP_TOKEN
cancel_ask_questionid*Cancel a pending question

The ask_questionget_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.

ToolParametersDescription
list_applicationsList all channels
create_applicationname*, description, default_priority, tagsCreate a channel. tags — up to 10 tags of [a-z0-9_-]; used by the tag scope of MCP codes
update_applicationid*, name, description, default_priority, tagsUpdate a channel; tags replace the current ones (omitted — unchanged)
delete_applicationid*Delete a channel
ToolParametersDescription
list_clientsList all clients (devices/tokens)
create_clientname*Create a client and obtain a client token
update_clientid, nameRename a client
delete_clientid*Delete a client (revoke token)
ToolParametersDescription
get_current_userInformation about the current user
list_usersList all users (requires admin)
get_userid*Get a user by ID (admin)
create_username, pass, adminCreate a user (admin)
delete_userid*Delete a user (admin)
request_password_resetemail*Request a password reset — a link with a token will be sent to the email (public, no auth)
confirm_password_resettoken, new_passwordConfirm reset: set a new password using the token from the email

More about heartbeats — see the Heartbeat page.

ToolParametersDescription
list_heartbeatsList heartbeat notifications (dead-man switch)
create_heartbeatappid, name, intervalSec, alertMessage, graceSec, alertTitle, alertPriority, recoveryTitle, recoveryMessageCreate a heartbeat. intervalSec ≥ 30; an alert is sent if a ping hasn’t arrived within intervalSec+graceSec seconds; alertPriority defaults to 8
update_heartbeatid, name, intervalSec, alertMessage, graceSec, alertTitle, alertPriority, recoveryTitle, recoveryMessageUpdate heartbeat settings
delete_heartbeatid*Delete a heartbeat
pause_heartbeatid*Pause (alerts will not be sent until resume_heartbeat)
resume_heartbeatid*Resume a paused heartbeat
ping_heartbeatping_token*Send a ping using a public ping token (as an app would from cron)

More about active checks — see the Monitors page.

ToolParametersDescription
list_monitorsList active monitors (HTTP URLs and TCP ports)
create_monitorappid, name, kind, target, intervalSec, alertMessage, timeoutSec, expectedStatus, consecutiveFails, alertTitle, alertPriority, recoveryTitle, recoveryMessageCreate a monitor. kind = http (then target is a URL) or tcp (target is host:port). intervalSec 30–86400; timeoutSec defaults to 10; expectedStatus 0 = “any 2xx”; consecutiveFails defaults to 1 (max. 20); alertPriority defaults to 8
update_monitorid, name, kind, target, intervalSec, alertMessage, timeoutSec, expectedStatus, consecutiveFails, alertTitle, alertPriority, recoveryTitle, recoveryMessageUpdate monitor settings
delete_monitorid*Delete a monitor
pause_monitorid*Pause (checks will not run until resume_monitor)
resume_monitorid*Resume a paused monitor

More — see the Channel sharing page.

ToolParametersDescription
list_channel_sharesapp_id*List share tokens (delegations) for a channel; available to the owner or a user with send permission
create_channel_shareapp_id, permission, recipient_email, noteCreate a delegation. permission = view (read) or send (read + send); an empty recipient_email = an open token
update_channel_shareid, permission, recipient_email, noteChange recipient email, access level or note (owner only)
delete_channel_shareid*Revoke a share token (delete the delegation)
list_incoming_sharesChannels the current user has been granted access to (incoming shares by email)
accept_shareid*Accept an incoming delegation (status → active)
decline_shareid*Decline an incoming delegation
join_share_by_tokentoken*Join a channel by share token (prefix S)
ToolParametersDescription
get_client_subscriptionsclient_id*Channels the device is subscribed to by client ID
set_client_subscriptionsclient_id, app_idsSet the full list of device subscriptions; app_ids is a JSON array of IDs, e.g. [1,2,3] (replaces current)
get_my_subscriptionsSubscriptions of the current device (authorize with device token via NOTIFLY_CLIENT_TOKEN)
subscribe_to_channelapp_id*Subscribe the current device to a channel
unsubscribe_from_channelchannel_id*Unsubscribe the current device from a channel
ToolParametersDescription
get_quota_breakdowndayDetailed breakdown of event quota usage for a day (YYYY-MM-DD, default — today in MSK)
topup_balanceamount_rubles*Top up balance (integer ≥ 1; admin only, others will receive payment details)

* — required parameter


Отправь уведомление в Notifly с заголовком "Деплой завершён"
и текстом "Версия 2.1.0 успешно развёрнута на prod" с приоритетом 7.

Claude will call the send_message tool automatically.

Покажи список всех каналов в Notifly и последние 10 сообщений
из канала с ID 3.

Claude will call list_applications and then list_application_messages sequentially.

Создай нового пользователя в Notifly с именем "devops"
и паролем "securepass", без прав администратора.

Claude will call create_user with the required parameters.


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_questionget_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.

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_id is 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.

{
"tool": "get_ask_answer",
"arguments": {"id": 8260183728527568, "wait_sec": 300}
}

get_ask_answer with wait_sec > 0 blocks until the answer arrives (polling every 3 seconds) and returns a terminal status:

{"id": 8260183728527568, "status": "answered", "answer": "yes"}

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 MCP code

Section titled “Security: a tag scope and TTL for the agent’s MCP code”

The agent does not need access to every channel in the account. Restrict its MCP code:

  1. Put an ai tag on the “agent” channels (the channel’s “Tags” field or create_application/update_application with tags: ["ai"]).
  2. Create an MCP code with a tag scope and an expiry:
POST /mcp/token
{
"name": "deploy-agent",
"access": "write",
"appTags": ["ai"],
"expiresAt": "2026-08-01T00:00:00Z"
}

Such a code sees only channels tagged ai — including ones created later (a dynamic scope, no code edits needed), and past expiresAt it stops working by itself: a token forgotten in an agent’s config does not remain a permanent hole. The scope can be combined with a static channel list (appIds) — access is granted if the channel is in the list or carries a matching tag; empty appIds and appTags mean “all channels”.


  • The MCP server runs locally over stdio — no network port is opened.
  • Tokens are stored only in the process environment variables.
  • The delete_all_messages and delete_user tools perform irreversible operations — the AI assistant should request confirmation before calling them.