# Context pack: Errors and rate limits

Source: https://nordvec.com/cs/docs/guides/errors-and-rate-limits
Pack: https://nordvec.com/cs/docs/packs/errors-and-rate-limits

This pack bundles one Nordvec guide with the guides it builds on and the guides it links to, in reading order, so an assistant reading it meets no reference it cannot follow.

## Contents

1. [Errors and rate limits](https://nordvec.com/cs/docs/guides/errors-and-rate-limits) (this guide)
2. [MCP](https://nordvec.com/cs/docs/guides/mcp) (linked from this guide)

---

# Errors and rate limits
Source: https://nordvec.com/cs/docs/guides/errors-and-rate-limits

The one error envelope every failed request returns, the rate-limit headers, and how to retry a write safely.



Every endpoint fails the same way, so a client handles errors, rate limits and
retries once and reuses that code everywhere, including over
[MCP](/docs/guides/mcp).

## The error envelope [#the-error-envelope]

Every non-2xx response is one JSON object:

```json
{
  "defined": false,
  "code": "TOO_MANY_REQUESTS",
  "message": "Too many requests",
  "data": { "reason": "rate_limit.exceeded", "retryAfterMs": 12000 }
}
```

* `code` is the HTTP-level error, for example `UNAUTHORIZED`, `FORBIDDEN`,
  `NOT_FOUND`, `BAD_REQUEST` or `TOO_MANY_REQUESTS`.
* `data.reason`, when present, is a finer machine-readable reason such as
  `auth.key_not_found` or `rate_limit.exceeded`. Branch on it rather than on
  `message`, which is for people and may change.
* `defined` is `true` when the operation lists that error in the
  [API reference](/docs/api), and `false` for errors any request can meet
  (authentication, rate limits, an unknown route).
* A validation failure answers `BAD_REQUEST` with the problems in
  `data.formErrors` and `data.fieldErrors`.

Every response also carries an `X-Request-ID`. Quote it when you contact
support, and we can find that exact request.

## Common statuses [#common-statuses]

| Status | Code                    | What to do                                                            |
| ------ | ----------------------- | --------------------------------------------------------------------- |
| `400`  | `BAD_REQUEST`           | Fix the request; `data.fieldErrors` names the fields                  |
| `401`  | `UNAUTHORIZED`          | Send a valid key or session                                           |
| `403`  | `FORBIDDEN`             | The key lacks the scope or the role the operation needs               |
| `404`  | `NOT_FOUND`             | The resource does not exist, or you are not allowed to see it         |
| `409`  | `CONFLICT`              | A duplicate write is still in flight; retry shortly                   |
| `413`  | `PAYLOAD_TOO_LARGE`     | The request body is over 1 MB; split a bulk push into smaller batches |
| `422`  | `UNPROCESSABLE_CONTENT` | The request is well formed but cannot be applied                      |
| `429`  | `TOO_MANY_REQUESTS`     | Wait for `Retry-After`, then retry                                    |

## Rate limits [#rate-limits]

Every response states the limit it was counted against, in two forms:

* the `X-RateLimit-*` headers;
* the IETF structured fields `RateLimit` (live state: `r` is the requests
  remaining, `t` the seconds until the window resets) and `RateLimit-Policy`
  (the quota: `q` is the limit, `w` the window in seconds).

A `429` also carries `Retry-After` in seconds and `data.retryAfterMs`. Wait at
least that long before the next request; retrying sooner is counted and
refused again.

## Retrying writes safely [#retrying-writes-safely]

A write operation that lists an `Idempotency-Key` header in the
[API reference](/docs/api) can be retried without doing the work twice. Send
one key per logical write and repeat the same key on every retry:

* the same key with the same body within 24 hours replays the stored response;
* the same key with a different body is refused with `422`;
* a duplicate that arrives while the first is still running gets `409`.

An operation without the header is not idempotent, so retry it only when you
know the first attempt did not land.


---

# MCP
Source: https://nordvec.com/cs/docs/guides/mcp

Connect an AI agent to your workspace knowledge base over the Model Context Protocol with an API key.



Nordvec serves the [Model Context Protocol](https://modelcontextprotocol.io)
(MCP), so an agent or an assistant that speaks MCP can discover your workspace's
operations as tools and call them natively, without an OpenAPI document to
reason about.

There are two servers:

| Server         | URL                              | Auth    | What it exposes                                                                                                      |
| -------------- | -------------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------- |
| Documentation  | `https://nordvec.com/api/mcp`    | none    | These guides and the connector catalog, for an agent that is integrating with Nordvec                                |
| Knowledge base | `https://nordvec.com/api/v1/mcp` | API key | Your workspace's documents, search and ingestion: the same operations as the [REST API](/docs/guides/authentication) |

Both run in the EU, on the same infrastructure as the rest of the API.

## Connecting a client [#connecting-a-client]

Point an MCP client at the knowledge-base server with your API key as a
bearer token. Most clients take a configuration block like this one:

```json
{
  "mcpServers": {
    "nordvec": {
      "url": "https://nordvec.com/api/v1/mcp",
      "headers": {
        "Authorization": "Bearer nv_eu_live_your_api_key"
      }
    }
  }
}
```

The server is stateless Streamable HTTP: every message is one `POST` carrying
one JSON-RPC request, and the answer comes back in the response body. There
are no sessions to keep and no server-initiated stream, so a `GET` on the URL
answers `405`, and a `POST` whose `Content-Type` is not `application/json`
answers `415` before the body is read.

<Callout type="warn">
  Only an API key can use the knowledge-base server. A signed-in browser session
  is refused, and every tool needs the key scope the matching REST operation
  needs, so a key created for one job can do exactly that job through MCP too.
</Callout>

The server authenticates with a static bearer key and does not offer OAuth
discovery. A client that lets you set request headers (coding agents, IDE
extensions, the MCP Inspector in header mode) connects as shown above; a
hosted client that only supports the OAuth authorization flow cannot connect
yet.

A client that sends the `MCP-Protocol-Version` header is answered under that
version when the server supports it (`2025-06-18` and `2024-11-05`) and
refused with `400` when it does not, so a version mismatch is reported at the
first message rather than as a malformed reply later.

## Tools [#tools]

The tools are derived from the REST API, one tool per operation an API key may
call. A tool's name is the operation's contract path in snake case, with a
segment that repeats the one before it dropped. The scope column is the API key
scope the tool needs:

| REST operation                        | Contract path                        | MCP tool                           | Scope          |
| ------------------------------------- | ------------------------------------ | ---------------------------------- | -------------- |
| `POST /documents/search`              | `documents.search`                   | `documents_search`                 | `search:read`  |
| `GET /documents/{id}`                 | `documents.get`                      | `documents_get`                    | `search:read`  |
| `POST /documents/batch`               | `documents.batchGet`                 | `documents_batch_get`              | `search:read`  |
| `GET /documents/list`                 | `documents.list`                     | `documents_list`                   | `search:read`  |
| `POST /documents/push`                | `documentPush.push`                  | `document_push`                    | `index:write`  |
| `POST /documents/push/bulk`           | `documentPush.pushBulk`              | `document_push_bulk`               | `index:write`  |
| `POST /documents/push/permissions`    | `documentPush.pushUpdatePermissions` | `document_push_update_permissions` | `index:write`  |
| `POST /documents/push/delete`         | `documentPush.pushDelete`            | `document_push_delete`             | `index:delete` |
| `GET /documents/push/status`          | `documentPush.pushStatus`            | `document_push_status`             | `index:status` |
| `GET /documents/push/upload`          | `documentPush.pushUploadStatus`      | `document_push_upload_status`      | `index:status` |
| `POST /documents/push/upload/restore` | `documentPush.pushUploadRestore`     | `document_push_upload_restore`     | `index:write`  |
| `GET /analytics/ingestion`            | `analytics.ingestion`                | `analytics_ingestion`              | `index:status` |
| `GET /quota/embedding`                | `quota.embedding`                    | `quota_embedding`                  | `quota:read`   |
| `GET /analytics/usage`                | `analytics.usage`                    | `analytics_usage`                  | `quota:read`   |
| `POST /tenant/audit-log/search`       | `auditLog.list`                      | `audit_log_list`                   | `audit:read`   |
| `GET /tenant/audit-log/catalog`       | `auditLog.catalog`                   | `audit_log_catalog`                | `audit:read`   |

`tools/list` is the authoritative catalog: it shows a key only the tools its
scopes admit, and each tool's description names the scope it requires. A tool's
`inputSchema` is the operation's request schema and, where the operation
returns an object, its `outputSchema` is the response schema and results carry
`structuredContent` alongside the JSON text.

Calling a tool the key's scopes do not admit answers with a tool error naming
`FORBIDDEN`, the same refusal the REST route gives, so a client holding a
cached listing from another key learns why rather than that the tool is
missing.

## Retrying a write [#retrying-a-write]

`document_push` and `document_push_bulk` take an optional `idempotencyKey`
argument, so an agent or client that retries a push does not index the
documents twice. Send one key per logical write, such as a UUID, and repeat the
same key with the same arguments on every retry:

* the same key with the same arguments within 24 hours returns the first
  call's result without running it again;
* the same key with different arguments is refused: the tool result has
  `isError: true` and names `UNPROCESSABLE_CONTENT`;
* a retry that arrives while the first call is still running is refused the
  same way, naming `CONFLICT`; retry it after a short delay.

A key is 1 to 256 printable ASCII characters and belongs to the API key that
sent it: another API key using the same value runs its own write. A key used
with the REST `Idempotency-Key` header is not shared with the MCP tool, so a
tool call that reuses it is refused with `UNPROCESSABLE_CONTENT`. A call
without the argument runs again every time, which is why these tools do not
declare `idempotentHint`. The REST behaviour is described under
[retrying writes safely](/docs/guides/errors-and-rate-limits).

## Limits and errors [#limits-and-errors]

A tool call draws on the same rate-limit bucket as the REST operation it
corresponds to, and every other message on the endpoint's own bucket. The
`X-RateLimit-*` headers, the `429` answer with its `Retry-After`, and the
error envelope are the same as on the
[REST API](/docs/guides/errors-and-rate-limits),
so a client that already handles them for REST handles them here.

A failed tool call returns an MCP tool result with `isError: true` whose text
is the REST error body (`code`, `message`, `data`); validation failures carry
the same `fieldErrors` shape the REST API returns. A JSON-RPC error is reserved
for the protocol itself: an unparsable body, an unknown method, or a server
fault.

Request bodies are capped at 1 MB, the same bound as the REST routes.

## A first exchange [#a-first-exchange]

```bash
# Discover the tools your key can call
curl https://nordvec.com/api/v1/mcp \
  -H "Authorization: Bearer $NORDVEC_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

# Search the workspace
curl https://nordvec.com/api/v1/mcp \
  -H "Authorization: Bearer $NORDVEC_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"documents_search","arguments":{"query":"data retention policy"}}}'
```

## The documentation server [#the-documentation-server]

The documentation server at `/api/mcp` needs no key. It offers `list_guides`,
`get_guide`, `search_docs` and `list_connectors`, so an agent that is building
an application on the API can read these guides directly. It is rate limited per IP like
the other public endpoints.
