Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,3 @@ jobs:

- name: Run tests
run: uv run pytest -vv
if: ${{ !(github.event.pull_request.head.repo.fork) }}
1 change: 0 additions & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,3 @@ repos:
hooks:
- id: pyright-pretty
files: ^src/|^tests/
exclude: ^examples/
9 changes: 9 additions & 0 deletions src/contrib/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# FastMCP Contrib Modules

This directory holds community-contributed modules for FastMCP. These modules extend FastMCP's functionality but are not officially maintained by the core team.

**Guarantees:**
* 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.
39 changes: 39 additions & 0 deletions src/contrib/mcp_mixin/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# MCP Mixin

This module provides the `MCPMixin` base class and associated decorators (`@mcp_tool`, `@mcp_resource`, `@mcp_prompt`).

It allows developers to easily define classes whose methods can be registered as tools, resources, or prompts with a `FastMCP` server instance using the `register_all()`, `register_tools()`, `register_resources()`, or `register_prompts()` methods provided by the mixin.

## Usage

Inherit from `MCPMixin` and use the decorators on the methods you want to register.

```python
from fastmcp import FastMCP
from contrib.mcp_mixin.mcp_mixin import MCPMixin, mcp_tool, mcp_resource

class MyComponent(MCPMixin):
@mcp_tool(name="my_tool", description="Does something cool.")
def tool_method(self):
return "Tool executed!"

@mcp_resource(uri="component://data")
def resource_method(self):
return {"data": "some data"}

mcp_server = FastMCP()
component = MyComponent()

# Register all decorated methods with a prefix
# Useful if you will have multiple instantiated objects of the same class
# and want to avoid name collisions.
component.register_all(mcp_server, prefix="my_comp")

# Register without a prefix
# component.register_all(mcp_server)

# Now 'my_comp_my_tool' tool and 'my_comp+component://data' resource are registered (if prefix used)
# Or 'my_tool' and 'component://data' are registered (if no prefix used)
```

The `prefix` argument in registration methods is optional. If omitted, methods are registered with their original decorated names/URIs. Individual separators (`tools_separator`, `resources_separator`, `prompts_separator`) can also be provided to `register_all` to change the separator for specific types.
52 changes: 52 additions & 0 deletions src/contrib/mcp_mixin/example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
"""Sample code for FastMCP using MCPMixin."""

import asyncio

from contrib.mcp_mixin.mcp_mixin import (
MCPMixin,
mcp_prompt,
mcp_resource,
mcp_tool,
)
from fastmcp import FastMCP

mcp = FastMCP()


class Sample(MCPMixin):
def __init__(self, name):
self.name = name

@mcp_tool()
def first_tool(self):
"""First tool description."""
return f"Executed tool {self.name}."

@mcp_resource(uri="test://test")
def first_resource(self):
"""First resource description."""
return f"Executed resource {self.name}."

@mcp_prompt()
def first_prompt(self):
"""First prompt description."""
return f"here's a prompt! {self.name}."


first_sample = Sample("First")
second_sample = Sample("Second")

first_sample.register_all(mcp_server=mcp, prefix="first")
second_sample.register_all(mcp_server=mcp, prefix="second")


async def list_components():
print("MCP Server running with registered components...")
print("Tools:", list(await mcp.get_tools()))
print("Resources:", list(await mcp.get_resources()))
print("Prompts:", list(await mcp.get_prompts()))


if __name__ == "__main__":
asyncio.run(list_components())
mcp.run()
208 changes: 208 additions & 0 deletions src/contrib/mcp_mixin/mcp_mixin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
"""Provides a base mixin class and decorators for easy registration of class methods with FastMCP."""

from collections.abc import Callable
from typing import TYPE_CHECKING, Any

if TYPE_CHECKING:
from fastmcp.server import FastMCP

_MCP_REGISTRATION_TOOL_ATTR = "_mcp_tool_registration"
_MCP_REGISTRATION_RESOURCE_ATTR = "_mcp_resource_registration"
_MCP_REGISTRATION_PROMPT_ATTR = "_mcp_prompt_registration"

_DEFAULT_SEPARATOR_TOOL = "_"
_DEFAULT_SEPARATOR_RESOURCE = "+"
_DEFAULT_SEPARATOR_PROMPT = "_"


def mcp_tool(
name: str | None = None,
description: str | None = None,
tags: set[str] | None = None,
) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
"""Decorator to mark a method as an MCP tool for later registration."""

def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
call_args = {
"name": name or func.__name__,
"description": description,
"tags": tags,
}
call_args = {k: v for k, v in call_args.items() if v is not None}
setattr(func, _MCP_REGISTRATION_TOOL_ATTR, call_args)
return func

return decorator


def mcp_resource(
uri: str,
*,
name: str | None = None,
description: str | None = None,
mime_type: str | None = None,
tags: set[str] | None = None,
) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
"""Decorator to mark a method as an MCP resource for later registration."""

def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
call_args = {
"uri": uri,
"name": name or func.__name__,
"description": description,
"mime_type": mime_type,
"tags": tags,
}
call_args = {k: v for k, v in call_args.items() if v is not None}

setattr(func, _MCP_REGISTRATION_RESOURCE_ATTR, call_args)

return func

return decorator


def mcp_prompt(
name: str | None = None,
description: str | None = None,
tags: set[str] | None = None,
) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
"""Decorator to mark a method as an MCP prompt for later registration."""

def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
call_args = {
"name": name or func.__name__,
"description": description,
"tags": tags,
}

call_args = {k: v for k, v in call_args.items() if v is not None}

setattr(func, _MCP_REGISTRATION_PROMPT_ATTR, call_args)
return func

return decorator


class MCPMixin:
"""Base mixin class for objects that can register tools, resources, and prompts
with a FastMCP server instance using decorators.

This mixin provides methods like `register_all`, `register_tools`, etc.,
which iterate over the methods of the inheriting class, find methods
decorated with `@mcp_tool`, `@mcp_resource`, or `@mcp_prompt`, and
register them with the provided FastMCP server instance.
"""

def _get_methods_to_register(self, registration_type: str):
"""Retrieves all methods marked for a specific registration type."""
return [
(
getattr(self, method_name),
getattr(getattr(self, method_name), registration_type).copy(),
)
for method_name in dir(self)
if callable(getattr(self, method_name))
and hasattr(getattr(self, method_name), registration_type)
]

def register_tools(
self,
mcp_server: "FastMCP",
prefix: str | None = None,
separator: str = _DEFAULT_SEPARATOR_TOOL,
) -> None:
"""Registers all methods marked with @mcp_tool with the FastMCP server.

Args:
mcp_server: The FastMCP server instance to register tools with.
prefix: Optional prefix to prepend to tool names. If provided, the
final name will be f"{prefix}{separator}{original_name}".
separator: The separator string used between prefix and original name.
Defaults to '_'.
"""
for method, registration_info in self._get_methods_to_register(
_MCP_REGISTRATION_TOOL_ATTR
):
if prefix:
registration_info["name"] = (
f"{prefix}{separator}{registration_info['name']}"
)
mcp_server.add_tool(fn=method, **registration_info)

def register_resources(
self,
mcp_server: "FastMCP",
prefix: str | None = None,
separator: str = _DEFAULT_SEPARATOR_RESOURCE,
) -> None:
"""Registers all methods marked with @mcp_resource with the FastMCP server.

Args:
mcp_server: The FastMCP server instance to register resources with.
prefix: Optional prefix to prepend to resource names and URIs. If provided,
the final name will be f"{prefix}{separator}{original_name}" and the
final URI will be f"{prefix}{separator}{original_uri}".
separator: The separator string used between prefix and original name/URI.
Defaults to '+'.
"""
for method, registration_info in self._get_methods_to_register(
_MCP_REGISTRATION_RESOURCE_ATTR
):
if prefix:
registration_info["name"] = (
f"{prefix}{separator}{registration_info['name']}"
)
registration_info["uri"] = (
f"{prefix}{separator}{registration_info['uri']}"
)
mcp_server.add_resource_fn(fn=method, **registration_info)

def register_prompts(
self,
mcp_server: "FastMCP",
prefix: str | None = None,
separator: str = _DEFAULT_SEPARATOR_PROMPT,
) -> None:
"""Registers all methods marked with @mcp_prompt with the FastMCP server.

Args:
mcp_server: The FastMCP server instance to register prompts with.
prefix: Optional prefix to prepend to prompt names. If provided, the
final name will be f"{prefix}{separator}{original_name}".
separator: The separator string used between prefix and original name.
Defaults to '_'.
"""
for method, registration_info in self._get_methods_to_register(
_MCP_REGISTRATION_PROMPT_ATTR
):
if prefix:
registration_info["name"] = (
f"{prefix}{separator}{registration_info['name']}"
)
mcp_server.add_prompt(fn=method, **registration_info)

def register_all(
self,
mcp_server: "FastMCP",
prefix: str | None = None,
tool_separator: str = _DEFAULT_SEPARATOR_TOOL,
resource_separator: str = _DEFAULT_SEPARATOR_RESOURCE,
prompt_separator: str = _DEFAULT_SEPARATOR_PROMPT,
) -> None:
"""Registers all marked tools, resources, and prompts with the server.

This method calls `register_tools`, `register_resources`, and `register_prompts`
internally, passing the provided prefix and separators.

Args:
mcp_server: The FastMCP server instance to register with.
prefix: Optional prefix applied to all registered items unless overridden
by a specific separator argument.
tool_separator: Separator for tool names (defaults to '_').
resource_separator: Separator for resource names/URIs (defaults to '+').
prompt_separator: Separator for prompt names (defaults to '_').
"""
self.register_tools(mcp_server, prefix=prefix, separator=tool_separator)
self.register_resources(mcp_server, prefix=prefix, separator=resource_separator)
self.register_prompts(mcp_server, prefix=prefix, separator=prompt_separator)
1 change: 1 addition & 0 deletions tests/contrib/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# This file makes Python treat the directory as a package.
Loading