pydantic-ai-slim 0.2.19__tar.gz → 0.2.20__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-slim might be problematic. Click here for more details.

Files changed (76) hide show
  1. {pydantic_ai_slim-0.2.19 → pydantic_ai_slim-0.2.20}/PKG-INFO +4 -4
  2. {pydantic_ai_slim-0.2.19 → pydantic_ai_slim-0.2.20}/pydantic_ai/_agent_graph.py +43 -9
  3. {pydantic_ai_slim-0.2.19 → pydantic_ai_slim-0.2.20}/pydantic_ai/_function_schema.py +12 -3
  4. {pydantic_ai_slim-0.2.19 → pydantic_ai_slim-0.2.20}/pydantic_ai/agent.py +3 -3
  5. {pydantic_ai_slim-0.2.19 → pydantic_ai_slim-0.2.20}/pydantic_ai/mcp.py +66 -5
  6. {pydantic_ai_slim-0.2.19 → pydantic_ai_slim-0.2.20}/pydantic_ai/models/google.py +13 -3
  7. {pydantic_ai_slim-0.2.19 → pydantic_ai_slim-0.2.20}/.gitignore +0 -0
  8. {pydantic_ai_slim-0.2.19 → pydantic_ai_slim-0.2.20}/LICENSE +0 -0
  9. {pydantic_ai_slim-0.2.19 → pydantic_ai_slim-0.2.20}/README.md +0 -0
  10. {pydantic_ai_slim-0.2.19 → pydantic_ai_slim-0.2.20}/pydantic_ai/__init__.py +0 -0
  11. {pydantic_ai_slim-0.2.19 → pydantic_ai_slim-0.2.20}/pydantic_ai/__main__.py +0 -0
  12. {pydantic_ai_slim-0.2.19 → pydantic_ai_slim-0.2.20}/pydantic_ai/_a2a.py +0 -0
  13. {pydantic_ai_slim-0.2.19 → pydantic_ai_slim-0.2.20}/pydantic_ai/_cli.py +0 -0
  14. {pydantic_ai_slim-0.2.19 → pydantic_ai_slim-0.2.20}/pydantic_ai/_griffe.py +0 -0
  15. {pydantic_ai_slim-0.2.19 → pydantic_ai_slim-0.2.20}/pydantic_ai/_output.py +0 -0
  16. {pydantic_ai_slim-0.2.19 → pydantic_ai_slim-0.2.20}/pydantic_ai/_parts_manager.py +0 -0
  17. {pydantic_ai_slim-0.2.19 → pydantic_ai_slim-0.2.20}/pydantic_ai/_system_prompt.py +0 -0
  18. {pydantic_ai_slim-0.2.19 → pydantic_ai_slim-0.2.20}/pydantic_ai/_utils.py +0 -0
  19. {pydantic_ai_slim-0.2.19 → pydantic_ai_slim-0.2.20}/pydantic_ai/common_tools/__init__.py +0 -0
  20. {pydantic_ai_slim-0.2.19 → pydantic_ai_slim-0.2.20}/pydantic_ai/common_tools/duckduckgo.py +0 -0
  21. {pydantic_ai_slim-0.2.19 → pydantic_ai_slim-0.2.20}/pydantic_ai/common_tools/tavily.py +0 -0
  22. {pydantic_ai_slim-0.2.19 → pydantic_ai_slim-0.2.20}/pydantic_ai/direct.py +0 -0
  23. {pydantic_ai_slim-0.2.19 → pydantic_ai_slim-0.2.20}/pydantic_ai/exceptions.py +0 -0
  24. {pydantic_ai_slim-0.2.19 → pydantic_ai_slim-0.2.20}/pydantic_ai/ext/__init__.py +0 -0
  25. {pydantic_ai_slim-0.2.19 → pydantic_ai_slim-0.2.20}/pydantic_ai/ext/langchain.py +0 -0
  26. {pydantic_ai_slim-0.2.19 → pydantic_ai_slim-0.2.20}/pydantic_ai/format_as_xml.py +0 -0
  27. {pydantic_ai_slim-0.2.19 → pydantic_ai_slim-0.2.20}/pydantic_ai/format_prompt.py +0 -0
  28. {pydantic_ai_slim-0.2.19 → pydantic_ai_slim-0.2.20}/pydantic_ai/messages.py +0 -0
  29. {pydantic_ai_slim-0.2.19 → pydantic_ai_slim-0.2.20}/pydantic_ai/models/__init__.py +0 -0
  30. {pydantic_ai_slim-0.2.19 → pydantic_ai_slim-0.2.20}/pydantic_ai/models/anthropic.py +0 -0
  31. {pydantic_ai_slim-0.2.19 → pydantic_ai_slim-0.2.20}/pydantic_ai/models/bedrock.py +0 -0
  32. {pydantic_ai_slim-0.2.19 → pydantic_ai_slim-0.2.20}/pydantic_ai/models/cohere.py +0 -0
  33. {pydantic_ai_slim-0.2.19 → pydantic_ai_slim-0.2.20}/pydantic_ai/models/fallback.py +0 -0
  34. {pydantic_ai_slim-0.2.19 → pydantic_ai_slim-0.2.20}/pydantic_ai/models/function.py +0 -0
  35. {pydantic_ai_slim-0.2.19 → pydantic_ai_slim-0.2.20}/pydantic_ai/models/gemini.py +0 -0
  36. {pydantic_ai_slim-0.2.19 → pydantic_ai_slim-0.2.20}/pydantic_ai/models/groq.py +0 -0
  37. {pydantic_ai_slim-0.2.19 → pydantic_ai_slim-0.2.20}/pydantic_ai/models/instrumented.py +0 -0
  38. {pydantic_ai_slim-0.2.19 → pydantic_ai_slim-0.2.20}/pydantic_ai/models/mistral.py +0 -0
  39. {pydantic_ai_slim-0.2.19 → pydantic_ai_slim-0.2.20}/pydantic_ai/models/openai.py +0 -0
  40. {pydantic_ai_slim-0.2.19 → pydantic_ai_slim-0.2.20}/pydantic_ai/models/test.py +0 -0
  41. {pydantic_ai_slim-0.2.19 → pydantic_ai_slim-0.2.20}/pydantic_ai/models/wrapper.py +0 -0
  42. {pydantic_ai_slim-0.2.19 → pydantic_ai_slim-0.2.20}/pydantic_ai/profiles/__init__.py +0 -0
  43. {pydantic_ai_slim-0.2.19 → pydantic_ai_slim-0.2.20}/pydantic_ai/profiles/_json_schema.py +0 -0
  44. {pydantic_ai_slim-0.2.19 → pydantic_ai_slim-0.2.20}/pydantic_ai/profiles/amazon.py +0 -0
  45. {pydantic_ai_slim-0.2.19 → pydantic_ai_slim-0.2.20}/pydantic_ai/profiles/anthropic.py +0 -0
  46. {pydantic_ai_slim-0.2.19 → pydantic_ai_slim-0.2.20}/pydantic_ai/profiles/cohere.py +0 -0
  47. {pydantic_ai_slim-0.2.19 → pydantic_ai_slim-0.2.20}/pydantic_ai/profiles/deepseek.py +0 -0
  48. {pydantic_ai_slim-0.2.19 → pydantic_ai_slim-0.2.20}/pydantic_ai/profiles/google.py +0 -0
  49. {pydantic_ai_slim-0.2.19 → pydantic_ai_slim-0.2.20}/pydantic_ai/profiles/grok.py +0 -0
  50. {pydantic_ai_slim-0.2.19 → pydantic_ai_slim-0.2.20}/pydantic_ai/profiles/meta.py +0 -0
  51. {pydantic_ai_slim-0.2.19 → pydantic_ai_slim-0.2.20}/pydantic_ai/profiles/mistral.py +0 -0
  52. {pydantic_ai_slim-0.2.19 → pydantic_ai_slim-0.2.20}/pydantic_ai/profiles/openai.py +0 -0
  53. {pydantic_ai_slim-0.2.19 → pydantic_ai_slim-0.2.20}/pydantic_ai/profiles/qwen.py +0 -0
  54. {pydantic_ai_slim-0.2.19 → pydantic_ai_slim-0.2.20}/pydantic_ai/providers/__init__.py +0 -0
  55. {pydantic_ai_slim-0.2.19 → pydantic_ai_slim-0.2.20}/pydantic_ai/providers/anthropic.py +0 -0
  56. {pydantic_ai_slim-0.2.19 → pydantic_ai_slim-0.2.20}/pydantic_ai/providers/azure.py +0 -0
  57. {pydantic_ai_slim-0.2.19 → pydantic_ai_slim-0.2.20}/pydantic_ai/providers/bedrock.py +0 -0
  58. {pydantic_ai_slim-0.2.19 → pydantic_ai_slim-0.2.20}/pydantic_ai/providers/cohere.py +0 -0
  59. {pydantic_ai_slim-0.2.19 → pydantic_ai_slim-0.2.20}/pydantic_ai/providers/deepseek.py +0 -0
  60. {pydantic_ai_slim-0.2.19 → pydantic_ai_slim-0.2.20}/pydantic_ai/providers/fireworks.py +0 -0
  61. {pydantic_ai_slim-0.2.19 → pydantic_ai_slim-0.2.20}/pydantic_ai/providers/google.py +0 -0
  62. {pydantic_ai_slim-0.2.19 → pydantic_ai_slim-0.2.20}/pydantic_ai/providers/google_gla.py +0 -0
  63. {pydantic_ai_slim-0.2.19 → pydantic_ai_slim-0.2.20}/pydantic_ai/providers/google_vertex.py +0 -0
  64. {pydantic_ai_slim-0.2.19 → pydantic_ai_slim-0.2.20}/pydantic_ai/providers/grok.py +0 -0
  65. {pydantic_ai_slim-0.2.19 → pydantic_ai_slim-0.2.20}/pydantic_ai/providers/groq.py +0 -0
  66. {pydantic_ai_slim-0.2.19 → pydantic_ai_slim-0.2.20}/pydantic_ai/providers/heroku.py +0 -0
  67. {pydantic_ai_slim-0.2.19 → pydantic_ai_slim-0.2.20}/pydantic_ai/providers/mistral.py +0 -0
  68. {pydantic_ai_slim-0.2.19 → pydantic_ai_slim-0.2.20}/pydantic_ai/providers/openai.py +0 -0
  69. {pydantic_ai_slim-0.2.19 → pydantic_ai_slim-0.2.20}/pydantic_ai/providers/openrouter.py +0 -0
  70. {pydantic_ai_slim-0.2.19 → pydantic_ai_slim-0.2.20}/pydantic_ai/providers/together.py +0 -0
  71. {pydantic_ai_slim-0.2.19 → pydantic_ai_slim-0.2.20}/pydantic_ai/py.typed +0 -0
  72. {pydantic_ai_slim-0.2.19 → pydantic_ai_slim-0.2.20}/pydantic_ai/result.py +0 -0
  73. {pydantic_ai_slim-0.2.19 → pydantic_ai_slim-0.2.20}/pydantic_ai/settings.py +0 -0
  74. {pydantic_ai_slim-0.2.19 → pydantic_ai_slim-0.2.20}/pydantic_ai/tools.py +0 -0
  75. {pydantic_ai_slim-0.2.19 → pydantic_ai_slim-0.2.20}/pydantic_ai/usage.py +0 -0
  76. {pydantic_ai_slim-0.2.19 → pydantic_ai_slim-0.2.20}/pyproject.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pydantic-ai-slim
3
- Version: 0.2.19
3
+ Version: 0.2.20
4
4
  Summary: Agent Framework / shim to use Pydantic with LLMs, slim package
5
5
  Author-email: Samuel Colvin <samuel@pydantic.dev>, Marcelo Trylesinski <marcelotryle@gmail.com>, David Montague <david@pydantic.dev>, Alex Hall <alex@pydantic.dev>
6
6
  License-Expression: MIT
@@ -30,11 +30,11 @@ Requires-Dist: exceptiongroup; python_version < '3.11'
30
30
  Requires-Dist: griffe>=1.3.2
31
31
  Requires-Dist: httpx>=0.27
32
32
  Requires-Dist: opentelemetry-api>=1.28.0
33
- Requires-Dist: pydantic-graph==0.2.19
33
+ Requires-Dist: pydantic-graph==0.2.20
34
34
  Requires-Dist: pydantic>=2.10
35
35
  Requires-Dist: typing-inspection>=0.4.0
36
36
  Provides-Extra: a2a
37
- Requires-Dist: fasta2a==0.2.19; extra == 'a2a'
37
+ Requires-Dist: fasta2a==0.2.20; extra == 'a2a'
38
38
  Provides-Extra: anthropic
39
39
  Requires-Dist: anthropic>=0.52.0; extra == 'anthropic'
40
40
  Provides-Extra: bedrock
@@ -48,7 +48,7 @@ Requires-Dist: cohere>=5.13.11; (platform_system != 'Emscripten') and extra == '
48
48
  Provides-Extra: duckduckgo
49
49
  Requires-Dist: duckduckgo-search>=7.0.0; extra == 'duckduckgo'
50
50
  Provides-Extra: evals
51
- Requires-Dist: pydantic-evals==0.2.19; extra == 'evals'
51
+ Requires-Dist: pydantic-evals==0.2.20; extra == 'evals'
52
52
  Provides-Extra: google
53
53
  Requires-Dist: google-genai>=1.15.0; extra == 'google'
54
54
  Provides-Extra: groq
@@ -12,6 +12,7 @@ from typing import TYPE_CHECKING, Any, Callable, Generic, Literal, Union, cast
12
12
  from opentelemetry.trace import Tracer
13
13
  from typing_extensions import TypeGuard, TypeVar, assert_never
14
14
 
15
+ from pydantic_ai._function_schema import _takes_ctx as is_takes_ctx # type: ignore
15
16
  from pydantic_ai._utils import is_async_callable, run_in_executor
16
17
  from pydantic_graph import BaseNode, Graph, GraphRunContext
17
18
  from pydantic_graph.nodes import End, NodeRunEndT
@@ -50,8 +51,20 @@ OutputT = TypeVar('OutputT')
50
51
 
51
52
  _HistoryProcessorSync = Callable[[list[_messages.ModelMessage]], list[_messages.ModelMessage]]
52
53
  _HistoryProcessorAsync = Callable[[list[_messages.ModelMessage]], Awaitable[list[_messages.ModelMessage]]]
53
- HistoryProcessor = Union[_HistoryProcessorSync, _HistoryProcessorAsync]
54
- """A function that processes a list of model messages and returns a list of model messages."""
54
+ _HistoryProcessorSyncWithCtx = Callable[[RunContext[DepsT], list[_messages.ModelMessage]], list[_messages.ModelMessage]]
55
+ _HistoryProcessorAsyncWithCtx = Callable[
56
+ [RunContext[DepsT], list[_messages.ModelMessage]], Awaitable[list[_messages.ModelMessage]]
57
+ ]
58
+ HistoryProcessor = Union[
59
+ _HistoryProcessorSync,
60
+ _HistoryProcessorAsync,
61
+ _HistoryProcessorSyncWithCtx[DepsT],
62
+ _HistoryProcessorAsyncWithCtx[DepsT],
63
+ ]
64
+ """A function that processes a list of model messages and returns a list of model messages.
65
+
66
+ Can optionally accept a `RunContext` as a parameter.
67
+ """
55
68
 
56
69
 
57
70
  @dataclasses.dataclass
@@ -92,7 +105,7 @@ class GraphAgentDeps(Generic[DepsT, OutputDataT]):
92
105
  output_schema: _output.OutputSchema[OutputDataT] | None
93
106
  output_validators: list[_output.OutputValidator[DepsT, OutputDataT]]
94
107
 
95
- history_processors: Sequence[HistoryProcessor]
108
+ history_processors: Sequence[HistoryProcessor[DepsT]]
96
109
 
97
110
  function_tools: dict[str, Tool[DepsT]] = dataclasses.field(repr=False)
98
111
  mcp_servers: Sequence[MCPServer] = dataclasses.field(repr=False)
@@ -328,7 +341,9 @@ class ModelRequestNode(AgentNode[DepsT, NodeRunEndT]):
328
341
 
329
342
  model_settings, model_request_parameters = await self._prepare_request(ctx)
330
343
  model_request_parameters = ctx.deps.model.customize_request_parameters(model_request_parameters)
331
- message_history = await _process_message_history(ctx.state.message_history, ctx.deps.history_processors)
344
+ message_history = await _process_message_history(
345
+ ctx.state.message_history, ctx.deps.history_processors, build_run_context(ctx)
346
+ )
332
347
  async with ctx.deps.model.request_stream(
333
348
  message_history, model_settings, model_request_parameters
334
349
  ) as streamed_response:
@@ -352,7 +367,9 @@ class ModelRequestNode(AgentNode[DepsT, NodeRunEndT]):
352
367
 
353
368
  model_settings, model_request_parameters = await self._prepare_request(ctx)
354
369
  model_request_parameters = ctx.deps.model.customize_request_parameters(model_request_parameters)
355
- message_history = await _process_message_history(ctx.state.message_history, ctx.deps.history_processors)
370
+ message_history = await _process_message_history(
371
+ ctx.state.message_history, ctx.deps.history_processors, build_run_context(ctx)
372
+ )
356
373
  model_response = await ctx.deps.model.request(message_history, model_settings, model_request_parameters)
357
374
  ctx.state.usage.incr(_usage.Usage())
358
375
 
@@ -762,7 +779,12 @@ async def _tool_from_mcp_server(
762
779
  # some weird edge case occurs.
763
780
  if not server.is_running: # pragma: no cover
764
781
  raise exceptions.UserError(f'MCP server is not running: {server}')
765
- result = await server.call_tool(tool_name, args)
782
+
783
+ if server.process_tool_call is not None:
784
+ result = await server.process_tool_call(ctx, server.call_tool, tool_name, args)
785
+ else:
786
+ result = await server.call_tool(tool_name, args)
787
+
766
788
  return result
767
789
 
768
790
  for server in ctx.deps.mcp_servers:
@@ -876,12 +898,24 @@ def build_agent_graph(
876
898
 
877
899
  async def _process_message_history(
878
900
  messages: list[_messages.ModelMessage],
879
- processors: Sequence[HistoryProcessor],
901
+ processors: Sequence[HistoryProcessor[DepsT]],
902
+ run_context: RunContext[DepsT],
880
903
  ) -> list[_messages.ModelMessage]:
881
904
  """Process message history through a sequence of processors."""
882
905
  for processor in processors:
906
+ takes_ctx = is_takes_ctx(processor)
907
+
883
908
  if is_async_callable(processor):
884
- messages = await processor(messages)
909
+ if takes_ctx:
910
+ messages = await processor(run_context, messages)
911
+ else:
912
+ async_processor = cast(_HistoryProcessorAsync, processor)
913
+ messages = await async_processor(messages)
885
914
  else:
886
- messages = await run_in_executor(processor, messages)
915
+ if takes_ctx:
916
+ sync_processor_with_ctx = cast(_HistoryProcessorSyncWithCtx[DepsT], processor)
917
+ messages = await run_in_executor(sync_processor_with_ctx, run_context, messages)
918
+ else:
919
+ sync_processor = cast(_HistoryProcessorSync, processor)
920
+ messages = await run_in_executor(sync_processor, messages)
887
921
  return messages
@@ -8,7 +8,7 @@ from __future__ import annotations as _annotations
8
8
  from collections.abc import Awaitable
9
9
  from dataclasses import dataclass, field
10
10
  from inspect import Parameter, signature
11
- from typing import TYPE_CHECKING, Any, Callable, cast
11
+ from typing import TYPE_CHECKING, Any, Callable, Union, cast
12
12
 
13
13
  from pydantic import ConfigDict
14
14
  from pydantic._internal import _decorators, _generate_schema, _typing_extra
@@ -17,7 +17,7 @@ from pydantic.fields import FieldInfo
17
17
  from pydantic.json_schema import GenerateJsonSchema
18
18
  from pydantic.plugin._schema_validator import create_schema_validator
19
19
  from pydantic_core import SchemaValidator, core_schema
20
- from typing_extensions import get_origin
20
+ from typing_extensions import Concatenate, ParamSpec, TypeIs, TypeVar, get_origin
21
21
 
22
22
  from pydantic_ai.tools import RunContext
23
23
 
@@ -218,7 +218,16 @@ def function_schema( # noqa: C901
218
218
  )
219
219
 
220
220
 
221
- def _takes_ctx(function: Callable[..., Any]) -> bool:
221
+ P = ParamSpec('P')
222
+ R = TypeVar('R')
223
+
224
+
225
+ WithCtx = Callable[Concatenate[RunContext[Any], P], R]
226
+ WithoutCtx = Callable[P, R]
227
+ TargetFunc = Union[WithCtx[P, R], WithoutCtx[P, R]]
228
+
229
+
230
+ def _takes_ctx(function: TargetFunc[P, R]) -> TypeIs[WithCtx[P, R]]:
222
231
  """Check if a function takes a `RunContext` first argument.
223
232
 
224
233
  Args:
@@ -180,7 +180,7 @@ class Agent(Generic[AgentDepsT, OutputDataT]):
180
180
  defer_model_check: bool = False,
181
181
  end_strategy: EndStrategy = 'early',
182
182
  instrument: InstrumentationSettings | bool | None = None,
183
- history_processors: Sequence[HistoryProcessor] | None = None,
183
+ history_processors: Sequence[HistoryProcessor[AgentDepsT]] | None = None,
184
184
  ) -> None: ...
185
185
 
186
186
  @overload
@@ -210,7 +210,7 @@ class Agent(Generic[AgentDepsT, OutputDataT]):
210
210
  defer_model_check: bool = False,
211
211
  end_strategy: EndStrategy = 'early',
212
212
  instrument: InstrumentationSettings | bool | None = None,
213
- history_processors: Sequence[HistoryProcessor] | None = None,
213
+ history_processors: Sequence[HistoryProcessor[AgentDepsT]] | None = None,
214
214
  ) -> None: ...
215
215
 
216
216
  def __init__(
@@ -235,7 +235,7 @@ class Agent(Generic[AgentDepsT, OutputDataT]):
235
235
  defer_model_check: bool = False,
236
236
  end_strategy: EndStrategy = 'early',
237
237
  instrument: InstrumentationSettings | bool | None = None,
238
- history_processors: Sequence[HistoryProcessor] | None = None,
238
+ history_processors: Sequence[HistoryProcessor[AgentDepsT]] | None = None,
239
239
  **_deprecated_kwargs: Any,
240
240
  ):
241
241
  """Create an agent.
@@ -4,7 +4,7 @@ import base64
4
4
  import functools
5
5
  import json
6
6
  from abc import ABC, abstractmethod
7
- from collections.abc import AsyncIterator, Sequence
7
+ from collections.abc import AsyncIterator, Awaitable, Sequence
8
8
  from contextlib import AbstractAsyncContextManager, AsyncExitStack, asynccontextmanager
9
9
  from dataclasses import dataclass
10
10
  from pathlib import Path
@@ -15,14 +15,20 @@ import anyio
15
15
  import httpx
16
16
  from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
17
17
  from mcp.client.streamable_http import GetSessionIdCallback, streamablehttp_client
18
+ from mcp.shared.exceptions import McpError
18
19
  from mcp.shared.message import SessionMessage
19
20
  from mcp.types import (
20
21
  AudioContent,
21
22
  BlobResourceContents,
23
+ CallToolRequest,
24
+ CallToolRequestParams,
25
+ CallToolResult,
26
+ ClientRequest,
22
27
  Content,
23
28
  EmbeddedResource,
24
29
  ImageContent,
25
30
  LoggingLevel,
31
+ RequestParams,
26
32
  TextContent,
27
33
  TextResourceContents,
28
34
  )
@@ -30,7 +36,7 @@ from typing_extensions import Self, assert_never, deprecated
30
36
 
31
37
  from pydantic_ai.exceptions import ModelRetry
32
38
  from pydantic_ai.messages import BinaryContent
33
- from pydantic_ai.tools import ToolDefinition
39
+ from pydantic_ai.tools import RunContext, ToolDefinition
34
40
 
35
41
  try:
36
42
  from mcp.client.session import ClientSession
@@ -60,6 +66,9 @@ class MCPServer(ABC):
60
66
  e.g. if `tool_prefix='foo'`, then a tool named `bar` will be registered as `foo_bar`
61
67
  """
62
68
 
69
+ process_tool_call: ProcessToolCallback | None = None
70
+ """Hook to customize tool calling and optionally pass extra metadata."""
71
+
63
72
  _client: ClientSession
64
73
  _read_stream: MemoryObjectReceiveStream[SessionMessage | Exception]
65
74
  _write_stream: MemoryObjectSendStream[SessionMessage]
@@ -113,13 +122,17 @@ class MCPServer(ABC):
113
122
  ]
114
123
 
115
124
  async def call_tool(
116
- self, tool_name: str, arguments: dict[str, Any]
117
- ) -> str | BinaryContent | dict[str, Any] | list[Any] | Sequence[str | BinaryContent | dict[str, Any] | list[Any]]:
125
+ self,
126
+ tool_name: str,
127
+ arguments: dict[str, Any],
128
+ metadata: dict[str, Any] | None = None,
129
+ ) -> ToolResult:
118
130
  """Call a tool on the server.
119
131
 
120
132
  Args:
121
133
  tool_name: The name of the tool to call.
122
134
  arguments: The arguments to pass to the tool.
135
+ metadata: Request-level metadata (optional)
123
136
 
124
137
  Returns:
125
138
  The result of the tool call.
@@ -127,7 +140,23 @@ class MCPServer(ABC):
127
140
  Raises:
128
141
  ModelRetry: If the tool call fails.
129
142
  """
130
- result = await self._client.call_tool(self.get_unprefixed_tool_name(tool_name), arguments)
143
+ try:
144
+ # meta param is not provided by session yet, so build and can send_request directly.
145
+ result = await self._client.send_request(
146
+ ClientRequest(
147
+ CallToolRequest(
148
+ method='tools/call',
149
+ params=CallToolRequestParams(
150
+ name=self.get_unprefixed_tool_name(tool_name),
151
+ arguments=arguments,
152
+ _meta=RequestParams.Meta(**metadata) if metadata else None,
153
+ ),
154
+ )
155
+ ),
156
+ CallToolResult,
157
+ )
158
+ except McpError as e:
159
+ raise ModelRetry(e.error.message)
131
160
 
132
161
  content = [self._map_tool_result_part(part) for part in result.content]
133
162
 
@@ -265,6 +294,9 @@ class MCPServerStdio(MCPServer):
265
294
  e.g. if `tool_prefix='foo'`, then a tool named `bar` will be registered as `foo_bar`
266
295
  """
267
296
 
297
+ process_tool_call: ProcessToolCallback | None = None
298
+ """Hook to customize tool calling and optionally pass extra metadata."""
299
+
268
300
  timeout: float = 5
269
301
  """ The timeout in seconds to wait for the client to initialize."""
270
302
 
@@ -359,6 +391,9 @@ class _MCPServerHTTP(MCPServer):
359
391
  For example, if `tool_prefix='foo'`, then a tool named `bar` will be registered as `foo_bar`
360
392
  """
361
393
 
394
+ process_tool_call: ProcessToolCallback | None = None
395
+ """Hook to customize tool calling and optionally pass extra metadata."""
396
+
362
397
  @property
363
398
  @abstractmethod
364
399
  def _transport_client(
@@ -517,3 +552,29 @@ class MCPServerStreamableHTTP(_MCPServerHTTP):
517
552
  @property
518
553
  def _transport_client(self):
519
554
  return streamablehttp_client # pragma: no cover
555
+
556
+
557
+ ToolResult = (
558
+ str | BinaryContent | dict[str, Any] | list[Any] | Sequence[str | BinaryContent | dict[str, Any] | list[Any]]
559
+ )
560
+ """The result type of a tool call."""
561
+
562
+ CallToolFunc = Callable[[str, dict[str, Any], dict[str, Any] | None], Awaitable[ToolResult]]
563
+ """A function type that represents a tool call."""
564
+
565
+ ProcessToolCallback = Callable[
566
+ [
567
+ RunContext[Any],
568
+ CallToolFunc,
569
+ str,
570
+ dict[str, Any],
571
+ ],
572
+ Awaitable[ToolResult],
573
+ ]
574
+ """A process tool callback.
575
+
576
+ It accepts a run context, the original tool call function, a tool name, and arguments.
577
+
578
+ Allows wrapping an MCP server tool call to customize it, including adding extra request
579
+ metadata.
580
+ """
@@ -10,9 +10,8 @@ from uuid import uuid4
10
10
 
11
11
  from typing_extensions import assert_never
12
12
 
13
- from pydantic_ai.providers import Provider
14
-
15
13
  from .. import UnexpectedModelBehavior, _utils, usage
14
+ from ..exceptions import UserError
16
15
  from ..messages import (
17
16
  BinaryContent,
18
17
  FileUrl,
@@ -30,6 +29,7 @@ from ..messages import (
30
29
  VideoUrl,
31
30
  )
32
31
  from ..profiles import ModelProfileSpec
32
+ from ..providers import Provider
33
33
  from ..settings import ModelSettings
34
34
  from ..tools import ToolDefinition
35
35
  from . import (
@@ -52,6 +52,7 @@ try:
52
52
  FunctionDeclarationDict,
53
53
  GenerateContentConfigDict,
54
54
  GenerateContentResponse,
55
+ HttpOptionsDict,
55
56
  Part,
56
57
  PartDict,
57
58
  SafetySettingDict,
@@ -252,8 +253,17 @@ class GoogleModel(Model):
252
253
  tool_config = self._get_tool_config(model_request_parameters, tools)
253
254
  system_instruction, contents = await self._map_messages(messages)
254
255
 
256
+ http_options: HttpOptionsDict = {
257
+ 'headers': {'Content-Type': 'application/json', 'User-Agent': get_user_agent()}
258
+ }
259
+ if timeout := model_settings.get('timeout'):
260
+ if isinstance(timeout, (int, float)):
261
+ http_options['timeout'] = int(1000 * timeout)
262
+ else:
263
+ raise UserError('Google does not support setting ModelSettings.timeout to a httpx.Timeout')
264
+
255
265
  config = GenerateContentConfigDict(
256
- http_options={'headers': {'Content-Type': 'application/json', 'User-Agent': get_user_agent()}},
266
+ http_options=http_options,
257
267
  system_instruction=system_instruction,
258
268
  temperature=model_settings.get('temperature'),
259
269
  top_p=model_settings.get('top_p'),