Recipe: Remote MCP Servers¶
Goal¶
Connect to remote MCP servers, either through provider-native support or by opening the session from this process.
Two routes¶
Provider-native ({"type": "mcp", ...}) |
Client-side ({"type": "mcp_remote_client", ...}) |
|
|---|---|---|
| Who connects | The provider | This process |
| Providers | Anthropic, OpenAI, Fireworks | All four, including Google |
| Execution path | Provider-native remote MCP | Function calling (works with output_schema) |
Visible to hooks / tools_used |
No | Yes |
| Auth | Forwarded to the provider | Attached to the initialize handshake (header, bearer token, or httpx.Auth) |
| Tool results in your prompt | No | Yes (they count toward input tokens) |
Both are supported and neither replaces the other; pick per server.
Example (provider-native)¶
from doc_processing_toolkit import Agent
agent = Agent(
model="gpt-4o",
instructions="Use remote documentation tools.",
tools=[
{
"type": "mcp",
"server_label": "deepwiki",
"server_url": "https://mcp.deepwiki.com/mcp",
"require_approval": "never",
}
],
)
result = await agent.run("Use available tools to find MCP protocol transport options.")
print(result.output)
Authenticated server example:
agent = Agent(
model="claude-sonnet-4-5",
instructions="Use authenticated remote MCP tools.",
tools=[
{
"type": "mcp",
"server_label": "private-server",
"server_url": "https://mcp.example.com/mcp",
"authorization_token": "${TOKEN}",
}
],
)
OpenAI Connector-style remote config example:
agent = Agent(
model="gpt-4o",
instructions="Use connector-backed remote MCP tools.",
tools=[
{
"type": "mcp",
"server_label": "calendar-connector",
"connector_id": "connector_googlecalendar",
"authorization": "${OPENAI_CONNECTOR_TOKEN}",
"require_approval": "never",
}
],
)
Fireworks example:
agent = Agent(
model="accounts/fireworks/models/kimi-k3",
instructions="Use remote documentation tools.",
tools=[
{
"type": "mcp",
"server_label": "deepwiki",
"server_url": "https://mcp.deepwiki.com/mcp",
}
],
)
How the Fireworks path is wired
Fireworks routes remote MCP through the Fireworks Response API. Since fireworks-ai >= 1.x
ships no binding for that surface, the toolkit drives it with the OpenAI SDK pointed at
https://api.fireworks.ai/inference/v1, which is OpenAI-compatible. The openai package and
FIREWORKS_API_KEY are both required. Entries default to "type": "mcp"; set "type": "sse"
explicitly if a server needs it. server_url and url are both accepted as the URL key, and
each entry forwards its type, URL, and label — authenticated Fireworks MCP servers are not
currently covered by this path.
Exposing only some of a server's tools¶
allowed_tools names the tools to expose. On the provider-native route each provider handles it
differently (0.5.1):
| Provider | What happens |
|---|---|
| OpenAI | Forwarded as OpenAI's own allowed_tools: a list of names (an empty list exposes none), or OpenAI's filter object {"tool_names": [...], "read_only": bool}, sent as given. Any other value raises TypeError at construction |
| Anthropic | Translated into the toolset's allow-list: default_config: {"enabled": false}, and configs: {name: {"enabled": true}} for each listed tool. A default_config / configs you give are kept; allowed_tools decides enabled, so a tool it does not list stays disabled even if configs enabled it |
| Fireworks | Refused at construction with a ValueError (and on a direct provider call): Fireworks accepts the field and still exposes every tool (measured 2026-09-27 on kimi-k3). Use the client-side route below, which filters on every provider |
agent = Agent(
model="claude-sonnet-5",
tools=[
{
"type": "mcp",
"server_label": "deepwiki",
"server_url": "https://mcp.deepwiki.com/mcp",
"allowed_tools": ["read_wiki_structure"],
}
],
)
Checked live on DeepWiki (2026-09-28): asked to list its tools, claude-haiku-4-5 named all three
without the key and only deepwiki_read_wiki_structure with it; gpt-5.6-luna likewise. On
Anthropic an empty list exposes no tools, and a name the server does not offer is ignored without an
error or a warning: a provider-run server's tool list never reaches the toolkit, so it cannot check
the names the way it does for the servers it runs.
Deferring a remote toolset¶
On Anthropic an entry's default_config and configs are forwarded as given, with two exceptions
the API would otherwise reject outright:
defer_loading: trueneeds thetool_searchbuilt-in tool enabled on the same request. Without it the deferred tools can never be discovered and the request is a 400, so the flag is stripped and the tools load eagerly, with one warning namingtool_search.defer_loadingcannot be combined withcache_controlon the same toolset. The breakpoint is dropped with a warning — a deferred toolset is excluded from the cached prefix anyway, so the breakpoint would have cached nothing.
Your own dicts are never mutated; the corrected entry is a copy. See Tool search and deferred tools for the client-tool half of the same feature.
Client-side connections (tool format 9)¶
Swap "type": "mcp" for "type": "mcp_remote_client" and the toolkit opens the session itself over
Streamable HTTP (or SSE), then executes the remote tools inside its own function-calling loop:
from doc_processing_toolkit import Agent
agent = Agent(
model="gemini-3.5-flash-lite", # works on every provider, not just the three above
instructions="Use the remote documentation tools.",
tools=[
{
"type": "mcp_remote_client",
"url": "https://mcp.deepwiki.com/mcp",
"name": "deepwiki",
# "authorization_token": "${TOKEN}", # -> Authorization: Bearer ...
# "headers": {"X-Api-Key": "..."}, # sent on every request
# "auth": oauth_provider, # any httpx.Auth instance
# "transport": "sse", # default is streamable_http
# "allowed_tools": ["read_wiki_structure"],
# "timeout": 30, "sse_read_timeout": 300,
}
],
max_iterations=6,
)
result = agent.run_sync("What transport protocols does the 2025-03-26 MCP spec support?")
assert result.error is None and result.tools_used
print(result.output, result.cost)
allowed_tools on this route reaches the model since 0.5.1
Before 0.5.1 the key trimmed only the toolkit's own tool list, while the provider listed the
session's tools itself, so the model still saw every tool. The filter now sits on the session: with
allowed_tools: ["read_wiki_structure"] the provider loads one DeepWiki tool instead of three
(checked live on gemini-3.5-flash-lite, 2026-09-28).
Authenticated server, async, reusing one session across turns:
async with Agent(
model="claude-sonnet-4-5",
tools=[
{
"type": "mcp_remote_client",
"url": "https://mcp.example.com/mcp",
"name": "private-server",
"authorization_token": "${TOKEN}",
}
],
) as agent:
session = agent.session()
first = await session.run("List the available tools and what they do.")
second = await session.run("Now use the first one on the 'billing' page.")
The session opens on the first run and is reused for every later run and session turn on that event
loop; close() (here, leaving the async with) releases it and keeps the config, so a later run
reconnects. See Tools for the full lifecycle and
Tools for every configuration key.
Cap what the remote server can return
Client-side tool results flow through your prompt, so an unbounded result dominates cost — a
measured run pulled 131,740 input tokens ($0.26) to answer a question a sibling run answered for
$0.012. Restrict the surface with allowed_tools, bound each result with
Agent(max_tool_result_chars=...) (by default a quarter of the context window), and, when the
model sometimes needs more than the cap, let it read on with
Agent(truncated_result_read_chars=...) and the built-in read_truncated_result tool
(Letting the model read past the cut).
Both apply to mcp_remote_client and every other server the toolkit runs, not to the
provider-run connector, whose results never pass through the toolkit.
Credentials never leave the transport
authorization_token, headers, and auth are attached to the HTTP client, so the very first
initialize request carries them. They are never stored on the loaded tools, logged, or included
in error messages; a rejected connection raises
RuntimeError: Failed to connect to remote MCP server 'private-server' at <url>: HTTP 401 Unauthorized.
The url itself must be an absolute http(s) URL without embedded credentials — a
https://user:secret@host URL is rejected with a ValueError, and every message or debug log
quotes a redacted scheme://host[:port]/path with no userinfo, query, or fragment.
What to validate¶
- remote MCP config is accepted
- tool-assisted answer is returned
- approval/auth settings are configured correctly for target provider
- for format 9: the remote tool name appears in
result.tools_used, and a second run on the same Agent reuses the session instead of listing the tools twice
Test sources¶
tests/e2e/test_tool_formats.py(test_format_6_remote_mcp_url,test_format_6_remote_mcp_with_auth)tests/integration/test_anthropic_function_calling.py(test_with_remote_mcp_deepwiki)tests/integration/test_combination_openai.py(test_remote_mcp_with_response_schema)tests/unit/test_remote_mcp_client.py(client-side format 9 against the in-process auth-gated server intests/fixtures/remote_mcp_server.py)