---
title: "MCP server"
description: "Drive the Aerovy Platform API from an AI agent through two spec-driven tools."
icon: "plug"
---

> **For AI agents:** the complete documentation index is at [llms.txt](/llms.txt). Append `.md` to any page URL for its markdown version.

<Note>The MCP server is in **beta**.</Note>

The Aerovy Platform ships a [Model Context Protocol](https://modelcontextprotocol.io) (MCP)
server that exposes the entire v2 API to an AI agent through **two tools**: `search` to
discover endpoints, and `execute` to call them. Both tools are driven by the v2 OpenAPI
document, so all 85+ operations are reachable without any per-endpoint wiring. The model works
entirely through tool arguments, never through generated code.

## Why two tools

The server is built around three invariants that keep your context window small and your
credential safe:

| Invariant | What it means |
|-----------|---------------|
| Spec stays in the host | The full OpenAPI document lives in the server. Only the per-search subset an agent asks for enters the model's context. |
| Key stays in the host | The `X-Api-Key` is held by the server. `execute` injects it on every call, so the model never sees a credential. |
| Responses are trimmed | Large responses are projected down to the fields you ask for before they enter context. |

Because both tools read the same spec, the model only ever sees the two tool descriptions at
startup. The 76 operations stay hidden until a search surfaces them.

## The `search` tool

Use `search` to find operations by keyword, then optionally pull a self-contained schema for
the one you want.

**Input**

```json
{
  "query": "ingest thing data",
  "method": "POST",
  "tag": "Ingestion",
  "includeSchemas": false,
  "limit": 20
}
```

| Field | Required | Purpose |
|-------|----------|---------|
| `query` | Yes | Matched against path, summary, `operationId`, and tags. |
| `method` | No | Filter to a single HTTP method. |
| `tag` | No | Filter to a single tag. |
| `includeSchemas` | No | When `true`, inline parameter, request body, and response schemas. |
| `limit` | No | Cap the number of results. |

**Output (default, slim)**

```json
[
  {
    "operationId": "IngestThingData",
    "method": "POST",
    "path": "/v2/thing/{thingId}/data",
    "summary": "Ingest thing data",
    "tags": ["Ingestion"]
  }
]
```

The query is tokenized and ranked: tokens that hit the path or `operationId` rank above
matches in the summary, which rank above tag matches. The top `limit` results are returned.

When you set `includeSchemas: true`, each item also carries its inlined `parameters[]`,
`requestBody` schema, and `responses` (200 and error shapes), with every `$ref` resolved. The
model sees a complete, self-contained shape in a single hop.

## The `execute` tool

Use `execute` to make an authenticated call against an endpoint you discovered.

**Input**

```json
{
  "method": "POST",
  "path": "/v2/thing/thing_01ABC/data",
  "query": { "limit": "50" },
  "body": { "frames": [] },
  "fields": ["thingId", "framesIngested"]
}
```

| Field | Required | Purpose |
|-------|----------|---------|
| `method` | Yes | One of `GET`, `POST`, `PUT`, `PATCH`, or `DELETE`. |
| `path` | Yes | Must start with the `/v2` path prefix. |
| `query` | No | Appended as the query string. |
| `body` | No | Sent as the `application/json` request body. |
| `fields` | No | Projects the response down to these keys to keep context small. |

**Output**

```json
{
  "status": 200,
  "ok": true,
  "body": {
    "thingId": "thing_01ABC",
    "framesIngested": 1,
    "metrics": ["humidity", "temperature"]
  },
  "truncated": false
}
```

The server builds the request, sets `X-Api-Key` from its own configuration, sends it, and
parses the JSON (or text) response. If you pass `fields`, it keeps only those top-level keys,
applying the projection across array elements too. A response that exceeds the byte cap comes
back with `truncated: true`.

HTTP errors are returned as-is. A `400` with a [ProblemDetails](/platform/api-fundamentals#errors)
body, for example, flows straight back to the model as its self-correction signal, so it can
retry with a fixed request. Transport-level failures return `{ "status": 0, "ok": false, "error": "..." }`.

## How an agent uses it

A typical loop is a fixed rhythm. Every hop is a call into one of the two tools; nothing is
compiled or evaluated at runtime.

<Steps>
  <Step title="Handshake">
    The model sees only the two tool descriptions. The spec and the key stay in the host.
  </Step>
  <Step title="Discover">
    `search("ingest thing data", method: "POST")` surfaces the matching operations. The rest
    stay hidden.
  </Step>
  <Step title="Inspect">
    `search(path, includeSchemas: true)` brings the one operation's request and response shape
    into context.
  </Step>
  <Step title="Execute">
    `execute(POST /v2/thing/{id}/data, body)` injects the `X-Api-Key`, calls the API, and
    returns `{ status, ok, body }`.
  </Step>
  <Step title="Answer">
    The model answers from the structured result.
  </Step>
</Steps>

To compose two endpoints, the agent makes two `execute` calls across turns. On an error, the
returned error body (such as a `400` listing the valid metrics) flows back to the model, which
retries `execute`. No special handling is needed.

## Security

Because the agent drives the server with data, not code, there is no code to sandbox. The
guardrails are structural:

- **Path prefix and method allow-list.** Calls are confined to `/v2/` paths and the five
  allowed HTTP methods, which blocks requests to other host paths.
- **Host-held key.** The `X-Api-Key` is held by the server. Client-supplied headers cannot
  override it.
- **Response cap and timeout.** Responses are size-capped, requests time out, and automatic
  redirects are disabled so there are no cross-host hops.
- **Scope-bound authorization.** Effective access is still governed by the API key's own
  resource scopes. See [Authenticating with API keys](/platform/api-keys) and
  [Resource filters](/platform/resource-filters).

## Connect an MCP client

The server runs as a local process and speaks MCP over **stdio**. Configure your MCP client to
launch it and pass the platform base URL and your API key through the environment.

<Note>Only the **stdio** transport is supported currently.</Note>

```json
{
  "mcpServers": {
    "aerovy-platform": {
      "command": "Spectra.EnterpriseService.Mcp",
      "env": {
        "EnterpriseApi__BaseUrl": "https://spectra.dev.aerovy.com",
        "EnterpriseApi__ApiKey": "avyex..."
      }
    }
  }
}
```

| Setting | Purpose |
|---------|---------|
| `EnterpriseApi__BaseUrl` | The platform [base URL](/platform/api-fundamentals#base-urls-and-environments) for your environment. |
| `EnterpriseApi__ApiKey` | The `X-Api-Key` the server injects on every call. |
| `EnterpriseApi__PathPrefix` | The allowed path prefix. Defaults to `/v2`. |

You can also tune the limits the server enforces:

| Setting | Purpose |
|---------|---------|
| `Limits__MaxSearchResults` | Default cap on `search` results. |
| `Limits__MaxResponseBytes` | Byte cap that triggers `truncated: true`. |
| `Limits__HttpTimeoutSeconds` | Per-request HTTP timeout. |

The server logs to **stderr**, because **stdout** carries the MCP protocol channel.

## Spec source

The server loads a bundled snapshot of the v2 OpenAPI document at startup, so it works offline
and against any environment. To fetch a fresh spec at startup instead, set the optional
`Spec__Url` override; the server falls back to the bundled snapshot if the URL is unavailable.

## Related docs

- [API fundamentals](/platform/api-fundamentals) covers auth, base URLs, versioning, and errors.
- [Authenticating with API keys](/platform/api-keys) explains what a key controls.
- [Ingesting data](/platform/ingesting-data) walks the v2 definition and telemetry endpoints.
