Skip to content
DocsConnecting agents
MCP Gateway

Connecting agents

Nothing in the agent changes except where it connects and the key it sends. An MCP client points at an endpoint URL; an agent with many endpoints can use one URL for all of them; an agent that calls a REST API over plain HTTP keeps its code and swaps the base URL.

Agents: an MCP key

Headless agents send an MCP key from the endpoint’s workspace on every request:

HTTP
Authorization: Bearer va_…
  • Created on MCP keys; shown once. Per key: a tool allowlist, calls per minute and per day.
  • Works on the gateway only. Sent to the VectorAxis API it is refused, so a leaked agent key cannot change settings.
  • Not a virtual key (those are for LLM providers) and not a management API key: both are refused, with a message saying which key to use.
  • Calls are logged under the key’s name. Revoking a key stops it on the next request.
  • Rotate secret (the key’s action menu, or POST /v1/mcp-keys/{id}/rotate?organization= with a workspace admin’s va_ key) issues a new secret, shown once; the old one stops working on the next request. The key keeps its name, allowlist, limits and call history, so only the secret in the agents’ configuration changes.

The gateway speaks MCP over Streamable HTTP, both the 2026-07-28 protocol and the 2025 ones. It passes the client’s own version negotiation through, so a client and a server that both speak 2026-07-28 use it.

People: sign in from the MCP client

Add the endpoint URL to Claude, Cursor, VS Code or Claude Code with no key. The first time it connects, the client opens a VectorAxis sign-in and consent page; after you approve, it calls the endpoint as you, under the endpoint’s user access rules.

Bash
claude mcp add --transport http linear https://mcp.vectoraxis.ai/s/mcps-0123456789abcdef
  • The approval is for that one endpoint. Access tokens last an hour and refresh for 30 days; each refresh checks the access rules again.
  • When the connector signs in each person at the provider, connect your own account once from the endpoint’s Connect tab. Until then your calls are refused with a link to do it.
  • Revoke a client on your profile, or an admin revokes it on the endpoint’s Connections tab.
One URL for every endpoint

Instead of adding each endpoint, an agent adds https://mcp.vectoraxis.ai/s/vectoraxis with its MCP key. It offers three tools, so the agent carries three tool definitions instead of every tool’s:

ToolArgumentsDoes
discoverquery, limit?Finds tools by what they do, across the workspace’s endpoints, within the key’s allowlist. Each comes back with an id <endpoint>/<tool>, whether it is Privileged or needs approval, and its health.
inspecttoolThe tool’s full definition and arguments, if the key may call it.
runtool, argumentsCalls it through its endpoint, with every control, logged under that endpoint.
JSON
{ "name": "run",
  "arguments": { "tool": "mcps-0123456789abcdef/create_issue",
                 "arguments": { "title": "Login page times out", "teamId": "ENG" } } }

MCP keys only for now; people sign in to endpoints directly.

Plain HTTP clients: egress mode

For a REST API connector (OpenAPI or declared endpoints), an agent that already calls the API with an HTTP client or SDK keeps its code: set the client’s base URL to the endpoint’s /h/ address and send the MCP key instead of the API’s credentials. Each request is matched to one of the connector’s tools and governed exactly like a call to that tool; the API’s own response comes back.

Bash
# before: curl -H "api_key: $PETSTORE_KEY" https://petstore3.swagger.io/api/v3/pet/findByStatus?status=sold
curl -H "Authorization: Bearer $VECTORAXIS_MCP_KEY" \
  "https://mcp.vectoraxis.ai/h/mcps-0123456789abcdef/pet/findByStatus?status=sold"
Python
import os, requests

api = requests.Session()
api.headers["Authorization"] = f"Bearer {os.environ['VECTORAXIS_MCP_KEY']}"
base = "https://mcp.vectoraxis.ai/h/mcps-0123456789abcdef"

pets = api.get(f"{base}/pet/findByStatus", params={"status": "sold"}).json()
created = api.post(f"{base}/pet", json={"name": "Rex", "photoUrls": []})
  • The key. Authorization: Bearer <MCP key>, or X-VectorAxis-Key: <MCP key> when the client’s own Authorization header is set for the API. The client’s own credentials are never sent on; the connector’s are.
  • Strict matching. The method and path must match an operation of the connector (the most specific path wins), and the tool must be enabled. A query parameter or body field the operation does not declare, a body where it takes none, or a body other than JSON or form-encoded is refused before anything is sent. A query parameter the connector holds a credential for is dropped.
  • The answer is the API’s status and body (after the guardrails) with its useful headers: content type, ETag, Location, Retry-After, Link, cache and rate-limit headers; never cookies. Each response says which tool it was in X-VectorAxis-Tool.
  • The gateway’s own answers (a refusal, a request it cannot match) are JSON with an X-VectorAxis-Gateway header naming the reason:
HTTP
HTTP/2 403
x-vectoraxis-gateway: TAINTED
x-vectoraxis-tool: mcps-0123456789abcdef/addPet

{"error":{"type":"vectoraxis_gateway","code":"TAINTED","message":"Blocked by the VectorAxis MCP Gateway: Tool 'addPet' is privileged, and this caller received untrusted content ... Ask a person to approve this call at https://vectoraxis.ai/mcp-approvals?run=..., then make the same call again"}}
StatusFrom the gateway when
400, 404, 405, 415The request names no operation, takes something it does not declare, uses another method (405 lists the methods in Allow), or sends another body type.
401, 402No valid MCP key; the organization’s plan has no MCP Gateway.
403A control refused it; the code is the refusal reason. For TAINTED and APPROVAL_REQUIRED, approve and repeat the identical request once.
429Over a rate limit, with Retry-After.
424The API’s answer cannot be passed on: a redirect, a binary body other than an image, or the connector’s credentials were refused.

Limits: request bodies up to 2 MB, responses are read whole (no streaming), HEAD only where the spec declares it. An MCP server connector has no egress address; use its /s/ URL. The Connect tab of a REST endpoint shows its /h/ address.

What an agent sees when something is stopped
  • A tool call that is refused comes back as a tool result with isError: true and text the model can read and pass on to its user, plus _meta["ai.vectoraxis/gateway"] = {"blocked": true, "control": "…"} for code.
  • Other requests get a JSON-RPC error -32003 with the control in data.
  • Rate limits are HTTP 429 with Retry-After.
  • An upstream server that fails or times out comes back as HTTP 424 with the original status in X-Upstream-Status, so the error reaches the agent instead of a proxy’s error page.
Hints on results

A tool result can carry _meta["ai.vectoraxis/hints"], only when there is something to say. Agents that do not read it lose nothing.

JSON
"_meta": {
  "ai.vectoraxis/hints": {
    "untrusted_output": true,
    "tainted_until": "2026-11-04T03:57:28Z",
    "tool_health": { "status": "DEGRADED", "reason": "2 of the last 12 calls failed",
                     "failure_rate": 0.17, "p95_ms": 840, "window_minutes": 60 }
  }
}

tainted_until is when Privileged tools stop needing approval for this caller. tool_health appears only for a tool that has been failing. A REST tool’s result also carries _meta["ai.vectoraxis/http"]: the API’s status and response headers.

Finding tools, and how they are doing

Find a tool on the Endpoints page (and discover) searches the workspace’s tools by meaning, listing only tools a call could reach. New and changed tools are searchable within a minute or two of a sync. Each tool’s health is worked out from the last hour of calls:

StatusWhen
OUTAGEHalf or more of the calls failed or timed out.
DEGRADED1 call in 10 failed, or the slowest 1 call in 20 took 15 seconds or more.
HEALTHYOtherwise.
UNKNOWNFewer than 5 calls.

A failing tool is ranked below one that works and marked, e.g. “outage: 7 of the last 7 calls failed in the last 60 minutes”. Refused calls do not count, and a tool that reports an error in its own result counts as working: the call reached it.

Logs, audit and inventory
  • Logs → MCP calls: every tool call, prompt request and resource read, and every refusal, with the caller (key or person), whose upstream account was used, the outcome, guardrail findings, approval state and, when your plan keeps log bodies, the arguments and result. A server’s own requests appear under their call. Kept as long as your plan keeps logs.
  • Audit log tabs on connectors and endpoints: who changed what, synced, approved, released, connected, and every approval decision.
  • Tool inventory: every tool your agents used in a period, through the gateway or declared in LLM requests, and which ones no connector exposes (shadow tools).
Tools on the chat API (every plan)

Agents that run tools themselves and send the results back through /v1/chat/completions or /v1/messages get two things with no setup. Every tool result in a request is scanned for injected instructions before the model reads it, and each request’s declared tools, the tool calls the model made and the number of results are logged, which is what the Tool inventory is built from. The scan’s outcome is shown in Logs as the tool scan status, separate from your own guardrails.

On the chat API a tool has already run by the time its result arrives. To stop a call before it happens, route the tool through the MCP Gateway.
What the scan does: set per virtual key

Each virtual key chooses what happens when a tool result in one of its requests matches. Your agent’s code does not change: the OpenAI and Anthropic SDKs keep the same base URL, key and calls.

ActionWhat happensYour agent
MONITORThe default. The finding is recorded and the request goes through unchanged.Nothing to handle.
REDACTThe matched text in the tool result is replaced before the model reads it. The rest of the result, images and documents included, is kept, and the request goes through.Nothing to handle: an ordinary completion comes back.
BLOCKThe request is refused before it reaches the provider, so nothing is billed: a 422 with x-guardrail-status: BLOCK and x-tool-scan-status: BLOCK.Must remove or replace that tool result before sending the conversation again (below).

Set it on the key under Virtual keys (Tool results, in the create and edit dialogs), or with PUT /v1/virtual-keys/{slug}/tool-scan (a workspace admin) and read it with GET on the same path. Keys that are not on the default show a “Tool results” tag in the key list. Send {"action": null} to go back to the default; a body without action ({}) changes nothing. A change applies within 30 seconds. A request through a routing config gets the action of the target key the config sends it to: a block by that key is the same 422, and no fallback target serves it instead. Requests on direct provider credentials use the default.

Bash
curl -X PUT https://api.vectoraxis.ai/v1/virtual-keys/vk-0123456789abcdef/tool-scan   -H "Authorization: Bearer va_…" -H "Content-Type: application/json"   -d '{"action": "REDACT"}'
# {"virtual_key_slug": "vk-0123456789abcdef", "action": "REDACT",
#  "effective_action": "REDACT", "default_action": "MONITOR"}

Handling a block. The flagged tool result stays in your conversation history, and the scan checks the newest results on every request, so sending the same history again is refused again. Both SDKs raise UnprocessableEntityError (status 422) and do not retry it. Check for x-tool-scan-status to tell a rejected tool result from a rejected prompt, then replace the result and continue, or stop and tell your user. If your agent cannot do that, use REDACT: it protects the model without stopping the conversation.

Python (OpenAI SDK)
import openai

try:
    reply = client.chat.completions.create(model="gpt-4o-mini", messages=messages, tools=tools)
except openai.UnprocessableEntityError as e:
    if e.response.headers.get("x-tool-scan-status") != "BLOCK":
        raise  # your own guardrail refused the prompt
    # A tool result was rejected: withhold it and let the model carry on without it.
    for m in messages:
        if m["role"] == "tool" and m["tool_call_id"] == last_tool_call_id:
            m["content"] = "Tool output withheld: it contained instructions to the assistant."
    reply = client.chat.completions.create(model="gpt-4o-mini", messages=messages, tools=tools)
Python (Anthropic SDK)
import anthropic

try:
    reply = client.messages.create(model="claude-haiku-4-5", max_tokens=1024, messages=messages, tools=tools)
except anthropic.UnprocessableEntityError as e:
    if e.response.headers.get("x-tool-scan-status") != "BLOCK":
        raise
    # e.body is Anthropic's shape: {"type": "error", "error": {"type": "invalid_request_error", ...}}
    replace_tool_result(messages, last_tool_use_id,
                        "Tool output withheld: it contained instructions to the assistant.")
    reply = client.messages.create(model="claude-haiku-4-5", max_tokens=1024, messages=messages, tools=tools)

A streamed request is refused the same way, as an ordinary HTTP error before the stream starts.

NVIDIA Inception Program member

VectorAxis is built by GK Labs, a member of NVIDIA Inception.