MCP servers (outbound)¶
This page covers an Agent connecting out to a third-party MCP (Model Context Protocol) server. An admin gives an agent a URL for a server you already have, AiFlow syncs it, and every tool that server exposes becomes available to the agent.
This is the opposite direction from AiFlow's own MCP server
MCP server documents the other direction: an
external MCP client (Claude Desktop, another agent framework)
calling into AiFlow, using AiFlow's own three tools
(place_outbound_call, search_knowledge_base,
fetch_call_transcript). This page is about an AiFlow Agent acting
as the MCP client, reaching out to a server someone else runs.
The two are unrelated to each other: connecting an agent to an
external server here has no effect on what AiFlow exposes at
/api/v1/mcp/, and vice versa.
Admin role required
GET /api/v1/agents/{agent_id}/mcp-servers and
GET /api/v1/mcp-servers/catalog need any logged-in admin (Owner,
Admin, Editor, or Viewer). POST, PATCH, DELETE, and the sync
action all need Owner or Admin.
Transport: Streamable HTTP only¶
AiFlow speaks only the Streamable HTTP transport; there is no stdio or local-process server support. A stdio server runs as a child process on whatever machine starts it, and supporting that here would mean AiFlow executing arbitrary code on your behalf, which is explicitly out of scope. If you're building your own server to connect an agent to, rather than pointing at an existing public one, see the official MCP documentation for the protocol spec, specifically its Streamable HTTP transport.
Field reference¶
| Field | Type | Required on create | Default | Description |
|---|---|---|---|---|
name |
string | Yes | None | 1 to 120 characters. A label for this connection, shown in the admin dashboard. |
url |
string | Yes | None | 1 to 500 characters. The remote MCP server's Streamable HTTP endpoint. |
enabled |
boolean | No | true |
Whether the agent can use this connection's tools right now. |
auth_type |
string | No | "static_header" |
"static_header" or "oauth". See OAuth below for the second mode. |
disabled_tools |
array of strings or null |
No | null |
Names of this connection's own discovered_tools to exclude from the agent's session, for a server that exposes more tools than are actually wanted. On PATCH, omitting this field leaves it unchanged; sending [] re-enables everything. A newly discovered tool after a re-sync is enabled by default, not hidden. |
auth_header_value |
string or null |
No | null |
Write-only, only meaningful when auth_type is "static_header". If set, sent as the value of an Authorization header on every call to the remote server. Encrypted at rest; never returned by any read endpoint, only has_auth_header (a boolean) tells you whether one is set. On PATCH, omitting this field (or sending null) leaves the stored value unchanged; sending an empty string "" clears it. |
Response shape¶
GET/POST/PATCH on a connection all return this shape:
{
"id": 1,
"agent_id": 1,
"name": "DeepWiki",
"url": "https://mcp.deepwiki.com/mcp",
"enabled": true,
"auth_type": "static_header",
"has_auth_header": false,
"oauth_connected": false,
"oauth_last_error": null,
"last_synced_at": "2026-01-15T10:31:00Z",
"last_sync_error": null,
"discovered_tools": [
{
"name": "ask_question",
"description": "Ask a question about a public GitHub repository."
}
],
"disabled_tools": []
}
discovered_tools is null until the connection has been synced at least
once (see below). last_sync_error holds the most recent sync failure's
message, or null if the last sync succeeded or none has run yet.
disabled_tools is null until an admin has excluded at least one
discovered tool by name via PATCH (see below); in the admin dashboard,
each discovered-tool chip on a synced connection toggles this directly.
oauth_connected and oauth_last_error are only meaningful when
auth_type is "oauth", see below.
Create a connection¶
Capped by your licence
This deployment's licence sets an mcp_connections limit. Creating one more
past that cap returns 402 rather than succeeding; see
Licence limits.
const response = await fetch(
"https://api.your-domain.com/api/v1/agents/1/mcp-servers",
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${adminToken}`,
},
body: JSON.stringify({
name: "DeepWiki",
url: "https://mcp.deepwiki.com/mcp",
}),
},
);
const connection = await response.json();
Returns 201 Created with the shape shown above. discovered_tools starts
null: no tools are actually available to the agent yet, since a sync is
a separate, explicit step (next).
For a server that requires authentication, add auth_header_value:
{
"name": "Orders API",
"url": "https://mcp.orders.example.com/mcp",
"auth_header_value": "Bearer sk_live_xxx"
}
Sync a connection¶
Calls tools/list on the remote server and caches the result. AiFlow
doesn't hold a long-lived connection to the remote server open between
sessions: every session an agent has re-reads whatever was cached at the
last sync. It doesn't sync automatically or on a schedule, so re-run this
whenever the remote server's own tool set changes.
A successful sync returns 200 OK with the connection's discovered tool
count:
A sync that fails to reach or read from the remote server still returns
200 OK (not a 4xx or 5xx), with synced: false and a message in
error. The connection's own enabled state and previously cached
discovered_tools are left untouched:
| Field | Type | Description |
|---|---|---|
synced |
boolean | Whether this sync attempt succeeded. |
tool_count |
integer | Number of tools discovered on a successful sync; 0 on failure. |
error |
string or null |
The failure message, or null on success. |
OAuth (any spec-compliant server, no AiFlow code)¶
The alternative to auth_header_value: instead of an admin pasting in a
static token, an agent 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. No provider-specific AiFlow code is
written for this the way the Salesforce and HubSpot connectors in
CRM connectors need, since dynamic client
registration means AiFlow never needs a provider's own client ID or
secret configured up front: it registers itself with whichever server
it's pointed at, the first time an admin connects.
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/agents/{agent_id}/mcp-servers
{"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-servers reflects the connection's new
oauth_connected (true or false) and oauth_last_error (set on
failure). Access tokens refresh automatically, ahead of expiry, on the
next call that needs one. A refresh failure, such as a revoked grant, is
recorded in oauth_last_error and surfaced the same way a sync failure
is; reconnecting (re-running the same authorize-url flow) is the
recovery path.
A 422 from the authorize-url endpoint means the target server doesn't
support the OAuth discovery this depends on: no
.well-known/oauth-authorization-server metadata reachable, or no
registration_endpoint in it. That server needs a pre-registered,
provider-specific client instead, which is exactly what
auth_header_value (a manually obtained static token) or a future
first-class connector would cover, not this generic path.
The catalog¶
A curated list of public MCP servers for popular corporate tools (Notion,
Linear, Stripe, Cloudflare, and more), each verified end to end: actually
running discovery, and for an oauth entry dynamic client registration
(DCR), against the real server, not just read off a vendor's
documentation page. It's meant as a one-click starting point in the admin
dashboard's connection form, browsable as a searchable gallery grouped by
category, not an exhaustive directory of every public MCP server
available. The gallery also ends in a "Don't see what you're looking
for?" callout linking out to MeridFlow's own contact page, for a server
not yet in the catalog.
Every entry defaults to oauth except DeepWiki, which needs no auth at
all. Some hosted servers cannot complete dynamic registration at all.
They may publish no registration_endpoint, enforce a closed allowlist of
pre-approved clients whatever the request says, or accept only a loopback
redirect URI.
None of those appear in the catalog as oauth. If a real request confirms
the server takes a plain bearer token, it is listed as static_header
instead, which is how GitHub, PagerDuty, Perplexity, and Tavily are
listed today. Otherwise it is left out, rather than shipped as a button that
can never connect.
A closed allowlist cannot be worked around generically. Those vendors support OAuth only for clients they have reviewed and approved in advance, the status Claude and Cursor hold with them, never for a caller arriving cold. See Orchestrators: OAuth.
Every entry is also a real, single, vendor-hosted URL that works the same for every admin. A server that only exists per-tenant, needing a placeholder segment in its URL edited before it works, isn't included either.
| Field | Type | Notes |
|---|---|---|
name |
string | A human-readable display name. |
url |
string | The server's Streamable HTTP endpoint, prefilled into url on Create a connection if chosen. |
description |
string | A short summary of what the server exposes. |
auth_type |
string enum | static_header or oauth, prefilled into auth_type so the connection form starts in the right mode for that server. |
logo |
string | A lookup key into the admin UI's own icon set, not binary image data or an external URL. |
category |
string | Groups entries in the admin UI's catalog gallery (Development, Project management, Communication, Sales & payments, Operations, Web search & research, Knowledge). |
[
{
"name": "DeepWiki",
"url": "https://mcp.deepwiki.com/mcp",
"description": "Ask questions about any public GitHub repository's structure and docs.",
"auth_type": "static_header",
"logo": "deepwiki",
"category": "Knowledge"
},
{
"name": "GitHub",
"url": "https://api.githubcopilot.com/mcp/",
"description": "Repositories, issues, pull requests, and code search. Doesn't support dynamic client registration; paste a personal access token as 'Bearer <token>' instead of connecting via OAuth.",
"auth_type": "static_header",
"logo": "github",
"category": "Development"
},
{
"name": "Notion",
"url": "https://mcp.notion.com/mcp",
"description": "Pages, databases, comments, and workspace search.",
"auth_type": "oauth",
"logo": "notion",
"category": "Project management"
}
]
Each entry is just name, url, and description: a starting point you
still need to create a real connection from via POST above. This
endpoint itself creates nothing.
List an agent's connections¶
Update a connection¶
name, url, enabled, disabled_tools, and auth_header_value are
all optional. Send only what you're changing, and remember the
empty-string-clears-the-credential behavior on auth_header_value
described in the field reference above.
To exclude specific tools from a server exposing more than an agent
needs, send disabled_tools with the discovered tool names to hide:
curl -X PATCH https://api.your-domain.com/api/v1/agents/1/mcp-servers/1 \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-d '{"disabled_tools": ["delete_channel", "post_message"]}'
Delete a connection¶
Returns 204 No Content. This is a hard delete with no soft-disable path.
Use PATCH {"enabled": false} instead if you want to temporarily stop the
agent from using it without losing the connection's configuration.
Next¶
- MCP server: the inbound direction, AiFlow acting as an MCP server for an external client.
- Tools: the built-in catalog every agent starts with.
- Custom tools: the other way to extend an agent without an existing MCP server to point at.