Share what a PydanticAI agent produced
PydanticAI's whole argument is that an agent's output should be a validated object rather than a hopeful string. That argument extends one step further than most people take it: if the deliverable is a report, the validated object should contain the report's address, not the report.
A tool, from a function signature
from pydantic_ai import Agent, ModelRetry
import httpx, os
agent = Agent("claude-sonnet-5", output_type=Analysis)
@agent.tool_plain
def publish_report(title: str, html: str) -> str:
"""Publish an HTML report and return its shareable URL."""
if "<body" not in html.lower():
raise ModelRetry("html must be a complete document with a <body>.")
r = httpx.post(
"https://commareports.com/api/v1/reports",
headers={"Authorization": f"Bearer {os.environ['COMMA_API_TOKEN']}"},
json={"title": title, "html": html},
)
r.raise_for_status()
return r.json()["url"]
The ModelRetry is the part worth copying. A model that hands you a fragment
instead of a document gets told why and tries again — far better than
publishing a page that renders as three orphaned <div>s and finding out from
the reader.
Put the URL in the type
from pydantic import BaseModel, HttpUrl
class Analysis(BaseModel):
headline: str
confidence: float
report_url: HttpUrl
Now "did this run actually produce something shareable?" is answered by validation rather than by inspection. An agent that skipped the tool cannot satisfy the type, and the failure happens in your process instead of in someone's inbox.
It is also the cheap version. The document lives at the URL; your program handles a short object — the argument in context budgeting for AI reports.
Or attach the MCP server
from pydantic_ai.mcp import MCPServerStreamableHTTP
comma = MCPServerStreamableHTTP(
"https://commareports.com/api/mcp",
headers={"Authorization": f"Bearer {os.environ['COMMA_API_TOKEN']}"},
)
agent = Agent("claude-sonnet-5", toolsets=[comma], output_type=Analysis)
Publishing, updating and comment reading all arrive at once, from the same endpoint your editor uses — see MCP setup.
Worth knowing
- Dependencies carry the token. If you use
deps_type, put the API token there rather than reading the environment inside the tool — it makes the agent testable without a live token. - One report id per recurring job. Store it and update, so a nightly agent keeps one address and one comment history.
reports:write, pluscomments:readfor revisions — see scoped tokens for AI agents.- 5 MB per report body.
Try it
Free — unlimited reports, commenters and revisions.