Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 125 additions & 0 deletions docs/connectors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# Connectors

Connectors package reusable tool surfaces for an [`Agent`][agents.Agent]. They do not add a
separate runtime. A connector resolves to existing SDK primitives:

- [`Tool`][agents.tool.Tool] instances, including function tools and hosted tools.
- Local MCP servers that the SDK already knows how to expose as function tools.
- Metadata and coarse policy labels that callers can use for approval, routing, or sandbox choices.

Use connectors when you want to mount a named bundle of tools or package-provided MCP servers on
one or more agents without manually copying every tool and server into each agent definition.

## SDK tool connectors

Use [`Connector.from_tools()`][agents.connectors.Connector.from_tools] when your integration is
implemented directly in Python.

```python
from agents import Agent, Connector, function_tool


@function_tool
def apply_discount(amount: float, percentage: float) -> str:
return f"discount={amount * percentage / 100:.2f}"


pricing = Connector.from_tools(
"pricing",
[apply_discount],
description="Pricing helpers implemented directly in Python.",
policy_labels={"read_only"},
)

agent = Agent(
name="Assistant",
instructions="Use pricing tools when needed.",
connectors=[pricing],
)
```

Connector tools are combined with the agent's normal `tools` list when the SDK prepares available
tools for a run.

## Package connectors

Use [`Connector.from_package()`][agents.connectors.Connector.from_package] to load a connector from a
shared package layout. The initial package bridge supports:

- `.codex-plugin/plugin.json` as the package manifest.
- `.mcp.json` for local or remote MCP server definitions referenced by `mcpServers`.
- Optional `.app.json` entries referenced by `apps` for hosted connector IDs.

For packages with local MCP servers, connect the servers before running the agent. The connector
still mounts through `Agent(connectors=[...])`; do not add the same MCP servers again through
`Agent(mcp_servers=[...])`.

```python
from agents import Agent, Connector
from agents.mcp import MCPServerManager


orders = Connector.from_package("./orders-plugin")

async with MCPServerManager(orders.mcp_servers, strict=True):
agent = Agent(
name="Assistant",
instructions="Use order tools when needed.",
connectors=[orders],
mcp_config={"include_server_in_tool_names": True},
)
```

If a package declares hosted app connectors, pass an authorization source to
[`Connector.from_package()`][agents.connectors.Connector.from_package]. The authorization can be one
token string, a mapping keyed by app name or connector ID, or a callback.

```python
calendar = Connector.from_package(
"./workspace-plugin",
authorization={"calendar": "conn_calendar_access_token"},
hosted_mcp_require_approval="always",
)
```

## Hosted connectors

Use [`Connector.from_hosted_connector()`][agents.connectors.Connector.from_hosted_connector] when
you already know the hosted connector ID and want the Responses API hosted MCP integration to call
it.

```python
import os

from agents import Agent, Connector


calendar = Connector.from_hosted_connector(
"calendar",
connector_id="connector_googlecalendar",
authorization=os.environ["GOOGLE_CALENDAR_AUTHORIZATION"],
server_label="google_calendar",
require_approval="never",
)

agent = Agent(
name="Assistant",
instructions="Use calendar tools when needed.",
connectors=[calendar],
)
```

Hosted connector tools are represented as [`HostedMCPTool`][agents.tool.HostedMCPTool] instances.
They are sent to the Responses API like other hosted tools.

## End-to-end demo

See `examples/connectors/package_demo.py` for a deterministic demo that needs no API key. It builds
a direct Python tool connector, creates a temporary plugin-style MCP package, mounts both on an
agent, invokes the discovered tools, and inspects a hosted connector config.

Run it with:

```bash
uv run --frozen python examples/connectors/package_demo.py --verify
```
5 changes: 5 additions & 0 deletions docs/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ Check out a variety of sample implementations of the SDK in the examples section
- Non-strict output types
- Previous response ID usage

- **[connectors](https://github.com/openai/openai-agents-python/tree/main/examples/connectors):**
Examples for packaging reusable connector surfaces, including:

- End-to-end connector package composition without an API key (`examples/connectors/package_demo.py`)

- **[customer_service](https://github.com/openai/openai-agents-python/tree/main/examples/customer_service):**
Example customer service system for an airline.

Expand Down
3 changes: 3 additions & 0 deletions docs/ref/connectors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# `Connectors`

::: agents.connectors
1 change: 1 addition & 0 deletions examples/connectors/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Connector examples."""
230 changes: 230 additions & 0 deletions examples/connectors/package_demo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
from __future__ import annotations

import argparse
import asyncio
import json
import sys
from pathlib import Path
from tempfile import TemporaryDirectory
from typing import Any

from agents import Agent, Connector, FunctionTool, HostedMCPTool, RunContextWrapper, function_tool
from agents.mcp import MCPServerManager
from agents.tool_context import ToolContext


@function_tool
def apply_discount(amount: float, percentage: float) -> str:
"""Calculate a discount amount."""
return f"discount={amount * percentage / 100:.2f}"


def build_sdk_tool_connector() -> Connector:
return Connector.from_tools(
"pricing",
[apply_discount],
description="Pricing tools implemented directly in Python.",
policy_labels={"read_only"},
)


def build_hosted_connector() -> Connector:
return Connector.from_hosted_connector(
"calendar",
connector_id="connector_googlecalendar",
authorization="demo_access_token",
server_label="calendar",
require_approval="never",
description="Hosted Google Calendar connector shape.",
)


def write_demo_plugin_package(package_root: Path) -> Path:
plugin_dir = package_root / "orders-plugin"
plugin_config_dir = plugin_dir / ".codex-plugin"
plugin_config_dir.mkdir(parents=True)

(plugin_config_dir / "plugin.json").write_text(
json.dumps(
{
"name": "orders",
"version": "0.1.0",
"description": "Order lookup tools packaged like a shared plugin.",
"mcpServers": "./.mcp.json",
"interface": {
"displayName": "Orders",
"capabilities": ["Read"],
},
},
indent=2,
)
)
(plugin_dir / ".mcp.json").write_text(
json.dumps(
{
"mcpServers": {
"orders": {
"command": sys.executable,
"args": ["demo_mcp_server.py"],
"cwd": ".",
}
}
},
indent=2,
)
)
(plugin_dir / "demo_mcp_server.py").write_text(
"\n".join(
[
"from mcp.server.fastmcp import FastMCP",
"",
"mcp = FastMCP('Orders connector demo')",
"",
"@mcp.tool()",
"def lookup_order(order_id: str) -> str:",
" return f'order {order_id}: fulfilled'",
"",
"if __name__ == '__main__':",
" mcp.run(transport='stdio')",
"",
]
)
)
return plugin_dir


def build_package_connector(package_root: Path) -> Connector:
return Connector.from_package(package_root)


async def verify_connector_demo() -> dict[str, Any]:
sdk_connector = build_sdk_tool_connector()
hosted_connector = build_hosted_connector()

with TemporaryDirectory(prefix="agents-connectors-demo-") as temp_dir:
package_dir = write_demo_plugin_package(Path(temp_dir))
package_connector = build_package_connector(package_dir)

async with MCPServerManager(
package_connector.mcp_servers,
strict=True,
connect_in_parallel=True,
):
agent = Agent(
name="Connector demo agent",
instructions="Use the mounted connector tools when useful.",
connectors=[
sdk_connector,
package_connector,
],
mcp_config={"include_server_in_tool_names": True},
)
tools = await agent.get_all_tools(RunContextWrapper(context=None))
tool_names = [tool.name for tool in tools]

direct_tool = _find_function_tool(tools, "apply_discount")
mcp_tool = _find_function_tool(tools, "mcp_orders__lookup_order")

direct_tool_result = await direct_tool.on_invoke_tool(
_tool_context("apply_discount", '{"amount":100,"percentage":25}'),
'{"amount":100,"percentage":25}',
)
mcp_tool_result = _tool_result_text(
await mcp_tool.on_invoke_tool(
_tool_context("mcp_orders__lookup_order", '{"order_id":"demo_order_1001"}'),
'{"order_id":"demo_order_1001"}',
)
)

hosted_tool = _find_hosted_mcp_tool(hosted_connector.tools)

return {
"agent_tool_names": tool_names,
"direct_tool_result": direct_tool_result,
"mcp_tool_result": mcp_tool_result,
"package_connector_name": package_connector.name,
"package_policy_labels": sorted(package_connector.policy_labels),
"hosted_connector_label": hosted_tool.tool_config["server_label"],
"hosted_connector_id": hosted_tool.tool_config["connector_id"],
}


def _find_function_tool(tools: list[Any], name: str) -> FunctionTool:
for tool in tools:
if isinstance(tool, FunctionTool) and tool.name == name:
return tool
raise RuntimeError(f"Expected function tool not found: {name}")


def _find_hosted_mcp_tool(tools: list[Any]) -> HostedMCPTool:
for tool in tools:
if isinstance(tool, HostedMCPTool):
return tool
raise RuntimeError("Expected hosted MCP tool not found.")


def _tool_result_text(result: Any) -> str:
if isinstance(result, str):
return result
if isinstance(result, dict):
text = result.get("text")
if isinstance(text, str):
return text
return json.dumps(result)


def _tool_context(tool_name: str, tool_arguments: str) -> ToolContext[Any]:
return ToolContext(
context=None,
tool_name=tool_name,
tool_call_id=f"call_{tool_name}",
tool_arguments=tool_arguments,
)


def print_summary(summary: dict[str, Any]) -> None:
print("Connector package demo")
print("======================")
print(f"Agent tools: {', '.join(summary['agent_tool_names'])}")
print(f"Direct tool output: {summary['direct_tool_result']}")
print(f"MCP tool output: {summary['mcp_tool_result']}")
print(
"Package connector: "
f"{summary['package_connector_name']} "
f"({', '.join(summary['package_policy_labels'])})"
)
print(
"Hosted connector config: "
f"{summary['hosted_connector_label']} -> {summary['hosted_connector_id']}"
)


async def main(*, verify: bool) -> None:
summary = await verify_connector_demo()
print_summary(summary)
if verify:
expected = {
"direct_tool_result": "discount=25.00",
"mcp_tool_result": "order demo_order_1001: fulfilled",
"hosted_connector_label": "calendar",
}
mismatches = {
key: (summary.get(key), expected_value)
for key, expected_value in expected.items()
if summary.get(key) != expected_value
}
if mismatches:
raise RuntimeError(f"Connector demo verification failed: {mismatches}")


if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Demonstrate Agents SDK connector package composition."
)
parser.add_argument(
"--verify",
action="store_true",
help="Run deterministic checks after printing the demo summary.",
)
args = parser.parse_args()
asyncio.run(main(verify=args.verify))
2 changes: 2 additions & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ plugins:
- Agent memory: sandbox/memory.md
- Models: models/index.md
- Tools: tools.md
- Connectors: connectors.md
- Guardrails: guardrails.md
- Running agents: running_agents.md
- Streaming: streaming.md
Expand Down Expand Up @@ -121,6 +122,7 @@ plugins:
- Run error handlers: ref/run_error_handlers.md
- Memory: ref/memory.md
- REPL: ref/repl.md
- Connectors: ref/connectors.md
- Tools: ref/tool.md
- Tool context: ref/tool_context.md
- Results: ref/result.md
Expand Down
Loading
Loading