Send in-app notifications from Claude Code
Every notification operation your secret key can do, as tools: send to a user id or broadcast to everyone, list what was sent with how many people read it, see what one user's bell shows, mark it read, and create the signing secret that lets your pages prove which user is asking. Useful when an agent runs a job on someone's behalf and should tell them when it is done, or when you ship something everyone should hear about.
Claude Code connects to remote MCP servers over HTTP directly. One command registers the server under the name nordva.
Setup
You need a secret key (nv_live_…) from Dashboard → API keys.
01Register the server
claude mcp add --transport http nordva https://mcp.nordva.dev \
--header "Authorization: Bearer nv_live_..."Run it in your project's folder. The name comes before the URL. By default the server is added for that folder only and stored outside the repository; add --scope user to make it available in every project. Claude Code loads servers when it starts, so quit and start it again in that folder; /mcp lists the Nordva tools.
02Or share it with the team without sharing the key
.mcp.json
{
"mcpServers": {
"nordva": {
"type": "http",
"url": "https://mcp.nordva.dev",
"headers": {
"Authorization": "Bearer ${NORDVA_API_KEY}"
}
}
}
}Claude Code expands ${NORDVA_API_KEY} from the environment, so the file can be committed while each developer keeps their own key in their shell profile.
03Check the connection
Run claude mcp list, or type /mcp inside a session. nordva should show as connected. Then ask Claude to run setup_check: it reports the project, the plan and which features are unlocked.
Then ask Claude Code
- Notify user 4812 that their import finished, with a link to /imports/77.
- Tell everyone about tonight's planned downtime, 22:00 to 22:30 UTC.
- How many people read last week's broadcast?
- Set up identity verification for the notifications bell.
For “Notify user 4812 that their import finished, with a link to /imports/77.” Claude Code calls send_notification with:
{
"user_id": "4812",
"title": "Import finished",
"body": "1,204 rows imported, 3 skipped.",
"action_url": "/imports/77"
} Returned: The notification id, the user id, the time it was created and this month's usage against the cap. The user's bell shows it the next time it checks.
Notifications tools and arguments
send_notification
Confirms firstSend an in-app notification to one user or to everyone
| user_id | optional | string, max 256 | The developer's own id for one recipient. |
| audience | optional | "user" | "users" | "all" | "all" for a broadcast to every signed-in user; "users" is implied by the group fields. |
| user_ids | optional | array | The developer's own user ids. |
| emails | optional | array | People by email; each is reached through their known user id. |
| contact_ids | optional | array | Contacts by id (from list_contacts). |
| contacts | optional | object | A Contacts filter, values as strings: { "stage": "customer" }, { "consent": "email_marketing" }, { "detail": "plan=pro" }, { "paying": "true" }. |
| title | required | string, max 200 | Notification headline. |
| body | required | string, max 2,000 | Notification body, plain text. |
| action_url | optional | string, max 2,048 | A full http(s) URL, or a path such as /billing. |
| icon | optional | "check" | "warning" | "info" | "error" | Visual icon hint. |
| category | optional | string | A short lower-case tab name, such as billing. |
| actions | optional | array | Up to two buttons. |
| data | optional | object | A small JSON object for the developer's page. |
| send_at | optional | string | ISO time to show it, at most 60 days ahead. |
| pinned | optional | boolean | Keep it at the top of the bell until each person archives it. |
| reactions | optional | union | true: the project's changelog reaction set; or the names in order. |
preview_notification_audience
Read-onlySee who a group notification would reach
| user_ids | optional | array | The developer's own user ids. |
| emails | optional | array | People by email; each is reached through their known user id. |
| contact_ids | optional | array | Contacts by id (from list_contacts). |
| contacts | optional | object | A Contacts filter, values as strings: { "stage": "customer" }, { "consent": "email_marketing" }, { "detail": "plan=pro" }, { "paying": "true" }. |
list_notifications
Read-onlyList sent notifications with reach and reads
| user_id | optional | string, max 256 | |
| audience | optional | "user" | "users" | "all" | |
| status | optional | "read" | "unread" | "all" | "scheduled" | |
| category | optional | string, max 24 | |
| limit | optional | number, 1–100 | |
| cursor | optional | string, max 64 |
get_notification
Read-onlyRead one sent notification
| notification_id | required | string, max 64 | The ntf_... id. |
delete_notification
Confirms firstDelete a sent notification
| notification_id | required | string, max 64 | The ntf_... id. |
get_unread_notifications
Read-onlySee what a user's bell shows
| user_id | optional | string, max 256 | The person whose bell to read. |
| include_read | optional | boolean | true for read and unread (the inbox). |
| limit | optional | number, 1–100 | Page size. |
| since | optional | string | Only notifications created after this ISO timestamp. |
| category | optional | string | With include_read: one category (a tab in the bell). |
| archived | optional | boolean | With include_read: true for only the archived ones. |
mark_notifications_read
Mark notifications read for a user
| notification_id | optional | string, max 128 | Mark this one read. |
| user_id | optional | string, max 256 | The person who read it. Required for a broadcast or a group. |
| category | optional | string | Without notification_id: only this category. |
archive_notification
Archive a notification for one user
| notification_id | required | string, max 64 | The ntf_... id. |
| user_id | required | string, max 256 | |
| archived | optional | boolean | false brings it back. |
record_notification_click
Record a click on a notification
| notification_id | required | string, max 64 | The ntf_... id. |
| user_id | required | string, max 256 |
set_notification_reaction
Set a user's reaction to a notification
| notification_id | required | string, max 64 | The ntf_... id. |
| user_id | required | string, max 256 | |
| reaction | required | "like" | "celebrate" | "love" | "laugh" | "fire" | "eyes" | "confused" | "dislike" | One the notification offers, or null to take it back. |
get_notification_settings
Read-onlyNotification settings, usage and warnings
No arguments.
update_notification_settings
Change notification settings
| require_user_hash | optional | boolean | Browser calls must prove their user with user_hash. |
| changelog_to_bell_default | optional | boolean | New changelog entries post to the bell unless they say otherwise. |
create_notifications_signing_secret
Create the notifications signing secret (shown once)
No arguments.
rotate_notifications_signing_secret
Confirms firstRotate the notifications signing secret (new one shown once)
No arguments.
Behaviour worth knowing
- Every plan can send notifications, capped per project per calendar month: Free 1,000, Indie 20,000, Builder 200,000. A broadcast counts once.
- send_notification reaches real people, so the server marks it destructive and clients ask before calling it. With audience "all" it is one broadcast that shows in every signed-in user's bell for 30 days.
- list_notifications shows, for a broadcast, how many people it reached (their bell checked after it was sent) and how many read it.
- Your pages read the bell with the publishable key, the user's id and a user_hash your server computes with the signing secret, so one browser cannot read another user's notifications.
- create_notifications_signing_secret and rotate_notifications_signing_secret return the secret once, in the text only; the agent is told to write it to your server's env file and never to the browser.
- publish_changelog_entry with notify_in_app posts the entry to the bell once when it publishes.
- Notifications older than 90 days are pruned.
- Tools appear to the model as mcp__nordva__<tool>, which is the form to use in permission allowlists.
- Read-only tools are marked as such by the server; Claude Code still asks before the first call to each tool unless you allow it.