MCP server
Drive the Aerovy Platform API from an AI agent through two spec-driven tools.
The Aerovy Platform ships a Model Context Protocol (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
{
"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)
[
{
"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
{
"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
{
"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
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.
The model sees only the two tool descriptions. The spec and the key stay in the host.
search("ingest thing data", method: "POST") surfaces the matching operations. The rest
stay hidden.
search(path, includeSchemas: true) brings the one operation's request and response shape
into context.
execute(POST /v2/thing/{id}/data, body) injects the X-Api-Key, calls the API, and
returns { status, ok, body }.
The model answers from the structured result.
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-Keyis 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 and 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.
{
"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 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 covers auth, base URLs, versioning, and errors.
- Authenticating with API keys explains what a key controls.
- Ingesting data walks the v2 definition and telemetry endpoints.