pydantic-ai-slim 1.3.0__py3-none-any.whl → 1.5.0__py3-none-any.whl

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.

pydantic_ai/__init__.py CHANGED
@@ -12,6 +12,7 @@ from .agent import (
12
12
  from .builtin_tools import (
13
13
  CodeExecutionTool,
14
14
  ImageGenerationTool,
15
+ MCPServerTool,
15
16
  MemoryTool,
16
17
  UrlContextTool,
17
18
  WebSearchTool,
@@ -213,6 +214,7 @@ __all__ = (
213
214
  'CodeExecutionTool',
214
215
  'ImageGenerationTool',
215
216
  'MemoryTool',
217
+ 'MCPServerTool',
216
218
  # output
217
219
  'ToolOutput',
218
220
  'NativeOutput',
@@ -20,7 +20,8 @@ from pydantic_ai._instrumentation import DEFAULT_INSTRUMENTATION_VERSION
20
20
  from pydantic_ai._tool_manager import ToolManager
21
21
  from pydantic_ai._utils import dataclasses_no_defaults_repr, get_union_args, is_async_callable, run_in_executor
22
22
  from pydantic_ai.builtin_tools import AbstractBuiltinTool
23
- from pydantic_graph import BaseNode, Graph, GraphRunContext
23
+ from pydantic_graph import BaseNode, GraphRunContext
24
+ from pydantic_graph.beta import Graph, GraphBuilder
24
25
  from pydantic_graph.nodes import End, NodeRunEndT
25
26
 
26
27
  from . import _output, _system_prompt, exceptions, messages as _messages, models, result, usage as _usage
@@ -588,7 +589,11 @@ class CallToolsNode(AgentNode[DepsT, NodeRunEndT]):
588
589
  # as the empty response and request will not create any items in the API payload,
589
590
  # in the hope the model will return a non-empty response this time.
590
591
  ctx.state.increment_retries(ctx.deps.max_result_retries, model_settings=ctx.deps.model_settings)
591
- self._next_node = ModelRequestNode[DepsT, NodeRunEndT](_messages.ModelRequest(parts=[]))
592
+ run_context = build_run_context(ctx)
593
+ instructions = await ctx.deps.get_instructions(run_context)
594
+ self._next_node = ModelRequestNode[DepsT, NodeRunEndT](
595
+ _messages.ModelRequest(parts=[], instructions=instructions)
596
+ )
592
597
  return
593
598
 
594
599
  text = ''
@@ -652,7 +657,11 @@ class CallToolsNode(AgentNode[DepsT, NodeRunEndT]):
652
657
  ctx.state.increment_retries(
653
658
  ctx.deps.max_result_retries, error=e, model_settings=ctx.deps.model_settings
654
659
  )
655
- self._next_node = ModelRequestNode[DepsT, NodeRunEndT](_messages.ModelRequest(parts=[e.tool_retry]))
660
+ run_context = build_run_context(ctx)
661
+ instructions = await ctx.deps.get_instructions(run_context)
662
+ self._next_node = ModelRequestNode[DepsT, NodeRunEndT](
663
+ _messages.ModelRequest(parts=[e.tool_retry], instructions=instructions)
664
+ )
656
665
 
657
666
  self._events_iterator = _run_stream()
658
667
 
@@ -1154,22 +1163,32 @@ def build_agent_graph(
1154
1163
  name: str | None,
1155
1164
  deps_type: type[DepsT],
1156
1165
  output_type: OutputSpec[OutputT],
1157
- ) -> Graph[GraphAgentState, GraphAgentDeps[DepsT, result.FinalResult[OutputT]], result.FinalResult[OutputT]]:
1166
+ ) -> Graph[
1167
+ GraphAgentState,
1168
+ GraphAgentDeps[DepsT, OutputT],
1169
+ UserPromptNode[DepsT, OutputT],
1170
+ result.FinalResult[OutputT],
1171
+ ]:
1158
1172
  """Build the execution [Graph][pydantic_graph.Graph] for a given agent."""
1159
- nodes = (
1160
- UserPromptNode[DepsT],
1161
- ModelRequestNode[DepsT],
1162
- CallToolsNode[DepsT],
1163
- SetFinalResult[DepsT],
1164
- )
1165
- graph = Graph[GraphAgentState, GraphAgentDeps[DepsT, Any], result.FinalResult[OutputT]](
1166
- nodes=nodes,
1173
+ g = GraphBuilder(
1167
1174
  name=name or 'Agent',
1168
1175
  state_type=GraphAgentState,
1169
- run_end_type=result.FinalResult[OutputT],
1176
+ deps_type=GraphAgentDeps[DepsT, OutputT],
1177
+ input_type=UserPromptNode[DepsT, OutputT],
1178
+ output_type=result.FinalResult[OutputT],
1170
1179
  auto_instrument=False,
1171
1180
  )
1172
- return graph
1181
+
1182
+ g.add(
1183
+ g.edge_from(g.start_node).to(UserPromptNode[DepsT, OutputT]),
1184
+ g.node(UserPromptNode[DepsT, OutputT]),
1185
+ g.node(ModelRequestNode[DepsT, OutputT]),
1186
+ g.node(CallToolsNode[DepsT, OutputT]),
1187
+ g.node(
1188
+ SetFinalResult[DepsT, OutputT],
1189
+ ),
1190
+ )
1191
+ return g.build(validate_graph_structure=False)
1173
1192
 
1174
1193
 
1175
1194
  async def _process_message_history(
pydantic_ai/_utils.py CHANGED
@@ -147,7 +147,7 @@ async def group_by_temporal(
147
147
  aiterable: The async iterable to group.
148
148
  soft_max_interval: Maximum interval over which to group items, this should avoid a trickle of items causing
149
149
  a group to never be yielded. It's a soft max in the sense that once we're over this time, we yield items
150
- as soon as `aiter.__anext__()` returns. If `None`, no grouping/debouncing is performed
150
+ as soon as `anext(aiter)` returns. If `None`, no grouping/debouncing is performed
151
151
 
152
152
  Returns:
153
153
  A context manager usable as an async iterable of lists of items produced by the input async iterable.
@@ -171,7 +171,7 @@ async def group_by_temporal(
171
171
  buffer: list[T] = []
172
172
  group_start_time = time.monotonic()
173
173
 
174
- aiterator = aiterable.__aiter__()
174
+ aiterator = aiter(aiterable)
175
175
  while True:
176
176
  if group_start_time is None:
177
177
  # group hasn't started, we just wait for the maximum interval
@@ -182,9 +182,9 @@ async def group_by_temporal(
182
182
 
183
183
  # if there's no current task, we get the next one
184
184
  if task is None:
185
- # aiter.__anext__() returns an Awaitable[T], not a Coroutine which asyncio.create_task expects
185
+ # anext(aiter) returns an Awaitable[T], not a Coroutine which asyncio.create_task expects
186
186
  # so far, this doesn't seem to be a problem
187
- task = asyncio.create_task(aiterator.__anext__()) # pyright: ignore[reportArgumentType]
187
+ task = asyncio.create_task(anext(aiterator)) # pyright: ignore[reportArgumentType]
188
188
 
189
189
  # we use asyncio.wait to avoid cancelling the coroutine if it's not done
190
190
  done, _ = await asyncio.wait((task,), timeout=wait_time)
@@ -284,10 +284,10 @@ class PeekableAsyncStream(Generic[T]):
284
284
 
285
285
  # Otherwise, we need to fetch the next item from the underlying iterator.
286
286
  if self._source_iter is None:
287
- self._source_iter = self._source.__aiter__()
287
+ self._source_iter = aiter(self._source)
288
288
 
289
289
  try:
290
- self._buffer = await self._source_iter.__anext__()
290
+ self._buffer = await anext(self._source_iter)
291
291
  except StopAsyncIteration:
292
292
  self._exhausted = True
293
293
  return UNSET
@@ -318,10 +318,10 @@ class PeekableAsyncStream(Generic[T]):
318
318
 
319
319
  # Otherwise, fetch the next item from the source.
320
320
  if self._source_iter is None:
321
- self._source_iter = self._source.__aiter__()
321
+ self._source_iter = aiter(self._source)
322
322
 
323
323
  try:
324
- return await self._source_iter.__anext__()
324
+ return await anext(self._source_iter)
325
325
  except StopAsyncIteration:
326
326
  self._exhausted = True
327
327
  raise
@@ -15,7 +15,6 @@ from pydantic.json_schema import GenerateJsonSchema
15
15
  from typing_extensions import Self, TypeVar, deprecated
16
16
 
17
17
  from pydantic_ai._instrumentation import DEFAULT_INSTRUMENTATION_VERSION, InstrumentationNames
18
- from pydantic_graph import Graph
19
18
 
20
19
  from .. import (
21
20
  _agent_graph,
@@ -41,7 +40,6 @@ from ..builtin_tools import AbstractBuiltinTool
41
40
  from ..models.instrumented import InstrumentationSettings, InstrumentedModel, instrument_model
42
41
  from ..output import OutputDataT, OutputSpec
43
42
  from ..profiles import ModelProfile
44
- from ..result import FinalResult
45
43
  from ..run import AgentRun, AgentRunResult
46
44
  from ..settings import ModelSettings, merge_model_settings
47
45
  from ..tools import (
@@ -542,6 +540,7 @@ class Agent(AbstractAgent[AgentDepsT, OutputDataT]):
542
540
  """
543
541
  if infer_name and self.name is None:
544
542
  self._infer_name(inspect.currentframe())
543
+
545
544
  model_used = self._get_model(model)
546
545
  del model
547
546
 
@@ -565,9 +564,7 @@ class Agent(AbstractAgent[AgentDepsT, OutputDataT]):
565
564
  tool_manager = ToolManager[AgentDepsT](toolset)
566
565
 
567
566
  # Build the graph
568
- graph: Graph[_agent_graph.GraphAgentState, _agent_graph.GraphAgentDeps[AgentDepsT, Any], FinalResult[Any]] = (
569
- _agent_graph.build_agent_graph(self.name, self._deps_type, output_type_)
570
- )
567
+ graph = _agent_graph.build_agent_graph(self.name, self._deps_type, output_type_)
571
568
 
572
569
  # Build the initial state
573
570
  usage = usage or _usage.RunUsage()
@@ -607,16 +604,7 @@ class Agent(AbstractAgent[AgentDepsT, OutputDataT]):
607
604
  else:
608
605
  instrumentation_settings = None
609
606
  tracer = NoOpTracer()
610
- if builtin_tools:
611
- # Deduplicate builtin tools passed to the agent and the run based on type
612
- builtin_tools = list(
613
- {
614
- **({type(tool): tool for tool in self._builtin_tools or []}),
615
- **({type(tool): tool for tool in builtin_tools}),
616
- }.values()
617
- )
618
- else:
619
- builtin_tools = list(self._builtin_tools)
607
+
620
608
  graph_deps = _agent_graph.GraphAgentDeps[AgentDepsT, RunOutputDataT](
621
609
  user_deps=deps,
622
610
  prompt=user_prompt,
@@ -629,14 +617,14 @@ class Agent(AbstractAgent[AgentDepsT, OutputDataT]):
629
617
  output_schema=output_schema,
630
618
  output_validators=output_validators,
631
619
  history_processors=self.history_processors,
632
- builtin_tools=builtin_tools,
620
+ builtin_tools=[*self._builtin_tools, *(builtin_tools or [])],
633
621
  tool_manager=tool_manager,
634
622
  tracer=tracer,
635
623
  get_instructions=get_instructions,
636
624
  instrumentation_settings=instrumentation_settings,
637
625
  )
638
626
 
639
- start_node = _agent_graph.UserPromptNode[AgentDepsT](
627
+ user_prompt_node = _agent_graph.UserPromptNode[AgentDepsT](
640
628
  user_prompt=user_prompt,
641
629
  deferred_tool_results=deferred_tool_results,
642
630
  instructions=instructions_literal,
@@ -663,7 +651,7 @@ class Agent(AbstractAgent[AgentDepsT, OutputDataT]):
663
651
 
664
652
  try:
665
653
  async with graph.iter(
666
- start_node,
654
+ inputs=user_prompt_node,
667
655
  state=state,
668
656
  deps=graph_deps,
669
657
  span=use_span(run_span) if run_span.is_recording() else None,
@@ -16,6 +16,7 @@ __all__ = (
16
16
  'UrlContextTool',
17
17
  'ImageGenerationTool',
18
18
  'MemoryTool',
19
+ 'MCPServerTool',
19
20
  )
20
21
 
21
22
  _BUILTIN_TOOL_TYPES: dict[str, type[AbstractBuiltinTool]] = {}
@@ -33,6 +34,14 @@ class AbstractBuiltinTool(ABC):
33
34
  kind: str = 'unknown_builtin_tool'
34
35
  """Built-in tool identifier, this should be available on all built-in tools as a discriminator."""
35
36
 
37
+ @property
38
+ def unique_id(self) -> str:
39
+ """A unique identifier for the builtin tool.
40
+
41
+ If multiple instances of the same builtin tool can be passed to the model, subclasses should override this property to allow them to be distinguished.
42
+ """
43
+ return self.kind
44
+
36
45
  def __init_subclass__(cls, **kwargs: Any) -> None:
37
46
  super().__init_subclass__(**kwargs)
38
47
  _BUILTIN_TOOL_TYPES[cls.kind] = cls
@@ -263,6 +272,68 @@ class MemoryTool(AbstractBuiltinTool):
263
272
  """The kind of tool."""
264
273
 
265
274
 
275
+ @dataclass(kw_only=True)
276
+ class MCPServerTool(AbstractBuiltinTool):
277
+ """A builtin tool that allows your agent to use MCP servers.
278
+
279
+ Supported by:
280
+
281
+ * OpenAI Responses
282
+ * Anthropic
283
+ """
284
+
285
+ id: str
286
+ """A unique identifier for the MCP server."""
287
+
288
+ url: str
289
+ """The URL of the MCP server to use.
290
+
291
+ For OpenAI Responses, it is possible to use `connector_id` by providing it as `x-openai-connector:<connector_id>`.
292
+ """
293
+
294
+ authorization_token: str | None = None
295
+ """Authorization header to use when making requests to the MCP server.
296
+
297
+ Supported by:
298
+
299
+ * OpenAI Responses
300
+ * Anthropic
301
+ """
302
+
303
+ description: str | None = None
304
+ """A description of the MCP server.
305
+
306
+ Supported by:
307
+
308
+ * OpenAI Responses
309
+ """
310
+
311
+ allowed_tools: list[str] | None = None
312
+ """A list of tools that the MCP server can use.
313
+
314
+ Supported by:
315
+
316
+ * OpenAI Responses
317
+ * Anthropic
318
+ """
319
+
320
+ headers: dict[str, str] | None = None
321
+ """Optional HTTP headers to send to the MCP server.
322
+
323
+ Use for authentication or other purposes.
324
+
325
+ Supported by:
326
+
327
+ * OpenAI Responses
328
+ """
329
+
330
+ kind: str = 'mcp_server'
331
+
332
+ @property
333
+ def unique_id(self) -> str:
334
+ return ':'.join([self.kind, self.id])
335
+
336
+
266
337
  def _tool_discriminator(tool_data: dict[str, Any] | AbstractBuiltinTool) -> str:
267
338
  if isinstance(tool_data, dict):
268
339
  return tool_data.get('kind', AbstractBuiltinTool.kind)
@@ -36,6 +36,17 @@ __all__ = [
36
36
  'TemporalWrapperToolset',
37
37
  ]
38
38
 
39
+ # We need eagerly import the anyio backends or it will happens inside workflow code and temporal has issues
40
+ # Note: It's difficult to add a test that covers this because pytest presumably does these imports itself
41
+ # when you have a @pytest.mark.anyio somewhere.
42
+ # I suppose we could add a test that runs a python script in a separate process, but I have not done that...
43
+ import anyio._backends._asyncio # pyright: ignore[reportUnusedImport]
44
+
45
+ try:
46
+ import anyio._backends._trio # noqa F401 # pyright: ignore[reportUnusedImport]
47
+ except ImportError:
48
+ pass
49
+
39
50
 
40
51
  class PydanticAIPlugin(ClientPlugin, WorkerPlugin):
41
52
  """Temporal client and worker plugin for Pydantic AI."""
@@ -2,11 +2,12 @@ from __future__ import annotations
2
2
 
3
3
  from collections.abc import Callable
4
4
  from dataclasses import dataclass
5
- from typing import Annotated, Any, Literal, assert_never
5
+ from typing import Annotated, Any, Literal
6
6
 
7
7
  from pydantic import ConfigDict, Discriminator, with_config
8
8
  from temporalio import activity, workflow
9
9
  from temporalio.workflow import ActivityConfig
10
+ from typing_extensions import assert_never
10
11
 
11
12
  from pydantic_ai import FunctionToolset, ToolsetTool
12
13
  from pydantic_ai.exceptions import ApprovalRequired, CallDeferred, ModelRetry, UserError
pydantic_ai/exceptions.py CHANGED
@@ -159,7 +159,7 @@ class ModelHTTPError(AgentRunError):
159
159
  super().__init__(message)
160
160
 
161
161
 
162
- class FallbackExceptionGroup(ExceptionGroup):
162
+ class FallbackExceptionGroup(ExceptionGroup[Any]):
163
163
  """A group of exceptions that can be raised when all fallback models fail."""
164
164
 
165
165
 
pydantic_ai/messages.py CHANGED
@@ -480,7 +480,7 @@ class BinaryContent:
480
480
  """
481
481
 
482
482
  _identifier: Annotated[str | None, pydantic.Field(alias='identifier', default=None, exclude=True)] = field(
483
- compare=False, default=None
483
+ compare=False, default=None, repr=False
484
484
  )
485
485
 
486
486
  kind: Literal['binary'] = 'binary'
@@ -410,9 +410,17 @@ class Model(ABC):
410
410
  they need to customize the preparation flow further, but most implementations should simply call
411
411
  ``self.prepare_request(...)`` at the start of their ``request`` (and related) methods.
412
412
  """
413
- merged_settings = merge_model_settings(self.settings, model_settings)
414
- customized_parameters = self.customize_request_parameters(model_request_parameters)
415
- return merged_settings, customized_parameters
413
+ model_settings = merge_model_settings(self.settings, model_settings)
414
+
415
+ if builtin_tools := model_request_parameters.builtin_tools:
416
+ # Deduplicate builtin tools
417
+ model_request_parameters = replace(
418
+ model_request_parameters,
419
+ builtin_tools=list({tool.unique_id: tool for tool in builtin_tools}.values()),
420
+ )
421
+
422
+ model_request_parameters = self.customize_request_parameters(model_request_parameters)
423
+ return model_settings, model_request_parameters
416
424
 
417
425
  @property
418
426
  @abstractmethod
@@ -3,7 +3,7 @@ from __future__ import annotations as _annotations
3
3
  import io
4
4
  from collections.abc import AsyncGenerator, AsyncIterable, AsyncIterator
5
5
  from contextlib import asynccontextmanager
6
- from dataclasses import dataclass, field
6
+ from dataclasses import dataclass, field, replace
7
7
  from datetime import datetime
8
8
  from typing import Any, Literal, cast, overload
9
9
 
@@ -13,7 +13,7 @@ from typing_extensions import assert_never
13
13
  from .. import ModelHTTPError, UnexpectedModelBehavior, _utils, usage
14
14
  from .._run_context import RunContext
15
15
  from .._utils import guard_tool_call_id as _guard_tool_call_id
16
- from ..builtin_tools import CodeExecutionTool, MemoryTool, WebSearchTool
16
+ from ..builtin_tools import CodeExecutionTool, MCPServerTool, MemoryTool, WebSearchTool
17
17
  from ..exceptions import UserError
18
18
  from ..messages import (
19
19
  BinaryContent,
@@ -68,6 +68,9 @@ try:
68
68
  BetaContentBlockParam,
69
69
  BetaImageBlockParam,
70
70
  BetaInputJSONDelta,
71
+ BetaMCPToolResultBlock,
72
+ BetaMCPToolUseBlock,
73
+ BetaMCPToolUseBlockParam,
71
74
  BetaMemoryTool20250818Param,
72
75
  BetaMessage,
73
76
  BetaMessageParam,
@@ -82,6 +85,8 @@ try:
82
85
  BetaRawMessageStreamEvent,
83
86
  BetaRedactedThinkingBlock,
84
87
  BetaRedactedThinkingBlockParam,
88
+ BetaRequestMCPServerToolConfigurationParam,
89
+ BetaRequestMCPServerURLDefinitionParam,
85
90
  BetaServerToolUseBlock,
86
91
  BetaServerToolUseBlockParam,
87
92
  BetaSignatureDelta,
@@ -264,7 +269,7 @@ class AnthropicModel(Model):
264
269
  ) -> BetaMessage | AsyncStream[BetaRawMessageStreamEvent]:
265
270
  # standalone function to make it easier to override
266
271
  tools = self._get_tools(model_request_parameters)
267
- tools, beta_features = self._add_builtin_tools(tools, model_request_parameters)
272
+ tools, mcp_servers, beta_features = self._add_builtin_tools(tools, model_request_parameters)
268
273
 
269
274
  tool_choice: BetaToolChoiceParam | None
270
275
 
@@ -300,6 +305,7 @@ class AnthropicModel(Model):
300
305
  model=self._model_name,
301
306
  tools=tools or OMIT,
302
307
  tool_choice=tool_choice or OMIT,
308
+ mcp_servers=mcp_servers or OMIT,
303
309
  stream=stream,
304
310
  thinking=model_settings.get('anthropic_thinking', OMIT),
305
311
  stop_sequences=model_settings.get('stop_sequences', OMIT),
@@ -318,11 +324,14 @@ class AnthropicModel(Model):
318
324
  def _process_response(self, response: BetaMessage) -> ModelResponse:
319
325
  """Process a non-streamed response, and prepare a message to return."""
320
326
  items: list[ModelResponsePart] = []
327
+ builtin_tool_calls: dict[str, BuiltinToolCallPart] = {}
321
328
  for item in response.content:
322
329
  if isinstance(item, BetaTextBlock):
323
330
  items.append(TextPart(content=item.text))
324
331
  elif isinstance(item, BetaServerToolUseBlock):
325
- items.append(_map_server_tool_use_block(item, self.system))
332
+ call_part = _map_server_tool_use_block(item, self.system)
333
+ builtin_tool_calls[call_part.tool_call_id] = call_part
334
+ items.append(call_part)
326
335
  elif isinstance(item, BetaWebSearchToolResultBlock):
327
336
  items.append(_map_web_search_tool_result_block(item, self.system))
328
337
  elif isinstance(item, BetaCodeExecutionToolResultBlock):
@@ -333,6 +342,13 @@ class AnthropicModel(Model):
333
342
  )
334
343
  elif isinstance(item, BetaThinkingBlock):
335
344
  items.append(ThinkingPart(content=item.thinking, signature=item.signature, provider_name=self.system))
345
+ elif isinstance(item, BetaMCPToolUseBlock):
346
+ call_part = _map_mcp_server_use_block(item, self.system)
347
+ builtin_tool_calls[call_part.tool_call_id] = call_part
348
+ items.append(call_part)
349
+ elif isinstance(item, BetaMCPToolResultBlock):
350
+ call_part = builtin_tool_calls.get(item.tool_use_id)
351
+ items.append(_map_mcp_server_result_block(item, call_part, self.system))
336
352
  else:
337
353
  assert isinstance(item, BetaToolUseBlock), f'unexpected item type {type(item)}'
338
354
  items.append(
@@ -383,8 +399,9 @@ class AnthropicModel(Model):
383
399
 
384
400
  def _add_builtin_tools(
385
401
  self, tools: list[BetaToolUnionParam], model_request_parameters: ModelRequestParameters
386
- ) -> tuple[list[BetaToolUnionParam], list[str]]:
402
+ ) -> tuple[list[BetaToolUnionParam], list[BetaRequestMCPServerURLDefinitionParam], list[str]]:
387
403
  beta_features: list[str] = []
404
+ mcp_servers: list[BetaRequestMCPServerURLDefinitionParam] = []
388
405
  for tool in model_request_parameters.builtin_tools:
389
406
  if isinstance(tool, WebSearchTool):
390
407
  user_location = UserLocation(type='approximate', **tool.user_location) if tool.user_location else None
@@ -408,11 +425,26 @@ class AnthropicModel(Model):
408
425
  tools = [tool for tool in tools if tool['name'] != 'memory']
409
426
  tools.append(BetaMemoryTool20250818Param(name='memory', type='memory_20250818'))
410
427
  beta_features.append('context-management-2025-06-27')
428
+ elif isinstance(tool, MCPServerTool) and tool.url:
429
+ mcp_server_url_definition_param = BetaRequestMCPServerURLDefinitionParam(
430
+ type='url',
431
+ name=tool.id,
432
+ url=tool.url,
433
+ )
434
+ if tool.allowed_tools is not None: # pragma: no branch
435
+ mcp_server_url_definition_param['tool_configuration'] = BetaRequestMCPServerToolConfigurationParam(
436
+ enabled=bool(tool.allowed_tools),
437
+ allowed_tools=tool.allowed_tools,
438
+ )
439
+ if tool.authorization_token: # pragma: no cover
440
+ mcp_server_url_definition_param['authorization_token'] = tool.authorization_token
441
+ mcp_servers.append(mcp_server_url_definition_param)
442
+ beta_features.append('mcp-client-2025-04-04')
411
443
  else: # pragma: no cover
412
444
  raise UserError(
413
445
  f'`{tool.__class__.__name__}` is not supported by `AnthropicModel`. If it should be, please file an issue.'
414
446
  )
415
- return tools, beta_features
447
+ return tools, mcp_servers, beta_features
416
448
 
417
449
  async def _map_message(self, messages: list[ModelMessage]) -> tuple[str, list[BetaMessageParam]]: # noqa: C901
418
450
  """Just maps a `pydantic_ai.Message` to a `anthropic.types.MessageParam`."""
@@ -458,6 +490,8 @@ class AnthropicModel(Model):
458
490
  | BetaCodeExecutionToolResultBlockParam
459
491
  | BetaThinkingBlockParam
460
492
  | BetaRedactedThinkingBlockParam
493
+ | BetaMCPToolUseBlockParam
494
+ | BetaMCPToolResultBlock
461
495
  ] = []
462
496
  for response_part in m.parts:
463
497
  if isinstance(response_part, TextPart):
@@ -508,7 +542,7 @@ class AnthropicModel(Model):
508
542
  input=response_part.args_as_dict(),
509
543
  )
510
544
  assistant_content_params.append(server_tool_use_block_param)
511
- elif response_part.tool_name == CodeExecutionTool.kind: # pragma: no branch
545
+ elif response_part.tool_name == CodeExecutionTool.kind:
512
546
  server_tool_use_block_param = BetaServerToolUseBlockParam(
513
547
  id=tool_use_id,
514
548
  type='server_tool_use',
@@ -516,6 +550,21 @@ class AnthropicModel(Model):
516
550
  input=response_part.args_as_dict(),
517
551
  )
518
552
  assistant_content_params.append(server_tool_use_block_param)
553
+ elif (
554
+ response_part.tool_name.startswith(MCPServerTool.kind)
555
+ and (server_id := response_part.tool_name.split(':', 1)[1])
556
+ and (args := response_part.args_as_dict())
557
+ and (tool_name := args.get('tool_name'))
558
+ and (tool_args := args.get('tool_args'))
559
+ ): # pragma: no branch
560
+ mcp_tool_use_block_param = BetaMCPToolUseBlockParam(
561
+ id=tool_use_id,
562
+ type='mcp_tool_use',
563
+ server_name=server_id,
564
+ name=tool_name,
565
+ input=tool_args,
566
+ )
567
+ assistant_content_params.append(mcp_tool_use_block_param)
519
568
  elif isinstance(response_part, BuiltinToolReturnPart):
520
569
  if response_part.provider_name == self.system:
521
570
  tool_use_id = _guard_tool_call_id(t=response_part)
@@ -547,6 +596,16 @@ class AnthropicModel(Model):
547
596
  ),
548
597
  )
549
598
  )
599
+ elif response_part.tool_name.startswith(MCPServerTool.kind) and isinstance(
600
+ response_part.content, dict
601
+ ): # pragma: no branch
602
+ assistant_content_params.append(
603
+ BetaMCPToolResultBlock(
604
+ tool_use_id=tool_use_id,
605
+ type='mcp_tool_result',
606
+ **cast(dict[str, Any], response_part.content), # pyright: ignore[reportUnknownMemberType]
607
+ )
608
+ )
550
609
  elif isinstance(response_part, FilePart): # pragma: no cover
551
610
  # Files generated by models are not sent back to models that don't themselves generate files.
552
611
  pass
@@ -661,6 +720,7 @@ class AnthropicStreamedResponse(StreamedResponse):
661
720
  async def _get_event_iterator(self) -> AsyncIterator[ModelResponseStreamEvent]: # noqa: C901
662
721
  current_block: BetaContentBlock | None = None
663
722
 
723
+ builtin_tool_calls: dict[str, BuiltinToolCallPart] = {}
664
724
  async for event in self._response:
665
725
  if isinstance(event, BetaRawMessageStartEvent):
666
726
  self._usage = _map_usage(event, self._provider_name, self._provider_url, self._model_name)
@@ -698,9 +758,11 @@ class AnthropicStreamedResponse(StreamedResponse):
698
758
  if maybe_event is not None: # pragma: no branch
699
759
  yield maybe_event
700
760
  elif isinstance(current_block, BetaServerToolUseBlock):
761
+ call_part = _map_server_tool_use_block(current_block, self.provider_name)
762
+ builtin_tool_calls[call_part.tool_call_id] = call_part
701
763
  yield self._parts_manager.handle_part(
702
764
  vendor_part_id=event.index,
703
- part=_map_server_tool_use_block(current_block, self.provider_name),
765
+ part=call_part,
704
766
  )
705
767
  elif isinstance(current_block, BetaWebSearchToolResultBlock):
706
768
  yield self._parts_manager.handle_part(
@@ -712,6 +774,32 @@ class AnthropicStreamedResponse(StreamedResponse):
712
774
  vendor_part_id=event.index,
713
775
  part=_map_code_execution_tool_result_block(current_block, self.provider_name),
714
776
  )
777
+ elif isinstance(current_block, BetaMCPToolUseBlock):
778
+ call_part = _map_mcp_server_use_block(current_block, self.provider_name)
779
+ builtin_tool_calls[call_part.tool_call_id] = call_part
780
+
781
+ args_json = call_part.args_as_json_str()
782
+ # Drop the final `{}}` so that we can add tool args deltas
783
+ args_json_delta = args_json[:-3]
784
+ assert args_json_delta.endswith('"tool_args":'), (
785
+ f'Expected {args_json_delta!r} to end in `"tool_args":`'
786
+ )
787
+
788
+ yield self._parts_manager.handle_part(
789
+ vendor_part_id=event.index, part=replace(call_part, args=None)
790
+ )
791
+ maybe_event = self._parts_manager.handle_tool_call_delta(
792
+ vendor_part_id=event.index,
793
+ args=args_json_delta,
794
+ )
795
+ if maybe_event is not None: # pragma: no branch
796
+ yield maybe_event
797
+ elif isinstance(current_block, BetaMCPToolResultBlock):
798
+ call_part = builtin_tool_calls.get(current_block.tool_use_id)
799
+ yield self._parts_manager.handle_part(
800
+ vendor_part_id=event.index,
801
+ part=_map_mcp_server_result_block(current_block, call_part, self.provider_name),
802
+ )
715
803
 
716
804
  elif isinstance(event, BetaRawContentBlockDeltaEvent):
717
805
  if isinstance(event.delta, BetaTextDelta):
@@ -749,7 +837,16 @@ class AnthropicStreamedResponse(StreamedResponse):
749
837
  self.provider_details = {'finish_reason': raw_finish_reason}
750
838
  self.finish_reason = _FINISH_REASON_MAP.get(raw_finish_reason)
751
839
 
752
- elif isinstance(event, BetaRawContentBlockStopEvent | BetaRawMessageStopEvent): # pragma: no branch
840
+ elif isinstance(event, BetaRawContentBlockStopEvent): # pragma: no branch
841
+ if isinstance(current_block, BetaMCPToolUseBlock):
842
+ maybe_event = self._parts_manager.handle_tool_call_delta(
843
+ vendor_part_id=event.index,
844
+ args='}',
845
+ )
846
+ if maybe_event is not None: # pragma: no branch
847
+ yield maybe_event
848
+ current_block = None
849
+ elif isinstance(event, BetaRawMessageStopEvent): # pragma: no branch
753
850
  current_block = None
754
851
 
755
852
  @property
@@ -817,3 +914,27 @@ def _map_code_execution_tool_result_block(
817
914
  content=code_execution_tool_result_content_ta.dump_python(item.content, mode='json'),
818
915
  tool_call_id=item.tool_use_id,
819
916
  )
917
+
918
+
919
+ def _map_mcp_server_use_block(item: BetaMCPToolUseBlock, provider_name: str) -> BuiltinToolCallPart:
920
+ return BuiltinToolCallPart(
921
+ provider_name=provider_name,
922
+ tool_name=':'.join([MCPServerTool.kind, item.server_name]),
923
+ args={
924
+ 'action': 'call_tool',
925
+ 'tool_name': item.name,
926
+ 'tool_args': cast(dict[str, Any], item.input),
927
+ },
928
+ tool_call_id=item.id,
929
+ )
930
+
931
+
932
+ def _map_mcp_server_result_block(
933
+ item: BetaMCPToolResultBlock, call_part: BuiltinToolCallPart | None, provider_name: str
934
+ ) -> BuiltinToolReturnPart:
935
+ return BuiltinToolReturnPart(
936
+ provider_name=provider_name,
937
+ tool_name=call_part.tool_name if call_part else MCPServerTool.kind,
938
+ content=item.model_dump(mode='json', include={'content', 'is_error'}),
939
+ tool_call_id=item.tool_use_id,
940
+ )