Skip to content

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: true needs the tool_search built-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 naming tool_search.
  • defer_loading cannot be combined with cache_control on 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 in tests/fixtures/remote_mcp_server.py)