@three-ws/notifications-mcp
Your three.ws notification inbox, delivery preferences, and Web Push devices — from any AI agent.
Install ·
Quick start ·
Tools ·
Authentication ·
Errors ·
three.ws
A Model Context Protocol server that gives an AI assistant its own three.ws notification inbox + delivery control over stdio. Read inbound events (pump/market alerts, sales & earnings, purchase receipts, social mentions, IRL interactions, account/security notices), mark them read, tune the per-category → per-channel delivery matrix, and register Web Push devices — all live, all account-scoped.
Why
An agent that trades, sells skills, or stands in the real world generates inbound events around the clock — a coin it launched pumps, a skill it published sells, someone taps its IRL pin. Without an inbox surface, the agent polls a dozen endpoints or misses everything. This server turns the platform's own notification feed into seven MCP tools: the agent reads what happened since it last looked, triages it, and controls exactly which channels (in_app, push, email, telegram) each category of event uses to reach its owner.
Every read and write hits the real three.ws API. The server is authenticated: it carries a three.ws API key (or OAuth access token) as a Bearer credential and resolves the owning account on every call. It signs nothing locally and holds no other secret.
Install
npm install @three-ws/notifications-mcp
Or run with npx (no install):
THREE_WS_API_KEY=sk_live_… npx @three-ws/notifications-mcp
Node 20+. Two runtime dependencies (@modelcontextprotocol/sdk, zod).
Quick start
Claude Code, one line:
claude mcp add notifications -e THREE_WS_API_KEY=sk_live_… -- npx -y @three-ws/notifications-mcp
Claude Desktop / Cursor / any MCP client — add to your MCP config (claude_desktop_config.json, .cursor/mcp.json, .mcp.json):
{
"mcpServers": {
"notifications": {
"command": "npx",
"args": ["-y", "@three-ws/notifications-mcp"],
"env": { "THREE_WS_API_KEY": "sk_live_…" }
}
}
}
Restart the client and the seven tools appear. Inspect the surface in a GUI:
npx -y @modelcontextprotocol/inspector npx -y @three-ws/notifications-mcp
Then ask in plain language:
Anything new in my inbox? Mark the pump alerts read, and turn off email for social mentions.
Runs list_notifications → mark_read → set_preferences.
Tools
Every tool ships MCP tool annotations: the two reads advertise readOnlyHint: true, and the two ⚠️ tools are flagged destructiveHint: true, so annotation-aware clients prompt before running them.
list_notifications
Read the account's inbox — the inbound-event feed the platform delivers: market/pump alerts, sales & earnings, purchase receipts, social mentions, IRL interactions, and account/security notices. Wraps GET /api/notifications.
type | string | no | Return only one notification type (e.g. "pump_alert", "skill_purchased", "referral_earned", "security_alert"). Lower_snake_case, ≤ 40 chars. Omit for all types. |
limit | number | no | How many to return, newest first. 1–50, default 20. |
Example call and response (shape illustration — your inbox contents will differ):
// call
{ "type": "pump_alert", "limit": 2 }
// response (example)
{
"ok": true,
"type": "pump_alert",
"unread_count": 5,
"count": 2,
"notifications": [
{
"id": "5d1c2f0a-9b1e-4c3d-8e7f-000000000001",
"type": "pump_alert",
"payload": { "mint": "FeMbDoX7R1Psc4GEcvJdsbNbZA3bfztcyDCatJVJpump", "symbol": "THREE" },
"read": false,
"read_at": null,
"created_at": "2026-07-11T18:04:12.000Z"
}
]
}
read is derived from read_at (null ⇒ unread). unread_count is the total unread across the whole inbox, not just this page.
mark_read
Mark notifications read. Wraps POST /api/notifications/:id/read (one) and POST /api/notifications/read-all (all). Pass exactly one of the two arguments. Marking read only sets read_at — nothing is deleted, and re-running is a no-op.
id | string (UUID) | one of | A single notification to mark read (from list_notifications). |
all | boolean | one of | true ⇒ mark every unread notification read. |
// call
{ "all": true }
// response (example)
{ "ok": true, "scope": "all", "marked_read": 5 }
With id, the response is { "ok": true, "scope": "one", "id": "…", "read_at": "…" }.
delete_notification
Permanently remove one notification. Wraps DELETE /api/notifications/:id. Irreversible — prefer mark_read for normal triage. Only a notification the caller owns can be deleted; a missing or already-deleted id returns a not-found error.
id | string (UUID) | yes | The notification to delete (from list_notifications). |
// call
{ "id": "5d1c2f0a-9b1e-4c3d-8e7f-000000000001" }
// response (example)
{ "ok": true, "id": "5d1c2f0a-9b1e-4c3d-8e7f-000000000001", "deleted": true }
get_preferences
Read the resolved delivery matrix — for each category, which channels deliver it. No arguments. Wraps GET /api/notifications/preferences. Read this before set_preferences so you patch from real current state.
// response (example)
{
"ok": true,
"categories": [
{ "key": "sales", "label": "Sales & earnings", "description": "…" },
{ "key": "alerts", "label": "Market & pump alerts", "description": "…" }
/* purchases, social, irl, account … */
],
"channels": ["in_app", "push", "email", "telegram"],
"prefs": {
"categories": { "alerts": { "in_app": true, "push": true, "email": false, "telegram": false } },
"telegram_chat_id": null
},
"push": { "subscribed_devices": 1 }
}
The six categories are sales, purchases, social, irl, alerts, account; the four channels are in_app, push, email, telegram. prefs.categories is the effective matrix with the user's sparse overrides already merged onto platform defaults.
set_preferences
Patch the delivery matrix. Wraps PUT /api/notifications/preferences. Provide at least one of the two arguments. Only the category/channel pairs you pass change; unknown keys are dropped server-side; untouched pairs keep their current value. Re-applying the same values is a no-op.
categories | object | at least one | Outer keys: sales, purchases, social, irl, alerts, account. Inner keys: in_app, push, email, telegram → boolean. |
telegram_chat_id | string | at least one | Numeric Telegram chat id to deliver the telegram channel to, or "" to unlink. ≤ 24 chars. |
// call — stop emailing social mentions, push pump alerts
{ "categories": { "social": { "email": false }, "alerts": { "push": true } } }
// response (example) — the full resolved matrix after the update
{ "ok": true, "prefs": { "categories": { "social": { "email": false /* … */ } }, "telegram_chat_id": null } }
register_push_device
Register a Web Push device so the account receives push notifications on it. Wraps POST /api/push/subscribe. The subscription argument is exactly what the browser's pushManager.subscribe().toJSON() returns. Push endpoints are globally unique — re-registering the same device upserts (latest owner wins), so this is idempotent. Whether a category actually delivers over push is still governed by set_preferences.
subscription | object | yes | { endpoint, keys: { p256dh, auth } } — endpoint is the push-service URL (≤ 2048 chars); p256dh/auth are the base64url-encoded keys from the browser. |
// call
{
"subscription": {
"endpoint": "https://fcm.googleapis.com/fcm/send/exampleSubscriptionId",
"keys": { "p256dh": "BNcRd…", "auth": "tBHI…" }
}
}
// response (example)
{ "ok": true, "registered": true, "endpoint": "https://fcm.googleapis.com/fcm/send/exampleSubscriptionId" }
unregister_push_device
Remove a Web Push device so it stops receiving pushes. Wraps DELETE /api/push/subscribe. Provide at least one of the two arguments — the endpoint is what locates the device. Idempotent: removing an endpoint that isn't registered still returns ok. Only push delivery to that device changes; in_app, email, and telegram preferences are untouched.
endpoint | string (URL) | one of | The push endpoint URL of the device to remove (preferred). |
subscription | object | one of | Alternatively the full subscription object; its endpoint is used. |
// call
{ "endpoint": "https://fcm.googleapis.com/fcm/send/exampleSubscriptionId" }
// response (example)
{ "ok": true, "unregistered": true, "endpoint": "https://fcm.googleapis.com/fcm/send/exampleSubscriptionId" }
Authentication
Every endpoint is account-scoped and returns 401 without a valid credential — this server can never read or change another account.
Set THREE_WS_API_KEY to either:
- a three.ws API key (
sk_live_… / sk_test_…) — create one in your three.ws dashboard, or
- an OAuth access token for the account.
Both are carried as Authorization: Bearer … on every request. Bearer auth is CSRF-exempt server-side, so writes need no extra token. THREE_WS_TOKEN and THREE_WS_BEARER are accepted aliases. Treat the credential like a password — it grants full read/write over the account's inbox and delivery settings.
Configuration
THREE_WS_API_KEY | yes | — | three.ws API key or OAuth access token (see Authentication). Aliases: THREE_WS_TOKEN, THREE_WS_BEARER. |
THREE_WS_BASE | no | https://three.ws | API base URL. Override only when self-hosting or targeting a preview. |
THREE_WS_TIMEOUT_MS | no | 20000 | Per-request timeout in milliseconds. Must be a positive number. |
The credential is checked when a tool runs, not at startup — the server boots and advertises its tool surface without one, so tools/list always works.
Errors
A failed tool call returns an MCP error result (isError: true) whose text is a single JSON object:
// error shape (example)
{ "ok": false, "error": "upstream_error", "message": "Not found", "status": 404, "detail": { /* API body */ } }
missing_credential | 401 | No THREE_WS_API_KEY (or alias) configured. | Set the env var and restart the client. |
validation_error | 400 | Bad arguments (e.g. neither id nor all on mark_read). | Fix the call — the message says exactly what's missing. |
upstream_error | as returned | The three.ws API rejected the request; status + detail carry the real response (401 bad key, 404 unknown id, 429 rate-limited). | Act on status — a 429 is safe to retry after a pause. |
timeout | — | No response within THREE_WS_TIMEOUT_MS. | Retry; raise the timeout if it recurs. |
network_error | — | The request never reached the API (DNS, offline). | Check connectivity / THREE_WS_BASE. |
Reads are always safe to retry. The writes are idempotent by design (mark_read, set_preferences, register_push_device, unregister_push_device re-run to the same state) — only delete_notification is not, and a repeat simply returns not-found.
Related
Links
Part of the three.ws SDK suite — 3D AI agents, on-chain identity, and agent payments.
Website · Changelog · GitHub