Skip to main content

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 GET on this path returns 405.
  • 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, or BEATS_WRITE sees zero tools.
  • Billing: every search_events call is charged. The reporting and beat tools are not — though a beat created with create_beat spends 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:

VariableRequiredMeaning
EVINOR_API_KEYYesYour evnr_live_… key. The bridge refuses to start without a well-formed key.
EVINOR_MCP_URLNoDefaults to https://api.evinor.ai/mcp. Must be https: (plain http: only for localhost).
EVINOR_MCP_ALLOW_CUSTOM_URLNoSet 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/list and tools/call are 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, retryable and guidance included.
  • One translation. The per-IP edge ban (see Per source IP) normally arrives as a bare 429 that clients cannot parse. The bridge turns it into a JSON-RPC error with data.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 every submit_report files 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 point EVINOR_MCP_URL at a host other than api.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:

  • Accept must list both application/json and text/event-stream. A request accepting only JSON is refused with 406, 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-18 and later. Earlier revisions are refused, because batching was still part of the protocol in them.

The tools​

ToolScopeBilledWhat it does
search_eventsEVENTS_READYesRuns a new search and returns its first page.
search_events_continueEVENTS_READNoReturns the next page of a search search_events already ran.
list_event_typesREPORTS_READNoThe event types a report can be filed against, with each role's type.
list_reporting_grantsREPORTS_READNoThe account's reporting grants: the grant_id values it can file under.
submit_reportREPORTS_WRITENoFiles a first-hand report, published under the key owner's name.
list_beatsBEATS_READNoEvery beat the account owns.
get_beatBEATS_READNoOne beat, by beat_id.
create_beatBEATS_WRITENoCreates a beat, with the plan's default spend cap. The beat then spends credits as it records entries.
get_beat_entriesBEATS_READNoStarts a read of a beat's entries, by window or by since.
get_beat_entries_continueBEATS_READNoReturns 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_WRITE only if you accept that. There is no retraction endpoint yet.
  • Retries are not deduplicated. Over REST, an Idempotency-Key fixes the report's id, so a retry cannot file twice. A tool call has no way to carry that key into the report id: every submit_report call 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 an Idempotency-Key.
  • Entity search is REST-only. GET /v1/entities/search is 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 from list_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, and get_beat_entries_continue take the beat's id as beat_id instead of in a path.
  • The entries read is two tools, like search. get_beat_entries starts a read and refuses a cursor; it takes exactly one of a window (from + until, or last) or since. get_beat_entries_continue takes beat_id, cursor, and optionally limit. Both are free. For incremental polling, start with since: "" and pass each response's poll_since as the next since — see Polling with poll_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_continue is 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-credits with 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?

SituationShape
Refused before dispatch — bad key, missing scope, rate limit, unknown tool, bad argumentA JSON-RPC error, with the HTTP status preserved (401, 403, …).
The tool ran and failed — no credits, an unusable filter, an upstream faultA 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…"
}
}
  • type is the same stable problem-type URI a REST client branches on. Branch on it here too.
  • retryable is Evinor's verdict on whether calling again unchanged could ever succeed.
  • guidance is 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.

ProblemVerdictWhat the agent is told
insufficient-creditspermanentStop. A human must top up the balance.
validation-failedpermanentFix the named arguments; an identical resend is refused.
missing-scopepermanentThe key's scopes must change first.
invalid-api-keypermanentNo number of attempts will authenticate this key.
search-execution-expiredpermanentThe cursor is dead. A fresh search_events is billed.
not-foundpermanentNothing matches the identifier supplied.
not-entitledpermanentThe account must be entitled by a human first.
quota-exceededpermanentA plan quota is reached. A human must make room or change plan.
state-conflictpermanentRead the resource and decide again from its current state.
entity-list-inaccessiblepermanentRemove or replace the entity list ids.
report-already-submittedpermanentThe report was already filed. Do not retry.
idempotency-key-conflictpermanentReissue with a new key.
rate-limitedretryableWait for the window, then resend unchanged.
write-unavailableretryableTransient, pre-charge. Retry shortly.
idempotency-in-flightretryableRetry — but under a new key. See below.
upstream-errorretryableRetry at most once. See below.
internal-errorretryableRetry 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.

A replay requires a byte-identical JSON-RPC envelope

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_events call claims its key non-reclaimably, because "crashed before it charged" and "charged, then crashed" are indistinguishable from the outside. Retrying under the same key returns idempotency-in-flight for the full 24-hour window, not eventually. Generate a new key.
  • search_events_continue is 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, a 400, or a 503, the same key works again once you have fixed the cause.
  • A 502 retains 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 422 idempotency-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.