# Ray notifications

> Send email, push, Slack, Discord, Telegram and webhook notifications and manage templates with Ray.

- name: io.github.gege-mn/ray-mcp
- version: 0.1.0
- connection class: R1 (autonomous)
- trust: 94/100
- quality: 82/100 (good)
- owner: not claimed
- descriptor: https://api.protogrid.dev/v1/servers/io.github.gege-mn%2Fray-mcp
- tools: https://api.protogrid.dev/v1/servers/io.github.gege-mn%2Fray-mcp/tools

**An agent can connect on its own if it already holds `ray_api_key`.** Remote endpoint behind a static API key or header. The registry never sees the value; the agent fills the placeholder.

## Connection (mcpServers)

```json
{
  "mcpServers": {
    "ray-mcp": {
      "type": "http",
      "url": "https://ray-api.gege.mn/mcp",
      "headers": {
        "Authorization": "Bearer ${ray_api_key}"
      }
    }
  }
}
```

Placeholders to fill: `${ray_api_key}`.

## Trust

- hygiene: 100
- liveness: 99
- freshness: 85
- provenance: 90
- drivers: +repository +namespace-matches-repo +website +reachable +uptime-98% ~updated-36d-ago
- Derived from observable signals (official registry feed and our own credential-free probes); no code audit performed.

## Quality 82/100 (good)

### protocol: 75 (weight 25)

- warn `protocol.modern`: newest supported version is 2025-11-25; 2026-07-28 not supported, older versions are deprecated until 2027-07-28
- n/a `protocol.stateless`: only applies to 2026-07-28 servers
- pass `protocol.transport`: streamable HTTP
- n/a `protocol.list_ttl`: only applies to 2026-07-28 servers

### authorization: n/a (weight 15)

- n/a `auth.prm`, `auth.as_metadata`, `auth.cimd`: no remote uses OAuth
- n/a `auth.secret_in_url`: no templated URL

### tool hygiene: 86 (weight 30)

- pass `tools.descriptions`: every tool has a description
- pass `tools.description_length`: descriptions are concise
- pass `tools.schemas`: every tool has an object input schema
- pass `tools.annotations`: 21 of 21 tools declare readOnlyHint or destructiveHint
- warn `tools.directory_hints`: 3 of 21 tools declare all four hints; missing: get_usage (openWorldHint); update_template_draft (openWorldHint); get_click_stats (openWorldHint); …
- warn `tools.token_cost`: about 8,321 tokens to load every tool
- pass `tools.api_dump`: tools are not a one-to-one API dump

### stability: 100 (weight 15)

- pass `stability.changes`: no tool changes in 30 days
- pass `stability.rug_pull`: no tool changed its meaning under the same name

### dependencies: 67 (weight 15)

- pass `deps.known_vulns`: no known advisory in 98 resolved packages
- fail `deps.mcp_sdk_version`: @modelcontextprotocol/sdk 1.30.1 has 1 known advisory (GHSA-6qxp-vccf-f47h); update @modelcontextprotocol/sdk to 1.31.0 or later
- pass `deps.resolvable`: @gege-mn/ray-mcp@0.1.0 resolved: 98 packages

- tools: 21, about 8,321 tokens to load them all
- tool set hash: 04312e76c6ee83f387a80b8c9214114c
- badge: [![protogrid quality](https://protogrid.dev/badge/io.github.gege-mn/ray-mcp.svg)](https://protogrid.dev/servers/io.github.gege-mn/ray-mcp)
- Checks run on what our credential-free, read-only probes observe; tools are never called and no code audit is performed.

### Tool changes

_No tool definition changes recorded._

## Dependencies

- npm @gege-mn/ray-mcp@0.1.0: resolved, 97 dependencies, MCP SDK @modelcontextprotocol/sdk 1.30.1
  - GHSA-6qxp-vccf-f47h (high) in `` @modelcontextprotocol/sdk@1.30.1 ``, direct, fixed in `` 1.31.0 ``, advisory summary: "MCP TypeScript SDK: OAuth client could send credentials to an authorization server chosen by the MCP server" https://osv.dev/vulnerability/GHSA-6qxp-vccf-f47h

Dependency graphs from deps.dev; advisories from OSV.dev, including the GitHub Advisory Database and the PyPI Advisory Database; all CC BY 4.0.

## Remotes

- https://ray-api.gege.mn/mcp (streamable-http, auth api_key ${ray_api_key}, reachable true, uptime30d 0.9830508)

## Packages

- npm @gege-mn/ray-mcp@0.1.0 secrets: RAY_API_KEY

## Tools (21)

- `archive_template`: Archive a template (POST /templates/{id}/archive; write scope). Sends that reference it fail until it is restored with unarchive_template; its name stays reserved meanwhile. Confirm with the user first if it may be in production use.
- `create_template`: Create a template (POST /templates; write scope). `channelKind` is permanent and must match the channels you'll send through (list_channels `templateKind`). It is saved as an unpublished draft unless `publish: true`; sending by templateId needs a published version (publish_template). Names are unique per workspace including archived templates: a collision returns 409, so unarchive_template the old one instead. Returns `{ id, requiredParams }`. Designed (drag-and-drop) email templates can only be made in the dashboard.
- `create_webhook`: Create an outbound webhook that Ray POSTs signed event payloads to (POST /tenant-webhooks; write scope; Pro plan or higher, otherwise 403). The URL must be public HTTPS. The response contains the signing `secret` exactly once: show it to the user to store (e.g. as an env var), since it can't be read again, only rotated with update_webhook. Read the `webhooks` docs page for the signature scheme.
- `delete_webhook`: Delete (archive) a webhook so it receives no more events (DELETE /tenant-webhooks/{id}; write scope). It cannot be restored through the API; confirm with the user first.
- `get_click_stats`: Email link click counts grouped by destination URL (GET /clicks) for a `sendId`, a `campaignId`, or both (at least one is required). Only links from email sends made with `trackClicks: true` are counted.
- `get_send_status`: Delivery status of a send (GET /sends/{id}): `aggregate` counts per status over the whole send (pending, claimed, delivered, failed_retryable, failed_terminal, suppressed) plus one page of per-recipient delivery rows including provider errors. Right after sending, rows are usually `pending`; check again after a few seconds. Feed-only sends have no rows. For more rows, pass the returned `nextCursor` as `cursor`.
- `get_template`: Get one template with its published version and its current draft (GET /templates/{id}): content, logTitle/logDescription, `requiredParams` a send must supply, and paramOverrides. Read this before update_template_draft, which needs the full body.
- `get_usage`: The workspace's plan, subscription status and this calendar month's usage against the monthly quota (GET /billing/subscription). `used`/`remaining` are null on unmetered plans. On a hard-capped plan, sends beyond the quota fail with HTTP 402 quota_exceeded.
- `get_webhook`: Get one outbound webhook (GET /tenant-webhooks/{id}) including its delivery health (consecutiveFailures, lastDeliveryAt, lastFailureAt, lastError).
- `list_channels`: List the delivery channels configured in this workspace (GET /channels). Call this FIRST before sending: each entry's `id` is the `channelConfigId` for send_notification and test_send_template, `kind` is the channel type (ses_email, smtp_email, fcm_push, slack_webhook, discord_webhook, telegram_bot, twilio_sms, sendsms_mn, generic_webhook), `templateKind` is the template channelKind it accepts, and `recipientSchema` is the exact JSON Schema of the `recipient` it expects. Channels and their provider credentials are set up in the Ray dashboard, not through this API; if the list is empty, ask the user to add one there.
- `list_feed_notifications`: Read one end user's in-app notification feed (GET /notifications): the entries created by sends with `feed` or `showInFeed`. This is what an in-app inbox would show, not delivery status (use get_send_status for that). Paginate by passing `nextCursor` back as `cursor`.
- `list_templates`: List message templates (GET /templates): id, name, folder, channelKind, and `publishedVersionId` (null = never published, so it cannot be sent by templateId yet). Archived templates are hidden unless `includeArchived` is true. Use get_template for content and required params.
- `list_webhooks`: List the workspace's outbound event webhooks (GET /tenant-webhooks): url, subscribed events, enabled flag and delivery health (consecutiveFailures, lastError). Signing secrets are never returned.
- `publish_template`: Publish a template's current draft as its new live version (POST /templates/{id}/publish; write scope). Sends by templateId use it immediately. Fails if there is no draft or the template is archived.
- `read_docs`: Read Ray's documentation as markdown. With no `slug` it returns the index (llms.txt) listing every page; then pass a page slug, i.e. the path after /docs/ without `.md`, such as `sending`, `templates`, `idempotency`, `errors`, `rate-limits`, `webhooks`, `status-and-feeds` or `channels/telegram`. Use it for anything the tool descriptions don't cover.
- `send_notification`: Send a notification (POST /send; needs a `write`-scoped key). Returns `{ sendId }` with HTTP 202: delivery is asynchronous, so check the outcome with get_send_status. Use exactly one mode:
1. Single: `channelConfigId` + `recipient`.
2. Fan-out: `channelConfigId` + `targets` (1-1000 `{ recipient, externalUserId? }`), one channel, one sendId.
3. Multi-channel: `deliveries` (1-10, each with its own `channelConfigId`, `recipient` and `templateId` or `content`) for ONE person over several channels; add `feed` + `externalUserId` for a single in-app feed entry.
4. Feed-only: `feed` (with `title`) + `externalUserId` and no channel: an in-app notification only.
In modes 1-2 give exactly one of `templateId` (a published template, plus `params`) or inline `content`; in mode 3 that choice is made per delivery. Get channel ids and each recipient shape from list_channels first. Pass `idempotencyKey` whenever a retry must not deliver twice.
- `test_send_template`: Send a real test of a template's PUBLISHED version to one recipient (POST /templates/{id}/test-send; write scope). It goes through the provider for real, so use a recipient the user controls. Test sends don't count toward the monthly quota (a small daily allowance applies instead) and never appear in feeds. Returns `{ sendId }` for get_send_status.
- `unarchive_template`: Restore an archived template (POST /templates/{id}/unarchive; write scope). This is how to reclaim a name held by an archived template instead of creating a new one.
- `update_template_draft`: Replace a template's draft (PATCH /templates/{id}; write scope). This is a full replacement, not a partial patch: pass content, logTitle, logDescription and paramOverrides as they should end up (get_template first). `name`, `folder` and `channelKind` are required for validation but not changed (channelKind must equal the original). Live sends keep using the published version until you publish, either with `publish: true` here or publish_template. Fails on archived templates.
- `update_webhook`: Change a webhook (PATCH /tenant-webhooks/{id}; write scope). Only the fields you pass change; pass at least one. `rotateSecret: true` issues a new signing secret, returned once in the response, and the old secret stops verifying immediately, so the receiving service must be updated at the same time.
- `whoami`: Identify the API key this connection uses (GET /me): `tenantId` (the workspace), `apiKeyId` and `scopes`. Write tools need the `write` scope; call this to check before attempting them.
