pydantic-ai 0.0.41__tar.gz → 0.0.42__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of pydantic-ai might be problematic. Click here for more details.

Files changed (99) hide show
  1. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/.gitignore +1 -1
  2. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/Makefile +9 -0
  3. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/PKG-INFO +3 -3
  4. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/pyproject.toml +17 -7
  5. pydantic_ai-0.0.42/tests/cassettes/test_mcp/test_agent_with_stdio_server.yaml +205 -0
  6. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/graph/test_graph.py +1 -1
  7. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/graph/test_persistence.py +1 -1
  8. pydantic_ai-0.0.42/tests/mcp_server.py +19 -0
  9. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/test_examples.py +25 -3
  10. pydantic_ai-0.0.42/tests/test_mcp.py +93 -0
  11. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/test_tools.py +61 -16
  12. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/LICENSE +0 -0
  13. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/README.md +0 -0
  14. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/__init__.py +0 -0
  15. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/assets/dummy.pdf +0 -0
  16. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/assets/kiwi.png +0 -0
  17. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/assets/marcelo.mp3 +0 -0
  18. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/conftest.py +0 -0
  19. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/example_modules/README.md +0 -0
  20. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/example_modules/bank_database.py +0 -0
  21. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/example_modules/fake_database.py +0 -0
  22. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/example_modules/weather_service.py +0 -0
  23. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/graph/__init__.py +0 -0
  24. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/graph/test_file_persistence.py +0 -0
  25. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/graph/test_mermaid.py +0 -0
  26. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/graph/test_state.py +0 -0
  27. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/graph/test_utils.py +0 -0
  28. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/import_examples.py +0 -0
  29. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/json_body_serializer.py +0 -0
  30. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/models/__init__.py +0 -0
  31. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/models/cassettes/test_anthropic/test_document_binary_content_input.yaml +0 -0
  32. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/models/cassettes/test_anthropic/test_document_url_input.yaml +0 -0
  33. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/models/cassettes/test_anthropic/test_image_url_input.yaml +0 -0
  34. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/models/cassettes/test_anthropic/test_image_url_input_invalid_mime_type.yaml +0 -0
  35. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/models/cassettes/test_anthropic/test_multiple_parallel_tool_calls.yaml +0 -0
  36. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/models/cassettes/test_anthropic/test_text_document_url_input.yaml +0 -0
  37. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/models/cassettes/test_bedrock/test_bedrock_model.yaml +0 -0
  38. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/models/cassettes/test_bedrock/test_bedrock_model_anthropic_model_without_tools.yaml +0 -0
  39. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/models/cassettes/test_bedrock/test_bedrock_model_iter_stream.yaml +0 -0
  40. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/models/cassettes/test_bedrock/test_bedrock_model_max_tokens.yaml +0 -0
  41. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/models/cassettes/test_bedrock/test_bedrock_model_retry.yaml +0 -0
  42. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/models/cassettes/test_bedrock/test_bedrock_model_stream.yaml +0 -0
  43. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/models/cassettes/test_bedrock/test_bedrock_model_structured_response.yaml +0 -0
  44. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/models/cassettes/test_bedrock/test_bedrock_model_top_p.yaml +0 -0
  45. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/models/cassettes/test_bedrock/test_document_url_input.yaml +0 -0
  46. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/models/cassettes/test_bedrock/test_image_as_binary_content_input.yaml +0 -0
  47. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/models/cassettes/test_bedrock/test_image_url_input.yaml +0 -0
  48. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/models/cassettes/test_bedrock/test_text_as_binary_content_input.yaml +0 -0
  49. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/models/cassettes/test_bedrock/test_text_document_url_input.yaml +0 -0
  50. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/models/cassettes/test_gemini/test_document_url_input.yaml +0 -0
  51. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/models/cassettes/test_gemini/test_image_as_binary_content_input.yaml +0 -0
  52. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/models/cassettes/test_gemini/test_image_url_input.yaml +0 -0
  53. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/models/cassettes/test_groq/test_image_as_binary_content_input.yaml +0 -0
  54. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/models/cassettes/test_groq/test_image_url_input.yaml +0 -0
  55. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/models/cassettes/test_openai/test_audio_as_binary_content_input.yaml +0 -0
  56. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/models/cassettes/test_openai/test_document_url_input.yaml +0 -0
  57. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/models/cassettes/test_openai/test_image_as_binary_content_input.yaml +0 -0
  58. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/models/cassettes/test_openai/test_openai_o1_mini_system_role[developer].yaml +0 -0
  59. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/models/cassettes/test_openai/test_openai_o1_mini_system_role[system].yaml +0 -0
  60. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/models/mock_async_stream.py +0 -0
  61. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/models/test_anthropic.py +0 -0
  62. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/models/test_bedrock.py +0 -0
  63. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/models/test_cohere.py +0 -0
  64. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/models/test_fallback.py +0 -0
  65. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/models/test_gemini.py +0 -0
  66. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/models/test_groq.py +0 -0
  67. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/models/test_instrumented.py +0 -0
  68. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/models/test_mistral.py +0 -0
  69. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/models/test_model.py +0 -0
  70. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/models/test_model_function.py +0 -0
  71. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/models/test_model_names.py +0 -0
  72. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/models/test_model_test.py +0 -0
  73. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/models/test_openai.py +0 -0
  74. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/models/test_vertexai.py +0 -0
  75. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/providers/__init__.py +0 -0
  76. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/providers/cassettes/test_azure/test_azure_provider_call.yaml +0 -0
  77. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/providers/test_anthropic.py +0 -0
  78. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/providers/test_azure.py +0 -0
  79. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/providers/test_bedrock.py +0 -0
  80. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/providers/test_deepseek.py +0 -0
  81. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/providers/test_google_gla.py +0 -0
  82. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/providers/test_google_vertex.py +0 -0
  83. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/providers/test_groq.py +0 -0
  84. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/providers/test_mistral.py +0 -0
  85. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/providers/test_provider_names.py +0 -0
  86. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/test_agent.py +0 -0
  87. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/test_cli.py +0 -0
  88. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/test_deps.py +0 -0
  89. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/test_format_as_xml.py +0 -0
  90. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/test_json_body_serializer.py +0 -0
  91. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/test_live.py +0 -0
  92. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/test_logfire.py +0 -0
  93. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/test_messages.py +0 -0
  94. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/test_parts_manager.py +0 -0
  95. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/test_streaming.py +0 -0
  96. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/test_usage_limits.py +0 -0
  97. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/test_utils.py +0 -0
  98. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/typed_agent.py +0 -0
  99. {pydantic_ai-0.0.41 → pydantic_ai-0.0.42}/tests/typed_graph.py +0 -0
@@ -1,5 +1,4 @@
1
1
  site
2
- .python-version
3
2
  .venv
4
3
  dist
5
4
  __pycache__
@@ -16,3 +15,4 @@ examples/pydantic_ai_examples/.chat_app_messages.sqlite
16
15
  .vscode/
17
16
  /question_graph_history.json
18
17
  /docs-site/.wrangler/
18
+ /CLAUDE.md
@@ -27,6 +27,10 @@ lint: ## Lint the code
27
27
  uv run ruff format --check
28
28
  uv run ruff check
29
29
 
30
+ .PHONY: lint-js
31
+ lint-js: ## Lint JS and TS code
32
+ cd mcp-run-python && npm run lint
33
+
30
34
  .PHONY: typecheck-pyright
31
35
  typecheck-pyright:
32
36
  @# PYRIGHT_PYTHON_IGNORE_WARNINGS avoids the overhead of making a request to github on every invocation
@@ -62,6 +66,11 @@ testcov: test ## Run tests and generate a coverage report
62
66
  @echo "building coverage html"
63
67
  @uv run coverage html
64
68
 
69
+ .PHONY: test-mrp
70
+ test-mrp: ## Build and tests of mcp-run-python
71
+ cd mcp-run-python && npm run prepare
72
+ uv run --package mcp-run-python pytest mcp-run-python -v
73
+
65
74
  .PHONY: update-examples
66
75
  update-examples: ## Update documentation examples
67
76
  uv run -m pytest --update-examples tests/test_examples.py
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pydantic-ai
3
- Version: 0.0.41
3
+ Version: 0.0.42
4
4
  Summary: Agent Framework / shim to use Pydantic with LLMs
5
5
  Project-URL: Homepage, https://ai.pydantic.dev
6
6
  Project-URL: Source, https://github.com/pydantic/pydantic-ai
@@ -28,9 +28,9 @@ Classifier: Topic :: Internet
28
28
  Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
29
29
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
30
30
  Requires-Python: >=3.9
31
- Requires-Dist: pydantic-ai-slim[anthropic,bedrock,cli,cohere,groq,mistral,openai,vertexai]==0.0.41
31
+ Requires-Dist: pydantic-ai-slim[anthropic,bedrock,cli,cohere,groq,mcp,mistral,openai,vertexai]==0.0.42
32
32
  Provides-Extra: examples
33
- Requires-Dist: pydantic-ai-examples==0.0.41; extra == 'examples'
33
+ Requires-Dist: pydantic-ai-examples==0.0.42; extra == 'examples'
34
34
  Provides-Extra: logfire
35
35
  Requires-Dist: logfire>=2.3; extra == 'logfire'
36
36
  Description-Content-Type: text/markdown
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "pydantic-ai"
7
- version = "0.0.41"
7
+ version = "0.0.42"
8
8
  description = "Agent Framework / shim to use Pydantic with LLMs"
9
9
  authors = [
10
10
  { name = "Samuel Colvin", email = "samuel@pydantic.dev" },
@@ -36,7 +36,7 @@ classifiers = [
36
36
  ]
37
37
  requires-python = ">=3.9"
38
38
  dependencies = [
39
- "pydantic-ai-slim[openai,vertexai,groq,anthropic,mistral,cohere,bedrock,cli]==0.0.41",
39
+ "pydantic-ai-slim[openai,vertexai,groq,anthropic,mistral,cohere,bedrock,cli,mcp]==0.0.42",
40
40
  ]
41
41
 
42
42
  [project.urls]
@@ -46,16 +46,17 @@ Documentation = "https://ai.pydantic.dev"
46
46
  Changelog = "https://github.com/pydantic/pydantic-ai/releases"
47
47
 
48
48
  [project.optional-dependencies]
49
- examples = ["pydantic-ai-examples==0.0.41"]
49
+ examples = ["pydantic-ai-examples==0.0.42"]
50
50
  logfire = ["logfire>=2.3"]
51
51
 
52
52
  [tool.uv.sources]
53
53
  pydantic-ai-slim = { workspace = true }
54
54
  pydantic-graph = { workspace = true }
55
55
  pydantic-ai-examples = { workspace = true }
56
+ mcp-run-python = { workspace = true }
56
57
 
57
58
  [tool.uv.workspace]
58
- members = ["pydantic_ai_slim", "pydantic_graph", "examples"]
59
+ members = ["pydantic_ai_slim", "pydantic_graph", "examples", "mcp-run-python"]
59
60
 
60
61
  [dependency-groups]
61
62
  # dev dependencies are defined in `pydantic-ai-slim/pyproject.toml` to allow for minimal testing
@@ -82,6 +83,7 @@ line-length = 120
82
83
  target-version = "py39"
83
84
  include = [
84
85
  "pydantic_ai_slim/**/*.py",
86
+ "mcp-run-python/**/*.py",
85
87
  "pydantic_graph/**/*.py",
86
88
  "examples/**/*.py",
87
89
  "tests/**/*.py",
@@ -116,18 +118,22 @@ quote-style = "single"
116
118
  "tests/**/*.py" = ["D"]
117
119
  "docs/**/*.py" = ["D"]
118
120
  "examples/**/*.py" = ["D101", "D103"]
121
+ "mcp-run-python/**/*.py" = ["D", "TID251"]
119
122
 
120
123
  [tool.pyright]
124
+ pythonVersion = "3.12"
121
125
  typeCheckingMode = "strict"
122
126
  reportMissingTypeStubs = false
123
127
  reportUnnecessaryIsInstance = false
124
128
  reportUnnecessaryTypeIgnoreComment = true
125
- include = ["pydantic_ai_slim", "pydantic_graph", "tests", "examples"]
129
+ reportMissingModuleSource = false
130
+ include = ["pydantic_ai_slim", "mcp-run-python", "pydantic_graph", "tests", "examples"]
126
131
  venvPath = ".venv"
127
132
  # see https://github.com/microsoft/pyright/issues/7771 - we don't want to error on decorated functions in tests
128
133
  # which are not otherwise used
129
134
  executionEnvironments = [{ root = "tests", reportUnusedFunction = false }]
130
- exclude = ["examples/pydantic_ai_examples/weather_agent_gradio.py"]
135
+ exclude = ["examples/pydantic_ai_examples/weather_agent_gradio.py", "mcp-run-python/node_modules"]
136
+ extraPaths = ["mcp-run-python/stubs"]
131
137
 
132
138
  [tool.mypy]
133
139
  files = "tests/typed_agent.py,tests/typed_graph.py"
@@ -139,7 +145,11 @@ xfail_strict = true
139
145
  filterwarnings = [
140
146
  "error",
141
147
  # boto3
142
- "ignore::DeprecationWarning:botocore.*"
148
+ "ignore::DeprecationWarning:botocore.*",
149
+ "ignore::RuntimeWarning:pydantic_ai.mcp",
150
+ # uvicorn (mcp server)
151
+ "ignore:websockets.legacy is deprecated.*:DeprecationWarning:websockets.legacy",
152
+ "ignore:websockets.server.WebSocketServerProtocol is deprecated:DeprecationWarning"
143
153
  ]
144
154
 
145
155
  # https://coverage.readthedocs.io/en/latest/config.html#run
@@ -0,0 +1,205 @@
1
+ interactions:
2
+ - request:
3
+ headers:
4
+ accept:
5
+ - application/json
6
+ accept-encoding:
7
+ - gzip, deflate
8
+ connection:
9
+ - keep-alive
10
+ content-length:
11
+ - '530'
12
+ content-type:
13
+ - application/json
14
+ host:
15
+ - api.openai.com
16
+ method: POST
17
+ parsed_body:
18
+ messages:
19
+ - content: What is 0 degrees Celsius in Fahrenheit?
20
+ role: user
21
+ model: gpt-4o
22
+ n: 1
23
+ stream: false
24
+ tool_choice: auto
25
+ tools:
26
+ - function:
27
+ description: "Convert Celsius to Fahrenheit.\n\n Args:\n celsius: Temperature in Celsius\n\n Returns:\n
28
+ \ Temperature in Fahrenheit\n "
29
+ name: celsius_to_fahrenheit
30
+ parameters:
31
+ properties:
32
+ celsius:
33
+ title: Celsius
34
+ type: number
35
+ required:
36
+ - celsius
37
+ title: celsius_to_fahrenheitArguments
38
+ type: object
39
+ type: function
40
+ uri: https://api.openai.com/v1/chat/completions
41
+ response:
42
+ headers:
43
+ access-control-expose-headers:
44
+ - X-Request-ID
45
+ alt-svc:
46
+ - h3=":443"; ma=86400
47
+ connection:
48
+ - keep-alive
49
+ content-length:
50
+ - '1085'
51
+ content-type:
52
+ - application/json
53
+ openai-organization:
54
+ - pydantic-28gund
55
+ openai-processing-ms:
56
+ - '594'
57
+ openai-version:
58
+ - '2020-10-01'
59
+ strict-transport-security:
60
+ - max-age=31536000; includeSubDomains; preload
61
+ transfer-encoding:
62
+ - chunked
63
+ parsed_body:
64
+ choices:
65
+ - finish_reason: tool_calls
66
+ index: 0
67
+ logprobs: null
68
+ message:
69
+ annotations: []
70
+ content: null
71
+ refusal: null
72
+ role: assistant
73
+ tool_calls:
74
+ - function:
75
+ arguments: '{"celsius":0}'
76
+ name: celsius_to_fahrenheit
77
+ id: call_UNesABTXfwIkYdh3HzXWw2wD
78
+ type: function
79
+ created: 1741776872
80
+ id: chatcmpl-BAE1IwTZc7FHM4TkNeBoPylR7rtCT
81
+ model: gpt-4o-2024-08-06
82
+ object: chat.completion
83
+ service_tier: default
84
+ system_fingerprint: fp_eb9dce56a8
85
+ usage:
86
+ completion_tokens: 19
87
+ completion_tokens_details:
88
+ accepted_prediction_tokens: 0
89
+ audio_tokens: 0
90
+ reasoning_tokens: 0
91
+ rejected_prediction_tokens: 0
92
+ prompt_tokens: 82
93
+ prompt_tokens_details:
94
+ audio_tokens: 0
95
+ cached_tokens: 0
96
+ total_tokens: 101
97
+ status:
98
+ code: 200
99
+ message: OK
100
+ - request:
101
+ headers:
102
+ accept:
103
+ - application/json
104
+ accept-encoding:
105
+ - gzip, deflate
106
+ connection:
107
+ - keep-alive
108
+ content-length:
109
+ - '879'
110
+ content-type:
111
+ - application/json
112
+ cookie:
113
+ - __cf_bm=GeO8TCYhlEUIV63eLxM4nKUU2OLlG.f8tMvM9shFTc8-1741776873-1.0.1.1-zxkkWGCAPhJIA05Uwt3Ii3DCg9da6owy45bo_yaZ1YmsoihITJCgZzpA6H4eL0xzFRDWrWkEIQYaFEXLYcrLePwDMsgwNUJbEf6sg1vm2YQ;
114
+ _cfuvid=AI06nwzbBcwVRHXv_BRehX1K7p9oe1qUXFkzXBWEUW0-1741776873043-0.0.1.1-604800000
115
+ host:
116
+ - api.openai.com
117
+ method: POST
118
+ parsed_body:
119
+ messages:
120
+ - content: What is 0 degrees Celsius in Fahrenheit?
121
+ role: user
122
+ - role: assistant
123
+ tool_calls:
124
+ - function:
125
+ arguments: '{"celsius":0}'
126
+ name: celsius_to_fahrenheit
127
+ id: call_UNesABTXfwIkYdh3HzXWw2wD
128
+ type: function
129
+ - content: '{"meta":null,"content":[{"type":"text","text":"32.0","annotations":null}],"isError":false}'
130
+ role: tool
131
+ tool_call_id: call_UNesABTXfwIkYdh3HzXWw2wD
132
+ model: gpt-4o
133
+ n: 1
134
+ stream: false
135
+ tool_choice: auto
136
+ tools:
137
+ - function:
138
+ description: "Convert Celsius to Fahrenheit.\n\n Args:\n celsius: Temperature in Celsius\n\n Returns:\n
139
+ \ Temperature in Fahrenheit\n "
140
+ name: celsius_to_fahrenheit
141
+ parameters:
142
+ properties:
143
+ celsius:
144
+ title: Celsius
145
+ type: number
146
+ required:
147
+ - celsius
148
+ title: celsius_to_fahrenheitArguments
149
+ type: object
150
+ type: function
151
+ uri: https://api.openai.com/v1/chat/completions
152
+ response:
153
+ headers:
154
+ access-control-expose-headers:
155
+ - X-Request-ID
156
+ alt-svc:
157
+ - h3=":443"; ma=86400
158
+ connection:
159
+ - keep-alive
160
+ content-length:
161
+ - '849'
162
+ content-type:
163
+ - application/json
164
+ openai-organization:
165
+ - pydantic-28gund
166
+ openai-processing-ms:
167
+ - '415'
168
+ openai-version:
169
+ - '2020-10-01'
170
+ strict-transport-security:
171
+ - max-age=31536000; includeSubDomains; preload
172
+ transfer-encoding:
173
+ - chunked
174
+ parsed_body:
175
+ choices:
176
+ - finish_reason: stop
177
+ index: 0
178
+ logprobs: null
179
+ message:
180
+ annotations: []
181
+ content: 0 degrees Celsius is 32.0 degrees Fahrenheit.
182
+ refusal: null
183
+ role: assistant
184
+ created: 1741776873
185
+ id: chatcmpl-BAE1Jy3AN974xW1pziTxd6wrxliCE
186
+ model: gpt-4o-2024-08-06
187
+ object: chat.completion
188
+ service_tier: default
189
+ system_fingerprint: fp_eb9dce56a8
190
+ usage:
191
+ completion_tokens: 13
192
+ completion_tokens_details:
193
+ accepted_prediction_tokens: 0
194
+ audio_tokens: 0
195
+ reasoning_tokens: 0
196
+ rejected_prediction_tokens: 0
197
+ prompt_tokens: 139
198
+ prompt_tokens_details:
199
+ audio_tokens: 0
200
+ cached_tokens: 0
201
+ total_tokens: 152
202
+ status:
203
+ code: 200
204
+ message: OK
205
+ version: 1
@@ -393,7 +393,7 @@ async def test_iter_next_error(mock_snapshot_id: object):
393
393
 
394
394
  assert isinstance(n, BaseNode)
395
395
  n = await run.next()
396
- assert n == snapshot(End(None))
396
+ assert n == snapshot(End(data=None))
397
397
 
398
398
  with pytest.raises(TypeError, match=r'`next` must be called with a `BaseNode` instance, got End\(data=None\).'):
399
399
  await run.next()
@@ -287,7 +287,7 @@ async def test_rerun_node(mock_snapshot_id: object):
287
287
  node = Foo()
288
288
  async with graph.iter(node, persistence=sp) as run:
289
289
  end = await run.next()
290
- assert end == snapshot(End(123))
290
+ assert end == snapshot(End(data=123))
291
291
 
292
292
  msg = "Incorrect snapshot status 'success', must be 'created' or 'pending'."
293
293
  with pytest.raises(GraphNodeStatusError, match=msg):
@@ -0,0 +1,19 @@
1
+ from mcp.server.fastmcp import FastMCP
2
+
3
+ mcp = FastMCP('PydanticAI MCP Server')
4
+
5
+
6
+ @mcp.tool()
7
+ async def celsius_to_fahrenheit(celsius: float) -> float:
8
+ """Convert Celsius to Fahrenheit.
9
+
10
+ Args:
11
+ celsius: Temperature in Celsius
12
+
13
+ Returns:
14
+ Temperature in Fahrenheit
15
+ """
16
+ return (celsius * 9 / 5) + 32
17
+
18
+
19
+ mcp.run()
@@ -59,7 +59,7 @@ def find_filter_examples() -> Iterable[CodeExample]:
59
59
 
60
60
 
61
61
  @pytest.mark.parametrize('example', find_filter_examples(), ids=str)
62
- def test_docs_examples(
62
+ def test_docs_examples( # noqa: C901
63
63
  example: CodeExample,
64
64
  eval_example: EvalExample,
65
65
  mocker: MockerFixture,
@@ -78,6 +78,10 @@ def test_docs_examples(
78
78
  mocker.patch('random.randint', return_value=4)
79
79
  mocker.patch('rich.prompt.Prompt.ask', side_effect=rich_prompt_ask)
80
80
 
81
+ if sys.version_info >= (3, 10):
82
+ mocker.patch('pydantic_ai.mcp.MCPServerHTTP', return_value=MockMCPServer())
83
+ mocker.patch('mcp.server.fastmcp.FastMCP')
84
+
81
85
  env.set('OPENAI_API_KEY', 'testing')
82
86
  env.set('GEMINI_API_KEY', 'testing')
83
87
  env.set('GROQ_API_KEY', 'testing')
@@ -136,10 +140,13 @@ def test_docs_examples(
136
140
  if opt_test.startswith('skip'):
137
141
  print(opt_test[4:].lstrip(' -') or 'running code skipped')
138
142
  else:
143
+ test_globals: dict[str, str] = {}
144
+ if opt_title == 'mcp_client.py':
145
+ test_globals['__name__'] = '__test__'
139
146
  if eval_example.update_examples: # pragma: no cover
140
- module_dict = eval_example.run_print_update(example, call=call_name)
147
+ module_dict = eval_example.run_print_update(example, call=call_name, module_globals=test_globals)
141
148
  else:
142
- module_dict = eval_example.run_print_check(example, call=call_name)
149
+ module_dict = eval_example.run_print_check(example, call=call_name, module_globals=test_globals)
143
150
 
144
151
  os.chdir(cwd)
145
152
  if title := opt_title:
@@ -182,7 +189,22 @@ def rich_prompt_ask(prompt: str, *_args: Any, **_kwargs: Any) -> str:
182
189
  raise ValueError(f'Unexpected prompt: {prompt}')
183
190
 
184
191
 
192
+ class MockMCPServer:
193
+ is_running = True
194
+
195
+ async def __aenter__(self) -> MockMCPServer:
196
+ return self
197
+
198
+ async def __aexit__(self, *args: Any) -> None:
199
+ pass
200
+
201
+ @staticmethod
202
+ async def list_tools() -> list[None]:
203
+ return []
204
+
205
+
185
206
  text_responses: dict[str, str | ToolCallPart] = {
207
+ 'How many days between 2000-01-01 and 2025-03-18?': 'There are 9,208 days between January 1, 2000, and March 18, 2025.',
186
208
  'What is the weather like in West London and in Wiltshire?': (
187
209
  'The weather in West London is raining, while in Wiltshire it is sunny.'
188
210
  ),
@@ -0,0 +1,93 @@
1
+ """Tests for the MCP (Model Context Protocol) server implementation."""
2
+
3
+ import pytest
4
+ from dirty_equals import IsInstance
5
+ from inline_snapshot import snapshot
6
+
7
+ from pydantic_ai.agent import Agent
8
+ from pydantic_ai.exceptions import UserError
9
+ from pydantic_ai.messages import ModelRequest, ModelResponse, TextPart, ToolCallPart, ToolReturnPart, UserPromptPart
10
+
11
+ from .conftest import IsDatetime, try_import
12
+
13
+ with try_import() as imports_successful:
14
+ from mcp.types import CallToolResult, TextContent
15
+
16
+ from pydantic_ai.mcp import MCPServerHTTP, MCPServerStdio
17
+ from pydantic_ai.models.openai import OpenAIModel
18
+ from pydantic_ai.providers.openai import OpenAIProvider
19
+
20
+
21
+ pytestmark = [
22
+ pytest.mark.skipif(not imports_successful(), reason='mcp and openai not installed'),
23
+ pytest.mark.anyio,
24
+ pytest.mark.vcr,
25
+ ]
26
+
27
+
28
+ async def test_stdio_server():
29
+ server = MCPServerStdio('python', ['-m', 'tests.mcp_server'])
30
+ async with server:
31
+ tools = await server.list_tools()
32
+ assert len(tools) == 1
33
+ assert tools[0].name == 'celsius_to_fahrenheit'
34
+ assert tools[0].description.startswith('Convert Celsius to Fahrenheit.')
35
+
36
+ # Test calling the temperature conversion tool
37
+ result = await server.call_tool('celsius_to_fahrenheit', {'celsius': 0})
38
+ assert result.content == snapshot([TextContent(type='text', text='32.0')])
39
+
40
+
41
+ def test_sse_server():
42
+ sse_server = MCPServerHTTP(url='http://localhost:8000/sse')
43
+ assert sse_server.url == 'http://localhost:8000/sse'
44
+
45
+
46
+ async def test_agent_with_stdio_server(allow_model_requests: None, openai_api_key: str):
47
+ server = MCPServerStdio('python', ['-m', 'tests.mcp_server'])
48
+ model = OpenAIModel('gpt-4o', provider=OpenAIProvider(api_key=openai_api_key))
49
+ agent = Agent(model, mcp_servers=[server])
50
+ async with agent.run_mcp_servers():
51
+ result = await agent.run('What is 0 degrees Celsius in Fahrenheit?')
52
+ assert result.data == snapshot('0 degrees Celsius is 32.0 degrees Fahrenheit.')
53
+ assert result.all_messages() == snapshot(
54
+ [
55
+ ModelRequest(
56
+ parts=[UserPromptPart(content='What is 0 degrees Celsius in Fahrenheit?', timestamp=IsDatetime())]
57
+ ),
58
+ ModelResponse(
59
+ parts=[
60
+ ToolCallPart(
61
+ tool_name='celsius_to_fahrenheit',
62
+ args='{"celsius":0}',
63
+ tool_call_id='call_UNesABTXfwIkYdh3HzXWw2wD',
64
+ )
65
+ ],
66
+ model_name='gpt-4o-2024-08-06',
67
+ timestamp=IsDatetime(),
68
+ ),
69
+ ModelRequest(
70
+ parts=[
71
+ ToolReturnPart(
72
+ tool_name='celsius_to_fahrenheit',
73
+ content=IsInstance(CallToolResult),
74
+ tool_call_id='call_UNesABTXfwIkYdh3HzXWw2wD',
75
+ timestamp=IsDatetime(),
76
+ )
77
+ ]
78
+ ),
79
+ ModelResponse(
80
+ parts=[TextPart(content='0 degrees Celsius is 32.0 degrees Fahrenheit.')],
81
+ model_name='gpt-4o-2024-08-06',
82
+ timestamp=IsDatetime(),
83
+ ),
84
+ ]
85
+ )
86
+
87
+
88
+ async def test_agent_with_server_not_running(openai_api_key: str):
89
+ server = MCPServerStdio('python', ['-m', 'tests.mcp_server'])
90
+ model = OpenAIModel('gpt-4o', provider=OpenAIProvider(api_key=openai_api_key))
91
+ agent = Agent(model, mcp_servers=[server])
92
+ with pytest.raises(UserError, match='MCP server is not running'):
93
+ await agent.run('What is 0 degrees Celsius in Fahrenheit?')
@@ -6,8 +6,9 @@ import pydantic_core
6
6
  import pytest
7
7
  from _pytest.logging import LogCaptureFixture
8
8
  from inline_snapshot import snapshot
9
- from pydantic import BaseModel, Field
10
- from pydantic_core import PydanticSerializationError
9
+ from pydantic import BaseModel, Field, WithJsonSchema
10
+ from pydantic.json_schema import GenerateJsonSchema, JsonSchemaValue
11
+ from pydantic_core import PydanticSerializationError, core_schema
11
12
 
12
13
  from pydantic_ai import Agent, RunContext, Tool, UserError
13
14
  from pydantic_ai.messages import (
@@ -100,8 +101,8 @@ def test_docstring_google(docstring_format: Literal['google', 'auto']):
100
101
  'description': 'Do foobar stuff, a lot.',
101
102
  'parameters_json_schema': {
102
103
  'properties': {
103
- 'foo': {'description': 'The foo thing.', 'title': 'Foo', 'type': 'integer'},
104
- 'bar': {'description': 'The bar thing.', 'title': 'Bar', 'type': 'string'},
104
+ 'foo': {'description': 'The foo thing.', 'type': 'integer'},
105
+ 'bar': {'description': 'The bar thing.', 'type': 'string'},
105
106
  },
106
107
  'required': ['foo', 'bar'],
107
108
  'type': 'object',
@@ -138,7 +139,7 @@ def test_docstring_sphinx(docstring_format: Literal['sphinx', 'auto']):
138
139
  'name': 'sphinx_style_docstring',
139
140
  'description': 'Sphinx style docstring.',
140
141
  'parameters_json_schema': {
141
- 'properties': {'foo': {'description': 'The foo thing.', 'title': 'Foo', 'type': 'integer'}},
142
+ 'properties': {'foo': {'description': 'The foo thing.', 'type': 'integer'}},
142
143
  'required': ['foo'],
143
144
  'type': 'object',
144
145
  'additionalProperties': False,
@@ -174,8 +175,8 @@ def test_docstring_numpy(docstring_format: Literal['numpy', 'auto']):
174
175
  'description': 'Numpy style docstring.',
175
176
  'parameters_json_schema': {
176
177
  'properties': {
177
- 'foo': {'description': 'The foo thing.', 'title': 'Foo', 'type': 'integer'},
178
- 'bar': {'description': 'The bar thing.', 'title': 'Bar', 'type': 'string'},
178
+ 'foo': {'description': 'The foo thing.', 'type': 'integer'},
179
+ 'bar': {'description': 'The bar thing.', 'type': 'string'},
179
180
  },
180
181
  'required': ['foo', 'bar'],
181
182
  'type': 'object',
@@ -234,8 +235,8 @@ def test_docstring_google_no_body(docstring_format: Literal['google', 'auto']):
234
235
  'description': '',
235
236
  'parameters_json_schema': {
236
237
  'properties': {
237
- 'foo': {'description': 'The foo thing.', 'title': 'Foo', 'type': 'integer'},
238
- 'bar': {'description': 'from fields', 'title': 'Bar', 'type': 'string'},
238
+ 'foo': {'description': 'The foo thing.', 'type': 'integer'},
239
+ 'bar': {'description': 'from fields', 'type': 'string'},
239
240
  },
240
241
  'required': ['foo', 'bar'],
241
242
  'type': 'object',
@@ -266,8 +267,8 @@ def test_takes_just_model():
266
267
  'description': None,
267
268
  'parameters_json_schema': {
268
269
  'properties': {
269
- 'x': {'title': 'X', 'type': 'integer'},
270
- 'y': {'title': 'Y', 'type': 'string'},
270
+ 'x': {'type': 'integer'},
271
+ 'y': {'type': 'string'},
271
272
  },
272
273
  'required': ['x', 'y'],
273
274
  'title': 'Foo',
@@ -298,8 +299,8 @@ def test_takes_model_and_int():
298
299
  '$defs': {
299
300
  'Foo': {
300
301
  'properties': {
301
- 'x': {'title': 'X', 'type': 'integer'},
302
- 'y': {'title': 'Y', 'type': 'string'},
302
+ 'x': {'type': 'integer'},
303
+ 'y': {'type': 'string'},
303
304
  },
304
305
  'required': ['x', 'y'],
305
306
  'title': 'Foo',
@@ -308,7 +309,7 @@ def test_takes_model_and_int():
308
309
  },
309
310
  'properties': {
310
311
  'model': {'$ref': '#/$defs/Foo'},
311
- 'z': {'title': 'Z', 'type': 'integer'},
312
+ 'z': {'type': 'integer'},
312
313
  },
313
314
  'required': ['model', 'z'],
314
315
  'type': 'object',
@@ -645,7 +646,7 @@ def test_json_schema_required_parameters(set_event_loop: None):
645
646
  'outer_typed_dict_key': None,
646
647
  'parameters_json_schema': {
647
648
  'additionalProperties': False,
648
- 'properties': {'a': {'title': 'A', 'type': 'integer'}, 'b': {'title': 'B', 'type': 'integer'}},
649
+ 'properties': {'a': {'type': 'integer'}, 'b': {'type': 'integer'}},
649
650
  'required': ['a'],
650
651
  'type': 'object',
651
652
  },
@@ -656,7 +657,7 @@ def test_json_schema_required_parameters(set_event_loop: None):
656
657
  'outer_typed_dict_key': None,
657
658
  'parameters_json_schema': {
658
659
  'additionalProperties': False,
659
- 'properties': {'a': {'title': 'A', 'type': 'integer'}, 'b': {'title': 'B', 'type': 'integer'}},
660
+ 'properties': {'a': {'type': 'integer'}, 'b': {'type': 'integer'}},
660
661
  'required': ['b'],
661
662
  'type': 'object',
662
663
  },
@@ -706,3 +707,47 @@ def test_call_tool_without_unrequired_parameters(set_event_loop: None):
706
707
  ]
707
708
  )
708
709
  assert tool_returns == snapshot([15, 17, 51, 68])
710
+
711
+
712
+ def test_schema_generator():
713
+ class MyGenerateJsonSchema(GenerateJsonSchema):
714
+ def typed_dict_schema(self, schema: core_schema.TypedDictSchema) -> JsonSchemaValue:
715
+ # Add useless property titles just to show we can
716
+ s = super().typed_dict_schema(schema)
717
+ for p in s.get('properties', {}):
718
+ s['properties'][p]['title'] = f'{s["properties"][p].get("title")} title'
719
+ return s
720
+
721
+ agent = Agent(FunctionModel(get_json_schema))
722
+
723
+ def my_tool(x: Annotated[Union[str, None], WithJsonSchema({'type': 'string'})] = None, **kwargs: Any):
724
+ return x # pragma: no cover
725
+
726
+ agent.tool_plain(name='my_tool_1')(my_tool)
727
+ agent.tool_plain(name='my_tool_2', schema_generator=MyGenerateJsonSchema)(my_tool)
728
+
729
+ result = agent.run_sync('Hello')
730
+ json_schema = json.loads(result.data)
731
+ assert json_schema == snapshot(
732
+ [
733
+ {
734
+ 'description': '',
735
+ 'name': 'my_tool_1',
736
+ 'outer_typed_dict_key': None,
737
+ 'parameters_json_schema': {
738
+ 'additionalProperties': True,
739
+ 'properties': {'x': {'type': 'string'}},
740
+ 'type': 'object',
741
+ },
742
+ },
743
+ {
744
+ 'description': '',
745
+ 'name': 'my_tool_2',
746
+ 'outer_typed_dict_key': None,
747
+ 'parameters_json_schema': {
748
+ 'properties': {'x': {'type': 'string', 'title': 'X title'}},
749
+ 'type': 'object',
750
+ },
751
+ },
752
+ ]
753
+ )
File without changes
File without changes