Skip to content
Closed
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
47 changes: 47 additions & 0 deletions .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
FROM python:3.12-bookworm

ARG GIT_USERNAME
ARG GIT_EMAIL
ARG USERNAME=coder
ARG USER_UID=1000
ARG USER_GID=1000
ARG WORKSPACE=/home/$USERNAME/workspace
ARG DEBIAN_FRONTEND=noninteractive


ENV SHELL=/bin/bash
ENV LANG=en_US.UTF-8
ENV LANGUAGE=en_US.UTF-8
ENV LC_ALL=en_US.UTF-8

RUN apt -y update \
&& apt -y upgrade

RUN apt -y install inetutils-ping vim-nox htop git curl wget net-tools bind9-dnsutils locales
RUN sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen && \
locale-gen
ENV LANG=en_US.UTF-8
ENV LANGUAGE=en_US:en
ENV LC_ALL=en_US.UTF-8

RUN apt -y autoremove \
&& apt -y clean \
&& rm -Rf /var/lib/apt/lists/*

RUN curl -sSL https://install.python-poetry.org | python3 -

RUN groupadd --gid $USER_GID $USERNAME \
&& useradd --uid $USER_UID --gid $USER_GID -m $USERNAME \
&& echo "$USERNAME:$USERNAME" | chpasswd \
&& adduser $USERNAME sudo

RUN mkdir -p $WORKSPACE \
&& chown -R $USERNAME:$USERNAME $WORKSPACE \
&& chmod -R u=rwX,go=rX $WORKSPACE

RUN pip install --no-cache-dir uv

USER $USERNAME

RUN git config --global user.email "$GIT_EMAIL"
RUN git config --global user.name "$GIT_USERNAME"
33 changes: 33 additions & 0 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"name": "pydantic-ai",
"dockerComposeFile": ["docker-compose.yml"],
"service": "pydantic-ai",
"remoteUser": "coder",
"shutdownAction": "stopCompose",
"workspaceFolder": "/home/coder/workspace",
"customizations": {
"vscode": {
"settings": {},
"extensions": [
"anweber.vscode-httpyac",
"donjayamanne.python-environment-manager",
"esbenp.prettier-vscode",
"github.copilot",
"github.copilot-chat",
"mhutchie.git-graph",
"ms-azuretools.vscode-docker",
"ms-python.debugpy",
"ms-python.python",
"ms-python.vscode-pylance",
"ms-toolsai.jupyter",
"ms-toolsai.jupyter-keymap",
"ms-toolsai.jupyter-renderers",
"ms-toolsai.vscode-jupyter-cell-tags",
"ms-toolsai.vscode-jupyter-slideshow",
"visualstudioexptteam.intellicode-api-usage-examples",
"visualstudioexptteam.vscodeintellicode"
]
}
},
"runArgs": "--network=maia"
}
21 changes: 21 additions & 0 deletions .devcontainer/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
name: pydantic-ai_devcontainer

services:
pydantic-ai:
build:
context: .
dockerfile: Dockerfile
args:
- GIT_EMAIL=${GIT_EMAIL}
- GIT_USERNAME=${GIT_USERNAME}
volumes:
- ..:/home/coder/workspace
- ~/.ssh:/home/coder/.ssh:ro
working_dir: /home/coder/workspace
hostname: pydantic-ai
command: /bin/sh -c "while sleep 1000; do :; done"

networks:
default:
external: true
name: maia
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@ examples/pydantic_ai_examples/.chat_app_messages.sqlite
node_modules/
**.idea/
.coverage*
uv.lock
7 changes: 0 additions & 7 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,6 @@ repos:
language: system
types_or: [python, markdown]
pass_filenames: false
- id: typecheck
name: Typecheck
entry: make
args: [typecheck]
language: system
types: [python]
pass_filenames: false

- repo: https://github.com/codespell-project/codespell
# Configuration for codespell is in pyproject.toml
Expand Down
11 changes: 10 additions & 1 deletion pydantic_ai_slim/pydantic_ai/mcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@

from pydantic_ai.exceptions import ModelRetry
from pydantic_ai.messages import BinaryContent
from pydantic_ai.tools import ToolDefinition
from pydantic_ai.tools import McpHttpClientFactory, ToolDefinition, create_mcp_http_client

try:
from mcp.client.session import ClientSession
Expand Down Expand Up @@ -358,6 +358,14 @@ async def main():
For example, if `tool_prefix='foo'`, then a tool named `bar` will be registered as `foo_bar`
"""

httpx_client_factory: McpHttpClientFactory = create_mcp_http_client
"""Factory function to create the underlying HTTPX client.

This allows customization of the HTTP client used for the SSE connection.

See <https://github.com/modelcontextprotocol/python-sdk/pull/752>
"""

@asynccontextmanager
async def client_streams(
self,
Expand All @@ -372,6 +380,7 @@ async def client_streams(
headers=self.headers,
timeout=self.timeout,
sse_read_timeout=self.sse_read_timeout,
httpx_client_factory=self.httpx_client_factory,
) as (read_stream, write_stream):
yield read_stream, write_stream

Expand Down
87 changes: 86 additions & 1 deletion pydantic_ai_slim/pydantic_ai/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
import json
from collections.abc import Awaitable, Sequence
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any, Callable, Generic, Literal, Union
from typing import TYPE_CHECKING, Any, Callable, Generic, Literal, Protocol, Union

import httpx
from opentelemetry.trace import Tracer
from pydantic import ValidationError
from pydantic.json_schema import GenerateJsonSchema, JsonSchemaValue
Expand Down Expand Up @@ -33,6 +34,8 @@
'Tool',
'ObjectJsonSchema',
'ToolDefinition',
'McpHttpClientFactory',
'create_mcp_http_client',
)

AgentDepsT = TypeVar('AgentDepsT', default=None, contravariant=True)
Expand Down Expand Up @@ -446,3 +449,85 @@ class ToolDefinition:
"""

__repr__ = _utils.dataclasses_no_defaults_repr


class McpHttpClientFactory(Protocol):
"""HTTP client factory for MCP.

This protocol defines a callable that creates an `httpx.AsyncClient` with MCP defaults.
It allows for customization of headers, timeout, and authentication.
"""

def __call__(
self,
headers: dict[str, str] | None = None,
timeout: httpx.Timeout | None = None,
auth: httpx.Auth | None = None,
) -> httpx.AsyncClient: ...


def create_mcp_http_client(
headers: dict[str, str] | None = None,
timeout: httpx.Timeout | None = None,
auth: httpx.Auth | None = None,
) -> httpx.AsyncClient:
"""Create a standardized httpx AsyncClient with MCP defaults.

This function provides common defaults used throughout the MCP codebase:
- follow_redirects=True (always enabled)
- Default timeout of 30 seconds if not specified

Args:
headers: Optional headers to include with all requests.
timeout: Request timeout as httpx.Timeout object.
Defaults to 30 seconds if not specified.
auth: Optional authentication handler.

Returns:
Configured httpx.AsyncClient instance with MCP defaults.

Note:
The returned AsyncClient must be used as a context manager to ensure
proper cleanup of connections.

Examples:
# Basic usage with MCP defaults
async with create_mcp_http_client() as client:
response = await client.get("https://api.example.com")

# With custom headers
headers = {"Authorization": "Bearer token"}
async with create_mcp_http_client(headers) as client:
response = await client.get("/endpoint")

# With both custom headers and timeout
timeout = httpx.Timeout(60.0, read=300.0)
async with create_mcp_http_client(headers, timeout) as client:
response = await client.get("/long-request")

# With authentication
from httpx import BasicAuth
auth = BasicAuth(username="user", password="pass")
async with create_mcp_http_client(headers, timeout, auth) as client:
response = await client.get("/protected-endpoint")
"""
# Set MCP defaults
kwargs: dict[str, Any] = {
'follow_redirects': True,
}

# Handle timeout
if timeout is None:
kwargs['timeout'] = httpx.Timeout(30.0)
else:
kwargs['timeout'] = timeout

# Handle headers
if headers is not None:
kwargs['headers'] = headers

# Handle authentication
if auth is not None:
kwargs['auth'] = auth

return httpx.AsyncClient(**kwargs)
3 changes: 2 additions & 1 deletion pydantic_ai_slim/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,14 @@ classifiers = [
"Topic :: Software Development :: Libraries :: Python Modules",
"Topic :: Internet",
]
requires-python = ">=3.9"
requires-python = ">=3.10"

[tool.hatch.metadata.hooks.uv-dynamic-versioning]
dependencies = [
"eval-type-backport>=0.2.0",
"griffe>=1.3.2",
"httpx>=0.27",
"mcp>=1.9.2",
"pydantic>=2.10",
"pydantic-graph=={{ version }}",
"exceptiongroup; python_version < '3.11'",
Expand Down
Loading
Loading