MCP server
Evinor exposes its event search over the Model Context Protocol so an agent can pull from the corpus directly, without you writing a REST client for it.
The MCP surface is not a second product. It is a projection of the same API documented on this site: the same API keys, the same scopes, the same credit balance, the same problem types. Everything you already know about searching events applies — the tools are that endpoint, described in a form a model can read.
- Endpoint:
POST https://api.evinor.ai/mcp - Transport: streamable HTTP, stateless. Every call is an independent
POST. There is no session to establish, nothing to resume, and no SSE stream —
a
GETon this path returns405. - Auth: the same
evnr_live_…API key you use for REST. - Scope required:
EVENTS_READ. A key without it sees zero tools. - Billing: every
search_eventscall is charged. See Billing.
Connecting
Point your MCP client at the endpoint and give it the API key as a bearer
token. The exact configuration shape depends on your client; the two things it
must send are the URL and the Authorization header:
{
"mcpServers": {
"evinor": {
"url": "https://api.evinor.ai/mcp",
"headers": {
"Authorization": "Bearer evnr_live_your_key_here"
}
}
}
}
Then run the ordinary handshake — initialize, then tools/list, then
tools/call. Your client does this for you.
If you are writing the HTTP calls yourself
Almost nobody should — use an MCP SDK. If you do, three requirements are easy to miss:
Acceptmust list bothapplication/jsonandtext/event-stream. A request accepting only JSON is refused with406, even though this server never opens a stream. That is a protocol-level requirement, not an Evinor one.- One JSON-RPC request object per POST. Array-form batches are refused. A batch would let many billed searches ride on a single rate-limit token and a single idempotency key, so it is rejected before anything is dispatched.
- Protocol version. This server accepts
2025-06-18and later. Earlier revisions are refused, because batching was still part of the protocol in them.
The tools
Two tools, and the split between them is the billing boundary made visible:
| Tool | Billed | What it does |
|---|---|---|
search_events | Yes | Runs a new search and returns its first page. |
search_events_continue | No | Returns the next page of a search search_events already ran. |
This deliberately differs from the REST shape. Over REST, one endpoint
(POST /v1/events/search) serves both calls and decides which you meant by
whether the body carried a cursor. Over MCP that would hide the cost of a
call behind an optional argument — and the thing reading the tool list is a
model deciding how often to call. So the two are separate tools with separate
descriptions, and the boundary cannot be crossed by accident: passing a
cursor to search_events is refused, not silently treated as a
continuation.
search_events
Takes the search filter — the same snake_case fields documented in
Searching events. lookback_days is
required (over REST it is required only when no cursor is present; here the
tool has no cursor, so the rule is simply part of the schema). A cursor
argument is rejected.
The result is the same JSON envelope the REST endpoint returns —
data, total_count, has_more, next_cursor, execution_id — carried as
the tool result's text content. The
response allowlist
applies unchanged: article bodies, source URLs, event-type internals, and
confidence scores are not in the tool result any more than they are in the REST
response.
search_events_continue
Takes cursor (required) and optionally limit. Filter arguments are
rejected, because a continuation page always re-uses the filter recorded on the
original execution — but limit is not a filter, so you can change the page
size as you go. Pass the next_cursor from a previous result and keep going
until has_more is false.
Note limit does not carry over from the search_events call that started
the execution: each continuation page uses the limit you send with it, and
falls back to the default page size if you send none.
Tool descriptions are a contract
The descriptions attached to these tools are read by a model, and they say
plainly that search_events is billed and search_events_continue is not.
They are generated from the same endpoint definitions this site's REST
documentation is generated from, so they cannot drift from the enforced
contract.
They are also unversioned. Changing a description changes how every customer's agent behaves, immediately, with no version to pin — so Evinor treats an edit to one as a contract change, not as copy editing.
Billing: every search_events call is charged
There is no free tier on the MCP surface and no metered allowance. A
search_events call is charged exactly as the equivalent REST call is, against
the same credit balance.
search_events_continueis never re-charged. Paging through a result you already paid for is free of charge (though it still costs a request against your rate-limit budget).- A refusal that happens before the search runs — an exhausted balance, a filter your account cannot use, a malformed argument — is not charged.
- When the balance cannot cover an execution the tool returns
insufficient-creditswith a permanent verdict, telling the agent to stop rather than retry.
Agents call more often than humans do. Size the balance for that, and watch the
insufficient-credits verdict rather than discovering the ceiling through a
stalled agent.
Errors
Errors arrive in one of two shapes, and which one you get tells you something useful: did the tool run?
| Situation | Shape |
|---|---|
| Refused before dispatch — bad key, missing scope, rate limit, unknown tool, bad argument | A JSON-RPC error, with the HTTP status preserved (401, 403, …). |
| The tool ran and failed — no credits, an unusable filter, an upstream fault | A tool result with isError: true (HTTP 200). |
The second shape exists because its content is fed back to the model. A failure the agent is supposed to reason about has to reach the agent, not just its client library.
Both shapes carry the same members, so you branch on the same values either way
— in error.data for a JSON-RPC error, and in the result's JSON body under
error for an isError result:
{
"error": {
"type": "https://docs.evinor.ai/problems/insufficient-credits",
"title": "Insufficient credits",
"status": 402,
"detail": "…",
"request_id": "8f0b2c1e-4a7d-4b93-9c2e-1d5a6f0b3c8e",
"as_of": "2026-09-06T00:00:00.000Z",
"retryable": false,
"guidance": "The account has no remaining search credits. STOP calling…"
}
}
typeis the same stable problem-type URI a REST client branches on. Branch on it here too.retryableis Evinor's verdict on whether calling again unchanged could ever succeed.guidanceis a sentence addressed to the model, spelling the verdict out in words it will act on.
The verdict is the part that matters on a billed surface: an agent that treats a permanent refusal as transient will loop, and every iteration of that loop is a charge. So the classification is deliberate rather than derived from the status code.
| Problem | Verdict | What the agent is told |
|---|---|---|
insufficient-credits | permanent | Stop. A human must top up the balance. |
validation-failed | permanent | Fix the named arguments; an identical resend is refused. |
missing-scope | permanent | The key's scopes must change first. |
invalid-api-key | permanent | No number of attempts will authenticate this key. |
search-execution-expired | permanent | The cursor is dead. A fresh search_events is billed. |
not-found | permanent | Nothing matches the identifier supplied. |
idempotency-key-conflict | permanent | Reissue with a new key. |
rate-limited | retryable | Wait for the window, then resend unchanged. |
write-unavailable | retryable | Transient, pre-charge. Retry shortly. |
idempotency-in-flight | retryable | Retry — but under a new key. See below. |
upstream-error | retryable | Retry at most once. See below. |
internal-error | retryable | Retry shortly; report the request_id if it persists. |
A 502 upstream-error is the one verdict worth
reading closely, because Evinor cannot tell from here whether the search was
charged before it failed. It is classified retryable because transient faults
dominate it, but the guidance bounds the retry: retry at most once; if you
sent an Idempotency-Key, retry under the same key (the claim is retained,
so a same-key retry is refused without charging again); and if you sent no key,
do not retry automatically, because a repeat may re-execute and re-charge.
Rate limits
Two independent ceilings sit in front of this endpoint, and an agent fleet can hit either. You need both numbers to size a deployment.
Per key — 60 requests per minute
The MCP endpoint has its own budget, separate from the REST budget on the
same key: spending the MCP allowance does not consume the REST one, or the
reverse. The window is one minute, and both search_events and
search_events_continue count against it. As everywhere else, treat the
RateLimit-Limit header as the source of truth rather than hard-coding the
number.
Exceeding it returns 429 rate-limited as a
JSON-RPC error, with Retry-After and the RateLimit-* headers described
under Rate limits. It is retryable: wait out the window.
Per source IP — 300 requests per minute, then a 10-minute ban
Ahead of the application there is an edge policy keyed on source IP, not on API key: more than 300 requests in 60 seconds from one IP bans that IP for 600 seconds.
Two properties of agent traffic make this worth planning for rather than discovering:
- Agent fleets are bursty. A REST integration paces itself; a fleet of agents fanning out on one task does not.
- The limit is per IP, not per customer. Egress through a shared NAT — a cloud NAT gateway, a corporate proxy, a CI runner pool — and your workers share one source IP with each other, and possibly with other Evinor customers. The 300 is spent collectively.
A ban is not an application response and it does not look like the errors
above: it is a bare 429 with no JSON-RPC envelope, so an MCP client will
report it as a malformed or unparseable response rather than as a rate limit.
If your agents suddenly start reporting transport-level failures rather than
rate-limited errors, check your egress IP first.
To stay clear of it: cap fleet concurrency, add jittered backoff so workers don't retry in lockstep, and spread large jobs across time rather than firing them simultaneously.
Idempotency
Idempotency works as it does over REST — see Idempotency — with the header carried on the MCP POST, and with one difference worth knowing before you rely on replays.
The stored request fingerprint covers the whole JSON-RPC body, including its
id. So a retry under the same Idempotency-Key only replays if it carries
the same id as the original call. A client that increments id per request —
which most do — will send a different envelope, and that is a 422
idempotency-key-conflict, not a
replay.
In practice: to retry a call and get the stored result back, resend the identical envelope. To issue a genuinely new search, use a new key.
The key is always yours to supply. Send it as an ordinary
Idempotency-Key HTTP header on the request. Evinor never derives one for you,
and in particular the JSON-RPC request id is not used as an idempotency
key: JSON-RPC ids are client-chosen and, because this transport is stateless,
clients routinely restart numbering at 1, so deriving a key from one would
replay a stale result for a genuinely new search.
There is no idempotency_key tool argument, and there will not be one — the
tools' input schemas are the search contract, and an argument the endpoint does
not accept would be rejected as unknown.
What follows from that:
- Omitting the key is safe, and it is the default. With no header the call simply executes and charges, which is the same thing the REST endpoint does for a keyless caller. Nothing is deduplicated — a duplicate delivery is a second charge — so send a key when you want that protection.
- A crashed billed call must be retried with a fresh key. A
search_eventscall claims its key non-reclaimably, because "crashed before it charged" and "charged, then crashed" are indistinguishable from the outside. Retrying under the same key returnsidempotency-in-flightfor the full 24-hour window, not eventually. Generate a new key. search_events_continueis not billed, so its claim behaves like every other endpoint's: a crashed page can be retried with the same key.- A pre-charge refusal releases the key. After a
402, a400, or a503, the same key works again once you have fixed the cause. - A
502retains the key on purpose. That retention is the protection described under Errors: a same-key retry is refused rather than billed a second time. Reaching for a fresh key defeats it. - Do not reuse one key across REST and MCP. A key is scoped to your API key
and to the exact request it was first used for, and an MCP request body is
the JSON-RPC envelope rather than a bare search body — so the same logical
search sent both ways is two different requests under one key. The second one
returns
422idempotency-key-conflict, not a replay. Give each transport its own keys.
What is not exposed
Only event search. The sensor endpoints — creating, updating, disabling, and
deleting sensors, and rotating their signing secrets — are not available as
MCP tools, regardless of the scopes your key holds. A SENSORS_WRITE key sees
no tools at all here. Managing sensors is a REST operation; see
Getting Started.