Orchestrator MCP servers¶
An Orchestrator can connect to external MCP (Model Context Protocol) servers the same way a native Agent can, but through a completely separate set of connections: connecting an MCP server to Orchestrator 1 has no effect on what a native Agent, or any other Orchestrator, can reach, and vice versa. This page covers two distinct resources that share the same shape:
- Per-Orchestrator MCP connections: reachable only by the one Orchestrator they're attached to.
- The shared MCP connection pool: reachable automatically by every Orchestrator in the deployment, with no per-Orchestrator attach step.
Both are covered in full below. Use the per-Orchestrator resource if you only need one Orchestrator talking to one MCP server. Use the shared pool instead if you have an MCP server, say, an internal orders API, that every Orchestrator you build should be able to reach, rather than wiring up the same connection on each one individually.
The admin dashboard's + Add MCP server button on either surface opens
the same browsable, searchable catalog gallery a native Agent's MCP
servers tab uses, see MCP servers: The
catalog; picking an entry prefills the
connection form, including the right auth_type for that server.
Gated behind a deployment setting, admin session token required
See the two notes at the top of Orchestrators: every endpoint here only exists when your deployment has Orchestrators turned on, and every endpoint here authenticates with an admin session token.
Nothing here is cached
This differs from a native Agent's MCP
connections. A native Agent's MCP connection stores a synced list of
the tools it discovered; an Orchestrator's doesn't. The Test
action below (POST .../test) calls tools/list on the remote
server live, every time you call it, and returns whatever comes back
transiently. Nothing about the result is written to the connection
row. An Orchestrator's actual tool discovery, when it runs a real
delegated task, works the same way: live, on every call, never from
a stored cache.
The connection object¶
Both the per-Orchestrator and shared resources use this same shape:
| Field | Type | Notes |
|---|---|---|
id |
integer | Response only. |
orchestrator_id |
integer or null |
Response only. The owning Orchestrator's id for a per-Orchestrator connection, or null for a shared connection. This is the only field that distinguishes the two; see Per-Orchestrator vs. shared below. |
name |
string | Required on create, 1 to 120 characters. A label to recognize this connection by later. |
url |
string | Required on create, 1 to 500 characters. The MCP server's Streamable HTTP endpoint. |
enabled |
boolean | Defaults to true on create. |
auth_type |
string enum | static_header (default) or oauth. See OAuth below. |
auth_header_value |
string or null |
Write-only. Accepted on create and update, never returned in any response. Encrypted at rest. Ignored when auth_type is oauth. Same three-state PATCH semantics as an Orchestrator's remote_auth_header_value, see Remote auth headers are write-only: omit to leave unchanged, send "" to clear, send a non-empty string to replace. |
has_auth_header |
boolean | Response only. true if an auth_header_value is currently stored, false otherwise. |
oauth_connected |
boolean | Response only. true once an OAuth connection has completed a successful token exchange. |
oauth_last_error |
string or null |
Response only. Set when the most recent OAuth authorize or refresh attempt failed; null otherwise. |
Per-Orchestrator vs. shared¶
Both resources are stored the same way and return the same object shape;
the only structural difference is whether orchestrator_id is set. A
per-Orchestrator connection is reachable only through paths that include
{orchestrator_id}. A shared connection is created and managed through
paths with no {orchestrator_id} segment at all, and is available to
every Orchestrator in the deployment automatically, with no separate
"attach this connection to Orchestrator N" step anywhere in this API.
Per-Orchestrator MCP connections¶
Base path: /api/v1/orchestrators/{orchestrator_id}/mcp-connections
List¶
Requires: Viewer and above.
[
{
"id": 1,
"orchestrator_id": 1,
"name": "Orders API",
"url": "https://mcp.orders.example.com/mcp",
"enabled": true,
"auth_type": "static_header",
"has_auth_header": true,
"oauth_connected": false,
"oauth_last_error": null
}
]
A nonexistent orchestrator_id gets a 404.
Create¶
Requires: Editor and above.
| Field | Type | Required | Default |
|---|---|---|---|
name |
string | Yes | none |
url |
string | Yes | none |
enabled |
boolean | No | true |
auth_type |
string enum | No | "static_header" |
auth_header_value |
string or null |
No | null |
import httpx
response = httpx.post(
"https://api.your-domain.com/api/v1/orchestrators/1/mcp-connections",
headers={"Authorization": f"Bearer {admin_token}"},
json={
"name": "Orders API",
"url": "https://mcp.orders.example.com/mcp",
"auth_header_value": "Bearer sk_live_...",
},
)
response.raise_for_status()
connection = response.json()
const response = await fetch(
"https://api.your-domain.com/api/v1/orchestrators/1/mcp-connections",
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${adminToken}`,
},
body: JSON.stringify({
name: "Orders API",
url: "https://mcp.orders.example.com/mcp",
auth_header_value: "Bearer sk_live_...",
}),
},
);
const connection = await response.json();
Returns 201 Created with the full connection object. auth_header_value
is never echoed back; check has_auth_header to confirm it was stored. A
nonexistent orchestrator_id gets a 404.
Update¶
Requires: Editor and above.
Every field is optional; only the fields present in the request body are changed.
| Field | Type |
|---|---|
name |
string |
url |
string |
enabled |
boolean |
auth_header_value |
string or null |
Returns 200 OK with the full, updated connection object. A nonexistent
connection_id (or one belonging to a different Orchestrator) gets a
404.
Delete¶
Requires: Editor and above.
Returns 204 No Content. A nonexistent connection_id gets a 404.
Test¶
Requires: Editor and above.
Connects to the MCP server right now, calls tools/list on it, and
returns what came back. Nothing about the result is stored, as covered in
the note at the top of this page; call it again and it goes back to the
remote server again.
| Field | Type | Notes |
|---|---|---|
reachable |
boolean | Whether the connection attempt succeeded. |
tool_count |
integer | Defaults to 0. The number of tools the server reported. |
tools |
array of objects | Defaults to an empty array. Each object has name (string), description (string), and input_schema (object, the tool's JSON-Schema input definition). |
error |
string or null |
Defaults to null. Set to the underlying error's message when reachable is false. |
This endpoint itself always returns 200 OK, even when the remote server
couldn't be reached; check reachable, not the HTTP status, to know
whether the test succeeded.
A reachable server:
{
"reachable": true,
"tool_count": 2,
"tools": [
{
"name": "lookup_order",
"description": "Looks up an order by its order number.",
"input_schema": {
"type": "object",
"properties": { "order_number": { "type": "string" } },
"required": ["order_number"]
}
},
{
"name": "list_recent_orders",
"description": "Lists orders placed in the last N days.",
"input_schema": {
"type": "object",
"properties": { "days": { "type": "integer" } }
}
}
],
"error": null
}
An unreachable server:
A nonexistent orchestrator_id or connection_id gets a 404.
OAuth (any spec-compliant server, no AiFlow code)¶
The alternative to auth_header_value: instead of an admin pasting in a
static token, an Orchestrator can connect to any remote MCP server that
implements OAuth 2.1 with dynamic client registration (RFC 7591), by
having the admin click Connect and complete that server's own consent
screen in a browser. The same generic, no-provider-specific-code
mechanism a native Agent's MCP connections use, see MCP servers:
OAuth for
the full explanation.
Create the connection with "auth_type": "oauth" (no auth_header_value
needed), then fetch the authorize URL and open it in a browser:
POST /api/v1/orchestrators/{orchestrator_id}/mcp-connections
{"name": "Team Docs", "url": "https://mcp.example.com/mcp", "auth_type": "oauth"}
The server redirects back to AiFlow's own callback once the admin
approves. A small static page confirms success or reports what went
wrong, then GET .../mcp-connections reflects the connection's new
oauth_connected (true or false) and oauth_last_error (set on
failure). Access tokens refresh automatically, ahead of expiry, the next
time the Orchestrator's agent tree is built and actually calls the
server. A 422 from the authorize-url endpoint means the target server
doesn't support OAuth discovery, the same failure mode and recovery path
MCP servers: OAuth
covers.
The shared MCP connection pool¶
Base path: /api/v1/orchestrator-mcp-connections, with no
{orchestrator_id} segment anywhere. Every endpoint here mirrors its
per-Orchestrator counterpart above exactly: same fields, same request and
response shapes, same role requirements. The only differences are the
base path, and that every connection created here always has
orchestrator_id: null in its response and is immediately usable by
every Orchestrator in the deployment.
Create¶
Requires: Editor and above.
import httpx
response = httpx.post(
"https://api.your-domain.com/api/v1/orchestrator-mcp-connections",
headers={"Authorization": f"Bearer {admin_token}"},
json={
"name": "Company Directory",
"url": "https://mcp.directory.example.com/mcp",
"auth_header_value": "Bearer sk_live_...",
},
)
response.raise_for_status()
connection = response.json()
const response = await fetch(
"https://api.your-domain.com/api/v1/orchestrator-mcp-connections",
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${adminToken}`,
},
body: JSON.stringify({
name: "Company Directory",
url: "https://mcp.directory.example.com/mcp",
auth_header_value: "Bearer sk_live_...",
}),
},
);
const connection = await response.json();
{
"id": 4,
"orchestrator_id": null,
"name": "Company Directory",
"url": "https://mcp.directory.example.com/mcp",
"enabled": true,
"auth_type": "static_header",
"has_auth_header": true,
"oauth_connected": false,
"oauth_last_error": null
}
orchestrator_id is always null in the response here; there's no way to
set it to anything else through this base path. Since there's no id to
submit on create, there's no parent-existence check to fail here, unlike
the per-Orchestrator variant above.
List, update, delete, test, and OAuth¶
These behave identically to their per-Orchestrator counterparts above,
just without the {orchestrator_id} path segment; every connection
returned has orchestrator_id: null.
| Action | Method & path | Requires |
|---|---|---|
| List | GET /api/v1/orchestrator-mcp-connections |
Viewer and above |
| Update | PATCH /api/v1/orchestrator-mcp-connections/{connection_id} |
Editor and above |
| Delete | DELETE /api/v1/orchestrator-mcp-connections/{connection_id} |
Editor and above |
| Test | POST /api/v1/orchestrator-mcp-connections/{connection_id}/test |
Editor and above |
| OAuth authorize URL | GET /api/v1/orchestrator-mcp-connections/{connection_id}/oauth/authorize-url |
Editor and above |
For example, testing a shared connection:
curl -X POST https://api.your-domain.com/api/v1/orchestrator-mcp-connections/4/test \
-H "Authorization: Bearer $ADMIN_TOKEN"
returns the exact same {"reachable": ..., "tool_count": ..., "tools": [...], "error": ...}
shape documented in Test above. A connection_id that belongs to
a per-Orchestrator connection, that is, one whose orchestrator_id isn't
null, isn't reachable through this base path and gets a 404 here. The
reverse is also true: a shared connection's id isn't reachable through
/api/v1/orchestrators/{orchestrator_id}/mcp-connections/{connection_id}
either. The two id spaces overlap numerically, since they're the same
database table, but each base path only ever resolves rows matching its
own orchestrator_id IS NULL or orchestrator_id = {orchestrator_id}
condition.