Getting started
MCP server
Connect Claude Code, Cursor, VS Code, Windsurf, Codex, Zed or Claude Desktop to the Ray MCP server so an agent can send and inspect notifications.
Ray runs a hosted Model Context Protocol (MCP) server at https://ray-api.gege.mn/mcp. Connect it to an AI coding agent or chat client and the agent calls Ray directly: it looks up your channel configs, creates and publishes templates, sends a test, checks delivery status, manages delivery webhooks and reads these docs, without you copying ids or curl commands back and forth.
The tools are thin wrappers over the REST API. Every tool call goes through the same authentication, scopes, validation, rate limits and quota as the equivalent REST request, so a tool can do exactly what the API key behind it can do and nothing more.
What an agent can do with it
- Discover the workspace:
whoami,list_channels(each channel'schannelConfigId, template kind and recipient JSON Schema),list_templates. - Build templates:
create_template,update_template_draft,publish_template, then try one on a real inbox withtest_send_template. - Send and follow up:
send_notification, thenget_send_statusfor the per-recipient outcome and provider errors. - Debug production: a user's in-app feed, click stats, delivery webhook health (
consecutiveFailures,lastError) and usage against the monthly quota. - Look things up:
read_docsreturns any page of these docs as markdown.
Channel credentials can't be created through the API or MCP. Set channels up in the dashboard under Channels first; the agent finds them with list_channels.
Endpoint and authentication
| Setting | Value |
|---|---|
| URL | https://ray-api.gege.mn/mcp |
| Transport | Streamable HTTP, stateless (no session id). POST only, JSON responses, no SSE stream. |
| Auth | Authorization: Bearer ck_live_... on every request. The same API keys as the REST API. |
| Server name | ray |
| Request body | 1 MiB per request. |
| Browsers | Not supported. The endpoint sends no CORS headers, so browser-based MCP clients can't call it. |
Without an Authorization header the server still connects and lists its tools (registries inspect it this way), but only read_docs works; every other tool answers with setup instructions. A malformed key gets HTTP 401 with a JSON error on every request, and a revoked or unknown key gets 401 when the client connects. The Bearer prefix is optional. Create keys in the dashboard at https://ray.gege.mn under API keys; see Authentication for scopes.
The 1 MiB limit applies to the whole MCP request, so a send_notification call can't carry large email attachments. Send those from your code with the TypeScript SDK or POST /send directly, which accepts up to 45 MiB.
Clients that only run local (stdio) servers, or that can't attach a custom header to a remote server, use the local package instead. It exposes the same tools.
Set up your client
Replace ck_live_... with your key. Keep config files that contain a key out of version control: prefer user-level config, or the client's secret prompt or environment variable expansion where it has one.
Claude Code
export RAY_API_KEY="ck_live_..."
claude mcp add --transport http ray https://ray-api.gege.mn/mcp --header "Authorization: Bearer $RAY_API_KEY"
Your shell expands $RAY_API_KEY when the command runs, and Claude Code stores the resulting header in its local config for this project. Add --scope user to make ray available in every project. Run /mcp inside Claude Code to check that it is connected.
To share the server with a team through a checked-in .mcp.json, reference the variable instead of the key. Claude Code expands ${RAY_API_KEY} from each developer's environment:
{
"mcpServers": {
"ray": {
"type": "http",
"url": "https://ray-api.gege.mn/mcp",
"headers": { "Authorization": "Bearer ${RAY_API_KEY}" }
}
}
}
Claude Desktop
Remote connectors added in Claude Desktop's settings have no field for an API key header, so run the local package. Open Settings > Developer > Edit Config (the file is ~/Library/Application Support/Claude/claude_desktop_config.json on macOS and %APPDATA%\Claude\claude_desktop_config.json on Windows) and add:
{
"mcpServers": {
"ray": {
"command": "npx",
"args": ["-y", "@gege-mn/ray-mcp"],
"env": { "RAY_API_KEY": "ck_live_..." }
}
}
}
Restart Claude Desktop. Node.js 18 or later must be installed so that npx is on the path.
Cursor
Add to ~/.cursor/mcp.json (all projects) or .cursor/mcp.json (one project):
{
"mcpServers": {
"ray": {
"url": "https://ray-api.gege.mn/mcp",
"headers": { "Authorization": "Bearer ck_live_..." }
}
}
}
VS Code
VS Code's mcp.json uses a servers key and can prompt for the key once and store it securely, so it never sits in the file. Add to .vscode/mcp.json, or to your user configuration (command MCP: Open User Configuration):
{
"inputs": [
{ "type": "promptString", "id": "ray-api-key", "description": "Ray API key", "password": true }
],
"servers": {
"ray": {
"type": "http",
"url": "https://ray-api.gege.mn/mcp",
"headers": { "Authorization": "Bearer ${input:ray-api-key}" }
}
}
}
Windsurf
Add to ~/.codeium/windsurf/mcp_config.json. Windsurf names the remote URL field serverUrl:
{
"mcpServers": {
"ray": {
"serverUrl": "https://ray-api.gege.mn/mcp",
"headers": { "Authorization": "Bearer ck_live_..." }
}
}
}
Windsurf also expands ${env:RAY_API_KEY} in this file if the variable is set in the environment Windsurf starts with.
Codex
Add to ~/.codex/config.toml:
[mcp_servers.ray]
command = "npx"
args = ["-y", "@gege-mn/ray-mcp"]
env = { RAY_API_KEY = "ck_live_..." }
Or from the command line: codex mcp add ray --env RAY_API_KEY=ck_live_... -- npx -y @gege-mn/ray-mcp.
Codex can also connect to the hosted endpoint directly and read the key from your environment, with nothing to install:
[mcp_servers.ray]
url = "https://ray-api.gege.mn/mcp"
bearer_token_env_var = "RAY_API_KEY"
Zed
Add to Zed's settings.json (command palette, zed: open settings):
{
"context_servers": {
"ray": {
"url": "https://ray-api.gege.mn/mcp",
"headers": { "Authorization": "Bearer ck_live_..." }
}
}
}
Other clients
A client that supports Streamable HTTP with custom headers needs the URL and the Authorization header. A client that only runs stdio servers uses the local package:
{
"mcpServers": {
"ray": {
"command": "npx",
"args": ["-y", "@gege-mn/ray-mcp"],
"env": { "RAY_API_KEY": "ck_live_..." }
}
}
}
Local package
@gege-mn/ray-mcp is a small stdio MCP server that forwards every request to the hosted endpoint. Its tool list is read from the hosted server, so it always matches this page. Your client starts it; you don't run it by hand.
npx -y @gege-mn/ray-mcp
| Environment variable | Required | Default |
|---|---|---|
RAY_API_KEY |
Yes | |
RAY_MCP_URL |
No | https://ray-api.gege.mn/mcp |
Use the local package when:
- the client only supports local stdio servers,
- the client can't send a custom header to a remote server (Claude Desktop), or
- you want the key in a process environment variable rather than in a header in a config file.
It needs Node.js 18 or later. In every other case use the hosted endpoint, which needs no install and no updates. Source and MCP registry metadata: https://github.com/gege-mn/ray-mcp.
Tools
Tools that only read carry the MCP readOnlyHint annotation, and archive_template and delete_webhook carry destructiveHint, so clients that confirm risky calls can tell them apart. The scope column is what the underlying endpoint needs: read endpoints accept any valid key, write endpoints need a key with the write scope.
| Tool | What it does | REST endpoint | Scope |
|---|---|---|---|
whoami |
The workspace id, key id and scopes of the key in use. | GET /me |
read |
get_usage |
Current plan, subscription and this month's usage (monthlyQuota, used, remaining). |
GET /billing/subscription |
read |
list_channels |
Channel configs with channelConfigId, templateKind and the recipient JSON Schema. |
GET /channels |
read |
send_notification |
Sends a notification. Takes the POST /send body (single recipient, fan-out, multi-channel or feed-only) and an optional idempotencyKey, sent as the Idempotency-Key header. |
POST /send |
write |
get_send_status |
Status counts and one page of delivery rows for a send, with provider ids and errors. Optional limit (1 to 200) and cursor for the next page. |
GET /sends/{id} |
read |
list_templates |
Templates in the workspace. Optional folder and includeArchived. |
GET /templates |
read |
get_template |
One template with its published version and draft. | GET /templates/{id} |
read |
create_template |
Creates a raw template, optionally published. | POST /templates |
write |
update_template_draft |
Writes the template's draft (a new draft version if the latest is published). | PATCH /templates/{id} |
write |
publish_template |
Promotes the draft to published. Sends use it immediately. | POST /templates/{id}/publish |
write |
archive_template |
Archives a template so it can't be sent or edited. | POST /templates/{id}/archive |
write |
unarchive_template |
Restores an archived template. | POST /templates/{id}/unarchive |
write |
test_send_template |
Delivers the published version to a real recipient. No feed entry, no webhooks, no monthly quota. | POST /templates/{id}/test-send |
write |
list_feed_notifications |
One end user's in-app feed, by externalUserId. |
GET /notifications |
read |
get_click_stats |
Click counts by URL for a sendId or campaignId. |
GET /clicks |
read |
list_webhooks |
Delivery webhook subscriptions and their health. Signing secrets are never returned. Optional includeArchived. |
GET /tenant-webhooks |
read |
get_webhook |
One delivery webhook. | GET /tenant-webhooks/{id} |
read |
create_webhook |
Subscribes an HTTPS endpoint to delivery events. Returns the signing secret once. Pro plan and above. | POST /tenant-webhooks |
write |
update_webhook |
Changes name, URL or events, enables or disables, or rotates the secret. | PATCH /tenant-webhooks/{id} |
write |
delete_webhook |
Archives a delivery webhook. | DELETE /tenant-webhooks/{id} |
write |
read_docs |
A page of these docs as markdown, by slug (sending, channels/telegram; a trailing .md is accepted). Without a slug it returns the llms.txt index. Makes no API call. |
none | any key |
Request and response fields are the same as the REST endpoints documented in these guides and in the OpenAPI spec.
Example prompts
Which Ray channels does this workspace have, and what recipient does each one expect?
Create an email_html template called "Password reset" with a {{resetUrl}} param, publish it,
and test-send it to [email protected] through our SMTP channel.
Send 6a7b8c9d-0e1f-4a2b-9c3d-4e5f6a7b8c9d had failures. Show the provider error for each failed row
and tell me which ones are worth retrying.
Our "production" delivery webhook stopped receiving events. Check its lastError and
consecutiveFailures and tell me what to fix.
How much of this month's Ray quota have we used?
Add an order-shipped push notification to this app with the Ray TypeScript SDK. Use list_channels
to find the push channel and its recipient shape, and read the sending and idempotency docs first.
Security
- Use the smallest scope. For exploring and debugging, connect a key that has only the
readscope. Tools that send or change state then fail withwrite scope requiredand nothing is delivered. Create a separateread+writekey when you want the agent to act, and revoke it in the dashboard when you're done. - Keys travel as headers. The hosted server takes the key only from the
Authorizationheader over HTTPS, never from a URL or a tool argument. The key is stored wherever your client keeps its MCP config, so keep that file private. - Sends are real.
send_notificationdelivers to real people through your provider accounts and counts toward your quota. When trying templates, have the agent usetest_send_templateto your own address: test sends don't create feed entries, don't fire webhooks, don't use monthly quota, and are capped per day. - Keep confirmations on for writes. Let the client ask before
send_notification,archive_templateand webhook changes run, and check the recipient andchannelConfigIdin the arguments. - Retries. Ask the agent to pass an
idempotencyKeyfor any send it might repeat, so a retried call can't deliver twice. See Idempotency. - Tool output is data. Template bodies, feed titles and provider error messages are returned as-is. An agent should not follow instructions that appear inside them.
- A key only ever sees its own workspace. Ids from another workspace return
404.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| Tools answer "This MCP connection has no Ray API key" | The client isn't sending an Authorization header. |
Add the header (or RAY_API_KEY for the local package) and restart the client. |
The client can't connect, or reports 401 |
A typo in the key, or a revoked key. | Check the key with curl https://ray-api.gege.mn/me -H "Authorization: Bearer $RAY_API_KEY", then fix the header in the client config. |
A tool fails with write scope required |
The key lacks the write scope. The endpoint answers 400 validation_error; Ray doesn't use 403 for scopes. |
Use a key with write, or stick to read-only tools. |
create_webhook fails with 403 forbidden |
Delivery webhooks need the Pro plan or higher. | Upgrade at https://ray.gege.mn/pricing. |
A tool fails with 429 rate_limit_exceeded |
Tool calls are API requests and share the workspace's per-second rate limit with your production traffic. | Wait the Retry-After seconds and make fewer parallel calls. See Rate limits. |
send_notification fails with 402 quota_exceeded |
The Free plan's monthly quota is used up. | Check with get_usage, then upgrade or wait for the next month. See Monthly quota. |
test_send_template fails with 402 quota_exceeded |
The daily test-send allowance is used up. | Wait for the next UTC day. See Test sends. |
A tool fails with 404 not_found |
Wrong id, an archived resource, or an id from another workspace. | List the resources first (list_channels, list_templates). |
413 payload_too_large |
The MCP request is over 1 MiB, usually base64 attachments in send_notification. |
Send attachments through the REST API or the SDK. |
405 from the server |
The client is opening an SSE stream with GET, or is configured for the legacy SSE transport. |
Configure the server as Streamable HTTP (http), not sse. |
| The local package doesn't start, or says Ray is not configured | Node.js 18 or later isn't installed, or RAY_API_KEY isn't set in the server's environment. |
Set the variable in the server's env block, check that npx runs in a terminal, and restart the client. |
| New tools don't appear | The client cached the tool list. | Restart the client or reconnect the server. |
To check the endpoint outside any client, list the tools with curl:
curl https://ray-api.gege.mn/mcp \
-H "Authorization: Bearer $RAY_API_KEY" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{ "jsonrpc": "2.0", "id": 1, "method": "tools/list" }'
Related
- TypeScript SDK: the same operations from your application code.
- Using Ray with AI coding agents: skills, llms.txt, markdown docs and a copy-paste prompt.
- Authentication: API keys and scopes.