---
title: "MCP security and limits"
description: "Review credential revocation, request parsing and unsupported features."
canonical_url: "https://docs.oz0.ai/build/apps/mcp-server/security-and-limits"
---
# MCP security and limits

> Review credential revocation, request parsing and unsupported features.

For: application developers.

Review credential revocation, request parsing and unsupported features.

## Keys, revocation and loops

- A scoped key ([`oz0 apikey create --scope agent:<name>`](https://docs.oz0.ai/build/apps/runtime-api/authentication#keys-scoped-to-agents-and-flows)) lists and calls only the agents and flows its scopes name; any other is an unknown tool, and a task or conversation of another agent is not found, with the answer an ID that does not exist gets.
- When access ends, a waiting call ends. A `tools/call` that waits for its job, with or without an event stream, and a `subscriptions/listen` stream end at once when the key is revoked, or the person loses the tenant, with a `permission_denied` error that names the job. The answer's end does not stop the job, but the revocation does when the job ran on that access: a revoked key's or person's jobs stop, and the revocation's [receipt](https://docs.oz0.ai/govern/kill-switches#receipts) lists them.
- One answer at a time. A `tasks/update`, or a retry with `inputResponses`, that answers a task while another answer to it is being handled is refused; read the task and answer what it still asks. A question or an approval whose deadline has passed is no longer asked: the job denies it then.

### Delegation loops

A call may name the job it comes from in `_meta["ai.oz0/origin-job"]`, as a relay or an agent that calls this server from a job should. A call whose agent is already on that job's chain (its job tree, and the jobs earlier calls came from, at most eight hops) is a tool error, and so is a chain deeper than eight. Only calls that carry the marker are checked.

The node runtime sends it on a plugin tool's call to a remote MCP server (`url:`) at one of this cluster's own addresses, which the edge tells the nodes (see [calls back into this cluster](https://docs.oz0.ai/build/apps/remote-agents/a2a#calls-back-into-this-cluster)), so a tool that points back at this cluster's `/mcp/<tenant>` cannot loop an agent into itself; it never sends a job's ID to any other server. A loop through an address the cluster does not know as its own, such as a proxy you did not list with `--public-origin`, is bounded by the tenant's [budget and quotas](https://docs.oz0.ai/spend/budgets-and-quotas) only.

Which clients were run against this server, and what each did: the [interop matrix](https://docs.oz0.ai/build/apps/interop).

## The AI label

Each result, task and elicitation carries the tenant's [AI label](https://docs.oz0.ai/govern/ai-labels) in `_meta["ai.oz0/ai-label"]`, such as `"AI agent"`, and an elicitation's message, which the client shows a person, starts with `[AI agent]`. MCP has no field of its own for who wrote a result, and the result's text stays the agent's: a client that shows the answer to a person shows the label with it. With the label turned off, none of these carry it.

## Requests with one reading

Every request needs a caller first, before its body is read: a request without credentials, a notification included, gets 401 with `WWW-Authenticate: Bearer` and the tenant's `resource_metadata`. A body that two JSON parsers could read differently is refused with `-32700` and HTTP 400: one that starts with a byte order mark, holds bytes that are not UTF-8 or a lone surrogate escape (`\ud800`), names a member twice or twice but for case (`"name"` and `"Name"`), or writes one of JSON-RPC's members in another case (`"Method"`).

These are the classes of ToolHive's and Docker MCP Gateway's advisories (such as GHSA-hxx3-f8cg-r974 and GHSA-9v4w-3mqh-6vmm); the operators' endpoint on the management port takes requests by the same rules.

## What it does not do

- No task notifications: poll `tasks/get` at `pollIntervalMs`.
- No resources or prompts, and no `Mcp-Session-Id` for older clients.
- No OpenID Connect for MCP clients (no ID tokens), no client secrets or confidential clients, no implicit or password grants, and no `client_credentials`: an app or a script uses an API key.

Every call is checked, started and audited as `StartJob` does it: an input that does not fit the schema, a kill switch or a spent quota comes back as a tool error the model can read. A refusal for spend (a quota at the start, or a budget, quota or tree limit that ended the job) also carries its numbers for the client: the tool error's `_meta` holds `ai.oz0/refusal`, `{"type", "message", "oz0"}` (see [Spend refusals](https://docs.oz0.ai/reference/spend-refusals)). For a tool without an `outputSchema`, the same object is the tool error's `structuredContent` too, as `{"error": ...}`.

A tool with an `outputSchema` leaves `structuredContent` out of a tool error, because clients check structured content against the declared schema even when `isError` is set, and an error never fits it.


## Sitemap

See the full [sitemap](https://docs.oz0.ai/sitemap.md) for all pages.
