MCP server
Evinor exposes its event search, structured event reporting, and beats over the Model Context Protocol so an agent can pull from the corpus, file a first-hand report, or watch a beat for new events, 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, reporting events, and beats applies — the tools are those endpoints, 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. - Scopes: each tool requires the scopes of the endpoint it wraps, and a key
sees only the tools its scopes allow (see The tools). A key with
none of
EVENTS_READ,REPORTS_READ,REPORTS_WRITE,BEATS_READ, orBEATS_WRITEsees zero tools. - Billing: every
search_eventscall is charged. The reporting and beat tools are not — though a beat created withcreate_beatspends credits as it records entries. 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.
Local (stdio) clients
Some agent hosts only launch MCP servers as local processes, or support remote
servers but not a custom Authorization header. For those, use the
first-party bridge @evinor/mcp,
which runs locally over stdio and forwards to the endpoint above:
{
"mcpServers": {
"evinor": {
"command": "npx",
"args": ["-y", "@evinor/mcp"],
"env": {
"EVINOR_API_KEY": "evnr_live_your_key_here"
}
}
}
}
It reads three environment variables:
| Variable | Required | Meaning |
|---|---|---|
EVINOR_API_KEY | Yes | Your evnr_live_… key. The bridge refuses to start without a well-formed key. |
EVINOR_MCP_URL | No | Defaults to https://api.evinor.ai/mcp. Must be https: (plain http: only for localhost). |
EVINOR_MCP_ALLOW_CUSTOM_URL | No | Set to 1 to allow an EVINOR_MCP_URL host other than api.evinor.ai. Only the literal 1 enables it. Needed for any non-default host, including localhost. |
There is deliberately no command-line flag for the key: flags leak into process listings and shell history.
The bridge is a pass-through. It adds a local process and nothing on the server side:
- Same tools. It defines no tools of its own.
tools/listandtools/callare forwarded to the endpoint, so you see exactly the tools your key's scopes allow, with the same descriptions, and you get server-side fixes without upgrading the bridge. - Same billing and rate limits. Every call is an ordinary call to the endpoint. Starting the bridge costs one handshake the first time a tool is used.
- Same errors. The errors described under Errors, including
rate-limited, reach your client unchanged,retryableandguidanceincluded. - One translation. The per-IP edge ban (see
Per source IP)
normally arrives as a bare
429that clients cannot parse. The bridge turns it into a JSON-RPC error withdata.status: 429,data.retryable: true, and guidance to wait out the ban. - No idempotency key. Like direct MCP calls, the bridge sends no
Idempotency-Key, so everysubmit_reportfiles a new report. If you need exactly-once filing, use the REST API or@evinor/sdk. See Idempotency. - Your key stays local. The bridge never writes the key to its output.
It never follows redirects, and it refuses non-HTTPS endpoints. The bridge
refuses a non-default host unless
EVINOR_MCP_ALLOW_CUSTOM_URL=1. If you set it and pointEVINOR_MCP_URLat a host other thanapi.evinor.ai, you are trusting that host with your key and with the content it returns to your agent. The bridge prints a notice when you do.
A generic stdio-to-HTTP proxy such as mcp-remote also works with the
configuration in Connecting. It will not translate the edge ban,
and you are trusting its publisher with your key.
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
| Tool | Scope | Billed | What it does |
|---|---|---|---|
search_events | EVENTS_READ | Yes | Runs a new search and returns its first page. |
search_events_continue | EVENTS_READ | No | Returns the next page of a search search_events already ran. |
list_event_types | REPORTS_READ | No | The event types a report can be filed against, with each role's type. |
list_reporting_grants | REPORTS_READ | No | The account's reporting grants: the grant_id values it can file under. |
submit_report | REPORTS_WRITE | No | Files a first-hand report, published under the key owner's name. |
list_beats | BEATS_READ | No | Every beat the account owns. |
get_beat | BEATS_READ | No | One beat, by beat_id. |
create_beat | BEATS_WRITE | No | Creates a beat, with the plan's default spend cap. The beat then spends credits as it records entries. |
get_beat_entries | BEATS_READ | No | Starts a read of a beat's entries, by window or by since. |
get_beat_entries_continue | BEATS_READ | No | Returns the next page of a read get_beat_entries started. |
tools/list returns only the tools your key's scopes allow, and tools/call
refuses any other tool with a 403 missing-scope.
The search tools
The split between search_events and search_events_continue is the billing
boundary made visible. 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.
list_event_types and list_reporting_grants
Both take no arguments and return the same JSON as
GET /v1/event-types and
GET /v1/reporting-grants.
An agent reads them to build a valid submit_report call.
submit_report
Takes the same body as
POST /v1/reports and
returns the filed report. Everything on the
Reporting events page applies, with three differences an
agent's operator must know:
- The report is first-hand and attributed to the key owner. The tool's
description tells the model to use it only for facts it has confirmed
first-hand, never for something it read, inferred, or was told, and never to
try the tool out. That instruction is the only thing standing between an
agent and an unconfirmed report published under your name. Give a key
REPORTS_WRITEonly if you accept that. There is no retraction endpoint yet. - Retries are not deduplicated. Over REST, an
Idempotency-Keyfixes the report's id, so a retry cannot file twice. A tool call has no way to carry that key into the report id: everysubmit_reportcall gets a freshly generated id, so repeating a call, including after an error or timeout whose outcome is unknown, can publish the same report twice. The description tells the model not to repeat a call that may already have succeeded. If you need exactly-once filing, use the REST endpoint with anIdempotency-Key. - Entity search is REST-only.
GET /v1/entities/searchis not a tool, so an agent can fill an entity role only with an id it already holds, such as a grant's allowed entity fromlist_reporting_grants, or one you give it.
The beat tools
The five beat tools wrap the beat endpoints and return the same JSON. Two things differ from REST, because a tool takes one flat argument object:
- The beat id is an argument.
get_beat,get_beat_entries, andget_beat_entries_continuetake the beat's id asbeat_idinstead of in a path. - The entries read is two tools, like search.
get_beat_entriesstarts a read and refuses acursor; it takes exactly one of a window (from+until, orlast) orsince.get_beat_entries_continuetakesbeat_id,cursor, and optionallylimit. Both are free. For incremental polling, start withsince: ""and pass each response'spoll_sinceas the nextsince— see Polling withpoll_since.
create_beat has no spend_cap_credits argument: a beat an agent creates
always gets the plan's default monthly cap, and a call that sends one is
refused. Its description warns the model that the beat keeps spending after
the call returns, and that repeating a call creates a second beat.
Changing an existing beat — replacing, deleting, enabling, disabling, resuming, or setting its spend cap — is not available as a tool. Those actions change or remove configuration you already rely on, and resume and set-spend-cap would let an agent lift the limit on its own spending. They stay REST and app actions.
Tool descriptions are a contract
The descriptions attached to these tools are read by a model. They say
plainly that search_events is billed and search_events_continue is not, and
that submit_report publishes under your name and does not deduplicate
retries, and that a beat created with create_beat spends credits on its own
while reading beats is free.
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. |
not-entitled | permanent | The account must be entitled by a human first. |
quota-exceeded | permanent | A plan quota is reached. A human must make room or change plan. |
state-conflict | permanent | Read the resource and decide again from its current state. |
entity-list-inaccessible | permanent | Remove or replace the entity list ids. |
report-already-submitted | permanent | The report was already filed. Do not retry. |
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 every tool call counts 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. (The @evinor/mcp bridge
reports a ban as a JSON-RPC error with data.status: 429 instead.)
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 the tools listed above. GET /v1/entities/search, GET /v1/reports, and
GET /v1/reports/{id} are REST-only, and so are the beat endpoints that change
an existing beat (see The beat tools). 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.