ntfy-mcp-server

v2.3.7

Send, manage, and replay ntfy push notifications via MCP. STDIO or Streamable HTTP.

public 4 tools 1 resources protocol 2026-07-28
m8f9gzz9vct7mhiinkcvbrcd.vps.totolol24.ovh/mcp
claude mcp add --transport http ntfy-mcp-server http://m8f9gzz9vct7mhiinkcvbrcd.vps.totolol24.ovh/mcp
codex mcp add ntfy-mcp-server --url http://m8f9gzz9vct7mhiinkcvbrcd.vps.totolol24.ovh/mcp
{
  "mcpServers": {
    "ntfy-mcp-server": {
      "url": "http://m8f9gzz9vct7mhiinkcvbrcd.vps.totolol24.ovh/mcp"
    }
  }
}
gemini mcp add --transport http ntfy-mcp-server http://m8f9gzz9vct7mhiinkcvbrcd.vps.totolol24.ovh/mcp
{
  "mcpServers": {
    "ntfy-mcp-server": {
      "command": "bunx",
      "args": [
        "mcp-remote",
        "http://m8f9gzz9vct7mhiinkcvbrcd.vps.totolol24.ovh/mcp"
      ]
    }
  }
}
{
  "mcpServers": {
    "ntfy-mcp-server": {
      "type": "http",
      "url": "http://m8f9gzz9vct7mhiinkcvbrcd.vps.totolol24.ovh/mcp"
    }
  }
}
curl -X POST http://m8f9gzz9vct7mhiinkcvbrcd.vps.totolol24.ovh/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"curl","version":"1.0.0"}}}'

Tools

4

read 2

ntfy_fetch_messages

open-world

Poll cached messages from one or more ntfy topics with optional filters. Returns a snapshot, not a live stream — use it to confirm delivery, replay missed alerts, or audit topic activity. Multiple topics are passed as a comma-separated list. One call returns the newest messages of the window; when more match, page through the whole window oldest-first by passing `nextCursor` back as `cursor`. Long bodies are truncated client-side to keep list responses bounded; refetch with that message `id` to read one in full.

read
invocation
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "ntfy_fetch_messages",
    "arguments": {}
  }
}
schema
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "topic": {
      "description": "Single topic or comma-separated list (e.g., `alerts` or `alerts,backups,phil_alerts`). Required unless `NTFY_DEFAULT_TOPIC` is set.",
      "type": "string",
      "minLength": 1,
      "pattern": "^[a-zA-Z0-9_-]+(?:,[a-zA-Z0-9_-]+)*$"
    },
    "since": {
      "default": "10m",
      "description": "Lower bound of the window. Default `10m`, tighter than ntfy's default to keep the common 'did my recent notification land?' lookup cheap. Accepts a duration in seconds, minutes, or hours (`30s`, `10m`, `2h`, `24h`), a Unix timestamp, the keywords `all` (every cached message — up to ~12h on ntfy.sh) or `latest` (most recent only), or a message ID, which returns only the messages after that one — an ID ntfy no longer holds returns the whole cache rather than an error. To page through a window larger than one call returns, use `cursor`, not `since`; keep `since` the same on every page, even a duration — the cursor holds the window where the walk began.",
      "type": "string"
    },
    "scheduled": {
      "default": false,
      "description": "Include delayed/not-yet-delivered messages.",
      "type": "boolean"
    },
    "priority": {
      "description": "Match any priority in the list (logical OR). Empty/omitted matches all priorities.",
      "type": "array",
      "items": {
        "type": "integer",
        "minimum": 1,
        "maximum": 5,
        "description": "Single priority value (1=min, 5=max)."
      }
    },
    "tags": {
      "description": "Match all tags in the list (logical AND). Empty/omitted matches all messages regardless of tags.",
      "type": "array",
      "items": {
        "type": "string",
        "minLength": 1
      }
    },
    "id": {
      "description": "Exact-match a single message ID. Pinning one message this way also returns its body in full — the ~500-char cap that applies to list results is lifted. `since` still bounds which cached messages are searched, so widen it when the target is older than the default window.",
      "type": "string"
    },
    "title": {
      "description": "Exact-match against the title string.",
      "type": "string"
    },
    "message": {
      "description": "Exact-match against the message body.",
      "type": "string"
    },
    "limit": {
      "default": 20,
      "description": "Client-side cap on returned messages. Default 20; one call returns at most 100, so a larger value is served as 100. Without `cursor`, the newest `limit` of the window are returned (listed oldest-first); with `cursor`, up to `limit` starting at the cursor message. `truncated` and `nextCursor` say when more remain.",
      "type": "integer",
      "exclusiveMinimum": 0,
      "maximum": 1000
    },
    "cursor": {
      "description": "Continue a paged read: pass the previous response's `nextCursor` verbatim, with the same `topic`, `since`, and filters. Returns up to `limit` messages starting at that message (inclusive), oldest-first, and a new `nextCursor` while more remain. Under a duration `since` (`10m`, `2h`), the cursor also holds the window where the walk began, so later pages stay reachable after the duration's lower bound has moved past the first messages. Omit it to get the newest messages of the window.",
      "type": "string"
    },
    "base_url": {
      "description": "Override `NTFY_BASE_URL` for this call — an absolute `http(s)://` URL. Server-configured auth credentials are forwarded only when the override matches a registered server (`NTFY_BASE_URL` or an `NTFY_SERVERS` entry); otherwise the request goes out unauthenticated.",
      "type": "string",
      "pattern": "^$|^https?:\\/\\/\\S+$"
    }
  },
  "required": [
    "since",
    "scheduled",
    "limit"
  ],
  "additionalProperties": false
}

ntfy_search_emoji_tags

Look up ntfy emoji tag short codes. Use the returned `tag` strings in `ntfy_publish_message`'s `tags` field to render emojis on the recipient's device. Without a query, returns the first slice of the full reference; pass a substring (e.g., `warning`, `tada`, `cd`) to filter, and `offset` to page through matches beyond `limit`.

read
invocation
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "ntfy_search_emoji_tags",
    "arguments": {}
  }
}
schema
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "query": {
      "description": "Substring to match against emoji tag names (case-insensitive). Omit to list the reference from the start in its documented order.",
      "type": "string"
    },
    "limit": {
      "default": 25,
      "description": "Maximum number of matches to return. Default 25. One call returns at most 200; a larger value returns 200 and names the next `offset`.",
      "type": "integer",
      "exclusiveMinimum": 0,
      "maximum": 1000
    },
    "offset": {
      "default": 0,
      "description": "Number of matches to skip before returning `limit` rows. Use it with `totalCount` to page past the `limit` cap — the reference holds far more tags than one call can return.",
      "type": "integer",
      "minimum": 0,
      "maximum": 9007199254740991
    }
  },
  "required": [
    "limit",
    "offset"
  ],
  "additionalProperties": false
}

unspecified 1

ntfy_publish_message

open-world

Send or update a push notification on an ntfy topic. Topics are created on first publish — treat the topic name as a secret because anyone who knows it can publish or subscribe. Set `sequence_id` to update a previously-published message; otherwise the call creates a new one. Use `ntfy_search_emoji_tags` to look up emoji short codes for `tags`. A publish carrying `email`, `call`, or a `broadcast`/`http` action button reaches beyond the notification drawer, so that call always asks the user to confirm the specific target first and comes back requesting that input rather than a result; reissue the same call with the answer attached to send it. Declining fails the call with `consent_declined`. A plain notification publishes on the first call.

unspecified
invocation
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "ntfy_publish_message",
    "arguments": {}
  }
}
schema
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "topic": {
      "description": "Target topic. Required unless `NTFY_DEFAULT_TOPIC` is configured. Treat the topic name as a secret — anyone who knows it can publish or subscribe.",
      "type": "string",
      "minLength": 1,
      "maxLength": 64,
      "pattern": "^[a-zA-Z0-9_-]+$"
    },
    "message": {
      "description": "Notification body, ≤4096 bytes (not characters — accented letters, CJK, and emoji cost 2–4 bytes each). Empty/missing is replaced server-side with the literal `triggered`; pass real content even for ping-style alerts.",
      "type": "string",
      "maxLength": 4096
    },
    "title": {
      "description": "Notification title. Defaults to the topic URL on the recipient device.",
      "type": "string"
    },
    "priority": {
      "type": "integer",
      "minimum": 1,
      "maximum": 5,
      "description": "Notification priority — 1=min, 2=low, 3=default, 4=high, 5=max/urgent. Server defaults to 3 when omitted."
    },
    "tags": {
      "description": "Up to 10 tags. Strings matching ntfy emoji short codes render as emojis prepended to the title; others render as a tag list. Use `ntfy_search_emoji_tags` to discover short codes.",
      "maxItems": 10,
      "type": "array",
      "items": {
        "type": "string",
        "minLength": 1
      }
    },
    "click": {
      "description": "URL opened when the notification is tapped — http(s), mailto:, geo:, ntfy://, etc.",
      "type": "string",
      "minLength": 1
    },
    "attach": {
      "description": "External attachment URL (absolute). ntfy fetches it on the recipient device — host the file somewhere reachable first; no local file uploads.",
      "type": "string"
    },
    "icon": {
      "description": "Absolute JPEG/PNG icon URL shown next to the message body. Cached for 24h.",
      "type": "string"
    },
    "filename": {
      "description": "Overrides the attachment's display filename in the client.",
      "type": "string"
    },
    "markdown": {
      "description": "Render the message body as Markdown (web app only; mobile renders plain text).",
      "type": "boolean"
    },
    "actions": {
      "description": "Up to 3 action buttons. Discriminated by `action` (`view`, `broadcast`, `http`, `copy`).",
      "maxItems": 3,
      "type": "array",
      "items": {
        "oneOf": [
          {
            "type": "object",
            "properties": {
              "action": {
                "type": "string",
                "const": "view",
                "description": "Discriminator — must be `view`."
              },
              "label": {
                "type": "string",
                "minLength": 1,
                "maxLength": 64,
                "description": "Button label shown in the notification."
              },
              "url": {
                "type": "string",
                "minLength": 1,
                "description": "URL opened on tap — http(s) opens the browser; mailto:, geo:, ntfy://, twitter:// open registered apps."
              },
              "clear": {
                "description": "Dismiss the notification after the button is tapped.",
                "type": "boolean"
              }
            },
            "required": [
              "action",
              "label",
              "url"
            ],
            "additionalProperties": false,
            "description": "Open a URL or app when the button is tapped."
          },
          {
            "type": "object",
            "properties": {
              "action": {
                "type": "string",
                "const": "broadcast",
                "description": "Discriminator — must be `broadcast`."
              },
              "label": {
                "type": "string",
                "minLength": 1,
                "maxLength": 64,
                "description": "Button label shown in the notification."
              },
              "intent": {
                "description": "Android intent name (defaults server-side to `io.heckel.ntfy.USER_ACTION`).",
                "type": "string"
              },
              "extras": {
                "description": "String extras passed alongside the broadcast.",
                "type": "object",
                "propertyNames": {
                  "type": "string"
                },
                "additionalProperties": {
                  "type": "string"
                }
              },
              "clear": {
                "description": "Dismiss the notification after the button is tapped.",
                "type": "boolean"
              }
            },
            "required": [
              "action",
              "label"
            ],
            "additionalProperties": false,
            "description": "Send an Android broadcast intent (Tasker/MacroDroid integrations)."
          },
          {
            "type": "object",
            "properties": {
              "action": {
                "type": "string",
                "const": "http",
                "description": "Discriminator — must be `http`."
              },
              "label": {
                "type": "string",
                "minLength": 1,
                "maxLength": 64,
                "description": "Button label shown in the notification."
              },
              "url": {
                "type": "string",
                "description": "Absolute URL the HTTP request is sent to."
              },
              "method": {
                "description": "HTTP method — defaults to POST when omitted.",
                "type": "string"
              },
              "headers": {
                "description": "Headers attached to the HTTP request.",
                "type": "object",
                "propertyNames": {
                  "type": "string"
                },
                "additionalProperties": {
                  "type": "string"
                }
              },
              "body": {
                "description": "Request body.",
                "type": "string"
              },
              "clear": {
                "description": "Dismiss the notification only when the request succeeds.",
                "type": "boolean"
              }
            },
            "required": [
              "action",
              "label",
              "url"
            ],
            "additionalProperties": false,
            "description": "Fire an HTTP request (default POST) when the button is tapped."
          },
          {
            "type": "object",
            "properties": {
              "action": {
                "type": "string",
                "const": "copy",
                "description": "Discriminator — must be `copy`."
              },
              "label": {
                "type": "string",
                "minLength": 1,
                "maxLength": 64,
                "description": "Button label shown in the notification."
              },
              "value": {
                "type": "string",
                "minLength": 1,
                "description": "Text copied to the device clipboard on tap."
              },
              "clear": {
                "description": "Dismiss the notification after the button is tapped.",
                "type": "boolean"
              }
            },
            "required": [
              "action",
              "label",
              "value"
            ],
            "additionalProperties": false,
            "description": "Copy a value to the clipboard when the button is tapped."
          }
        ],
        "description": "A single action button. Discriminated by `action` — `view`, `broadcast`, `http`, or `copy`."
      }
    },
    "delay": {
      "description": "Schedule delivery for later. Accepts a duration (`30m`, `2h`, `1d`), a Unix timestamp, or natural-language time (`tomorrow, 10am`, `Tuesday, 7am`). 10s minimum; maximum 3 days by default (configurable per ntfy server).",
      "type": "string"
    },
    "email": {
      "description": "Forward to email — an address (`phil@example.com`) or `'yes'` to use the authenticated user's first verified address.",
      "type": "string"
    },
    "call": {
      "description": "Trigger a voice call — `+CCNNNNNNNNNN` or `'yes'` for the authenticated user's first verified number. Requires an authenticated ntfy account; ntfy.sh limits this to Pro plans.",
      "type": "string"
    },
    "sequence_id": {
      "description": "Update or replace a previously-published message instead of creating a new one. Same identifier used by `ntfy_manage_message`. 1–64 chars, `[a-zA-Z0-9_-]`.",
      "type": "string",
      "pattern": "^[a-zA-Z0-9_-]{1,64}$"
    },
    "cache": {
      "description": "Set false to skip server-side caching. Live subscribers still receive the message but `ntfy_fetch_messages` will not see it. Sent as `X-Cache: no`.",
      "type": "boolean"
    },
    "firebase": {
      "description": "Set false to skip Firebase Cloud Messaging. Delays Android delivery up to 15 min unless the recipient enabled instant delivery. Sent as `X-Firebase: no`.",
      "type": "boolean"
    },
    "base_url": {
      "description": "Override the configured `NTFY_BASE_URL` for this call — an absolute `http(s)://` URL. Server-configured auth credentials are forwarded only when the override matches a registered server (`NTFY_BASE_URL` or an `NTFY_SERVERS` entry); otherwise the request goes out unauthenticated.",
      "type": "string",
      "pattern": "^$|^https?:\\/\\/\\S+$"
    }
  },
  "additionalProperties": false
}

destructive 1

ntfy_manage_message

open-world

Clear (mark read & dismiss) or delete a previously-sent ntfy notification by `sequence_id`. Append-only: the original message stays in cache and a `message_clear`/`message_delete` event is emitted to subscribers. Re-issuing the same operation is safe — message state does not change, but a fresh event fires each time. ntfy.sh accepts unknown sequence IDs without error; stricter ntfy variants surface a `not_found` failure. The first call always asks the user to confirm the topic, `sequence_id`, and operation and comes back requesting that input rather than a result; reissue the same call with the answer attached to carry it out. Declining fails the call with `consent_declined`.

destructive
invocation
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "ntfy_manage_message",
    "arguments": {
      "sequence_id": "<sequence_id>",
      "operation": "<operation>"
    }
  }
}
schema
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "topic": {
      "description": "Topic the message was published to. Required unless NTFY_DEFAULT_TOPIC is set.",
      "type": "string",
      "minLength": 1,
      "maxLength": 64,
      "pattern": "^[a-zA-Z0-9_-]+$"
    },
    "sequence_id": {
      "type": "string",
      "pattern": "^[a-zA-Z0-9_-]{1,64}$",
      "description": "Identifier of the previously-published message. Same value used as `sequence_id` on `ntfy_publish_message`. 1–64 chars, `[a-zA-Z0-9_-]`."
    },
    "operation": {
      "type": "string",
      "enum": [
        "clear",
        "delete"
      ],
      "description": "What to do: `clear` marks the notification read & dismisses it (subscribers see `message_clear`); `delete` removes it from the drawer (subscribers see `message_delete`). Safe to re-issue — the original message stays in cache, but a fresh event is emitted to subscribers each call."
    },
    "base_url": {
      "description": "Override the configured `NTFY_BASE_URL` for this call — an absolute `http(s)://` URL. Server-configured auth credentials are forwarded only when the override matches a registered server (`NTFY_BASE_URL` or an `NTFY_SERVERS` entry); otherwise the request goes out unauthenticated.",
      "type": "string",
      "pattern": "^$|^https?:\\/\\/\\S+$"
    }
  },
  "required": [
    "sequence_id",
    "operation"
  ],
  "additionalProperties": false
}

Resources

1

Snapshot of a topic's recently-cached messages — latest 20 from the last 1 hour, plus the topic's browser URL. Messages are oldest-first with ISO 8601 timestamps, and bodies longer than ~500 characters are truncated with the dropped count reported as `messageTruncated`. `upstreamTruncated` is true when ntfy itself capped the replay and dropped older messages before responding. Discoverable 'what's happening on topic X' lookup; for filters, custom windows, or replay use `ntfy_fetch_messages`.

uri ntfy://{topic} mime application/json