diff --git a/docs/api/toolsets.md b/docs/api/toolsets.md index 6b22b23a9f..2b0a22881b 100644 --- a/docs/api/toolsets.md +++ b/docs/api/toolsets.md @@ -14,3 +14,5 @@ - PreparedToolset - WrapperToolset - ToolsetFunc + +::: pydantic_ai.toolsets.fastmcp diff --git a/docs/install.md b/docs/install.md index 600f81bd68..57d476e506 100644 --- a/docs/install.md +++ b/docs/install.md @@ -55,6 +55,7 @@ pip/uv-add "pydantic-ai-slim[openai]" * `tavily` - installs `tavily-python` [PyPI ↗](https://pypi.org/project/tavily-python){:target="_blank"} * `cli` - installs `rich` [PyPI ↗](https://pypi.org/project/rich){:target="_blank"}, `prompt-toolkit` [PyPI ↗](https://pypi.org/project/prompt-toolkit){:target="_blank"}, and `argcomplete` [PyPI ↗](https://pypi.org/project/argcomplete){:target="_blank"} * `mcp` - installs `mcp` [PyPI ↗](https://pypi.org/project/mcp){:target="_blank"} +* `fastmcp` - installs `fastmcp` [PyPI ↗](https://pypi.org/project/fastmcp){:target="_blank"} * `a2a` - installs `fasta2a` [PyPI ↗](https://pypi.org/project/fasta2a){:target="_blank"} * `ag-ui` - installs `ag-ui-protocol` [PyPI ↗](https://pypi.org/project/ag-ui-protocol){:target="_blank"} and `starlette` [PyPI ↗](https://pypi.org/project/starlette){:target="_blank"} * `dbos` - installs [`dbos`](durable_execution/dbos.md) [PyPI ↗](https://pypi.org/project/dbos){:target="_blank"} diff --git a/docs/mcp/client.md b/docs/mcp/client.md index 74f8c318b0..b0181cdfc7 100644 --- a/docs/mcp/client.md +++ b/docs/mcp/client.md @@ -5,7 +5,7 @@ to use their tools. ## Install -You need to either install [`pydantic-ai`](../install.md), or[`pydantic-ai-slim`](../install.md#slim-install) with the `mcp` optional group: +You need to either install [`pydantic-ai`](../install.md), or [`pydantic-ai-slim`](../install.md#slim-install) with the `mcp` optional group: ```bash pip/uv-add "pydantic-ai-slim[mcp]" diff --git a/docs/mcp/fastmcp-client.md b/docs/mcp/fastmcp-client.md new file mode 100644 index 0000000000..538b2afbbb --- /dev/null +++ b/docs/mcp/fastmcp-client.md @@ -0,0 +1,88 @@ +# FastMCP Client + +[FastMCP](https://gofastmcp.com/) is a higher-level MCP framework that bills itself as "The fast, Pythonic way to build MCP servers and clients." It supports additional capabilities on top of the MCP specification like [Tool Transformation](https://gofastmcp.com/patterns/tool-transformation), [OAuth](https://gofastmcp.com/clients/auth/oauth), and more. + +As an alternative to Pydantic AI's standard [`MCPServer` MCP client](client.md) built on the [MCP SDK](https://github.com/modelcontextprotocol/python-sdk), you can use the [`FastMCPToolset`][pydantic_ai.toolsets.fastmcp.FastMCPToolset] [toolset](../toolsets.md) that leverages the [FastMCP Client](https://gofastmcp.com/clients/) to connect to local and remote MCP servers, whether or not they're built using [FastMCP Server](https://gofastmcp.com/servers/). + +Note that it does not yet support integration elicitation or sampling, which are supported by the [standard `MCPServer` client](client.md). + +## Install + +To use the `FastMCPToolset`, you will need to install [`pydantic-ai-slim`](../install.md#slim-install) with the `fastmcp` optional group: + +```bash +pip/uv-add "pydantic-ai-slim[fastmcp]" +``` + +## Usage + +A `FastMCPToolset` can then be created from: + +- A FastMCP Server: `#!python FastMCPToolset(fastmcp.FastMCP('my_server'))` +- A FastMCP Client: `#!python FastMCPToolset(fastmcp.Client(...))` +- A FastMCP Transport: `#!python FastMCPToolset(fastmcp.StdioTransport(command='uvx', args=['mcp-run-python', 'stdio']))` +- A Streamable HTTP URL: `#!python FastMCPToolset('http://localhost:8000/mcp')` +- An HTTP SSE URL: `#!python FastMCPToolset('http://localhost:8000/sse')` +- A Python Script: `#!python FastMCPToolset('my_server.py')` +- A Node.js Script: `#!python FastMCPToolset('my_server.js')` +- A JSON MCP Configuration: `#!python FastMCPToolset({'mcpServers': {'my_server': {'command': 'uvx', 'args': ['mcp-run-python', 'stdio']}}})` + +If you already have a [FastMCP Server](https://gofastmcp.com/servers) in the same codebase as your Pydantic AI agent, you can create a `FastMCPToolset` directly from it and save agent a network round trip: + +```python +from fastmcp import FastMCP + +from pydantic_ai import Agent +from pydantic_ai.toolsets.fastmcp import FastMCPToolset + +fastmcp_server = FastMCP('my_server') +@fastmcp_server.tool() +async def add(a: int, b: int) -> int: + return a + b + +toolset = FastMCPToolset(fastmcp_server) + +agent = Agent('openai:gpt-5', toolsets=[toolset]) + +async def main(): + result = await agent.run('What is 7 plus 5?') + print(result.output) + #> The answer is 12. +``` + +_(This example is complete, it can be run "as is" — you'll need to add `asyncio.run(main())` to run `main`)_ + +Connecting your agent to a Streamable HTTP MCP Server is as simple as: + +```python +from pydantic_ai import Agent +from pydantic_ai.toolsets.fastmcp import FastMCPToolset + +toolset = FastMCPToolset('http://localhost:8000/mcp') + +agent = Agent('openai:gpt-5', toolsets=[toolset]) +``` + +_(This example is complete, it can be run "as is" — you'll need to add `asyncio.run(main())` to run `main`)_ + +You can also create a `FastMCPToolset` from a JSON MCP Configuration: + +```python +from pydantic_ai import Agent +from pydantic_ai.toolsets.fastmcp import FastMCPToolset + +mcp_config = { + 'mcpServers': { + 'time_mcp_server': { + 'command': 'uvx', + 'args': ['mcp-run-python', 'stdio'] + } + } +} + +toolset = FastMCPToolset(mcp_config) + +agent = Agent('openai:gpt-5', toolsets=[toolset]) +``` + +_(This example is complete, it can be run "as is" — you'll need to add `asyncio.run(main())` to run `main`)_ diff --git a/docs/mcp/overview.md b/docs/mcp/overview.md index ce00808fa6..f5ebee5ac2 100644 --- a/docs/mcp/overview.md +++ b/docs/mcp/overview.md @@ -1,11 +1,12 @@ # Model Context Protocol (MCP) -Pydantic AI supports [Model Context Protocol (MCP)](https://modelcontextprotocol.io) in two ways: +Pydantic AI supports [Model Context Protocol (MCP)](https://modelcontextprotocol.io) in multiple ways: -1. [Agents](../agents.md) can connect to MCP servers and user their tools - 1. Pydantic AI can act as an MCP client and connect directly to local and remote MCP servers, [learn more …](client.md) - 2. Some model providers can themselves connect to remote MCP servers, [learn more …](../builtin-tools.md#mcp-server-tool) -2. Agents can be used within MCP servers, [learn more …](server.md) +1. [Agents](../agents.md) can connect to MCP servers and use their tools using three different methods: + 1. Pydantic AI can act as an MCP client and connect directly to local and remote MCP servers. [Learn more](client.md) about [`MCPServer`][pydantic_ai.mcp.MCPServer]. + 2. Pydantic AI can use the [FastMCP Client](https://gofastmcp.com/clients/client/) to connect to local and remote MCP servers, whether or not they're built using [FastMCP Server](https://gofastmcp.com/servers). [Learn more](fastmcp-client.md) about [`FastMCPToolset`][pydantic_ai.toolsets.fastmcp.FastMCPToolset]. + 3. Some model providers can themselves connect to remote MCP servers using a "built-in tool". [Learn more](../builtin-tools.md#mcp-server-tool) about [`MCPServerTool`][pydantic_ai.builtin_tools.MCPServerTool]. +2. Agents can be used within MCP servers. [Learn more](server.md) ## What is MCP? diff --git a/docs/toolsets.md b/docs/toolsets.md index d0c3768431..9947c883d7 100644 --- a/docs/toolsets.md +++ b/docs/toolsets.md @@ -661,7 +661,10 @@ If you want to reuse a network connection or session across tool listings and ca ### MCP Servers -See the [MCP Client](./mcp/client.md) documentation for how to use MCP servers with Pydantic AI. +Pydantic AI provides two toolsets that allow an agent to connect to and call tools on local and remote MCP Servers: + +1. `MCPServer`: the [MCP SDK-based Client](./mcp/client.md) which offers more direct control by leveraging the MCP SDK directly +2. `FastMCPToolset`: the [FastMCP-based Client](./mcp/fastmcp-client.md) which offers additional capabilities like Tool Transformation, simpler OAuth configuration, and more. ### LangChain Tools {#langchain-tools} diff --git a/mkdocs.yml b/mkdocs.yml index ba2273334e..9feb626d61 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -49,6 +49,7 @@ nav: - MCP: - Overview: mcp/overview.md - mcp/client.md + - mcp/fastmcp-client.md - mcp/server.md - Multi-Agent Patterns: multi-agent-applications.md - Testing: testing.md diff --git a/pydantic_ai_slim/pydantic_ai/toolsets/fastmcp.py b/pydantic_ai_slim/pydantic_ai/toolsets/fastmcp.py new file mode 100644 index 0000000000..f751459541 --- /dev/null +++ b/pydantic_ai_slim/pydantic_ai/toolsets/fastmcp.py @@ -0,0 +1,215 @@ +from __future__ import annotations + +import base64 +from asyncio import Lock +from contextlib import AsyncExitStack +from dataclasses import KW_ONLY, dataclass +from pathlib import Path +from typing import TYPE_CHECKING, Any, Literal + +from pydantic import AnyUrl +from typing_extensions import Self, assert_never + +from pydantic_ai import messages +from pydantic_ai.exceptions import ModelRetry +from pydantic_ai.tools import AgentDepsT, RunContext, ToolDefinition +from pydantic_ai.toolsets import AbstractToolset +from pydantic_ai.toolsets.abstract import ToolsetTool + +try: + from fastmcp.client import Client + from fastmcp.client.transports import ClientTransport + from fastmcp.exceptions import ToolError + from fastmcp.mcp_config import MCPConfig + from fastmcp.server import FastMCP + from mcp.server.fastmcp import FastMCP as FastMCP1Server + from mcp.types import ( + AudioContent, + BlobResourceContents, + ContentBlock, + EmbeddedResource, + ImageContent, + ResourceLink, + TextContent, + TextResourceContents, + Tool as MCPTool, + ) + + from pydantic_ai.mcp import TOOL_SCHEMA_VALIDATOR + +except ImportError as _import_error: + raise ImportError( + 'Please install the `fastmcp` package to use the FastMCP server, ' + 'you can use the `fastmcp` optional group — `pip install "pydantic-ai-slim[fastmcp]"`' + ) from _import_error + + +if TYPE_CHECKING: + from fastmcp.client.client import CallToolResult + + +FastMCPToolResult = messages.BinaryContent | dict[str, Any] | str | None + +ToolErrorBehavior = Literal['model_retry', 'error'] + +UNKNOWN_BINARY_MEDIA_TYPE = 'application/octet-stream' + + +@dataclass(init=False) +class FastMCPToolset(AbstractToolset[AgentDepsT]): + """A FastMCP Toolset that uses the FastMCP Client to call tools from a local or remote MCP Server. + + The Toolset can accept a FastMCP Client, a FastMCP Transport, or any other object which a FastMCP Transport can be created from. + + See https://gofastmcp.com/clients/transports for a full list of transports available. + """ + + client: Client[Any] + """The FastMCP client to use.""" + + _: KW_ONLY + + tool_error_behavior: Literal['model_retry', 'error'] + """The behavior to take when a tool error occurs.""" + + max_retries: int + """The maximum number of retries to attempt if a tool call fails.""" + + _id: str | None + + def __init__( + self, + client: Client[Any] + | ClientTransport + | FastMCP + | FastMCP1Server + | AnyUrl + | Path + | MCPConfig + | dict[str, Any] + | str, + *, + max_retries: int = 1, + tool_error_behavior: Literal['model_retry', 'error'] = 'model_retry', + id: str | None = None, + ) -> None: + if isinstance(client, Client): + self.client = client + else: + self.client = Client[Any](transport=client) + + self._id = id + self.max_retries = max_retries + self.tool_error_behavior = tool_error_behavior + + self._enter_lock: Lock = Lock() + self._running_count: int = 0 + self._exit_stack: AsyncExitStack | None = None + + @property + def id(self) -> str | None: + return self._id + + async def __aenter__(self) -> Self: + async with self._enter_lock: + if self._running_count == 0: + self._exit_stack = AsyncExitStack() + await self._exit_stack.enter_async_context(self.client) + + self._running_count += 1 + + return self + + async def __aexit__(self, *args: Any) -> bool | None: + async with self._enter_lock: + self._running_count -= 1 + if self._running_count == 0 and self._exit_stack: + await self._exit_stack.aclose() + self._exit_stack = None + + return None + + async def get_tools(self, ctx: RunContext[AgentDepsT]) -> dict[str, ToolsetTool[AgentDepsT]]: + async with self: + mcp_tools: list[MCPTool] = await self.client.list_tools() + + return { + tool.name: _convert_mcp_tool_to_toolset_tool(toolset=self, mcp_tool=tool, retries=self.max_retries) + for tool in mcp_tools + } + + async def call_tool( + self, name: str, tool_args: dict[str, Any], ctx: RunContext[AgentDepsT], tool: ToolsetTool[AgentDepsT] + ) -> Any: + async with self: + try: + call_tool_result: CallToolResult = await self.client.call_tool(name=name, arguments=tool_args) + except ToolError as e: + if self.tool_error_behavior == 'model_retry': + raise ModelRetry(message=str(e)) from e + else: + raise e + + # If we have structured content, return that + if call_tool_result.structured_content: + return call_tool_result.structured_content + + # Otherwise, return the content + return _map_fastmcp_tool_results(parts=call_tool_result.content) + + +def _convert_mcp_tool_to_toolset_tool( + toolset: FastMCPToolset[AgentDepsT], + mcp_tool: MCPTool, + retries: int, +) -> ToolsetTool[AgentDepsT]: + """Convert an MCP tool to a toolset tool.""" + return ToolsetTool[AgentDepsT]( + tool_def=ToolDefinition( + name=mcp_tool.name, + description=mcp_tool.description, + parameters_json_schema=mcp_tool.inputSchema, + metadata={ + 'meta': mcp_tool.meta, + 'annotations': mcp_tool.annotations.model_dump() if mcp_tool.annotations else None, + 'output_schema': mcp_tool.outputSchema or None, + }, + ), + toolset=toolset, + max_retries=retries, + args_validator=TOOL_SCHEMA_VALIDATOR, + ) + + +def _map_fastmcp_tool_results(parts: list[ContentBlock]) -> list[FastMCPToolResult] | FastMCPToolResult: + """Map FastMCP tool results to toolset tool results.""" + mapped_results = [_map_fastmcp_tool_result(part) for part in parts] + + if len(mapped_results) == 1: + return mapped_results[0] + + return mapped_results + + +def _map_fastmcp_tool_result(part: ContentBlock) -> FastMCPToolResult: + if isinstance(part, TextContent): + return part.text + elif isinstance(part, ImageContent | AudioContent): + return messages.BinaryContent(data=base64.b64decode(part.data), media_type=part.mimeType) + elif isinstance(part, EmbeddedResource): + if isinstance(part.resource, BlobResourceContents): + return messages.BinaryContent( + data=base64.b64decode(part.resource.blob), + media_type=part.resource.mimeType or UNKNOWN_BINARY_MEDIA_TYPE, + ) + elif isinstance(part.resource, TextResourceContents): + return part.resource.text + else: + assert_never(part.resource) + elif isinstance(part, ResourceLink): + # ResourceLink is not yet supported by the FastMCP toolset as reading resources is not yet supported. + raise NotImplementedError( + 'ResourceLink is not supported by the FastMCP toolset as reading resources is not yet supported.' + ) + else: + assert_never(part) diff --git a/pydantic_ai_slim/pyproject.toml b/pydantic_ai_slim/pyproject.toml index 6b9f1acd24..84d6b7e745 100644 --- a/pydantic_ai_slim/pyproject.toml +++ b/pydantic_ai_slim/pyproject.toml @@ -88,6 +88,8 @@ cli = [ ] # MCP mcp = ["mcp>=1.12.3"] +# FastMCP +fastmcp = ["fastmcp>=2.12.0"] # Evals evals = ["pydantic-evals=={{ version }}"] # A2A diff --git a/pyproject.toml b/pyproject.toml index b09a172045..c88234fe2c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,7 +46,7 @@ requires-python = ">=3.10" [tool.hatch.metadata.hooks.uv-dynamic-versioning] dependencies = [ - "pydantic-ai-slim[openai,vertexai,google,groq,anthropic,mistral,cohere,bedrock,huggingface,cli,mcp,evals,ag-ui,retries,temporal,logfire]=={{ version }}", + "pydantic-ai-slim[openai,vertexai,google,groq,anthropic,mistral,cohere,bedrock,huggingface,cli,mcp,fastmcp,evals,ag-ui,retries,temporal,logfire]=={{ version }}", ] [tool.hatch.metadata.hooks.uv-dynamic-versioning.optional-dependencies] @@ -232,6 +232,7 @@ filterwarnings = [ "ignore:unclosed FastMCP: + """Create a real in-memory FastMCP server for testing.""" + server = FastMCP('test_server') + + @server.tool() + async def test_tool(param1: str, param2: int = 0) -> str: + """A test tool that returns a formatted string.""" + return f'param1={param1}, param2={param2}' + + @server.tool() + async def another_tool(value: float) -> dict[str, Any]: + """Another test tool that returns structured data.""" + return {'result': 'success', 'value': value, 'doubled': value * 2} + + @server.tool() + async def error_tool() -> str: + """A tool that can fail for testing error handling.""" + raise ValueError('This is a test error') + + @server.tool() + async def binary_tool() -> ImageContent: + """A tool that returns binary content.""" + fake_image_data = b'fake_image_data' + encoded_data = base64.b64encode(fake_image_data).decode('utf-8') + return ImageContent(type='image', data=encoded_data, mimeType='image/png') + + @server.tool() + async def audio_tool() -> AudioContent: + """A tool that returns audio content.""" + fake_audio_data = b'fake_audio_data' + encoded_data = base64.b64encode(fake_audio_data).decode('utf-8') + return AudioContent(type='audio', data=encoded_data, mimeType='audio/mpeg') + + @server.tool() + async def text_tool(message: str) -> str: + """A tool that returns text content.""" + return f'Echo: {message}' + + @server.tool() + async def text_list_tool(message: str) -> list[TextContent]: + """A tool that returns text content without a return annotation.""" + return [ + TextContent(type='text', text=f'Echo: {message}'), + TextContent(type='text', text=f'Echo: {message} again'), + ] + + @server.tool() + async def resource_link_tool(message: str) -> ResourceLink: + """A tool that returns text content without a return annotation.""" + return ResourceLink(type='resource_link', uri=AnyUrl('resource://message.txt'), name='message.txt') + + @server.tool() + async def resource_tool(message: str) -> EmbeddedResource: + """A tool that returns resource content.""" + return EmbeddedResource( + type='resource', resource=TextResourceContents(uri=AnyUrl('resource://message.txt'), text=message) + ) + + @server.tool() + async def resource_tool_blob(message: str) -> EmbeddedResource: + """A tool that returns blob content.""" + base64_message = base64.b64encode(message.encode('utf-8')).decode('utf-8') + return EmbeddedResource( + type='resource', resource=BlobResourceContents(uri=AnyUrl('resource://message.txt'), blob=base64_message) + ) + + @server.tool() + async def text_tool_wo_return_annotation(message: str): + """A tool that returns text content.""" + return f'Echo: {message}' + + @server.tool() + async def json_tool(data: dict[str, Any]) -> str: + """A tool that returns JSON data.""" + import json + + return json.dumps({'received': data, 'processed': True}) + + return server + + +@pytest.fixture +async def fastmcp_client(fastmcp_server: FastMCP) -> Client[FastMCPTransport]: + """Create a real FastMCP client connected to the test server.""" + return Client(transport=fastmcp_server) + + +@pytest.fixture +def run_context() -> RunContext[None]: + """Create a run context for testing.""" + return RunContext( + deps=None, + model=TestModel(), + usage=RunUsage(), + prompt=None, + messages=[], + run_step=0, + ) + + +class TestFastMCPToolsetInitialization: + """Test FastMCP Toolset initialization and basic functionality.""" + + async def test_init_with_client(self, fastmcp_client: Client[FastMCPTransport]): + """Test initialization with a FastMCP client.""" + toolset = FastMCPToolset(fastmcp_client) + + # Test that the client is accessible via the property + assert toolset.id is None + + async def test_init_with_id(self, fastmcp_client: Client[FastMCPTransport]): + """Test initialization with an id.""" + toolset = FastMCPToolset(fastmcp_client, id='test_id') + + # Test that the client is accessible via the property + assert toolset.id == 'test_id' + + async def test_init_with_custom_retries_and_error_behavior(self, fastmcp_client: Client[FastMCPTransport]): + """Test initialization with custom retries and error behavior.""" + toolset = FastMCPToolset(fastmcp_client, max_retries=5, tool_error_behavior='model_retry') + + # Test that the toolset was created successfully + assert toolset.client is fastmcp_client + + async def test_id_property(self, fastmcp_client: Client[FastMCPTransport]): + """Test that the id property returns None.""" + toolset = FastMCPToolset(fastmcp_client) + assert toolset.id is None + + +class TestFastMCPToolsetContextManagement: + """Test FastMCP Toolset context management.""" + + async def test_context_manager_single_enter_exit( + self, fastmcp_client: Client[FastMCPTransport], run_context: RunContext[None] + ): + """Test single enter/exit cycle.""" + toolset = FastMCPToolset(fastmcp_client) + + async with toolset: + # Test that we can get tools when the context is active + tools = await toolset.get_tools(run_context) + assert len(tools) > 0 + assert 'test_tool' in tools + + # After exit, the toolset should still be usable but the client connection is closed + + async def test_context_manager_no_enter( + self, fastmcp_client: Client[FastMCPTransport], run_context: RunContext[None] + ): + """Test no enter/exit cycle.""" + toolset = FastMCPToolset(fastmcp_client) + + # Test that we can get tools when the context is not active + tools = await toolset.get_tools(run_context) + assert len(tools) > 0 + assert 'test_tool' in tools + + async def test_context_manager_nested_enter_exit( + self, fastmcp_client: Client[FastMCPTransport], run_context: RunContext[None] + ): + """Test nested enter/exit cycles.""" + toolset = FastMCPToolset(fastmcp_client) + + async with toolset: + tools1 = await toolset.get_tools(run_context) + async with toolset: + tools2 = await toolset.get_tools(run_context) + assert tools1 == tools2 + # Should still work after inner context exits + tools3 = await toolset.get_tools(run_context) + assert tools1 == tools3 + + +class TestFastMCPToolsetToolDiscovery: + """Test FastMCP Toolset tool discovery functionality.""" + + async def test_get_tools( + self, + fastmcp_client: Client[FastMCPTransport], + run_context: RunContext[None], + ): + """Test getting tools from the FastMCP client.""" + toolset = FastMCPToolset(fastmcp_client) + + async with toolset: + tools = await toolset.get_tools(run_context) + + # Should have all the tools we defined in the server + expected_tools = { + 'test_tool', + 'another_tool', + 'audio_tool', + 'error_tool', + 'binary_tool', + 'text_tool', + 'text_list_tool', + 'text_tool_wo_return_annotation', + 'json_tool', + 'resource_link_tool', + 'resource_tool', + 'resource_tool_blob', + } + assert set(tools.keys()) == expected_tools + + # Check tool definitions + test_tool = tools['test_tool'] + assert test_tool.tool_def.name == 'test_tool' + assert test_tool.tool_def.description is not None + assert 'test tool that returns a formatted string' in test_tool.tool_def.description + assert test_tool.max_retries == 1 + assert test_tool.toolset is toolset + + # Check that the tool has proper schema + schema = test_tool.tool_def.parameters_json_schema + assert schema['type'] == 'object' + assert 'param1' in schema['properties'] + assert 'param2' in schema['properties'] + + async def test_get_tools_with_empty_server(self, run_context: RunContext[None]): + """Test getting tools from an empty FastMCP server.""" + empty_server = FastMCP('empty_server') + empty_client = Client(transport=empty_server) + toolset = FastMCPToolset(empty_client) + + async with toolset: + tools = await toolset.get_tools(run_context) + assert len(tools) == 0 + + +class TestFastMCPToolsetToolCalling: + """Test FastMCP Toolset tool calling functionality.""" + + @pytest.fixture + async def fastmcp_toolset(self, fastmcp_client: Client[FastMCPTransport]) -> FastMCPToolset[None]: + """Create a FastMCP Toolset.""" + return FastMCPToolset(fastmcp_client) + + async def test_call_tool_success( + self, + fastmcp_toolset: FastMCPToolset[None], + run_context: RunContext[None], + ): + """Test successful tool call.""" + async with fastmcp_toolset: + tools = await fastmcp_toolset.get_tools(run_context) + test_tool = tools['test_tool'] + + result = await fastmcp_toolset.call_tool( + name='test_tool', tool_args={'param1': 'hello', 'param2': 42}, ctx=run_context, tool=test_tool + ) + + assert result == {'result': 'param1=hello, param2=42'} + + async def test_call_tool_with_structured_content( + self, + fastmcp_toolset: FastMCPToolset[None], + run_context: RunContext[None], + ): + """Test tool call with structured content.""" + async with fastmcp_toolset: + tools = await fastmcp_toolset.get_tools(run_context) + another_tool = tools['another_tool'] + + result = await fastmcp_toolset.call_tool( + name='another_tool', tool_args={'value': 3.14}, ctx=run_context, tool=another_tool + ) + + assert result == {'result': 'success', 'value': 3.14, 'doubled': 6.28} + + async def test_call_tool_with_binary_content( + self, + fastmcp_toolset: FastMCPToolset[None], + run_context: RunContext[None], + ): + """Test tool call that returns binary content.""" + async with fastmcp_toolset: + tools = await fastmcp_toolset.get_tools(run_context) + binary_tool = tools['binary_tool'] + + result = await fastmcp_toolset.call_tool( + name='binary_tool', tool_args={}, ctx=run_context, tool=binary_tool + ) + + assert result == snapshot( + BinaryContent(data=b'fake_image_data', media_type='image/png', identifier='427d68') + ) + + async def test_call_tool_with_audio_content( + self, + fastmcp_toolset: FastMCPToolset[None], + run_context: RunContext[None], + ): + """Test tool call that returns audio content.""" + async with fastmcp_toolset: + tools = await fastmcp_toolset.get_tools(run_context) + audio_tool = tools['audio_tool'] + + result = await fastmcp_toolset.call_tool(name='audio_tool', tool_args={}, ctx=run_context, tool=audio_tool) + + assert result == snapshot( + BinaryContent(data=b'fake_audio_data', media_type='audio/mpeg', identifier='f1220f') + ) + + async def test_call_tool_with_text_content( + self, + fastmcp_toolset: FastMCPToolset[None], + run_context: RunContext[None], + ): + """Test tool call that returns text content.""" + async with fastmcp_toolset: + tools = await fastmcp_toolset.get_tools(run_context) + text_tool = tools['text_tool'] + + result = await fastmcp_toolset.call_tool( + name='text_tool', tool_args={'message': 'Hello World'}, ctx=run_context, tool=text_tool + ) + + assert result == snapshot({'result': 'Echo: Hello World'}) + + text_list_tool = tools['text_list_tool'] + + result = await fastmcp_toolset.call_tool( + name='text_list_tool', tool_args={'message': 'Hello World'}, ctx=run_context, tool=text_list_tool + ) + + assert result == snapshot(['Echo: Hello World', 'Echo: Hello World again']) + + async def test_call_tool_with_unknown_text_content( + self, + fastmcp_toolset: FastMCPToolset[None], + run_context: RunContext[None], + ): + """Test tool call that returns text content.""" + async with fastmcp_toolset: + tools = await fastmcp_toolset.get_tools(run_context) + text_tool = tools['text_tool_wo_return_annotation'] + + result = await fastmcp_toolset.call_tool( + name='text_tool_wo_return_annotation', + tool_args={'message': 'Hello World'}, + ctx=run_context, + tool=text_tool, + ) + + assert result == snapshot('Echo: Hello World') + + async def test_call_tool_with_json_content( + self, + fastmcp_toolset: FastMCPToolset[None], + run_context: RunContext[None], + ): + """Test tool call that returns JSON content.""" + async with fastmcp_toolset: + tools = await fastmcp_toolset.get_tools(run_context) + json_tool = tools['json_tool'] + + result = await fastmcp_toolset.call_tool( + name='json_tool', tool_args={'data': {'key': 'value'}}, ctx=run_context, tool=json_tool + ) + + # Should parse the JSON string into a dict + assert result == snapshot({'result': '{"received": {"key": "value"}, "processed": true}'}) + + async def test_call_tool_with_resource_link( + self, + fastmcp_toolset: FastMCPToolset[None], + run_context: RunContext[None], + ): + """Test tool call that returns resource link content.""" + async with fastmcp_toolset: + tools = await fastmcp_toolset.get_tools(run_context) + resource_link_tool = tools['resource_link_tool'] + + with pytest.raises( + NotImplementedError, + match='ResourceLink is not supported by the FastMCP toolset as reading resources is not yet supported.', + ): + await fastmcp_toolset.call_tool( + name='resource_link_tool', + tool_args={'message': 'Hello World'}, + ctx=run_context, + tool=resource_link_tool, + ) + + async def test_call_tool_with_embedded_resource( + self, + fastmcp_toolset: FastMCPToolset[None], + run_context: RunContext[None], + ): + """Test tool call that returns resource content.""" + async with fastmcp_toolset: + tools = await fastmcp_toolset.get_tools(run_context) + resource_tool = tools['resource_tool'] + + result = await fastmcp_toolset.call_tool( + name='resource_tool', tool_args={'message': 'Hello World'}, ctx=run_context, tool=resource_tool + ) + + assert result == snapshot('Hello World') + + async def test_call_tool_with_resource_tool_blob( + self, + fastmcp_toolset: FastMCPToolset[None], + run_context: RunContext[None], + ): + """Test tool call that returns resource blob content.""" + async with fastmcp_toolset: + tools = await fastmcp_toolset.get_tools(run_context) + resource_tool_blob = tools['resource_tool_blob'] + + result = await fastmcp_toolset.call_tool( + name='resource_tool_blob', + tool_args={'message': 'Hello World'}, + ctx=run_context, + tool=resource_tool_blob, + ) + + assert result == snapshot(BinaryContent(data=b'Hello World', media_type='application/octet-stream')) + + async def test_call_tool_with_error_behavior_raise( + self, + fastmcp_client: Client[FastMCPTransport], + run_context: RunContext[None], + ): + """Test tool call with error behavior set to raise.""" + toolset = FastMCPToolset(fastmcp_client, tool_error_behavior='error') + + async with toolset: + tools = await toolset.get_tools(run_context) + error_tool = tools['error_tool'] + + with pytest.raises(ToolError, match='This is a test error'): + await toolset.call_tool('error_tool', {}, run_context, error_tool) + + async def test_call_tool_with_error_behavior_model_retry( + self, + fastmcp_client: Client[FastMCPTransport], + run_context: RunContext[None], + ): + """Test tool call with error behavior set to model retry.""" + toolset = FastMCPToolset(fastmcp_client, tool_error_behavior='model_retry') + + async with toolset: + tools = await toolset.get_tools(run_context) + error_tool = tools['error_tool'] + + with pytest.raises(ModelRetry, match='This is a test error'): + await toolset.call_tool('error_tool', {}, run_context, error_tool) + + +class TestFastMCPToolsetFactoryMethods: + """Test FastMCP Toolset factory methods.""" + + async def test_python_stdio(self, run_context: RunContext[None]): + """Test creating toolset from FastMCP server.""" + server_script = """ +from fastmcp import FastMCP + +server = FastMCP('test_server') + +@server.tool() +async def test_tool(param1: str, param2: int = 0) -> str: + return f'param1={param1}, param2={param2}' + +server.run()""" + with TemporaryDirectory() as temp_dir: + server_py = Path(temp_dir) / 'server.py' + server_py.write_text(server_script) + toolset = FastMCPToolset(server_py) + + assert isinstance(toolset, FastMCPToolset) + assert toolset.id is None + + async with toolset: + tools = await toolset.get_tools(run_context) + assert 'test_tool' in tools + + async def test_transports(self): + """Test creating toolset from different transports.""" + toolset = FastMCPToolset('http://localhost:8000/mcp') + assert isinstance(toolset.client.transport, StreamableHttpTransport) + + toolset = FastMCPToolset('http://localhost:8000/sse') + assert isinstance(toolset.client.transport, SSETransport) + + toolset = FastMCPToolset(StdioTransport(command='python', args=['-c', 'print("test")'])) + assert isinstance(toolset.client.transport, StdioTransport) + + with TemporaryDirectory() as temp_dir: + server_py: Path = Path(temp_dir) / 'server.py' + server_py.write_text(data='') + toolset = FastMCPToolset(server_py) + assert isinstance(toolset.client.transport, PythonStdioTransport) + toolset = FastMCPToolset(str(server_py)) + assert isinstance(toolset.client.transport, PythonStdioTransport) + + server_js: Path = Path(temp_dir) / 'server.js' + server_js.write_text(data='') + toolset = FastMCPToolset(server_js) + assert isinstance(toolset.client.transport, NodeStdioTransport) + toolset = FastMCPToolset(str(server_js)) + assert isinstance(toolset.client.transport, NodeStdioTransport) + + toolset = FastMCPToolset( + {'mcpServers': {'test_server': {'command': 'python', 'args': ['-c', 'print("test")']}}} + ) + assert isinstance(toolset.client.transport, MCPConfigTransport) + + @pytest.mark.parametrize( + 'invalid_transport', ['tomato_is_not_a_valid_transport', '/path/to/server.ini', 'ftp://localhost'] + ) + async def test_invalid_transports_uninferrable(self, invalid_transport: str): + """Test creating toolset from invalid transports.""" + with pytest.raises(ValueError, match='Could not infer a valid transport from:'): + FastMCPToolset(invalid_transport) + + async def test_bad_transports(self): + """Test creating toolset from invalid transports.""" + with pytest.raises(ValueError, match='No MCP servers defined in the config'): + FastMCPToolset({'bad_transport': 'bad_value'}) + + async def test_in_memory_transport(self, run_context: RunContext[None]): + """Test creating toolset from stdio transport.""" + fastmcp_server = FastMCP('test_server') + + @fastmcp_server.tool() + def test_tool(param1: str, param2: int = 0) -> str: + return f'param1={param1}, param2={param2}' + + toolset = FastMCPToolset(fastmcp_server) + async with toolset: + tools = await toolset.get_tools(run_context) + assert 'test_tool' in tools + + result = await toolset.call_tool( + name='test_tool', tool_args={'param1': 'hello', 'param2': 42}, ctx=run_context, tool=tools['test_tool'] + ) + assert result == {'result': 'param1=hello, param2=42'} + + async def test_from_mcp_config_dict(self): + """Test creating toolset from MCP config dictionary.""" + + config_dict = {'mcpServers': {'test_server': {'command': 'python', 'args': ['-c', 'print("test")']}}} + + toolset = FastMCPToolset(config_dict) + client = toolset.client + assert isinstance(client.transport, MCPConfigTransport) diff --git a/uv.lock b/uv.lock index 25d229da64..7f739a3b18 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 2 requires-python = ">=3.10" resolution-markers = [ "python_full_version >= '3.13' and platform_python_implementation == 'PyPy'", @@ -397,6 +397,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5d/35/be73b6015511aa0173ec595fc579133b797ad532996f2998fd6b8d1bbe6b/audioop_lts-0.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:78bfb3703388c780edf900be66e07de5a3d4105ca8e8720c5c4d67927e0b15d0", size = 23918, upload-time = "2024-08-04T21:14:42.803Z" }, ] +[[package]] +name = "authlib" +version = "1.6.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography", version = "45.0.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "cryptography", version = "46.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/bb/73a1f1c64ee527877f64122422dafe5b87a846ccf4ac933fe21bcbb8fee8/authlib-1.6.4.tar.gz", hash = "sha256:104b0442a43061dc8bc23b133d1d06a2b0a9c2e3e33f34c4338929e816287649", size = 164046, upload-time = "2025-09-17T09:59:23.897Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/aa/91355b5f539caf1b94f0e66ff1e4ee39373b757fce08204981f7829ede51/authlib-1.6.4-py2.py3-none-any.whl", hash = "sha256:39313d2a2caac3ecf6d8f95fbebdfd30ae6ea6ae6a6db794d976405fdd9aa796", size = 243076, upload-time = "2025-09-17T09:59:22.259Z" }, +] + [[package]] name = "babel" version = "2.17.0" @@ -1089,6 +1102,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9d/3a/e39436efe51894243ff145a37c4f9a030839b97779ebcc4f13b3ba21c54e/cssselect2-0.7.0-py3-none-any.whl", hash = "sha256:fd23a65bfd444595913f02fc71f6b286c29261e354c41d722ca7a261a49b5969", size = 15586, upload-time = "2022-09-19T12:55:07.56Z" }, ] +[[package]] +name = "cyclopts" +version = "3.24.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "docstring-parser", marker = "python_full_version < '4'" }, + { name = "rich" }, + { name = "rich-rst" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/30/ca/7782da3b03242d5f0a16c20371dff99d4bd1fedafe26bc48ff82e42be8c9/cyclopts-3.24.0.tar.gz", hash = "sha256:de6964a041dfb3c57bf043b41e68c43548227a17de1bad246e3a0bfc5c4b7417", size = 76131, upload-time = "2025-09-08T15:40:57.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/8b/2c95f0645c6f40211896375e6fa51f504b8ccb29c21f6ae661fe87ab044e/cyclopts-3.24.0-py3-none-any.whl", hash = "sha256:809d04cde9108617106091140c3964ee6fceb33cecdd537f7ffa360bde13ed71", size = 86154, upload-time = "2025-09-08T15:40:56.41Z" }, +] + [[package]] name = "datasets" version = "4.0.0" @@ -1236,6 +1265,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, ] +[[package]] +name = "dnspython" +version = "2.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" }, +] + [[package]] name = "docker" version = "7.1.0" @@ -1259,6 +1297,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896, upload-time = "2025-07-21T07:35:00.684Z" }, ] +[[package]] +name = "docutils" +version = "0.22.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4a/c0/89fe6215b443b919cb98a5002e107cb5026854ed1ccb6b5833e0768419d1/docutils-0.22.2.tar.gz", hash = "sha256:9fdb771707c8784c8f2728b67cb2c691305933d68137ef95a75db5f4dfbc213d", size = 2289092, upload-time = "2025-09-20T17:55:47.994Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/66/dd/f95350e853a4468ec37478414fc04ae2d61dad7a947b3015c3dcc51a09b9/docutils-0.22.2-py3-none-any.whl", hash = "sha256:b0e98d679283fc3bb0ead8a5da7f501baa632654e7056e9c5846842213d674d8", size = 632667, upload-time = "2025-09-20T17:55:43.052Z" }, +] + [[package]] name = "duckdb" version = "1.3.2" @@ -1309,6 +1356,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/75/21/fc2c821a2c92c021f8f8adf9fb36235d1b49525b7cd953e85624296aab94/duckduckgo_search-7.5.0-py3-none-any.whl", hash = "sha256:6a2d3f12ae29b3e076cd43be61f5f73cd95261e0a0f318fe0ad3648d7a5dff03", size = 20238, upload-time = "2025-02-24T14:50:48.179Z" }, ] +[[package]] +name = "email-validator" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dnspython" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/22/900cb125c76b7aaa450ce02fd727f452243f2e91a61af068b40adba60ea9/email_validator-2.3.0.tar.gz", hash = "sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426", size = 51238, upload-time = "2025-08-26T13:09:06.831Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", size = 35604, upload-time = "2025-08-26T13:09:05.858Z" }, +] + [[package]] name = "eval-type-backport" version = "0.2.2" @@ -1405,6 +1465,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1f/2c/43927e22a2d57587b3aa09765098a6d833246b672d34c10c5f135414745a/fastavro-1.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:86baf8c9740ab570d0d4d18517da71626fe9be4d1142bea684db52bd5adb078f", size = 483967, upload-time = "2024-12-20T12:57:37.618Z" }, ] +[[package]] +name = "fastmcp" +version = "2.12.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "authlib" }, + { name = "cyclopts" }, + { name = "exceptiongroup" }, + { name = "httpx" }, + { name = "mcp" }, + { name = "openapi-core" }, + { name = "openapi-pydantic" }, + { name = "pydantic", extra = ["email"] }, + { name = "pyperclip" }, + { name = "python-dotenv" }, + { name = "rich" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a8/b2/57845353a9bc63002995a982e66f3d0be4ec761e7bcb89e7d0638518d42a/fastmcp-2.12.4.tar.gz", hash = "sha256:b55fe89537038f19d0f4476544f9ca5ac171033f61811cc8f12bdeadcbea5016", size = 7167745, upload-time = "2025-09-26T16:43:27.71Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/c7/562ff39f25de27caec01e4c1e88cbb5fcae5160802ba3d90be33165df24f/fastmcp-2.12.4-py3-none-any.whl", hash = "sha256:56188fbbc1a9df58c537063f25958c57b5c4d715f73e395c41b51550b247d140", size = 329090, upload-time = "2025-09-26T16:43:25.314Z" }, +] + [[package]] name = "ffmpy" version = "0.5.0" @@ -1928,6 +2010,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0a/66/7f8c48009c72d73bc6bbe6eb87ac838d6a526146f7dab14af671121eb379/invoke-2.2.0-py3-none-any.whl", hash = "sha256:6ea924cc53d4f78e3d98bc436b08069a03077e6f85ad1ddaa8a116d7dad15820", size = 160274, upload-time = "2023-07-12T18:05:16.294Z" }, ] +[[package]] +name = "isodate" +version = "0.7.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/4d/e940025e2ce31a8ce1202635910747e5a87cc3a6a6bb2d00973375014749/isodate-0.7.2.tar.gz", hash = "sha256:4cd1aa0f43ca76f4a6c6c0292a85f40b35ec2e43e315b59f06e6d32171a953e6", size = 29705, upload-time = "2024-10-08T23:04:11.5Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/aa/0aca39a37d3c7eb941ba736ede56d689e7be91cab5d9ca846bde3999eba6/isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15", size = 22320, upload-time = "2024-10-08T23:04:09.501Z" }, +] + [[package]] name = "jinja2" version = "3.1.6" @@ -2057,6 +2148,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fe/54/c86cd8e011fe98803d7e382fd67c0df5ceab8d2b7ad8c5a81524f791551c/jsonschema-4.25.0-py3-none-any.whl", hash = "sha256:24c2e8da302de79c8b9382fee3e76b355e44d2a4364bb207159ce10b517bd716", size = 89184, upload-time = "2025-07-18T15:39:42.956Z" }, ] +[[package]] +name = "jsonschema-path" +version = "0.3.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pathable" }, + { name = "pyyaml" }, + { name = "referencing" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6e/45/41ebc679c2a4fced6a722f624c18d658dee42612b83ea24c1caf7c0eb3a8/jsonschema_path-0.3.4.tar.gz", hash = "sha256:8365356039f16cc65fddffafda5f58766e34bebab7d6d105616ab52bc4297001", size = 11159, upload-time = "2025-01-24T14:33:16.547Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/58/3485da8cb93d2f393bce453adeef16896751f14ba3e2024bc21dc9597646/jsonschema_path-0.3.4-py3-none-any.whl", hash = "sha256:f502191fdc2b22050f9a81c9237be9d27145b9001c55842bece5e94e382e52f8", size = 14810, upload-time = "2025-01-24T14:33:14.652Z" }, +] + [[package]] name = "jsonschema-specifications" version = "2025.4.1" @@ -2069,6 +2175,51 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/01/0e/b27cdbaccf30b890c40ed1da9fd4a3593a5cf94dae54fb34f8a4b74fcd3f/jsonschema_specifications-2025.4.1-py3-none-any.whl", hash = "sha256:4653bffbd6584f7de83a67e0d620ef16900b390ddc7939d56684d6c81e33f1af", size = 18437, upload-time = "2025-04-23T12:34:05.422Z" }, ] +[[package]] +name = "lazy-object-proxy" +version = "1.12.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/08/a2/69df9c6ba6d316cfd81fe2381e464db3e6de5db45f8c43c6a23504abf8cb/lazy_object_proxy-1.12.0.tar.gz", hash = "sha256:1f5a462d92fd0cfb82f1fab28b51bfb209fabbe6aabf7f0d51472c0c124c0c61", size = 43681, upload-time = "2025-08-22T13:50:06.783Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/2b/d5e8915038acbd6c6a9fcb8aaf923dc184222405d3710285a1fec6e262bc/lazy_object_proxy-1.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:61d5e3310a4aa5792c2b599a7a78ccf8687292c8eb09cf187cca8f09cf6a7519", size = 26658, upload-time = "2025-08-22T13:42:23.373Z" }, + { url = "https://files.pythonhosted.org/packages/da/8f/91fc00eeea46ee88b9df67f7c5388e60993341d2a406243d620b2fdfde57/lazy_object_proxy-1.12.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1ca33565f698ac1aece152a10f432415d1a2aa9a42dfe23e5ba2bc255ab91f6", size = 68412, upload-time = "2025-08-22T13:42:24.727Z" }, + { url = "https://files.pythonhosted.org/packages/07/d2/b7189a0e095caedfea4d42e6b6949d2685c354263bdf18e19b21ca9b3cd6/lazy_object_proxy-1.12.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d01c7819a410f7c255b20799b65d36b414379a30c6f1684c7bd7eb6777338c1b", size = 67559, upload-time = "2025-08-22T13:42:25.875Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ad/b013840cc43971582ff1ceaf784d35d3a579650eb6cc348e5e6ed7e34d28/lazy_object_proxy-1.12.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:029d2b355076710505c9545aef5ab3f750d89779310e26ddf2b7b23f6ea03cd8", size = 66651, upload-time = "2025-08-22T13:42:27.427Z" }, + { url = "https://files.pythonhosted.org/packages/7e/6f/b7368d301c15612fcc4cd00412b5d6ba55548bde09bdae71930e1a81f2ab/lazy_object_proxy-1.12.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc6e3614eca88b1c8a625fc0a47d0d745e7c3255b21dac0e30b3037c5e3deeb8", size = 66901, upload-time = "2025-08-22T13:42:28.585Z" }, + { url = "https://files.pythonhosted.org/packages/61/1b/c6b1865445576b2fc5fa0fbcfce1c05fee77d8979fd1aa653dd0f179aefc/lazy_object_proxy-1.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:be5fe974e39ceb0d6c9db0663c0464669cf866b2851c73971409b9566e880eab", size = 26536, upload-time = "2025-08-22T13:42:29.636Z" }, + { url = "https://files.pythonhosted.org/packages/01/b3/4684b1e128a87821e485f5a901b179790e6b5bc02f89b7ee19c23be36ef3/lazy_object_proxy-1.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1cf69cd1a6c7fe2dbcc3edaa017cf010f4192e53796538cc7d5e1fedbfa4bcff", size = 26656, upload-time = "2025-08-22T13:42:30.605Z" }, + { url = "https://files.pythonhosted.org/packages/3a/03/1bdc21d9a6df9ff72d70b2ff17d8609321bea4b0d3cffd2cea92fb2ef738/lazy_object_proxy-1.12.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:efff4375a8c52f55a145dc8487a2108c2140f0bec4151ab4e1843e52eb9987ad", size = 68832, upload-time = "2025-08-22T13:42:31.675Z" }, + { url = "https://files.pythonhosted.org/packages/3d/4b/5788e5e8bd01d19af71e50077ab020bc5cce67e935066cd65e1215a09ff9/lazy_object_proxy-1.12.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1192e8c2f1031a6ff453ee40213afa01ba765b3dc861302cd91dbdb2e2660b00", size = 69148, upload-time = "2025-08-22T13:42:32.876Z" }, + { url = "https://files.pythonhosted.org/packages/79/0e/090bf070f7a0de44c61659cb7f74c2fe02309a77ca8c4b43adfe0b695f66/lazy_object_proxy-1.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3605b632e82a1cbc32a1e5034278a64db555b3496e0795723ee697006b980508", size = 67800, upload-time = "2025-08-22T13:42:34.054Z" }, + { url = "https://files.pythonhosted.org/packages/cf/d2/b320325adbb2d119156f7c506a5fbfa37fcab15c26d13cf789a90a6de04e/lazy_object_proxy-1.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a61095f5d9d1a743e1e20ec6d6db6c2ca511961777257ebd9b288951b23b44fa", size = 68085, upload-time = "2025-08-22T13:42:35.197Z" }, + { url = "https://files.pythonhosted.org/packages/6a/48/4b718c937004bf71cd82af3713874656bcb8d0cc78600bf33bb9619adc6c/lazy_object_proxy-1.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:997b1d6e10ecc6fb6fe0f2c959791ae59599f41da61d652f6c903d1ee58b7370", size = 26535, upload-time = "2025-08-22T13:42:36.521Z" }, + { url = "https://files.pythonhosted.org/packages/0d/1b/b5f5bd6bda26f1e15cd3232b223892e4498e34ec70a7f4f11c401ac969f1/lazy_object_proxy-1.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8ee0d6027b760a11cc18281e702c0309dd92da458a74b4c15025d7fc490deede", size = 26746, upload-time = "2025-08-22T13:42:37.572Z" }, + { url = "https://files.pythonhosted.org/packages/55/64/314889b618075c2bfc19293ffa9153ce880ac6153aacfd0a52fcabf21a66/lazy_object_proxy-1.12.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4ab2c584e3cc8be0dfca422e05ad30a9abe3555ce63e9ab7a559f62f8dbc6ff9", size = 71457, upload-time = "2025-08-22T13:42:38.743Z" }, + { url = "https://files.pythonhosted.org/packages/11/53/857fc2827fc1e13fbdfc0ba2629a7d2579645a06192d5461809540b78913/lazy_object_proxy-1.12.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:14e348185adbd03ec17d051e169ec45686dcd840a3779c9d4c10aabe2ca6e1c0", size = 71036, upload-time = "2025-08-22T13:42:40.184Z" }, + { url = "https://files.pythonhosted.org/packages/2b/24/e581ffed864cd33c1b445b5763d617448ebb880f48675fc9de0471a95cbc/lazy_object_proxy-1.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c4fcbe74fb85df8ba7825fa05eddca764138da752904b378f0ae5ab33a36c308", size = 69329, upload-time = "2025-08-22T13:42:41.311Z" }, + { url = "https://files.pythonhosted.org/packages/78/be/15f8f5a0b0b2e668e756a152257d26370132c97f2f1943329b08f057eff0/lazy_object_proxy-1.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:563d2ec8e4d4b68ee7848c5ab4d6057a6d703cb7963b342968bb8758dda33a23", size = 70690, upload-time = "2025-08-22T13:42:42.51Z" }, + { url = "https://files.pythonhosted.org/packages/5d/aa/f02be9bbfb270e13ee608c2b28b8771f20a5f64356c6d9317b20043c6129/lazy_object_proxy-1.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:53c7fd99eb156bbb82cbc5d5188891d8fdd805ba6c1e3b92b90092da2a837073", size = 26563, upload-time = "2025-08-22T13:42:43.685Z" }, + { url = "https://files.pythonhosted.org/packages/f4/26/b74c791008841f8ad896c7f293415136c66cc27e7c7577de4ee68040c110/lazy_object_proxy-1.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:86fd61cb2ba249b9f436d789d1356deae69ad3231dc3c0f17293ac535162672e", size = 26745, upload-time = "2025-08-22T13:42:44.982Z" }, + { url = "https://files.pythonhosted.org/packages/9b/52/641870d309e5d1fb1ea7d462a818ca727e43bfa431d8c34b173eb090348c/lazy_object_proxy-1.12.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:81d1852fb30fab81696f93db1b1e55a5d1ff7940838191062f5f56987d5fcc3e", size = 71537, upload-time = "2025-08-22T13:42:46.141Z" }, + { url = "https://files.pythonhosted.org/packages/47/b6/919118e99d51c5e76e8bf5a27df406884921c0acf2c7b8a3b38d847ab3e9/lazy_object_proxy-1.12.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be9045646d83f6c2664c1330904b245ae2371b5c57a3195e4028aedc9f999655", size = 71141, upload-time = "2025-08-22T13:42:47.375Z" }, + { url = "https://files.pythonhosted.org/packages/e5/47/1d20e626567b41de085cf4d4fb3661a56c159feaa73c825917b3b4d4f806/lazy_object_proxy-1.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:67f07ab742f1adfb3966c40f630baaa7902be4222a17941f3d85fd1dae5565ff", size = 69449, upload-time = "2025-08-22T13:42:48.49Z" }, + { url = "https://files.pythonhosted.org/packages/58/8d/25c20ff1a1a8426d9af2d0b6f29f6388005fc8cd10d6ee71f48bff86fdd0/lazy_object_proxy-1.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:75ba769017b944fcacbf6a80c18b2761a1795b03f8899acdad1f1c39db4409be", size = 70744, upload-time = "2025-08-22T13:42:49.608Z" }, + { url = "https://files.pythonhosted.org/packages/c0/67/8ec9abe15c4f8a4bcc6e65160a2c667240d025cbb6591b879bea55625263/lazy_object_proxy-1.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:7b22c2bbfb155706b928ac4d74c1a63ac8552a55ba7fff4445155523ea4067e1", size = 26568, upload-time = "2025-08-22T13:42:57.719Z" }, + { url = "https://files.pythonhosted.org/packages/23/12/cd2235463f3469fd6c62d41d92b7f120e8134f76e52421413a0ad16d493e/lazy_object_proxy-1.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4a79b909aa16bde8ae606f06e6bbc9d3219d2e57fb3e0076e17879072b742c65", size = 27391, upload-time = "2025-08-22T13:42:50.62Z" }, + { url = "https://files.pythonhosted.org/packages/60/9e/f1c53e39bbebad2e8609c67d0830cc275f694d0ea23d78e8f6db526c12d3/lazy_object_proxy-1.12.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:338ab2f132276203e404951205fe80c3fd59429b3a724e7b662b2eb539bb1be9", size = 80552, upload-time = "2025-08-22T13:42:51.731Z" }, + { url = "https://files.pythonhosted.org/packages/4c/b6/6c513693448dcb317d9d8c91d91f47addc09553613379e504435b4cc8b3e/lazy_object_proxy-1.12.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c40b3c9faee2e32bfce0df4ae63f4e73529766893258eca78548bac801c8f66", size = 82857, upload-time = "2025-08-22T13:42:53.225Z" }, + { url = "https://files.pythonhosted.org/packages/12/1c/d9c4aaa4c75da11eb7c22c43d7c90a53b4fca0e27784a5ab207768debea7/lazy_object_proxy-1.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:717484c309df78cedf48396e420fa57fc8a2b1f06ea889df7248fdd156e58847", size = 80833, upload-time = "2025-08-22T13:42:54.391Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ae/29117275aac7d7d78ae4f5a4787f36ff33262499d486ac0bf3e0b97889f6/lazy_object_proxy-1.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a6b7ea5ea1ffe15059eb44bcbcb258f97bcb40e139b88152c40d07b1a1dfc9ac", size = 79516, upload-time = "2025-08-22T13:42:55.812Z" }, + { url = "https://files.pythonhosted.org/packages/19/40/b4e48b2c38c69392ae702ae7afa7b6551e0ca5d38263198b7c79de8b3bdf/lazy_object_proxy-1.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:08c465fb5cd23527512f9bd7b4c7ba6cec33e28aad36fbbe46bf7b858f9f3f7f", size = 27656, upload-time = "2025-08-22T13:42:56.793Z" }, + { url = "https://files.pythonhosted.org/packages/ef/3a/277857b51ae419a1574557c0b12e0d06bf327b758ba94cafc664cb1e2f66/lazy_object_proxy-1.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c9defba70ab943f1df98a656247966d7729da2fe9c2d5d85346464bf320820a3", size = 26582, upload-time = "2025-08-22T13:49:49.366Z" }, + { url = "https://files.pythonhosted.org/packages/1a/b6/c5e0fa43535bb9c87880e0ba037cdb1c50e01850b0831e80eb4f4762f270/lazy_object_proxy-1.12.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6763941dbf97eea6b90f5b06eb4da9418cc088fce0e3883f5816090f9afcde4a", size = 71059, upload-time = "2025-08-22T13:49:50.488Z" }, + { url = "https://files.pythonhosted.org/packages/06/8a/7dcad19c685963c652624702f1a968ff10220b16bfcc442257038216bf55/lazy_object_proxy-1.12.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fdc70d81235fc586b9e3d1aeef7d1553259b62ecaae9db2167a5d2550dcc391a", size = 71034, upload-time = "2025-08-22T13:49:54.224Z" }, + { url = "https://files.pythonhosted.org/packages/12/ac/34cbfb433a10e28c7fd830f91c5a348462ba748413cbb950c7f259e67aa7/lazy_object_proxy-1.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0a83c6f7a6b2bfc11ef3ed67f8cbe99f8ff500b05655d8e7df9aab993a6abc95", size = 69529, upload-time = "2025-08-22T13:49:55.29Z" }, + { url = "https://files.pythonhosted.org/packages/6f/6a/11ad7e349307c3ca4c0175db7a77d60ce42a41c60bcb11800aabd6a8acb8/lazy_object_proxy-1.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:256262384ebd2a77b023ad02fbcc9326282bcfd16484d5531154b02bc304f4c5", size = 70391, upload-time = "2025-08-22T13:49:56.35Z" }, + { url = "https://files.pythonhosted.org/packages/59/97/9b410ed8fbc6e79c1ee8b13f8777a80137d4bc189caf2c6202358e66192c/lazy_object_proxy-1.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:7601ec171c7e8584f8ff3f4e440aa2eebf93e854f04639263875b8c2971f819f", size = 26988, upload-time = "2025-08-22T13:49:57.302Z" }, + { url = "https://files.pythonhosted.org/packages/41/a0/b91504515c1f9a299fc157967ffbd2f0321bce0516a3d5b89f6f4cad0355/lazy_object_proxy-1.12.0-pp39.pp310.pp311.graalpy311-none-any.whl", hash = "sha256:c3b2e0af1f7f77c4263759c4824316ce458fabe0fceadcd24ef8ca08b2d1e402", size = 15072, upload-time = "2025-08-22T13:50:05.498Z" }, +] + [[package]] name = "logfire" version = "4.0.0" @@ -2547,6 +2698,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7a/c3/4d9fbb14285698b7ae9f64423048ca9d28f5eb08b99f768b978f9118f780/modal-1.0.4-py3-none-any.whl", hash = "sha256:6c0d96bb49b09fa47e407a13e49545e32fe0803803b4330fbeb38de5e71209cc", size = 579637, upload-time = "2025-06-13T14:46:58.712Z" }, ] +[[package]] +name = "more-itertools" +version = "10.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ea/5d/38b681d3fce7a266dd9ab73c66959406d565b3e85f21d5e66e1181d93721/more_itertools-10.8.0.tar.gz", hash = "sha256:f638ddf8a1a0d134181275fb5d58b086ead7c6a72429ad725c67503f13ba30bd", size = 137431, upload-time = "2025-09-02T15:23:11.018Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl", hash = "sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b", size = 69667, upload-time = "2025-09-02T15:23:09.635Z" }, +] + [[package]] name = "multidict" version = "6.1.0" @@ -2807,6 +2967,67 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d3/65/e51a77a368eed7b9cc22ce394087ab43f13fa2884724729b716adf2da389/openai-1.107.2-py3-none-any.whl", hash = "sha256:d159d4f3ee3d9c717b248c5d69fe93d7773a80563c8b1ca8e9cad789d3cf0260", size = 946937, upload-time = "2025-09-12T19:52:19.355Z" }, ] +[[package]] +name = "openapi-core" +version = "0.19.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "isodate" }, + { name = "jsonschema" }, + { name = "jsonschema-path" }, + { name = "more-itertools" }, + { name = "openapi-schema-validator" }, + { name = "openapi-spec-validator" }, + { name = "parse" }, + { name = "typing-extensions" }, + { name = "werkzeug" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/35/1acaa5f2fcc6e54eded34a2ec74b479439c4e469fc4e8d0e803fda0234db/openapi_core-0.19.5.tar.gz", hash = "sha256:421e753da56c391704454e66afe4803a290108590ac8fa6f4a4487f4ec11f2d3", size = 103264, upload-time = "2025-03-20T20:17:28.193Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/6f/83ead0e2e30a90445ee4fc0135f43741aebc30cca5b43f20968b603e30b6/openapi_core-0.19.5-py3-none-any.whl", hash = "sha256:ef7210e83a59394f46ce282639d8d26ad6fc8094aa904c9c16eb1bac8908911f", size = 106595, upload-time = "2025-03-20T20:17:26.77Z" }, +] + +[[package]] +name = "openapi-pydantic" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/2e/58d83848dd1a79cb92ed8e63f6ba901ca282c5f09d04af9423ec26c56fd7/openapi_pydantic-0.5.1.tar.gz", hash = "sha256:ff6835af6bde7a459fb93eb93bb92b8749b754fc6e51b2f1590a19dc3005ee0d", size = 60892, upload-time = "2025-01-08T19:29:27.083Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/cf/03675d8bd8ecbf4445504d8071adab19f5f993676795708e36402ab38263/openapi_pydantic-0.5.1-py3-none-any.whl", hash = "sha256:a3a09ef4586f5bd760a8df7f43028b60cafb6d9f61de2acba9574766255ab146", size = 96381, upload-time = "2025-01-08T19:29:25.275Z" }, +] + +[[package]] +name = "openapi-schema-validator" +version = "0.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsonschema" }, + { name = "jsonschema-specifications" }, + { name = "rfc3339-validator" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/f3/5507ad3325169347cd8ced61c232ff3df70e2b250c49f0fe140edb4973c6/openapi_schema_validator-0.6.3.tar.gz", hash = "sha256:f37bace4fc2a5d96692f4f8b31dc0f8d7400fd04f3a937798eaf880d425de6ee", size = 11550, upload-time = "2025-01-10T18:08:22.268Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/21/c6/ad0fba32775ae749016829dace42ed80f4407b171da41313d1a3a5f102e4/openapi_schema_validator-0.6.3-py3-none-any.whl", hash = "sha256:f3b9870f4e556b5a62a1c39da72a6b4b16f3ad9c73dc80084b1b11e74ba148a3", size = 8755, upload-time = "2025-01-10T18:08:19.758Z" }, +] + +[[package]] +name = "openapi-spec-validator" +version = "0.7.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsonschema" }, + { name = "jsonschema-path" }, + { name = "lazy-object-proxy" }, + { name = "openapi-schema-validator" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/82/af/fe2d7618d6eae6fb3a82766a44ed87cd8d6d82b4564ed1c7cfb0f6378e91/openapi_spec_validator-0.7.2.tar.gz", hash = "sha256:cc029309b5c5dbc7859df0372d55e9d1ff43e96d678b9ba087f7c56fc586f734", size = 36855, upload-time = "2025-06-07T14:48:56.299Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/dd/b3fd642260cb17532f66cc1e8250f3507d1e580483e209dc1e9d13bd980d/openapi_spec_validator-0.7.2-py3-none-any.whl", hash = "sha256:4bbdc0894ec85f1d1bea1d6d9c8b2c3c8d7ccaa13577ef40da9c006c9fd0eb60", size = 39713, upload-time = "2025-06-07T14:48:54.077Z" }, +] + [[package]] name = "opentelemetry-api" version = "1.30.0" @@ -3130,6 +3351,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ab/5f/b38085618b950b79d2d9164a711c52b10aefc0ae6833b96f626b7021b2ed/pandas-2.2.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ad5b65698ab28ed8d7f18790a0dc58005c7629f227be9ecc1072aa74c0c1d43a", size = 13098436, upload-time = "2024-09-20T13:09:48.112Z" }, ] +[[package]] +name = "parse" +version = "1.20.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4f/78/d9b09ba24bb36ef8b83b71be547e118d46214735b6dfb39e4bfde0e9b9dd/parse-1.20.2.tar.gz", hash = "sha256:b41d604d16503c79d81af5165155c0b20f6c8d6c559efa66b4b695c3e5a0a0ce", size = 29391, upload-time = "2024-06-11T04:41:57.34Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/31/ba45bf0b2aa7898d81cbbfac0e88c267befb59ad91a19e36e1bc5578ddb1/parse-1.20.2-py2.py3-none-any.whl", hash = "sha256:967095588cb802add9177d0c0b6133b5ba33b1ea9007ca800e526f42a85af558", size = 20126, upload-time = "2024-06-11T04:41:55.057Z" }, +] + +[[package]] +name = "pathable" +version = "0.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/67/93/8f2c2075b180c12c1e9f6a09d1a985bc2036906b13dff1d8917e395f2048/pathable-0.4.4.tar.gz", hash = "sha256:6905a3cd17804edfac7875b5f6c9142a218c7caef78693c2dbbbfbac186d88b2", size = 8124, upload-time = "2025-01-10T18:43:13.247Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/eb/b6260b31b1a96386c0a880edebe26f89669098acea8e0318bff6adb378fd/pathable-0.4.4-py3-none-any.whl", hash = "sha256:5ae9e94793b6ef5a4cbe0a7ce9dbbefc1eec38df253763fd0aeeacf2762dbbc2", size = 9592, upload-time = "2025-01-10T18:43:11.88Z" }, +] + [[package]] name = "pathspec" version = "0.12.1" @@ -3647,11 +3886,16 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b", size = 444782, upload-time = "2025-06-14T08:33:14.905Z" }, ] +[package.optional-dependencies] +email = [ + { name = "email-validator" }, +] + [[package]] name = "pydantic-ai" source = { editable = "." } dependencies = [ - { name = "pydantic-ai-slim", extra = ["ag-ui", "anthropic", "bedrock", "cli", "cohere", "evals", "google", "groq", "huggingface", "logfire", "mcp", "mistral", "openai", "retries", "temporal", "vertexai"] }, + { name = "pydantic-ai-slim", extra = ["ag-ui", "anthropic", "bedrock", "cli", "cohere", "evals", "fastmcp", "google", "groq", "huggingface", "logfire", "mcp", "mistral", "openai", "retries", "temporal", "vertexai"] }, ] [package.optional-dependencies] @@ -3715,7 +3959,7 @@ lint = [ requires-dist = [ { name = "fasta2a", marker = "extra == 'a2a'", specifier = ">=0.4.1" }, { name = "pydantic-ai-examples", marker = "extra == 'examples'", editable = "examples" }, - { name = "pydantic-ai-slim", extras = ["ag-ui", "anthropic", "bedrock", "cli", "cohere", "evals", "google", "groq", "huggingface", "logfire", "mcp", "mistral", "openai", "retries", "temporal", "vertexai"], editable = "pydantic_ai_slim" }, + { name = "pydantic-ai-slim", extras = ["ag-ui", "anthropic", "bedrock", "cli", "cohere", "evals", "fastmcp", "google", "groq", "huggingface", "logfire", "mcp", "mistral", "openai", "retries", "temporal", "vertexai"], editable = "pydantic_ai_slim" }, { name = "pydantic-ai-slim", extras = ["dbos"], marker = "extra == 'dbos'", editable = "pydantic_ai_slim" }, { name = "pydantic-ai-slim", extras = ["prefect"], marker = "extra == 'prefect'", editable = "pydantic_ai_slim" }, ] @@ -3850,6 +4094,9 @@ duckduckgo = [ evals = [ { name = "pydantic-evals" }, ] +fastmcp = [ + { name = "fastmcp" }, +] google = [ { name = "google-genai" }, ] @@ -3899,6 +4146,7 @@ requires-dist = [ { name = "ddgs", marker = "extra == 'duckduckgo'", specifier = ">=9.0.0" }, { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "fasta2a", marker = "extra == 'a2a'", specifier = ">=0.4.1" }, + { name = "fastmcp", marker = "extra == 'fastmcp'", specifier = ">=2.12.0" }, { name = "genai-prices", specifier = ">=0.0.35" }, { name = "google-auth", marker = "extra == 'vertexai'", specifier = ">=2.36.0" }, { name = "google-genai", marker = "extra == 'google'", specifier = ">=1.46.0" }, @@ -3925,7 +4173,7 @@ requires-dist = [ { name = "tenacity", marker = "extra == 'retries'", specifier = ">=8.2.3" }, { name = "typing-inspection", specifier = ">=0.4.0" }, ] -provides-extras = ["a2a", "ag-ui", "anthropic", "bedrock", "cli", "cohere", "dbos", "duckduckgo", "evals", "google", "groq", "huggingface", "logfire", "mcp", "mistral", "openai", "prefect", "retries", "tavily", "temporal", "vertexai"] +provides-extras = ["a2a", "ag-ui", "anthropic", "bedrock", "cli", "cohere", "dbos", "duckduckgo", "evals", "fastmcp", "google", "groq", "huggingface", "logfire", "mcp", "mistral", "openai", "prefect", "retries", "tavily", "temporal", "vertexai"] [[package]] name = "pydantic-core" @@ -4234,11 +4482,11 @@ wheels = [ [[package]] name = "python-dotenv" -version = "1.0.1" +version = "1.1.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bc/57/e84d88dfe0aec03b7a2d4327012c1627ab5f03652216c63d49846d7a6c58/python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", size = 39115, upload-time = "2024-01-23T06:33:00.505Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/b0/4bc07ccd3572a2f9df7e6782f52b0c6c90dcbb803ac4a167702d7d0dfe1e/python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab", size = 41978, upload-time = "2025-06-24T04:21:07.341Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863, upload-time = "2024-01-23T06:32:58.246Z" }, + { url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload-time = "2025-06-24T04:21:06.073Z" }, ] [[package]] @@ -4509,6 +4757,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424, upload-time = "2024-11-01T16:43:55.817Z" }, ] +[[package]] +name = "rich-rst" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, + { name = "rich" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b0/69/5514c3a87b5f10f09a34bb011bc0927bc12c596c8dae5915604e71abc386/rich_rst-1.3.1.tar.gz", hash = "sha256:fad46e3ba42785ea8c1785e2ceaa56e0ffa32dbe5410dec432f37e4107c4f383", size = 13839, upload-time = "2024-04-30T04:40:38.125Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/bc/cc4e3dbc5e7992398dcb7a8eda0cbcf4fb792a0cdb93f857b478bf3cf884/rich_rst-1.3.1-py3-none-any.whl", hash = "sha256:498a74e3896507ab04492d326e794c3ef76e7cda078703aa592d1853d91098c1", size = 11621, upload-time = "2024-04-30T04:40:32.619Z" }, +] + [[package]] name = "rpds-py" version = "0.26.0" @@ -5542,6 +5803,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7b/c8/d529f8a32ce40d98309f4470780631e971a5a842b60aec864833b3615786/websockets-14.2-py3-none-any.whl", hash = "sha256:7a6ceec4ea84469f15cf15807a747e9efe57e369c384fa86e022b3bea679b79b", size = 157416, upload-time = "2025-01-19T21:00:54.843Z" }, ] +[[package]] +name = "werkzeug" +version = "3.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/32/af/d4502dc713b4ccea7175d764718d5183caf8d0867a4f0190d5d4a45cea49/werkzeug-3.1.1.tar.gz", hash = "sha256:8cd39dfbdfc1e051965f156163e2974e52c210f130810e9ad36858f0fd3edad4", size = 806453, upload-time = "2024-11-01T16:40:45.462Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/ea/c67e1dee1ba208ed22c06d1d547ae5e293374bfc43e0eb0ef5e262b68561/werkzeug-3.1.1-py3-none-any.whl", hash = "sha256:a71124d1ef06008baafa3d266c02f56e1836a5984afd6dd6c9230669d60d9fb5", size = 224371, upload-time = "2024-11-01T16:40:43.994Z" }, +] + [[package]] name = "whenever" version = "0.8.9"