---
title: "Call agents over MCP"
description: "Connect a client and run agents or flows as MCP tools."
canonical_url: "https://docs.oz0.ai/build/apps/mcp-server"
---
# Call agents over MCP

> Connect a client and run agents or flows as MCP tools.

For: application developers.

Connect an MCP client to `POST https://<edge>:7443/mcp/<tenant>` to call the tenant's agents and flows as tools. An assistant or application can list them and delegate work through that endpoint.

Each call creates a job with its quotas, budget, approvals, trace and cost.

The server speaks MCP 2026-07-28 over Streamable HTTP, with the Tasks extension for long jobs. Clients of 2025-11-25 and earlier, which start with `initialize`, can list and call tools too, without tasks.

## Connect

A client connects in one of two ways:

- As a person, with OAuth. Give the client the URL, `https://<edge>:7443/mcp/<tenant>`. It finds the tenant's authorization server by itself, sends you to the console to sign in and approve it, and then acts as you, with your own permissions, until you revoke it. This is how IDEs and desktop assistants connect: see [Connect a client with OAuth](https://docs.oz0.ai/build/apps/mcp-server/oauth#connect-a-client-with-oauth).
- As an app or a script, with a tenant [API key](https://docs.oz0.ai/build/apps/runtime-api/authentication#authentication) as the bearer token: an operator key runs agents, a reader key sees no tools.

The edge's certificate comes from the cluster's CA, so the client needs to trust `ca.crt` (or put the edge behind a certificate it already trusts, with `--public-edge-url`).

```python [client.py]
import ssl, httpx2
from mcp import Client
from mcp.client.streamable_http import streamable_http_client

http = httpx2.AsyncClient(
    headers={"Authorization": f"Bearer {key}"}, verify=ssl.create_default_context(cafile="ca.crt")
)
async with Client(streamable_http_client("https://edge.example.com:7443/mcp/acme", http_client=http)) as c:
    tools = await c.list_tools()
    result = await c.call_tool("acme-reviewer", {"repo": "acme/api", "pr": 42})
```

Every request is authenticated, `server/discover` too. Without credentials the edge answers `401` with `WWW-Authenticate: Bearer realm="orchestrator-zero", resource_metadata="https://<edge>/.well-known/oauth-protected-resource/mcp/<tenant>", scope="mcp"`, which is where an OAuth client starts.

## Tools

`tools/list` lists the agents and flows the caller may run, in name order, in pages of at most 1,000 tools: when more follow, the result has `nextCursor`, which the next `tools/list` sends as `cursor`. The cursor is opaque and valid only for this tenant's list; any other is refused with `-32602` (Invalid params), as [MCP's pagination](https://modelcontextprotocol.io/specification/2026-07-28/server/utilities/pagination) says. A tenant of up to 1,000 agents gets one page. In each tool:

- `inputSchema` is the agent's input schema from its [contract](https://docs.oz0.ai/build/parts/contracts-and-limits). An agent that takes text gets an object with one string, `input`: call it with `{"input": "..."}`. MCP's schemas are objects, so a contract schema whose root is not an object (a list, say, which a contract allows with a warning) becomes the one property `input` of an object: call it with `{"input": [...]}`.
- `outputSchema` is its output schema, when it has one. The answer then comes as `structuredContent` too, next to the JSON as text. An output schema whose root is not an object is wrapped the same way, as `result`, and `structuredContent` is `{"result": ...}`.
- Annotations say what an agent is: not read-only, possibly destructive, not idempotent, and open-world. Treat them as hints.
- An agent that a [kill switch](https://docs.oz0.ai/govern/kill-switches) stops is left out, and a reader's key gets an empty list.

The list carries the catalog's hash in `_meta["ai.oz0/catalog"]`, and `ttlMs` of two seconds, as often as edges reload their catalog. Its HTTP `ETag` is the version of the list itself, the catalog's hash and the names on it (and the page's cursor), so it changes when a plugin install changes the catalog and also when a kill switch takes a tool off the list or puts it back. A client that opens `subscriptions/listen` with `toolsListChanged` gets `notifications/tools/list_changed` whenever that version changes.

## Conversations

Every result's `_meta` names the call's conversation in `ai.oz0/conversation`: the job's own ID, or its [conversation](https://docs.oz0.ai/agents/conversations) once it is a turn of one. A `tools/call` that sends that value back in its `_meta` is the conversation's next turn, and the agent gets the earlier calls' inputs and answers.

MCP 2026-07-28 has no sessions, so the conversation travels with each call; clients of earlier revisions may send the same key. For an agent that takes no part in conversations, or a key without the content permission, the value starts a new conversation instead. A value the server never gave is a tool error.

Only the caller that started a conversation continues it: the same key, operator or person, or a person its [chat](https://docs.oz0.ai/agents/conversations) is shared with. Anyone else who sends the value back, another key of the same tenant included, gets the same tool error as for a value the server never gave, so a conversation's ID is no way into someone else's history.

## Calls that wait, and calls that are tasks

Without the Tasks extension, `tools/call` starts the job and waits for it. The answer is the agent's output as text, or `isError: true` with why the job failed. Clients that accept an event stream get one, with a comment every 15 seconds while the job runs. If the client closes the call, the job is canceled (clients of 2025-11-25 and before leave it running, as their revision says).

A server that stops or restarts while a call waits does not cancel the job: the call ends with an error that names the job, which goes on, and which you can follow with `GetJob` in the runtime API. When the job asks a person something and the client declared the `elicitation` capability, the call answers with an `InputRequiredResult`: the client shows the question, and retries the call with the answer and the `requestState` it got, which the SDKs do for you.

With the Tasks extension declared in the request's capabilities, `tools/call` answers at once with a task in `working` (every task begins there, even when the job asks a person at once), whose `taskId` is the job's ID; `tasks/get` then says where the job is:

<table>
<thead>
  <tr>
    <th>
      Task status
    </th>
    
    <th>
      The job
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        working
      </code>
    </td>
    
    <td>
      runs
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        input_required
      </code>
    </td>
    
    <td>
      waits for an <a href="https://docs.oz0.ai/agents/approvals">
        approval
      </a>
      
       or an answer to a <a href="https://docs.oz0.ai/agents/asking-people">
        question
      </a>
      
      , its own or a delegated job's; <code>
        inputRequests
      </code>
      
       asks for them. A client that calls with a tenant API key answers, here, the questions of the task it started, and approves only where the key is named (<a href="https://docs.oz0.ai/agents/approvals/configure#keys-that-decide">
        keys that decide
      </a>
      
      )
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        completed
      </code>
    </td>
    
    <td>
      ended; <code>
        result
      </code>
      
       is what a waiting call would have answered, with <code>
        isError
      </code>
      
       when the job failed
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        cancelled
      </code>
    </td>
    
    <td>
      was canceled
    </td>
  </tr>
</tbody>
</table>

```json [tasks/get]
{
  "resultType": "complete", "taskId": "job-3c9...", "status": "input_required", "pollIntervalMs": 1000,
  "inputRequests": {
    "a-7f2c": {"method": "elicitation/create", "params": {"mode": "form", "message": "Which colour should the report use?",
      "requestedSchema": {"type": "object", "properties": {"answer": {"type": "string", "enum": ["red", "blue"]}}, "required": ["answer"]}}}
  }
}
```

Each input request is keyed by the approval's ID. Answer with `tasks/update`:

- A question: `{"action": "accept", "content": {"answer": "blue"}}`. An answer that is not one of its choices is refused.
- An approval of a tool call or a hook: `{"action": "accept", "content": {"approve": true, "comment": "..."}}`, or `"approve": false` to deny it.
- `decline` or `cancel` denies either.

The decision goes through the runtime API's `DecideApproval` with the caller's rights: an operator key may decide, and a question takes the content permission. `tasks/cancel` cancels the job. A client that did not declare `elicitation` sees a job that waits as `working`, with a `statusMessage`; someone answers it in the console or with `oz0 approval`.

## Related tasks

- [MCP client OAuth](https://docs.oz0.ai/build/apps/mcp-server/oauth): Authorize an MCP client through a person's sign-in.
- [MCP security and limits](https://docs.oz0.ai/build/apps/mcp-server/security-and-limits): Review credential revocation, request parsing and unsupported features.

<legacy-sections>

- <span id="connect-a-client-with-oauth">

Section

</span>

: [connect a client with oauth](https://docs.oz0.ai/build/apps/mcp-server/oauth#connect-a-client-with-oauth)
- <span id="keys-revocation-and-loops">

Section

</span>

: [keys revocation and loops](https://docs.oz0.ai/build/apps/mcp-server/security-and-limits#keys-revocation-and-loops)
- <span id="requests-with-one-reading">

Section

</span>

: [requests with one reading](https://docs.oz0.ai/build/apps/mcp-server/security-and-limits#requests-with-one-reading)
- <span id="see-and-revoke-your-clients">

Section

</span>

: [see and revoke your clients](https://docs.oz0.ai/build/apps/mcp-server/oauth#see-and-revoke-your-clients)
- <span id="the-ai-label">

Section

</span>

: [the ai label](https://docs.oz0.ai/build/apps/mcp-server/security-and-limits#the-ai-label)
- <span id="what-it-does-not-do">

Section

</span>

: [what it does not do](https://docs.oz0.ai/build/apps/mcp-server/security-and-limits#what-it-does-not-do)
- <span id="who-may-connect">

Section

</span>

: [who may connect](https://docs.oz0.ai/build/apps/mcp-server/oauth#who-may-connect)

</legacy-sections>


## Sitemap

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