Pylon
Pylon connects an agent to Pylon’s hosted MCP server so it can search, read, create, and update support issues, look up and update accounts, and look up contacts as the signed-in user.
While Pydantic AI Harness is on 0.x releases, the API may change between minor releases; when it does, deprecation warnings and release-note migration guidance tell you (or your agent) exactly how to upgrade. See the version policy.
You need a Pylon Member or Admin seat with the MCP Access role; Viewer and Integration users cannot sign in. Pylon MCP takes an OAuth access token. Pylon REST API tokens do not work with it.
Your application gets one by running the standard MCP authorization flow: the server names its authorization server and supports dynamic client registration, so any MCP OAuth client library can sign the user in. Store the token it returns and pass it as auth or PYLON_ACCESS_TOKEN.
pip install "pydantic-ai-harness[pylon]" "pydantic-ai-slim[openai]"
uv add "pydantic-ai-harness[pylon]" "pydantic-ai-slim[openai]"
The second package installs the OpenAI provider the example uses. For another model, install that provider’s extra instead.
from pydantic_ai import Agent
from pydantic_ai_harness import Pylon
agent = Agent('openai:gpt-6.1-sol', capabilities=[Pylon()])
result = agent.run_sync('Summarize my open Pylon issues')
print(result.output)
Set PYLON_ACCESS_TOKEN to a Pylon access token, or pass auth= a token. On your own machine, auth='oauth' signs you in through the browser instead. To serve several users from one agent, pass a function instead (see Per-user credentials). The agent sees the same issues, accounts, and contacts that user sees in the Pylon dashboard.
auth decides which Pylon account each run uses:
auth | Account used |
|---|---|
Not set, None, or '' | PYLON_ACCESS_TOKEN. If that is not set either, creating the agent raises an error. |
| An access token | That token, for every run. |
'oauth' | The account you sign in to through the browser. This only works on your own machine. |
| A function | Called at the start of each run, and can be async. The token it returns is used for that run. If it returns None or '', that run has no Pylon tools. A function never uses PYLON_ACCESS_TOKEN, and must not return 'oauth'. |
A fixed token or PYLON_ACCESS_TOKEN suits a script or an agent on your own machine, where every run is the same account.
In an app where each user connects their own Pylon account, one agent serves all of them, so the token cannot be fixed when the agent is created. Pass a function that reads the current user’s token from the run’s deps:
from dataclasses import dataclass
from pydantic_ai import Agent, RunContext
from pydantic_ai_harness import Pylon
@dataclass
class Deps:
pylon_token: str | None
def pylon_token(ctx: RunContext[Deps]) -> str | None:
return ctx.deps.pylon_token
agent = Agent('openai:gpt-6.1-sol', deps_type=Deps, capabilities=[Pylon(auth=pylon_token)])
Each run connects as its own user, so concurrent runs never share an account.
Your app gets each user’s token, stores it, and refreshes it. For example, a “Connect Pylon” button that runs the OAuth flow from Before you start and saves the token to their account. Before each run, put it in the deps, or load it in an async auth function.
With durable execution such as Temporal, read the token from the run’s deps rather than from a global, since the function may run in another process. The capability’s id defaults to pylon, so defer_loading=True works without one. To add more than one Pylon to an agent, give each a distinct id and tool_prefix; two that share an id but differ raise an error.
The agent sees each of the server’s tools under the pylon_ prefix, as in pylon_<tool>, so they don’t collide with other tools on the agent. Set tool_prefix to use a different prefix, or tool_prefix=None to keep the names the server gives them.
read_only selects tools by what the server marks as read-only, not by name, so the prefix does not change what it keeps. A toolset wrapper on capability.get_toolset(), such as filtered(), sees the names the agent sees, prefix included.
Pylon used to give the agent the server’s tool names unprefixed. They now reach the agent as pylon_<tool>. Code that refers to them by name, such as a filtered() wrapper, an approval rule, or a ToolGuardrail(tools=[...]), needs the new names. A rule that no longer matches stops applying, so an approval rule or guardrail that named an old tool lets its calls through without approval or checks until you update it. Pass tool_prefix=None to keep the old names. If you wrapped Pylon in PrefixTools to avoid collisions, you can drop the wrapper and set tool_prefix instead; keeping it adds both prefixes.
- Durable runs in flight. A Temporal or DBOS run replays the model responses it recorded, and their tool calls use the old names, so a run started before the change that calls a
Pylontool after it no longer replays. Activity and step names come from the capability’sid, which is unchanged. Let those runs finish, keeptool_prefix=Noneuntil they have, or deploy the change with Temporal worker versioning or a new DBOS application version.
read_only=True gives the agent only the tools that Pylon’s server labels as read-only, and leaves out all the others. If Pylon has not labeled its read tools, the agent gets no Pylon tools and emits an MCPReadOnlyNoToolsWarning. The token’s permissions still decide what the agent can reach.
To filter tools or require approval in your application, wrap the toolset with the existing toolset wrappers. For example, this asks for approval before every tool call, which suits tools that create or update issues and accounts:
from pydantic_ai import Agent
from pydantic_ai.messages import DeferredToolRequests
from pydantic_ai_harness import Pylon
capability = Pylon()
agent = Agent(
'openai:gpt-6.1-sol',
toolsets=[capability.get_toolset().approval_required()],
output_type=[str, DeferredToolRequests],
)
Handle the approval requests with the deferred tools workflow. To cap the size of tool output, add Tool Output Limits.
Pass client when you need control of the connection itself: your own FastMCP client or transport, for example one with a proxy or MCP handlers. The client then owns the URL and authentication, so passing client together with auth raises an error. read_only still applies. include_instructions=False stops the server’s own instructions from reaching the agent.
A client is one connection shared by every run; see Per-user credentials to connect each user separately.
Pylon emits no spans of its own. Core’s instrumentation already records each Pylon tool call as a tool span.
Loading a YAML file also needs the spec extra:
pip install "pydantic-ai-slim[spec]"
uv add "pydantic-ai-slim[spec]"
# agent.yaml
model: openai:gpt-6.1-sol
capabilities:
- Pylon: {}
from pydantic_ai import Agent
from pydantic_ai_harness import Pylon
agent = Agent.from_file('agent.yaml', custom_capability_types=[Pylon])
Bases: AbstractCapability[AgentDepsT]
Let an agent manage support issues, look up and update accounts, and look up contacts in Pylon.
Set PYLON_ACCESS_TOKEN or pass a Pylon access token as auth. The agent then acts as that user and
sees the same data they see in the Pylon dashboard.
from pydantic_ai import Agent
from pydantic_ai_harness import Pylon
agent = Agent('openai:gpt-5.6-sol', capabilities=[Pylon()])
Stable capability and toolset ID, so defer_loading=True needs none.
One Pylon is one connection to one account. Two sharing this id are
one connection stated twice when they agree, and an error when they differ; give each its own id to keep both.
Routing description used when the capability is loaded on demand.
Type: str | None Default: _DEFAULT_DESCRIPTION
Prepended with _ to the server’s tool names, as in pylon_<tool>, so they don’t collide with other tools.
Give two instances distinct prefixes to use both. None keeps the names the server gives its tools.
Type: str | None Default: 'pylon'
A Pylon access token, 'oauth' to sign in through the browser locally, or a sync or async function
of the run context that returns a token.
Unset, it uses PYLON_ACCESS_TOKEN. A function never does: if it returns None or '', that run has no Pylon tools.
Type: str | Callable[[RunContext[AgentDepsT]], str | None | Awaitable[str | None]] | None Default: field(default=None, repr=False)
Give the agent only the tools the server labels as read-only. A tool without that label is left out.
Type: bool Default: False
Pass the server’s own instructions to the agent.
Type: bool Default: True
Your own MCP client or transport, for full control of the connection. It cannot be combined with auth.
Type: MCPToolsetClient | None Default: field(default=None, repr=False)
@classmethod
def combine(
cls,
capabilities: Sequence[AbstractCapability[AgentDepsT]],
) -> AbstractCapability[AgentDepsT]
Two under one id are one connection stated twice; two that disagree raise rather than merge.
AbstractCapability[AgentDepsT]
def get_toolset() -> AbstractToolset[AgentDepsT]
Return the Pylon MCP tools.
AbstractToolset[AgentDepsT]