Merge branch 'main' into patch-2

This commit is contained in:
Jeremiah Lowin 2025-04-23 11:34:36 -07:00 committed by GitHub
commit 6ce6b116a7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 149 additions and 38 deletions

View file

@ -61,7 +61,8 @@
"patterns/composition",
"patterns/decorating-methods",
"patterns/openapi",
"patterns/fastapi"
"patterns/fastapi",
"patterns/contrib"
]
},
{

42
docs/patterns/contrib.mdx Normal file
View file

@ -0,0 +1,42 @@
---
title: "Contrib Modules"
description: "Community-contributed modules extending FastMCP"
icon: "cubes"
---
FastMCP includes a `contrib` package that holds community-contributed modules. These modules extend FastMCP's functionality but aren't officially maintained by the core team.
Contrib modules provide additional features, integrations, or patterns that complement the core FastMCP library. They offer a way for the community to share useful extensions while keeping the core library focused and maintainable.
The available modules can be viewed in the [contrib directory](https://github.com/jlowin/fastmcp/tree/main/src/contrib).
## Usage
To use a contrib module, import it from the `fastmcp.contrib` package:
```python
from fastmcp.contrib import my_module
```
## Important Considerations
- **Stability**: Modules in `contrib` may have different testing requirements or stability guarantees compared to the core library.
- **Compatibility**: Changes to core FastMCP might break modules in `contrib` without explicit warnings in the main changelog.
- **Dependencies**: Contrib modules may have additional dependencies not required by the core library. These dependencies are typically documented in the module's README or separate requirements files.
## Contributing
We welcome contributions to the `contrib` package! If you have a module that extends FastMCP in a useful way, consider contributing it:
1. Create a new directory in `src/fastmcp/contrib/` for your module
3. Add proper tests for your module in `tests/contrib/`
2. Include comprehensive documentation in a README.md file, including usage and examples, as well as any additional dependencies or installation instructions
5. Submit a pull request
The ideal contrib module:
- Solves a specific use case or integration need
- Follows FastMCP coding standards
- Includes thorough documentation and examples
- Has comprehensive tests
- Specifies any additional dependencies

View file

@ -45,7 +45,8 @@ class Client:
):
self.transport = infer_transport(transport)
self._session: ClientSession | None = None
self._session_cms: list[AbstractAsyncContextManager[ClientSession]] = []
self._session_cm: AbstractAsyncContextManager[ClientSession] | None = None
self._nesting_counter: int = 0
self._session_kwargs: SessionKwargs = {
"sampling_callback": None,
@ -85,29 +86,21 @@ class Client:
return self._session is not None
async def __aenter__(self):
if self.is_connected():
# We're already connected, no need to add None to the session_cms list
return self
if self._nesting_counter == 0:
# create new session
self._session_cm = self.transport.connect_session(**self._session_kwargs)
self._session = await self._session_cm.__aenter__()
try:
session_cm = self.transport.connect_session(**self._session_kwargs)
self._session_cms.append(session_cm)
self._session = await self._session_cms[-1].__aenter__()
return self
except Exception as e:
# Ensure cleanup if __aenter__ fails partially
self._session = None
if self._session_cms:
self._session_cms.pop()
raise ConnectionError(
f"Failed to connect using {self.transport}: {e}"
) from e
self._nesting_counter += 1
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
if self._session_cms:
await self._session_cms[-1].__aexit__(exc_type, exc_val, exc_tb)
self._nesting_counter -= 1
if self._nesting_counter == 0 and self._session_cm is not None:
await self._session_cm.__aexit__(exc_type, exc_val, exc_tb)
self._session_cm = None
self._session = None
self._session_cms.pop()
# --- MCP Client Methods ---
async def ping(self) -> None:

View file

@ -3,6 +3,7 @@ import contextlib
import datetime
import os
import shutil
import sys
from collections.abc import AsyncIterator
from pathlib import Path
from typing import (
@ -185,7 +186,7 @@ class PythonStdioTransport(StdioTransport):
args: list[str] | None = None,
env: dict[str, str] | None = None,
cwd: str | None = None,
python_cmd: str = "python",
python_cmd: str = sys.executable,
):
"""
Initialize a Python transport.

View file

@ -6,4 +6,14 @@ This directory holds community-contributed modules for FastMCP. These modules ex
* Modules in `contrib` may have different testing requirements or stability guarantees compared to the core library.
* Changes to the core FastMCP library might break modules in `contrib` without explicit warnings in the main changelog.
Use these modules at your own discretion. Contributions are welcome, but please include tests and documentation.
Use these modules at your own discretion. Contributions are welcome, but please include tests and documentation.
## Usage
To use a contrib module, import it from the `fastmcp.contrib` package.
```python
from fastmcp.contrib import my_module
```
Note that the contrib modules may have different dependencies than the core library, which can be noted in their respective README's or even separate requirements / dependency files.

View file

@ -3,10 +3,14 @@ from typing import Any
from mcp.types import CallToolResult
from pydantic import BaseModel, Field
from contrib.mcp_mixin.mcp_mixin import _DEFAULT_SEPARATOR_TOOL, MCPMixin, mcp_tool
from fastmcp import FastMCP
from fastmcp.client import Client
from fastmcp.client.transports import FastMCPTransport
from fastmcp.contrib.mcp_mixin.mcp_mixin import (
_DEFAULT_SEPARATOR_TOOL,
MCPMixin,
mcp_tool,
)
class CallToolRequest(BaseModel):

View file

@ -1,7 +1,7 @@
"""Sample code for FastMCP using MCPMixin."""
from contrib.bulk_tool_caller import BulkToolCaller
from fastmcp import FastMCP
from fastmcp.contrib.bulk_tool_caller import BulkToolCaller
mcp = FastMCP()

View file

@ -10,7 +10,7 @@ Inherit from `MCPMixin` and use the decorators on the methods you want to regist
```python
from fastmcp import FastMCP
from contrib.mcp_mixin import MCPMixin, mcp_tool, mcp_resource
from fastmcp.contrib.mcp_mixin import MCPMixin, mcp_tool, mcp_resource
class MyComponent(MCPMixin):
@mcp_tool(name="my_tool", description="Does something cool.")

View file

@ -2,13 +2,13 @@
import asyncio
from contrib.mcp_mixin import (
from fastmcp import FastMCP
from fastmcp.contrib.mcp_mixin import (
MCPMixin,
mcp_prompt,
mcp_resource,
mcp_tool,
)
from fastmcp import FastMCP
mcp = FastMCP()

View file

@ -3,7 +3,9 @@ from urllib.parse import quote
import mcp.types
from mcp.server.lowlevel.helper_types import ReadResourceContents
from mcp.shared.exceptions import McpError
from mcp.types import (
METHOD_NOT_FOUND,
BlobResourceContents,
EmbeddedResource,
GetPromptResult,
@ -173,7 +175,14 @@ class FastMCPProxy(FastMCP):
tools = await super().get_tools()
async with self.client:
for tool in await self.client.list_tools():
try:
client_tools = await self.client.list_tools()
except McpError as e:
if e.error.code == METHOD_NOT_FOUND:
client_tools = []
else:
raise e
for tool in client_tools:
tool_proxy = await ProxyTool.from_client(self.client, tool)
tools[tool_proxy.name] = tool_proxy
@ -183,7 +192,14 @@ class FastMCPProxy(FastMCP):
resources = await super().get_resources()
async with self.client:
for resource in await self.client.list_resources():
try:
client_resources = await self.client.list_resources()
except McpError as e:
if e.error.code == METHOD_NOT_FOUND:
client_resources = []
else:
raise e
for resource in client_resources:
resource_proxy = await ProxyResource.from_client(self.client, resource)
resources[str(resource_proxy.uri)] = resource_proxy
@ -193,7 +209,14 @@ class FastMCPProxy(FastMCP):
templates = await super().get_resource_templates()
async with self.client:
for template in await self.client.list_resource_templates():
try:
client_templates = await self.client.list_resource_templates()
except McpError as e:
if e.error.code == METHOD_NOT_FOUND:
client_templates = []
else:
raise e
for template in client_templates:
template_proxy = await ProxyTemplate.from_client(self.client, template)
templates[template_proxy.uri_template] = template_proxy
@ -203,7 +226,14 @@ class FastMCPProxy(FastMCP):
prompts = await super().get_prompts()
async with self.client:
for prompt in await self.client.list_prompts():
try:
client_prompts = await self.client.list_prompts()
except McpError as e:
if e.error.code == METHOD_NOT_FOUND:
client_prompts = []
else:
raise e
for prompt in client_prompts:
prompt_proxy = await ProxyPrompt.from_client(self.client, prompt)
prompts[prompt_proxy.name] = prompt_proxy
return prompts

View file

@ -160,6 +160,36 @@ async def test_client_connection(fastmcp_server):
assert not client.is_connected()
async def test_client_nested_context_manager(fastmcp_server):
"""Test that the client connects and disconnects once in nested context manager."""
client = Client(fastmcp_server)
# Before connection
assert not client.is_connected()
assert client._session is None
# During connection
async with client:
assert client.is_connected()
assert client._session is not None
session = client._session
# Re-use the same session
async with client:
assert client.is_connected()
assert client._session is session
# Re-use the same session
async with client:
assert client.is_connected()
assert client._session is session
# After connection
assert not client.is_connected()
assert client._session is None
async def test_resource_template(fastmcp_server):
"""Test using a resource template with InMemoryClient."""
client = Client(transport=FastMCPTransport(fastmcp_server))

View file

@ -3,12 +3,12 @@ from typing import Any
import pytest
from mcp.types import EmbeddedResource, ImageContent, TextContent
from contrib.bulk_tool_caller.bulk_tool_caller import (
from fastmcp import FastMCP
from fastmcp.contrib.bulk_tool_caller.bulk_tool_caller import (
BulkToolCaller,
CallToolRequest,
CallToolRequestResult,
)
from fastmcp import FastMCP
ContentType = TextContent | ImageContent | EmbeddedResource

View file

@ -2,18 +2,18 @@
import pytest
from contrib.mcp_mixin import (
from fastmcp import FastMCP
from fastmcp.contrib.mcp_mixin import (
MCPMixin,
mcp_prompt,
mcp_resource,
mcp_tool,
)
from contrib.mcp_mixin.mcp_mixin import (
from fastmcp.contrib.mcp_mixin.mcp_mixin import (
_DEFAULT_SEPARATOR_PROMPT,
_DEFAULT_SEPARATOR_RESOURCE,
_DEFAULT_SEPARATOR_TOOL,
)
from fastmcp import FastMCP
class TestMCPMixin: