{
  "markdown": "# ntfy-mcp\n\n<!-- badges: start -->\n\n[![CI](https://img.shields.io/github/actions/workflow/status/ni-c/ntfy-mcp/ci.yml?branch=main&label=CI)](https://github.com/ni-c/ntfy-mcp/actions/workflows/ci.yml)\n[![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/ni-c/ntfy-mcp/badge)](https://scorecard.dev/viewer/?uri=github.com/ni-c/ntfy-mcp)\n<a href=\"https://socket.dev/npm/package/@ni-c/ntfy-mcp\"><img src=\"https://socket.dev/api/badge/npm/package/@ni-c/ntfy-mcp\" alt=\"Socket supply-chain report\" height=\"20\"></a>\n[![Glama score](https://glama.ai/mcp/servers/ni-c/ntfy-mcp/badges/score.svg)](https://glama.ai/mcp/servers/ni-c/ntfy-mcp)\n<br>\n[![npm version](https://img.shields.io/npm/v/%40ni-c%2Fntfy-mcp)](https://www.npmjs.com/package/@ni-c/ntfy-mcp)\n[![container image](https://img.shields.io/badge/ghcr.io-ni--c%2Fntfy--mcp-4f46e5?logo=docker&logoColor=white)](https://github.com/ni-c/ntfy-mcp/pkgs/container/ntfy-mcp)\n[![HTTP via mcp-hub](https://img.shields.io/badge/HTTP-via%20mcp--hub-4f46e5?logo=modelcontextprotocol&logoColor=white)](https://mcp-hub.ni-c.de)\n<br>\n[![docs](https://img.shields.io/badge/docs-ntfy--mcp.ni--c.de-4f46e5?logo=readthedocs&logoColor=white)](https://ntfy-mcp.ni-c.de)\n[![sponsor](https://img.shields.io/badge/sponsor-ni--c-ea4aaa?logo=githubsponsors&logoColor=white)](https://github.com/sponsors/ni-c)\n<!-- badges: end -->\n\nA [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server for\n[ntfy](https://ntfy.sh), the pub-sub notification service that sends push messages\nto your phone with an HTTP request and nothing else.\n\nLets MCP clients like Claude Code, Claude Desktop or Codex send you notifications,\nread back what was sent, revise a notification in place while a job runs, and — with\nan admin account — create accounts and grant or revoke their access to topics, which\notherwise means the `ntfy` command line on the server.\n\nThirteen tools is the ceiling, not the floor: `NTFY_ALLOW_TOOLS=essential` registers\na curated six instead, and a model picks the right tool far more reliably from six\nthan from thirteen — see [choosing which tools load](#choosing-which-tools-load).\n\n<!-- <picture> is resolved against the colour scheme of the page showing it, so GitHub\n     picks the variant that matches its own theme toggle. npm strips <picture> and\n     <source> when it sanitises the README and keeps the <img>, which is why that\n     fallback brings its own dark card instead of relying on a media query.\n\n     The URLs must stay absolute: a relative path is simply invisible on npm. -->\n<picture>\n  <source media=\"(prefers-color-scheme: dark)\" srcset=\"https://ntfy-mcp.ni-c.de/architecture-dark.svg\">\n  <source media=\"(prefers-color-scheme: light)\" srcset=\"https://ntfy-mcp.ni-c.de/architecture-light.svg\">\n  <img src=\"https://ntfy-mcp.ni-c.de/architecture.svg\" alt=\"An MCP client speaks stdio to ntfy-mcp, which publishes notifications and polls the message cache over HTTPS against an ntfy server that pushes them to the subscribed devices\" width=\"800\">\n</picture>\n\n<!-- Recorded with vhs from docs/demo.tape against a throwaway ntfy container, so it\n     reproduces without touching a real instance — see the header of that file. -->\n\n![Demo: listing the tools, publishing a notification and revising it in place through the MCP Inspector CLI](https://ntfy-mcp.ni-c.de/demo.gif)\n\n## What makes it different\n\n**A progress report stays one notification.** The id `publish_message` returns is\nalso the notification's sequence id, and `update_message` replaces its content in\nplace — subscribers watch one notification change from \"building\" to \"deployed\"\ninstead of collecting five.\n\n**`NTFY_TOPICS` is the fence.** On ntfy a topic name is a bearer credential:\nknowing it is often the whole of the access control. One variable names the topics\nthis server may touch and supplies the default when a tool omits one, so the name\nstays out of the tool arguments and out of the model's context.\n\n## Requirements\n\n- Node.js ≥ 22\n- A reachable **ntfy** server. Credentials are optional: an instance that allows\n  anonymous access needs none.\n\n## Configuration\n\n| Variable            | Required | Description                                                                                                                                                             |\n| ------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `NTFY_URL`          | yes      | Base URL, e.g. `https://ntfy.example.net`. There is deliberately no default — `https://ntfy.sh` would make a misconfiguration publish to the public internet.           |\n| `NTFY_TOKEN`        | no       | Access token (`tk_…`). Mutually exclusive with the two below.                                                                                                           |\n| `NTFY_USERNAME`     | no       | Basic-auth user. Must be set together with `NTFY_PASSWORD`.                                                                                                             |\n| `NTFY_PASSWORD`     | no       | Basic-auth password.                                                                                                                                                    |\n| `NTFY_TOPICS`       | no       | Comma-separated topics this server may use. The first is the default when a tool omits one, and the list restricts every tool, read and write — access grants included. |\n| `NTFY_READ_ONLY`    | no       | `true`, `1` or `yes` (any case) registers only the six read tools. Default `false`.                                                                                     |\n| `NTFY_ALLOW_TOOLS`  | no       | Comma-separated tool names, `list_*` prefixes, or `essential` for a curated preset                                                                                      |\n| `NTFY_DENY_TOOLS`   | no       | Same syntax; removed from whatever `NTFY_ALLOW_TOOLS` left                                                                                                              |\n| `NTFY_INSECURE_TLS` | no       | `true` accepts self-signed certificates (scoped to this connection)                                                                                                     |\n\nSetting `NTFY_TOKEN` together with `NTFY_USERNAME`/`NTFY_PASSWORD` is refused at\nstartup rather than resolved by a precedence rule: which credential is in force must\nnever be ambiguous.\n\n> **Use `https://`.** Over plain http the credentials travel unencrypted — basic\n> auth is base64, not encryption — and the server prints a warning unless the host is\n> local. For self-signed certificates prefer a proper internal CA over\n> `NTFY_INSECURE_TLS`.\n\nWithout configuration the server still starts and lists its tools (so registries and\ninspectors can introspect it), but every call fails with setup instructions instead\nof reaching the API.\n\n### Writes are on by default\n\n`NTFY_READ_ONLY` defaults to `false`. ntfy exists to publish, and a read-only default\nwould ship a notification server that cannot notify — this is the opposite of\n[imap-mcp](https://github.com/ni-c/imap-mcp), where the same variable defaults to\n`true` because a mailbox is an irreplaceable archive.\n\nTwo consequences worth knowing:\n\n- **A typo still fails open.** `true`, `1` and `yes` are all read as read-only, in\n  any case — a protection switch is parsed generously on purpose. But\n  `NTFY_READ_ONLY=ture` is not any of them, and because the default is permissive it\n  leaves the write tools enabled, where in imap-mcp it would fail closed.\n- **A client that can publish can publish anywhere on the instance** unless you say\n  otherwise. Confirmation tokens do not help against that — publishing is not a\n  destructive operation. `NTFY_TOPICS` is the control that does.\n\nThe recommended shape for anything unattended:\n\n```sh\nNTFY_TOPICS=deploys                 # the server can only touch this topic\nNTFY_ALLOW_TOOLS=essential          # or:\nNTFY_DENY_TOOLS=delete_messages,create_user,delete_user,manage_user_access\n```\n\nOn a self-hosted instance, also give the server its own ntfy account with `write-only`\naccess to exactly the topics it needs.\n\n### Choosing which tools load\n\n`NTFY_ALLOW_TOOLS` and `NTFY_DENY_TOOLS` take comma-separated tool names; a trailing\n`*` matches a whole family. `essential` is a curated preset — `get_server_info`,\n`check_topic_access`, `publish_message`, `list_messages`, `get_message` and\n`update_message` — marked as such in the\n[tool reference](https://ntfy-mcp.ni-c.de/reference/tools). Four of the six are read\ntools, so the preset stays useful under `NTFY_READ_ONLY=true`.\n\n```sh\nNTFY_ALLOW_TOOLS=essential\nNTFY_ALLOW_TOOLS=publish_message,list_messages\nNTFY_DENY_TOOLS=delete_*,create_user,manage_user_access\n```\n\nAn entry that matches no tool aborts startup and names it, so a typo cannot silently\nhide a tool — an absent tool is not something anyone traces back to an environment\nvariable. A filtered tool is never registered, so it is absent from `tools/list` and\nunknown to `tools/call` alike, exactly like a write tool under `NTFY_READ_ONLY`.\n\nIf you run several of these servers at once, [mcp-hub](https://mcp-hub.ni-c.de) is\nthe other answer — its `/hub` endpoint replaces every server's tools with six\nmeta-tools.\n\n## Installation\n\n### Claude Code\n\n```sh\nclaude mcp add ntfy-mcp -- npx -y @ni-c/ntfy-mcp\n```\n\n### Claude Desktop\n\n```json\n{\n  \"mcpServers\": {\n    \"ntfy-mcp\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"@ni-c/ntfy-mcp\"],\n      \"env\": {\n        \"NTFY_URL\": \"https://ntfy.example.net\",\n        \"NTFY_TOKEN\": \"…\",\n        \"NTFY_TOPICS\": \"deploys\"\n      }\n    }\n  }\n}\n```\n\n### Codex\n\n```toml\n[mcp_servers.ntfy-mcp]\ncommand = \"npx\"\nargs = [\"-y\", \"@ni-c/ntfy-mcp\"]\nenv = { NTFY_URL = \"https://ntfy.example.net\", NTFY_TOKEN = \"…\", NTFY_TOPICS = \"deploys\" }\n```\n\n### Docker\n\n```sh\ndocker run -i --rm \\\n  -e NTFY_URL=https://ntfy.example.net -e NTFY_TOPICS=deploys \\\n  ghcr.io/ni-c/ntfy-mcp\n```\n\n### Through mcp-hub\n\nA client that cannot spawn a local process — ChatGPT connectors, Claude on the web,\nCursor, LibreChat — reaches ntfy-mcp through [mcp-hub](https://mcp-hub.ni-c.de): one\ncontainer serves many stdio MCP servers over Streamable HTTP, with an OAuth 2.1 login\nbehind a single password and long-lived tokens for the clients that cannot do OAuth. Its\n`/hub` endpoint puts every server behind six meta-tools, so one connector reaches all of\nthem without N×tool schemas in the model's context, and it speaks both protocol revisions\n— a question this server asks travels through it to the person at the far end.\n\nIts `/config/mcp.json` uses Claude Code's format, so the entry is the one you already\nhave:\n\n```json\n{\n  \"mcpServers\": {\n    \"ntfy-mcp\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"@ni-c/ntfy-mcp\"],\n      \"env\": {\n        \"NTFY_URL\": \"https://ntfy.example.net\",\n        \"NTFY_TOKEN\": \"…\",\n        \"NTFY_TOPICS\": \"alerts\",\n        \"NTFY_ALLOW_TOOLS\": \"essential\"\n      },\n      \"denyTools\": [\"delete_messages\"]\n    }\n  }\n}\n```\n\n`allowTools` and `denyTools` there are the hub's **own** per-server filter, which is not\nthe same thing as `*_ALLOW_TOOLS` in `env` — the difference, and the mistake it invites,\nare in the [client guide](https://ntfy-mcp.ni-c.de/guide/clients#through-mcp-hub).\n\n## Tools\n\n`*` marks the `essential` preset.\n\n| Tool                    | Description                                                                                      |\n| ----------------------- | ------------------------------------------------------------------------------------------------ |\n| `list_messages` *       | Poll the cached messages of one or more topics, with filters, and get a cursor for the next call |\n| `get_message` *         | One message in full, including its action buttons and attachment                                 |\n| `check_topic_access` *  | Whether the credentials may subscribe to a topic — see the note below                            |\n| `get_server_info` *     | Health, capabilities, usage, and whether the admin tools are worth trying                        |\n| `get_account`           | Identity, role, limits and usage of the configured credentials                                   |\n| `list_users`            | Every account and its per-topic grants (admin)                                                   |\n| `publish_message` *     | Send a notification to one or more topics                                                        |\n| `update_message` *      | Revise a notification in place, so a progress report stays one notification                      |\n| `mark_messages_read`    | Clear notifications on subscribers' devices                                                      |\n| `delete_messages` 👤    | Delete notifications and cancel scheduled ones                                                   |\n| `create_user` 👤        | Create a non-admin account (admin)                                                               |\n| `delete_user` 👤        | Remove an account and its grants (admin)                                                         |\n| `manage_user_access` 👤 | Grant, deny or revoke access to a topic or pattern (admin)                                       |\n\n👤 asks a person through MCP elicitation · falls back to a two-call\n`confirm_token` where the client cannot show a dialog.\n\n### Structured output\n\nEvery tool declares an `outputSchema` and answers with `structuredContent`\nalongside the text block, so a client can use the result without parsing prose:\n\n```jsonc\n{\n  \"untrusted\": true,\n  \"source\": \"ntfy\",\n  \"topics\": [\"alerts\"],\n  \"count\": 2,\n  \"next_since\": \"TmkVCUCmDdWL\",\n  \"messages\": [{ \"id\": \"TmkVCUCmDdWL\", \"topic\": \"alerts\", \"title\": \"…\" }],\n}\n```\n\nThe `untrusted` marker is a field and not only a sentence in the text, because a\nclient that reads the structured half and ignores the text would otherwise get a\npublisher's prose with no framing at all. It is on the four tools that report\nwhat someone else wrote: `list_messages`, `get_message`, `get_account` and\n`list_users`. `get_server_info` does not carry it — its sections are the\ninstance's own configuration and counters.\n\nFields this server builds are described exactly; documents it merely passes on\nfrom ntfy (`/v1/config`, `/v1/stats`, a publisher's attachment metadata) are\ndeclared as objects with no fixed shape. A schema stricter than the data is not\na better contract: the SDK validates every result against it, so an upstream\nrelease that adds a field would take the tool out entirely rather than show you\none field you did not expect.\n\n### Two things about ntfy that surprise people\n\n**Read and write are granted separately per topic.** A write-only publishing token\ncannot poll the very topic it publishes to, and that is a correct configuration\nrather than a fault. `check_topic_access` tests the _read_ side only, so a denial\nthere does not mean publishing will fail — the tool says so in its result.\n\n**There is no way to list the topics on a server.** A topic exists because someone\npublished to it, and on an open instance its name is the whole of the access control.\nTreat topic names as secrets; `get_account` and `list_users` are the only places\nexisting ones show up.\n\n### Not implemented, on purpose\n\n- **Sending email or placing a phone call** from a published message. ntfy supports\n  both; an MCP tool that mails an arbitrary address on model output is a spam relay\n  driven by injectable content, and `call` places a real, billable call.\n- **Creating, reading or exchanging an access token.** Every such endpoint hands back\n  a live credential, which would then live in the conversation transcript.\n- **Streaming subscriptions** (`/sse`, `/ws`, `/raw`). A tool call is\n  request/response under a timeout; `list_messages` returns the same data, bounded.\n- **Attachment upload.** Either base64 through the model's context or a local\n  filesystem surface this server has no business having. `attach` covers the real case\n  by URL.\n- Reservations, billing, web push, email and phone verification, and the Matrix\n  gateway. `GET /v1/account` returns several of them anyway; `get_account` drops\n  them rather than passing on a payload no tool here uses.\n\n## Not exposed, on purpose\n\n**No topic enumeration** — ntfy has no such API, and neither does anything else. A\ntopic exists because someone published to it, and on an instance with the default\naccess rules its name is the whole of the access control, so a list endpoint would\nbe a list of credentials. `get_account` names the topics the account is subscribed\nto and `list_users` the per-topic grants; those are the two honest answers.\n\n## Safety\n\n- **A person is asked, not just told.** Where the client supports MCP elicitation,\n  `delete_messages`, `delete_user`, `manage_user_access`, `create_user` and\n  `update_message` raise a real dialog that the model cannot answer on its behalf.\n  Where it does not, they fall back to a short-lived token bound to a fingerprint of\n  the exact target — and say so, rather than implying somebody approved. A\n  confirmation for one target cannot execute another, a longer list, or — for\n  `manage_user_access` — the same three arguments in a different order. See\n  [Asking a person](https://ntfy-mcp.ni-c.de/guide/approval).\n- **`NTFY_TOPICS` bounds the access tools too.** `manage_user_access` refuses a\n  pattern that reaches past the list, `*` included: a grant is permanent access to\n  every topic it covers, and no finite allowlist covers a wildcard. `list_users`\n  reports each account's grants against the allowed topics only, because a grant\n  pattern is a topic name and a topic name is a bearer credential.\n- **Confirmation prompts never quote content from ntfy.** They name the topic, the\n  count or the username and nothing else, because that text is read by a model. And\n  never the password `create_user` was given: it is a live credential, so it is in\n  neither the prompt nor the token's binding.\n- **Returned content is marked as untrusted data**, because it is: everything in a\n  notification was written by whoever could publish to the topic.\n- **`get_account` answers from an allowlist, not a denylist.** It reports the\n  identity, role, tier, limits, usage and the _metadata_ of each access token, and\n  drops everything else ntfy sends — the token values, which ntfy returns in\n  plaintext, but also the phone numbers, the billing identifiers and the\n  reservation and subscription topic names. A key a future ntfy adds is dropped\n  without an edit here.\n- **Caller-supplied URLs must be `http` or `https`.** `click`, `icon`, `attach` and\n  action-button URLs are opened by the recipient's device, not by the server, and\n  ntfy stores whatever it is given — including a `javascript:` URL.\n- **Every field a publisher controls is bounded.** ntfy caps the message body at 4096\n  bytes but not the title or the tag list, so those are capped here.\n- `NTFY_READ_ONLY=true` does not register the write tools at all, and `NTFY_DENY_TOOLS`\n  cuts finer along the same line — a filtered tool is never built, not refused at call\n  time.\n\n## Documentation\n\nThe full guide, tool reference and security notes live at\n**[ntfy-mcp.ni-c.de](https://ntfy-mcp.ni-c.de)** (source in [`docs/`](docs/)).\n\n## Development\n\n```sh\nnpm install\nnpm run build\nnpm run lint && npm run typecheck && npm run test:coverage\n```\n\nThe architecture diagram and the social card are generated: edit\n`docs/assets/architecture.source.svg` or `docs/assets/og.json` and run\n`npm run assets`, never the rendered copies under `docs/public/`. CI runs\n`npm run assets:check` and fails if they have drifted.\n\nThe documentation site has its own manifest, so its toolchain never lands in the\ncontainer image or the test matrix:\n\n```sh\ncd docs && npm install && npm run build\n```\n\n## Releasing\n\nReleases are tag-driven. Bump `package.json`, move the `[Unreleased]` notes in\n`CHANGELOG.md` under the new version, commit, then:\n\n```sh\ngit tag -s vX.Y.Z -m \"vX.Y.Z\"\ngit push origin main vX.Y.Z\n```\n\nThe release workflow publishes to npm via Trusted Publishing (OIDC, with\nprovenance), pushes the multi-arch container image to GHCR, creates the GitHub\nrelease from the CHANGELOG section, and updates the entry in the official MCP\nregistry.\n\n## Contributing\n\nIssues, discussions and pull requests are welcome — see\n[CONTRIBUTING.md](CONTRIBUTING.md). For vulnerabilities please use\n[private reporting](https://github.com/ni-c/ntfy-mcp/security/advisories/new)\nrather than a public issue; the policy is in [SECURITY.md](SECURITY.md).\n\n## License\n\n[MIT](LICENSE) © Willi Thiel\n",
  "bytes": 20745,
  "sha": "1c886f723e97928939b612f04f58cedf7047dacc83fa76e9516193a7f60eabdf",
  "repo_slug": "ni-c/ntfy-mcp",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_ni_c_ntfy_mcp_27da661e/readme"
}