-
Notifications
You must be signed in to change notification settings - Fork 0
Open
Description
Trial Strategy: Multi-Server Plugin Discovery using @mcp.tool Decorators
Summary
Each plugin is implemented as a standalone FastMCP sub-server that defines one or more tools using the native @mcp.tool decorator.
At runtime, the main application walks the plugin folder tree, imports each sub-server, and composes them into a single aggregated FastMCP instance using main.import_server(...).
This approach keeps full FastMCP compatibility, preserves metadata supplied through decorator arguments, and supports modular composition for large tool libraries.
Design Goals
- Allow each plugin to be fully self-contained, with its own
FastMCPinstance. - Support normal FastMCP decorator arguments (
description,tags, etc.). - Automatically discover and import all plugin sub-servers at startup.
- Preserve namespaces and prevent naming collisions through prefixes.
- Keep the system compatible with manual or build-time registry generation if performance tuning becomes necessary later.
Example Folder Layout
toolvault/
server.py
plugins/
geo/
__init__.py
filters/
__init__.py
acoustic/
__init__.py
Example Plugin (plugins/geo/__init__.py)
from fastmcp import FastMCP
mcp = FastMCP(name="geo")
@mcp.tool(
description="Set color to red for a given feature",
tags=["geo", "style"]
)
def change_color_to_red(feature: dict) -> dict:
f = dict(feature)
f.setdefault("properties", {})["color"] = "red"
return fEach plugin can define multiple tools and resources on its own FastMCP instance.
Main Aggregating Server (server.py)
from fastmcp import FastMCP
import importlib, pkgutil
main = FastMCP(name="ToolVaultLike")
def import_plugins(root_pkg: str):
"""Walk a package tree and import any FastMCP subservers."""
pkg = importlib.import_module(root_pkg)
for m in pkgutil.walk_packages(pkg.__path__, prefix=f"{root_pkg}."):
mod = importlib.import_module(m.name)
sub = getattr(mod, "mcp", None)
if sub:
prefix = m.name.split(".")[-1]
main.import_server(
sub,
prefix=prefix,
on_duplicate_tools="error",
on_duplicate_resources="ignore",
on_duplicate_prompts="ignore",
)
if __name__ == "__main__":
import_plugins("plugins")
main.run()Behavior
- Each discovered plugin is imported once.
- All
@mcp.toolmetadata (e.g.,description,tags) is preserved automatically. - Tool names are prefixed using their folder name, preventing collisions.
- Tools, prompts, and resources are merged into the unified server.
Benefits
- Full FastMCP compliance: Tools use the official
@mcp.tooldecorator and schema inference. - Zero coupling: Each plugin can be developed and tested as an independent server.
- Automatic composition: No manual registry maintenance required during development.
- Extensible: Registry generation or lazy-loading can be added later if startup becomes heavy.
Limitations
- Every plugin module must be importable at startup (imports drive discovery).
- Startup time scales with number of plugins; for large libraries, a build-time registry or lazy-load mechanism can later be introduced.
- Duplicate tool names must be avoided or handled via
prefixor duplicate-handling flags.
Future Enhancements
- Registry generation: Build-time creation of a static import manifest for faster startup.
- Lazy import: Generate lightweight proxy entries for large plugin sets.
- Namespace management: Optionally group subservers by category or domain.
- Hot-reload mode: File watcher to reload individual plugin servers without restarting the main process.
Acceptance Criteria
- Each plugin defines its own FastMCP instance and uses
@mcp.tool(...). - The main server dynamically imports all plugin subservers at startup.
- All tool metadata is preserved and visible through
/tools/list. - Duplicate names are prevented via prefixing.
- The strategy runs successfully with multiple plugins in the
plugins/folder.
Labels: trial, architecture, fastmcp, plugin-system
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
No labels