-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Feature: Minimal MCP client Resources support #3024
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
fennb
wants to merge
22
commits into
pydantic:main
Choose a base branch
from
fennb:feature/1783-minimal-mcp-resources-support
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 12 commits
Commits
Show all changes
22 commits
Select commit
Hold shift + click to select a range
9fde764
Add initial list_resources() method to MCPServer.
fennb a7404a2
Add list_resource_templates() to MCPServer.
fennb ecea466
Workaround limitations with inline-snapshot & AnyUrl.
fennb 1886299
Add basic read_resource() to MCPServer.
fennb dfb54db
Update mcp client docs to add Resources section.
fennb f52bb71
Organize imports in doc examples.
fennb 232ff04
Update MCP resource example docs.
fennb 38675da
Merge branch 'pydantic:main' into feature/1783-minimal-mcp-resources-…
fennb 317416c
Create native Resource and ResourceTemplate types and update MCPServe…
fennb 67f9b07
Update MCPServer.read_resource() to decode/return native types.
fennb e6cb086
Add native MCP ResourceAnnotations type.
fennb b8424d8
Allow MCPServer.read_resource() to read resources by Resource.
fennb 50cab26
Merge branch 'main' into feature/1783-minimal-mcp-resources-support
fennb 6857ef2
Bump dependency mcp>=1.18.0
fennb 18743cd
Add test coverage for ResourceAnnotations now that we can.
fennb 4e5d627
Add MCPServer.capabilities property.
fennb 6f3a87a
Introduce native MCPError type and use it in MCP server resource meth…
fennb 72d66ac
Simplify error messages.
fennb 3000511
Add MCPServerError and use it appropriately upon upstream resource er…
fennb 30f4e7f
Cleanup MCP error naming and usage.
fennb 456e714
Increase MCP server test coverage.
fennb 3d95521
Fix test coverage gap.
fennb File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,12 @@ | ||
| import base64 | ||
| from abc import ABC | ||
| from collections.abc import Sequence | ||
| from typing import Literal | ||
| from dataclasses import dataclass | ||
| from typing import Annotated, Any, Literal | ||
|
|
||
| from . import exceptions, messages | ||
| from pydantic import Field | ||
|
|
||
| from . import _utils, exceptions, messages | ||
|
|
||
| try: | ||
| from mcp import types as mcp_types | ||
|
|
@@ -13,6 +17,63 @@ | |
| ) from _import_error | ||
|
|
||
|
|
||
| @dataclass(repr=False, kw_only=True) | ||
| class ResourceAnnotations: | ||
| """Additional properties describing MCP entities.""" | ||
|
|
||
| audience: list[mcp_types.Role] | None = None | ||
| """Intended audience for this entity.""" | ||
|
|
||
| priority: Annotated[float, Field(ge=0.0, le=1.0)] | None = None | ||
| """Priority level for this entity, ranging from 0.0 to 1.0.""" | ||
|
|
||
| __repr__ = _utils.dataclasses_no_defaults_repr | ||
|
|
||
|
|
||
| @dataclass(repr=False, kw_only=True) | ||
| class BaseResource(ABC): | ||
| """Base class for MCP resources.""" | ||
|
|
||
| name: str | ||
| """The programmatic name of the resource.""" | ||
|
|
||
| title: str | None = None | ||
| """Human-readable title for UI contexts.""" | ||
|
|
||
| description: str | None = None | ||
| """A description of what this resource represents.""" | ||
|
|
||
| mime_type: str | None = None | ||
| """The MIME type of the resource, if known.""" | ||
|
|
||
| annotations: ResourceAnnotations | None = None | ||
| """Optional annotations for the resource.""" | ||
|
|
||
| meta: dict[str, Any] | None = None | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we call this |
||
| """Optional metadata for the resource.""" | ||
|
|
||
| __repr__ = _utils.dataclasses_no_defaults_repr | ||
|
|
||
|
|
||
| @dataclass(repr=False, kw_only=True) | ||
| class Resource(BaseResource): | ||
| """A resource that can be read from an MCP server.""" | ||
|
|
||
| uri: str | ||
| """The URI of the resource.""" | ||
|
|
||
| size: int | None = None | ||
| """The size of the raw resource content in bytes (before base64 encoding), if known.""" | ||
|
|
||
|
|
||
| @dataclass(repr=False, kw_only=True) | ||
| class ResourceTemplate(BaseResource): | ||
| """A template for parameterized resources on an MCP server.""" | ||
|
|
||
| uri_template: str | ||
| """URI template (RFC 6570) for constructing resource URIs.""" | ||
|
|
||
|
|
||
| def map_from_mcp_params(params: mcp_types.CreateMessageRequestParams) -> list[messages.ModelMessage]: | ||
| """Convert from MCP create message request parameters to pydantic-ai messages.""" | ||
| pai_messages: list[messages.ModelMessage] = [] | ||
|
|
@@ -121,3 +182,38 @@ def map_from_sampling_content( | |
| return messages.TextPart(content=content.text) | ||
| else: | ||
| raise NotImplementedError('Image and Audio responses in sampling are not yet supported') | ||
|
|
||
|
|
||
| def map_from_mcp_resource(mcp_resource: mcp_types.Resource) -> Resource: | ||
fennb marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| """Convert from MCP Resource to native Pydantic AI Resource.""" | ||
| return Resource( | ||
| uri=str(mcp_resource.uri), | ||
| name=mcp_resource.name, | ||
| title=mcp_resource.title, | ||
| description=mcp_resource.description, | ||
| mime_type=mcp_resource.mimeType, | ||
| size=mcp_resource.size, | ||
| annotations=( | ||
fennb marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| ResourceAnnotations(audience=mcp_resource.annotations.audience, priority=mcp_resource.annotations.priority) | ||
| if mcp_resource.annotations | ||
| else None | ||
| ), | ||
| meta=mcp_resource.meta, | ||
| ) | ||
|
|
||
|
|
||
| def map_from_mcp_resource_template(mcp_template: mcp_types.ResourceTemplate) -> ResourceTemplate: | ||
| """Convert from MCP ResourceTemplate to native Pydantic AI ResourceTemplate.""" | ||
| return ResourceTemplate( | ||
| uri_template=mcp_template.uriTemplate, | ||
| name=mcp_template.name, | ||
| title=mcp_template.title, | ||
| description=mcp_template.description, | ||
| mime_type=mcp_template.mimeType, | ||
| annotations=( | ||
| ResourceAnnotations(audience=mcp_template.annotations.audience, priority=mcp_template.annotations.priority) | ||
| if mcp_template.annotations | ||
| else None | ||
| ), | ||
| meta=mcp_template.meta, | ||
| ) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,13 +10,13 @@ | |
| from dataclasses import field, replace | ||
| from datetime import timedelta | ||
| from pathlib import Path | ||
| from typing import Annotated, Any | ||
| from typing import Annotated, Any, overload | ||
|
|
||
| import anyio | ||
| import httpx | ||
| import pydantic_core | ||
| from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream | ||
| from pydantic import BaseModel, Discriminator, Field, Tag | ||
| from pydantic import AnyUrl, BaseModel, Discriminator, Field, Tag | ||
| from pydantic_core import CoreSchema, core_schema | ||
| from typing_extensions import Self, assert_never, deprecated | ||
|
|
||
|
|
@@ -303,6 +303,52 @@ def tool_for_tool_def(self, tool_def: ToolDefinition) -> ToolsetTool[Any]: | |
| args_validator=TOOL_SCHEMA_VALIDATOR, | ||
| ) | ||
|
|
||
| async def list_resources(self) -> list[_mcp.Resource]: | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we return these types, they should be public! |
||
| """Retrieve resources that are currently present on the server. | ||
|
|
||
| Note: | ||
| - We don't cache resources as they might change. | ||
| - We also don't subscribe to resource changes to avoid complexity. | ||
| """ | ||
| async with self: # Ensure server is running | ||
| result = await self._client.list_resources() | ||
| return [_mcp.map_from_mcp_resource(r) for r in result.resources] | ||
|
|
||
| async def list_resource_templates(self) -> list[_mcp.ResourceTemplate]: | ||
| """Retrieve resource templates that are currently present on the server.""" | ||
| async with self: # Ensure server is running | ||
| result = await self._client.list_resource_templates() | ||
| return [_mcp.map_from_mcp_resource_template(t) for t in result.resourceTemplates] | ||
|
|
||
| @overload | ||
| async def read_resource(self, uri: str) -> str | messages.BinaryContent | list[str | messages.BinaryContent]: ... | ||
|
|
||
| @overload | ||
| async def read_resource( | ||
| self, uri: _mcp.Resource | ||
| ) -> str | messages.BinaryContent | list[str | messages.BinaryContent]: ... | ||
|
|
||
| async def read_resource( | ||
| self, uri: str | _mcp.Resource | ||
| ) -> str | messages.BinaryContent | list[str | messages.BinaryContent]: | ||
| """Read the contents of a specific resource by URI. | ||
|
|
||
| Args: | ||
| uri: The URI of the resource to read, or a Resource object. | ||
|
|
||
| Returns: | ||
| The resource contents. If the resource has a single content item, returns that item directly. | ||
| If the resource has multiple content items, returns a list of items. | ||
| """ | ||
| resource_uri = uri if isinstance(uri, str) else uri.uri | ||
| async with self: # Ensure server is running | ||
| result = await self._client.read_resource(AnyUrl(resource_uri)) | ||
| return ( | ||
| self._get_content(result.contents[0]) | ||
| if len(result.contents) == 1 | ||
| else [self._get_content(resource) for resource in result.contents] | ||
| ) | ||
|
|
||
| async def __aenter__(self) -> Self: | ||
| """Enter the MCP server context. | ||
|
|
||
|
|
@@ -397,12 +443,7 @@ async def _map_tool_result_part( | |
| resource = part.resource | ||
| return self._get_content(resource) | ||
| elif isinstance(part, mcp_types.ResourceLink): | ||
| resource_result: mcp_types.ReadResourceResult = await self._client.read_resource(part.uri) | ||
| return ( | ||
| self._get_content(resource_result.contents[0]) | ||
| if len(resource_result.contents) == 1 | ||
| else [self._get_content(resource) for resource in resource_result.contents] | ||
| ) | ||
| return await self.read_resource(str(part.uri)) | ||
| else: | ||
| assert_never(part) | ||
|
|
||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's make it extra explicit that resources are not automatically shared with the LLM, unless a tool returns a resource link.