Join our Newsletter — 33% off our NHI Course
Home› Guides› Pydantic AI Security Guide: Securing Tools, Dependencies, MCP…
Guide MCP, Protocols & Agent Platforms

Pydantic AI Security Guide: Securing Tools, Dependencies, MCP and Model Credentials

← All guides
By NHI Mgmt Group, edited by Lalit Choda Updated 14 November 2026 13 min read
On this page

A Pydantic AI agent runs inside your own Python process with the authority of whatever tools, dependencies and credentials your code hands it, so its security is decided almost entirely by application code. Pydantic AI is an open-source, MIT-licensed agent framework from the team behind Pydantic. The documentation is explicit that there is no Pydantic-hosted agent runtime: the agent, its tool functions and its provider API keys live in your service, and the optional hosted services (Pydantic Logfire for tracing and the Pydantic AI Gateway for model access) plug in only if you choose them. This guide covers the security-relevant parts of the framework: the trust boundary the project documents, tool authorisation and approval, dependency injection of per-caller identity, MCP connections, model credentials, untrusted message history, telemetry and cost limits. It is for engineers and security reviewers taking a Pydantic AI agent to production.

This is an independent guide from NHI Mgmt Group, based on Pydantic's public documentation as of November 2026. It is not sponsored or reviewed by Pydantic. Product features change, so confirm settings against Pydantic's current documentation.

Key takeaways

  • The application owns authorisation: the project's threat model places authentication, tenant identity, tool permissions, provider credentials and stored state with the server application, and says schema validation does not establish a caller's permission.
  • Scope tools per caller, per run: build or filter the toolset against the authenticated user carried in the run's dependencies, so the model can only ever see tools that user is entitled to.
  • Use deferred tools for sensitive actions: requires_approval=True, ApprovalRequired and ApprovalRequiredToolset end the run with a DeferredToolRequests output, and the docs describe approval as a guard against the model acting alone, so re-check the caller's authority inside the tool.
  • Treat client-supplied history as untrusted: message_history is trusted server-side state, so pass anything from a browser through sanitize_messages first, or keep the history on the server.
  • Telemetry includes content by default: traces carry prompts, completions and tool arguments unless you set include_content=False, so decide what may leave the process before turning instrumentation on.

The trust boundary Pydantic documents

The repository's THREAT_MODEL.md, published as a working draft for maintainer review, is the best starting point for a security review. It lists the deployment profiles and who owns each boundary.

Deployment profileWho owns the boundary (per the threat model)What to review
Server application using AgentThe application: authentication, tenant identity, tool permissions, provider credentials, stored stateRoute authentication, per-caller toolsets, secret storage
Server application with a UI adapter (AG-UI, Vercel AI)Adapter sanitises client protocol messages; the host application owns HTTP, session, CORS and CSRF securitySanitisation settings, server-held history, conversation ownership
Local CLI or Harness toolsTools run with the launching process's authority unless an execution backend isolates themWhich commands and paths the agent can touch
Sandboxed or remote workspaceThe chosen backend's credentials, mounts, network settings and lifetimeSandbox egress, mounted secrets
Durable agent (DBOS, Temporal, Restate, Prefect and others)Application and engine own persisted workflow state, queues, credentials and replayState store access, what workflow state contains

Two statements in the threat model shape everything else. First, model-generated tool arguments are untrusted input to the tool implementation: Pydantic validation checks the argument schema, but a valid argument can still name an account, file or URL the caller may not touch. Second, the project treats a client forging an approval or rewriting history as a documented design boundary rather than a framework flaw, because possession of the endpoint is the authorisation boundary. Apply vendor security updates promptly, and read the threat model before triaging a suspected framework issue, since it states which checks the library performs and which belong to your application.

Tools and least privilege

Tools are plain Python functions registered with @agent.tool (with a RunContext) or @agent.tool_plain, or grouped in toolsets. Toolsets are where least privilege is applied.

ControlAPIUse it for
Filter toolstoolset.filtered(lambda ctx, tool_def: ...)Hide tools the current user or tenant may not use
Build tools per runDynamically built toolset from the run contextConstruct only the tools the authenticated caller needs
Prepare definitionsprepare_tools hook, toolset prepareAdjust or drop tool definitions per step
Prefix or renameprefixed(), renamed()Avoid name collisions between MCP servers and local tools
Require approvalapproval_required(...) / ApprovalRequiredToolsetApproval for some calls, decided from context, tool definition and arguments
Bound executiontool_timeout, Tool(max_retries=N)Stop hung or looping tools
  • Authorise inside the tool: look up the caller from ctx.deps and check the requested resource against your own policy before acting, rather than trusting that the model picked an allowed argument.
  • Raise ModelRetry for refusals the model can fix: a refused argument becomes a tool result the model can correct; keep the message free of internal detail.
  • Make side-effecting tools idempotent: the production docs note that under durable execution a step in flight when the process died runs again from its start, so pass an idempotency key to payment and write APIs.
  • Isolate model-written code: run untrusted shell commands and generated code in a VM or microVM-backed sandbox from the Pydantic AI Harness execution environments. The docs describe the Bubblewrapped Workspace and FileSystem path patterns as guards against accidental writes by a trusted agent, so use a VM-backed sandbox where isolation is the goal. See the AI Agent Sandboxing and Code Execution Guide.

Dependencies: carrying caller identity into the run

Pydantic AI's dependency injection passes a typed object, declared with deps_type and supplied as deps= on each run, to tools, instructions and output validators through RunContext. That makes it the natural place to carry the authenticated user, tenant, scoped database handle and per-user tokens.

  • Populate deps from verified identity: build the deps object in your authenticated route handler from the session or token claims, never from fields in the prompt or history.
  • Hand tools scoped clients, not root credentials: put a tenant-scoped database connection or a user-delegated token in deps, so even a misused tool reaches only that caller's data.
  • Keep secrets out of instructions: deps are passed to code, not shown to the model, unless an instructions function writes them into the prompt; review dynamic instructions for leaked values.
  • Bind state to the user: the threat model requires conversations, memory namespaces, deferred results and workspace reattachment to be bound to the authorised user; a client-supplied conversation_id is not proof of ownership.

Human approval with deferred tools

When a tool marked requires_approval=True is called, the run ends with a DeferredToolRequests output listing the pending calls, and nothing stays in memory while you wait. You store the run's Conversation, ask a person, and continue with deferred_tool_results built from the decision. For approval that depends on arguments or context, raise ApprovalRequired from the tool; RunContext.tool_call_approved is True once approved. See the Human-in-the-Loop for AI Agents Guide.

  1. Persist paused runs server-side: the docs recommend resuming with your own deferred_tool_results rather than the client's when stakes are high.
  2. Authorise the approver: where a HandleDeferredToolCalls handler issues a resume token, the threat model says to treat it as a bearer credential and to check who is calling in the code that resolves it; answered_by and decision metadata are not proof of who approved.
  3. Default to deny on timeout: the production example treats an unanswered approval as a denial after a set period.
  4. Know what a model reviewer is: ToolCallJudge and ApprovalReviewer in the Harness are documented as model-based filters, not security boundaries, and they send reviewed arguments to the reviewing model's provider.

MCP servers

MCPToolset connects an agent to MCP servers over Streamable HTTP, SSE or stdio. See the MCP Security Guide for the protocol view.

  • Per-user authentication: a shared MCPToolset connects as a single identity. Pass a function as auth (or headers) that reads the user's token from ctx.deps; each run then opens its own MCP session. The docs warn against sharing a fixed-auth toolset across concurrent runs for different users, because one session would serve several users.
  • Configuration files are code: an MCP config file names executables to spawn and expands ${VAR} against the full process environment, so the docs say to load only files you control.
  • Server instructions stay off unless trusted: on MCPToolset, including a server's instructions is off by default because the model receives them with your own instructions' authority; the MCP capability turns it on, so pass include_instructions=False for servers you do not trust.
  • Sampling is opt-in: a server can only make model calls through your client once you set a sampling_model, and those calls spend your model credentials.
  • Transport hardening: pass your own http_client for an internal CA or mTLS; its cookie jar is shared by every run, so keep it off toolsets acting for different users.
  • Approval for MCP tools: wrap the toolset with approval_required() for write operations.

Model credentials and cost

Each provider reads its key from an environment variable such as OPENAI_API_KEY or ANTHROPIC_API_KEY, or takes api_key= on its provider class, so keys can be loaded from a secret manager like any other credential. The optional Gateway uses one PYDANTIC_AI_GATEWAY_API_KEY for all providers, and each Gateway key can carry daily, weekly or monthly spending caps at project, user and key level. See How to find LLM API keys in code and CI and the LLMjacking Guide.

LimitSettingNote from the docs
Per-run capsUsageLimits(request_limit, tool_calls_limit, cost_limit)Raises UsageLimitExceeded; cost is best-effort, so keep request_limit alongside cost_limit
Longer budgetsSpendLimits (Harness)A brake, not a reservation; spans processes only with a shared store; set on_unpriced='raise'
Gateway capsPer project, user and keyEnforced before a request reaches the provider
Timetool_timeout, run timeouts, cancellationBound each step and the whole run
Concurrencymax_concurrency, ConcurrencyLimitedModelConcurrency, not requests per minute

The threat model adds a rule worth enforcing in review: a credential must reach only the service it was issued for, and a provider's base_url is part of that service, so a configurable base_url from an untrusted source can redirect your key. See How to set spend limits on LLM API keys.

Message history, UI adapters and fetching

  • Sanitise client history: sanitize_messages strips client-supplied system prompts and instructions, drops non-HTTP file URL schemes and uploaded file references, resets workspace references and model IDs and removes unresolved tool calls. The UI adapters apply the same defaults.
  • Prefer server-held history: with manage_message_history='server', only the client's new turn crosses the adapter boundary; you still authorise the conversation ID you load.
  • Authenticate at the transport: the docs say to run the agent inside your own authenticated route and treat every caller who gets through as able to submit any history.
  • Keep the SSRF defaults: local downloads use safe_download, which blocks private addresses by default and keeps metadata addresses blocked when local URLs are enabled.

For the web layer around the agent, see the FastAPI Security Guide.

Telemetry and data handling

Pydantic AI emits OpenTelemetry traces following the GenAI semantic conventions to Logfire or any OTel backend. Every run carries a run_id and conversation_id, and run metadata such as a tenant or user ID lands on the run span.

  • Decide on content: set include_content=False on InstrumentationSettings to keep prompts, completions and tool arguments out of spans, and include_binary_content=False for files. Neither touches run metadata, your own spans or captured HTTP bodies.
  • Choose where traces go: Logfire runs in a US or EU region, as a dedicated deployment, or self-hosted on Kubernetes; the enterprise docs note the Logfire SDK scrubs likely-sensitive values such as tokens and passwords, but not prompt and response content.
  • Review every export channel: the threat model notes that suppressing one content field does not guarantee the content is absent from every event, exception or integration.

See Agent Observability vs Audit Logging.

Where identity fits

Every production agent carries at least two kinds of non-human identity: the provider API key (or Gateway key) that pays for model calls, and the credentials its tools use against databases, SaaS APIs and MCP servers. Issue a separate Gateway or provider key per service and environment with its own cap, give each agent its own scoped identity for downstream calls instead of a shared service account, and prefer per-user delegated tokens passed through deps for MCP and API calls made on a user's behalf. See AI Agent Identity vs Service Account.

Detection and measurement

  • Approval coverage: share of write-capable tools behind requires_approval or ApprovalRequiredToolset (target: all of them).
  • Usage limit hits: UsageLimitExceeded rate per agent; a spike suggests loops or abuse.
  • Refused tool calls: authorisation refusals inside tools per tenant, alerting on bursts.
  • Credential hygiene: number of provider keys shared across services or environments (target: zero) and age of each key.
  • Content exposure: share of instrumented agents with an explicit include_content decision recorded.
  • Cost per tenant: token and cost metrics by provider and model, compared with caps.

Common failure patterns

  • One global toolset for every user: the model sees tools the caller should not, and refusal relies on prompt wording.
  • Trusting validated arguments: a schema-valid account ID is still someone else's account; authorise in the tool.
  • Passing browser history straight to message_history: lets a client inject system prompts and fake tool results.
  • Sharing a fixed-auth MCP toolset across users: one MCP session serves several users.
  • Instrumenting with default content capture into a shared backend: prompts and tool arguments with personal data or secrets end up in traces.
  • Relying on cost_limit alone: unpriced models are not counted; keep request_limit too.

Framework mapping

RiskOWASP LLM Top 10 2025MITRE ATLASNIST SP 800-53 Rev 5Pydantic AI control
Injected content steering tool useLLM01:2025 Prompt InjectionAML.T0051AC-6Per-caller toolsets, authorisation in tools, sanitised history
Agent taking unapproved actionsLLM06:2025 Excessive AgencyAML.T0053AC-6, CM-7Deferred tools, approval-required toolsets
Secrets and personal data in tracesLLM02:2025 Sensitive Information DisclosureAU-2include_content=False, backend choice
Runaway cost or loopsLLM10:2025 Unbounded ConsumptionSC-7UsageLimits, SpendLimits, Gateway caps
Leaked or shared provider keysIA-5Secret manager, per-service keys

In NIST CSF 2.0 terms, per-caller tool scoping sits under PR.AA-05 and trace monitoring under DE.CM-09.

Practitioner checklist

  • Read THREAT_MODEL.md and map your deployment profile.
  • Build deps from verified identity in an authenticated route; never from prompt content.
  • Filter or build toolsets per caller and authorise every resource inside each tool.
  • Put every write or payment tool behind approval, persist paused runs server-side and authorise approvers.
  • Use per-user auth functions for MCP and load MCP config only from files you control.
  • Load provider keys from a secret manager, one key per service and environment, with caps.
  • Set UsageLimits with both request and cost limits, plus timeouts.
  • Sanitise or server-hold message history and decide include_content before enabling traces.
  • Run model-written code in a VM-backed sandbox.

Standards and references

Related NHI Mgmt Group resources: FastAPI Security Guide, AI Agent Tool and Plugin Security Guide, MCP Security Guide.

Free weekly newsletter

Subscribe to the NHI & AI Identity Journal

The latest on NHI and Agentic AI security – articles, research, breaches, news and events every week.

Bonus 33% off our NHI Course when you subscribe.

NHIMG Editorial Note
Researched and written by the NHI Mgmt Group editorial team using AI tools, under the editorial direction of Lalit Choda. Facts are drawn from the sources listed on this page and checked by our automated quality process. Last updated 14 November 2026. Spotted an error? Use our contact page.
NHI Mgmt Group, the independent authority on Non-Human Identity, IAM and Agentic AI security. nhimg.org