Skip to main content

MCP endpoint

Nervly exposes a Model Context Protocol endpoint so an AI agent can inspect the gateway the way an operator would — answering "is Nervly reaching the broker right now?" without a shell, a dashboard login or a metrics scrape.

Reference: Invoke a Model Context Protocol tool.

Every call is a JSON-RPC 2.0 POST /v1/mcp with an McpRequest body:

FieldTypeRequiredMeaning
methodstringYestools/list enumerates the tools; tools/call executes one.
paramsobject | nullNoMethod arguments. For tools/call, {"name": ..., "arguments": {...}}.
idinteger | nullNoCorrelation id. Echoed in the response; defaults to 1.

The gateway answers with an McpResponse in which exactly one of result or error is present:

FieldTypeMeaning
jsonrpcstringProtocol version. Always "2.0".
resultobjectTool result on success. Shape depends on the tool.
errorobject | nullJSON-RPC error on failure, with code, message and optional data.
idintegerEcho of the request id.

Protocol-level failures — an unknown method, an unknown tool, or invalid parameters — are still HTTP 200 with an error object, as JSON-RPC requires. Only a missing or invalid bearer token produces a non-2xx status.

Listing the tools​

curl -X POST https://api.nervly.io/v1/mcp \
-H "Authorization: Bearer $NERVLY_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "method": "tools/list" }'
{
"jsonrpc": "2.0",
"result": {
"tools": [
{
"name": "gateway_status",
"description": "Inspect real-time edge gateway status: process uptime and whether the NATS broker connection is live or buffering.",
"inputSchema": { "type": "object", "properties": {}, "additionalProperties": false }
},
{
"name": "idempotency_inspect",
"description": "Check whether an Idempotency-Key is cached for this workspace, and how long its replay window has left.",
"inputSchema": {
"type": "object",
"properties": {
"idempotencyKey": { "type": "string", "minLength": 1, "description": "The Idempotency-Key value to inspect." }
},
"required": ["idempotencyKey"],
"additionalProperties": false
}
},
{
"name": "send_notification",
"description": "Trigger a multi-channel notification (SMS, voice, email, WhatsApp or push) for one subscriber. Sandboxed by default: the call validates routing and compliance and returns the dispatch it would have made without sending anything. Pass `live: true` to run the same validation, routing, rate limiting and broker publish path as POST /v1/events/trigger and contact the carrier.",
"inputSchema": {
"type": "object",
"properties": {
"name": { "type": "string", "minLength": 1, "description": "Workflow/event name configured in the workspace." },
"to": {
"type": "object",
"properties": {
"subscriberId": { "type": "string", "minLength": 1 },
"email": { "type": ["string", "null"] },
"phone": { "type": ["string", "null"], "description": "E.164 number, e.g. +2348012345678." },
"deviceTokens": { "type": ["array", "null"], "items": { "type": "string" } }
},
"required": ["subscriberId"],
"additionalProperties": false
},
"payload": { "type": ["object", "null"], "description": "Template variables referenced as {{key}}." },
"overrides": { "type": ["object", "null"] },
"category": { "type": ["string", "null"] },
"priority": { "type": ["string", "null"], "enum": ["CRITICAL", "HIGH", "NORMAL", "LOW", null] },
"idempotencyKey": { "type": ["string", "null"] },
"live": { "type": "boolean", "description": "Explicit opt-in to send for real. Only a literal `true` dispatches; omit it (or send `false`) to get a sandbox preview. The live path is never reached by omission, a typo or a defaulted argument." }
},
"required": ["name", "to"],
"additionalProperties": false
}
},
{
"name": "check_delivery_status",
"description": "Look up one message's delivery state, provider dispatch details and normalized delivery code, scoped to this workspace.",
"inputSchema": {
"type": "object",
"properties": { "eventId": { "type": "string", "minLength": 1, "description": "evt_<hex> or UUID." } },
"required": ["eventId"],
"additionalProperties": false
}
},
{
"name": "list_templates",
"description": "List the workspace's message templates with their required {{variable}} placeholders, optionally narrowed to one event name or channel.",
"inputSchema": {
"type": "object",
"properties": {
"eventName": { "type": ["string", "null"] },
"channel": { "type": ["string", "null"], "enum": ["sms", "email", "push", "whatsapp", "voice", null] }
},
"additionalProperties": false
}
},
{
"name": "verify_subscriber_channel",
"description": "Check a subscriber's opt-in status, NCC DND state and channel prioritization, and report which channel would be selected.",
"inputSchema": {
"type": "object",
"properties": {
"subscriberId": { "type": "string", "minLength": 1 },
"channel": { "type": ["string", "null"], "enum": ["sms", "email", "push", "whatsapp", "voice", null] },
"category": { "type": ["string", "null"], "description": "Category override to evaluate instead of the channel default." },
"eventName": { "type": ["string", "null"], "description": "Event name the DND intent is classified from when category is omitted." }
},
"required": ["subscriberId"],
"additionalProperties": false
}
},
{
"name": "read_api_schema",
"description": "Read the gateway's published API contract. With no arguments, list every operation and component name; with operationId, return that operation with the component schemas it references; with component, return one named schema. The contract carries no tenant data.",
"inputSchema": {
"type": "object",
"properties": {
"operationId": { "type": ["string", "null"], "description": "Operation id from the index, e.g. trigger-notification." },
"component": { "type": ["string", "null"], "description": "Component schema name from the index, e.g. TriggerRequest." }
},
"additionalProperties": false
}
},
{
"name": "generate_integration_code",
"description": "Generate integration code that triggers a notification, using this workspace's own event names and template variables. Supported languages: curl, javascript, python, go and php.",
"inputSchema": {
"type": "object",
"properties": {
"language": { "type": "string", "enum": ["curl", "javascript", "python", "go", "php"] },
"eventName": { "type": ["string", "null"], "description": "Event name to generate for; must exist in this workspace's templates. Defaults to the workspace's first template." },
"subscriberId": { "type": ["string", "null"] },
"channel": { "type": ["string", "null"], "enum": ["sms", "email", "push", "whatsapp", "voice", null] }
},
"required": ["language"],
"additionalProperties": false
}
}
]
},
"id": 1
}

The catalog above is what a read-write key sees. A read-only key receives the same list without send_notification.

Calling a tool​

curl -X POST https://api.nervly.io/v1/mcp \
-H "Authorization: Bearer $NERVLY_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "method": "tools/call", "params": { "name": "gateway_status" } }'
{
"jsonrpc": "2.0",
"result": {
"service": "nervly-gateway",
"uptime": 86400,
"nats_status": "CONNECTED"
},
"id": 1
}

nats_status is CONNECTED while the gateway holds a broker connection and FALLBACK_BUFFER when it is buffering instead — the value an operator would look for when asking "is Nervly reaching the broker right now?".

The tool catalog​

tools/list advertises eight tools to a read-write key and seven to a read-only key: send_notification requires the write scope, so it is omitted for a read-only key and rejected if called by name. Every tool validates its arguments strictly: unknown parameters are rejected, so a typo or a smuggled workspaceId fails rather than being ignored.

ToolArgumentsWhat it returns
gateway_statusnoneProcess uptime and whether NATS is connected or buffering.
idempotency_inspectidempotencyKeyWhether that replay key is cached for this workspace, and its remaining TTL.
send_notificationname, to, optional payload, overrides, category, priority, idempotencyKey, liveA sandbox preview by default; with live: true, the same TriggerResponse as POST /v1/events/trigger.
check_delivery_statuseventIdDelivery status, provider dispatch details, timeline, and a normalized delivery code.
list_templatesoptional eventName, channelWorkspace templates with their required {{variable}} placeholders.
verify_subscriber_channelsubscriberId, optional channel, category, eventNameOpt-in status, NCC DND state, channel prioritization, and the channel that would be selected. eventName is the fallback the DND intent is classified from when category is omitted.
read_api_schemaoptional operationId, componentThe gateway's published API contract: an operation index, one operation with its referenced schemas, or one named component schema.
generate_integration_codelanguage, optional eventName, subscriberId, channelA ready-to-run snippet that triggers a notification, bound to this workspace's event names and template variables.

Sending a notification​

send_notification is sandboxed by default. A call without the live opt-in validates the request and returns the dispatch it would have made without sending anything; a real send requires the explicit live: true argument, at which point the tool runs the same validation, rate limiting, persistence and broker publish path as the REST trigger endpoint, so an agent cannot bypass routing or tenancy. The call is tools/call with arguments: { name, to, payload?, overrides?, category?, priority?, idempotencyKey?, live? }, and a live result is a TriggerResponse:

{
"jsonrpc": "2.0",
"result": {
"eventId": "evt_9c4f1a2b3d5e6f708192a3b4c5d6e7f8",
"status": "QUEUED",
"idempotencyKey": "order-123",
"priority": "NORMAL",
"channel": "sms",
"timestamp": "2026-08-28T09:15:04.221Z"
},
"id": 7
}

Checking a delivery​

{
"jsonrpc": "2.0",
"result": {
"event_id": "evt_9c4f1a2b3d5e6f708192a3b4c5d6e7f8",
"status": "DELIVERED",
"normalized_code": "delivered",
"provider": "termii",
"provider_message_id": "msg_ext_778899",
"created_at": "2026-08-28T09:15:04.221Z",
"updated_at": "2026-08-28T09:15:09.004Z",
"events": [
{ "seq": 1, "status": "SENT", "normalized_code": "in_transit", "occurred_at": "2026-08-28T09:15:05.100Z" }
]
},
"id": 8
}

Sandbox test mode​

Sandbox is the default. A send_notification call that omits the opt-in — or passes "live": false — validates the request and returns the dispatch it would have made, without sending anything. A real send requires the explicit "live": true argument, so a call can never reach a real recipient through a real carrier by omission, a typo or a defaulted argument.

In sandbox mode the gateway resolves routing and evaluates the subscriber's compliance state — opt-in, email suppression and cached NCC DND — then returns a preview instead of publishing:

{
"jsonrpc": "2.0",
"result": {
"sandbox": true,
"dispatched": false,
"would_dispatch": true,
"would_deliver": true,
"event_name": "otp",
"subscriber_id": "sub-1",
"priority": "NORMAL",
"channel": "sms",
"subject": "notify.bulk.sms",
"stream_class": "bulk",
"eligible_channels": ["sms"],
"compliance": { "selected_channel": "sms", "erased": false },
"reason": null
},
"id": 9
}

would_dispatch is the gateway's routing decision — it would publish an event. would_deliver folds in the compliance view the worker applies before a carrier call, so a subscriber who opted out, is erased, is suppressed or is on the NCC DND register previews as non-deliverable with a reason. A sandbox call is read-only: it does not consume the rate limit, does not cache an idempotency key, writes no message row and publishes nothing to the broker, so no carrier is ever contacted. When the recipient has no reachable channel, would_dispatch is false and reason explains why — a validation result, not an accepted-and-dropped event. A preview always labels itself "sandbox": true and "dispatched": false; a live TriggerResponse carries neither label, so the two can never be confused.

Reading the API schema​

read_api_schema lets an agent read the same contract the reference site publishes, derived from the server's own source. With no arguments it returns the operation index and the component names:

{
"jsonrpc": "2.0",
"result": {
"operations": [
{ "operation_id": "trigger-notification", "method": "POST", "path": "/v1/events/trigger", "summary": "Trigger a notification", "tags": ["Events"] }
],
"components": ["RecipientDto", "TriggerRequest", "TriggerResponse"]
},
"id": 10
}

With operationId it returns that operation together with every component schema it references, transitively — so one call yields the complete request shape, nested DTOs included. With component it returns one named schema. An unknown name is a -32004 resource error, not an empty answer. The contract is platform-wide and carries no tenant data.

Generating integration code​

generate_integration_code produces a snippet that calls POST /v1/events/trigger, bound to the calling workspace's own templates. It supports curl, javascript, python, go and php.

Pass a language and, optionally, an eventName. When eventName is omitted the workspace's first template is used; when it is supplied it must exist in this workspace, otherwise the call is a -32004 resource error. The response carries the generated code, the variables the template expects, and the event_name it used:

{
"jsonrpc": "2.0",
"result": {
"language": "python",
"operation_id": "trigger-notification",
"path": "/v1/events/trigger",
"event_name": "payment-received",
"variables": ["amount", "name", "order_id"],
"code": "import os\nimport requests\n..."
},
"id": 11
}

Because the event name comes from the workspace's own templates, the snippet can only ever name events the key already owns.

Errors​

A protocol or parameter failure returns HTTP 200 with a JSON-RPC 2.0 error object:

{
"jsonrpc": "2.0",
"error": {
"code": -32602,
"message": "Invalid arguments for check_delivery_status",
"data": { "tool": "check_delivery_status" }
},
"id": 1
}
CodeMeaning
-32601Method not found — an unrecognised method.
-32602Invalid params — an unknown tool, a missing required argument, or a bad enum value.
-32603Internal error — a dependency (Postgres, Redis, NATS) failed.
-32003Permission denied — the API key lacks the read or write scope the tool requires.
-32004Resource not found — the event or template is not visible to this workspace.
-32000Tool failed — a rejected dispatch or an exceeded rate limit.

id echoes the value you sent, or 1 when the request omitted one, so concurrent calls can be correlated.

Security​

This endpoint sits behind the same bearer API key as the rest of /v1. The tools a key can reach depend on its scopes: a key with write can call every tool, while a read-only key (read) receives only the diagnostic tools — gateway_status, idempotency_inspect, check_delivery_status, list_templates, verify_subscriber_channel, read_api_schema and generate_integration_code. send_notification is omitted from tools/list for a read-only key and rejected if called by name.

Every tool is confined to the workspace that owns the key. No tool accepts a workspace identifier as an argument, and every query is scoped to the authenticated tenant, so an AI tool call cannot read or write across a workspace boundary. read_api_schema reads the platform contract, which carries no tenant data; generate_integration_code reads only the calling workspace's templates.

Give an inspecting agent a read-only key

send_notification is sandboxed by default, but live: true still sends. For an agent that must never send at all, issue a key with the read scope: it receives the seven diagnostic tools and cannot call send_notification even by name. See Managing API keys.

Next​