mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-22 05:24:18 +02:00
Merge branch 'main' into patch-2
This commit is contained in:
commit
6ce6b116a7
17 changed files with 149 additions and 38 deletions
|
|
@ -61,7 +61,8 @@
|
|||
"patterns/composition",
|
||||
"patterns/decorating-methods",
|
||||
"patterns/openapi",
|
||||
"patterns/fastapi"
|
||||
"patterns/fastapi",
|
||||
"patterns/contrib"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
|
|
|||
42
docs/patterns/contrib.mdx
Normal file
42
docs/patterns/contrib.mdx
Normal 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
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -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):
|
||||
|
|
@ -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()
|
||||
|
||||
|
|
@ -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.")
|
||||
|
|
@ -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()
|
||||
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue