initial mcp server setup
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
# MCP Server implementations
|
||||
from .modular_server import ModularMCPServer
|
||||
from .server_factory import MCPServerFactory
|
||||
|
||||
__all__ = ['ModularMCPServer', 'MCPServerFactory']
|
||||
@@ -0,0 +1,99 @@
|
||||
"""
|
||||
Modular MCP Server Implementation
|
||||
"""
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
from typing import Optional, List
|
||||
from .tools.tool_registry import ServerToolRegistry
|
||||
from .prompts.prompt_registry import ServerPromptRegistry
|
||||
from .resources.resource_registry import ServerResourceRegistry
|
||||
|
||||
|
||||
class ModularMCPServer:
|
||||
"""Modular MCP Server that automatically discovers and registers tools, prompts, and resources"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
host: str = "0.0.0.0",
|
||||
port: int = 8050,
|
||||
stateless_http: bool = True,
|
||||
tools_directory: Optional[str] = None,
|
||||
prompts_directory: Optional[str] = None,
|
||||
resources_directory: Optional[str] = None
|
||||
):
|
||||
self.name = name
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.stateless_http = stateless_http
|
||||
|
||||
# Initialize registries
|
||||
self.tool_registry = ServerToolRegistry(tools_directory)
|
||||
self.prompt_registry = ServerPromptRegistry(prompts_directory)
|
||||
self.resource_registry = ServerResourceRegistry(resources_directory)
|
||||
|
||||
# Create FastMCP server
|
||||
self.mcp = FastMCP(
|
||||
name=name,
|
||||
host=host,
|
||||
port=port,
|
||||
stateless_http=stateless_http
|
||||
)
|
||||
|
||||
self._initialized = False
|
||||
|
||||
async def initialize(self) -> None:
|
||||
"""Initialize the server and register all components"""
|
||||
if self._initialized:
|
||||
return
|
||||
|
||||
print(f"Initializing {self.name} server...")
|
||||
|
||||
# Discover and register tools
|
||||
print("Discovering tools...")
|
||||
self.tool_registry.register_tools_with_server(self.mcp)
|
||||
tool_count = len(self.tool_registry.get_all_tools())
|
||||
print(f"Registered {tool_count} tools")
|
||||
|
||||
# Discover and register prompts
|
||||
print("Discovering prompts...")
|
||||
self.prompt_registry.register_prompts_with_server(self.mcp)
|
||||
prompt_count = len(self.prompt_registry.get_all_prompts())
|
||||
print(f"Registered {prompt_count} prompts")
|
||||
|
||||
# Discover and register resources
|
||||
print("Discovering resources...")
|
||||
self.resource_registry.register_resources_with_server(self.mcp)
|
||||
resource_count = len(self.resource_registry.get_all_resources())
|
||||
print(f"Registered {resource_count} resources")
|
||||
|
||||
self._initialized = True
|
||||
print(f"Server initialization complete!")
|
||||
|
||||
def run(self, transport: str = "stdio") -> None:
|
||||
"""Run the server with the specified transport"""
|
||||
if not self._initialized:
|
||||
import asyncio
|
||||
asyncio.run(self.initialize())
|
||||
|
||||
print(f"Starting {self.name} server with {transport} transport...")
|
||||
self.mcp.run(transport=transport)
|
||||
|
||||
def get_server_info(self) -> dict:
|
||||
"""Get information about the server and its components"""
|
||||
return {
|
||||
"name": self.name,
|
||||
"host": self.host,
|
||||
"port": self.port,
|
||||
"tools": {
|
||||
"count": len(self.tool_registry.get_all_tools()),
|
||||
"names": self.tool_registry.get_tool_names()
|
||||
},
|
||||
"prompts": {
|
||||
"count": len(self.prompt_registry.get_all_prompts()),
|
||||
"names": self.prompt_registry.get_prompt_names()
|
||||
},
|
||||
"resources": {
|
||||
"count": len(self.resource_registry.get_all_resources()),
|
||||
"uris": self.resource_registry.get_resource_uris()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
"""
|
||||
Server Prompts Module
|
||||
"""
|
||||
from .base_prompt import BaseServerPrompt
|
||||
from .prompt_registry import ServerPromptRegistry
|
||||
|
||||
__all__ = ['BaseServerPrompt', 'ServerPromptRegistry']
|
||||
@@ -0,0 +1,51 @@
|
||||
"""
|
||||
Base Server Prompt Class
|
||||
"""
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
|
||||
class BaseServerPrompt(ABC):
|
||||
"""Base class for server prompts that can be registered with FastMCP"""
|
||||
|
||||
def __init__(self, name: str, description: str, template: str, arguments: Optional[Dict[str, Any]] = None):
|
||||
self.name = name
|
||||
self.description = description
|
||||
self.template = template
|
||||
self.arguments = arguments or {}
|
||||
|
||||
@abstractmethod
|
||||
async def generate(self, **kwargs) -> str:
|
||||
"""Generate the prompt with the provided arguments"""
|
||||
pass
|
||||
|
||||
def get_prompt_definition(self) -> Dict[str, Any]:
|
||||
"""Get the prompt definition for FastMCP registration"""
|
||||
return {
|
||||
"name": self.name,
|
||||
"description": self.description,
|
||||
"arguments": self.arguments
|
||||
}
|
||||
|
||||
def create_fastmcp_prompt(self, mcp_server):
|
||||
"""Create a FastMCP prompt decorator for this prompt"""
|
||||
@mcp_server.prompt()
|
||||
async def prompt_wrapper(**kwargs):
|
||||
return await self.generate(**kwargs)
|
||||
|
||||
# Set metadata
|
||||
prompt_wrapper.__name__ = self.name
|
||||
prompt_wrapper.__doc__ = self.description
|
||||
|
||||
return prompt_wrapper
|
||||
|
||||
def _substitute_template(self, **kwargs) -> str:
|
||||
"""Helper method to substitute variables in template"""
|
||||
result = self.template
|
||||
|
||||
# Apply provided arguments
|
||||
for key, value in kwargs.items():
|
||||
placeholder = f"{{{key}}}"
|
||||
result = result.replace(placeholder, str(value))
|
||||
|
||||
return result
|
||||
@@ -0,0 +1,44 @@
|
||||
"""
|
||||
Greeting Prompt Example
|
||||
"""
|
||||
from .base_prompt import BaseServerPrompt
|
||||
|
||||
|
||||
class GreetingPrompt(BaseServerPrompt):
|
||||
"""A greeting prompt template"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
name="greeting_prompt",
|
||||
description="Generate a greeting prompt for AI models",
|
||||
template="Please write a {style} greeting for someone named {name}. The greeting should be {tone} and include a {element}.",
|
||||
arguments={
|
||||
"style": {
|
||||
"type": "string",
|
||||
"enum": ["friendly", "formal", "casual"],
|
||||
"description": "Style of greeting",
|
||||
"default": "friendly"
|
||||
},
|
||||
"tone": {
|
||||
"type": "string",
|
||||
"enum": ["warm", "professional", "relaxed"],
|
||||
"description": "Tone of the greeting",
|
||||
"default": "warm"
|
||||
},
|
||||
"element": {
|
||||
"type": "string",
|
||||
"enum": ["compliment", "question", "observation"],
|
||||
"description": "Element to include in greeting",
|
||||
"default": "compliment"
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
async def generate(self, name: str, style: str = "friendly", tone: str = "warm", element: str = "compliment") -> str:
|
||||
"""Generate the greeting prompt"""
|
||||
return self._substitute_template(
|
||||
name=name,
|
||||
style=style,
|
||||
tone=tone,
|
||||
element=element
|
||||
)
|
||||
@@ -0,0 +1,100 @@
|
||||
"""
|
||||
Server Prompt Registry for dynamic prompt registration
|
||||
"""
|
||||
import os
|
||||
import importlib
|
||||
import inspect
|
||||
from typing import List, Dict, Any, Type, Optional
|
||||
from pathlib import Path
|
||||
from .base_prompt import BaseServerPrompt
|
||||
|
||||
|
||||
class ServerPromptRegistry:
|
||||
"""Registry for managing server prompts from files"""
|
||||
|
||||
def __init__(self, prompts_directory: str = None):
|
||||
self.prompts_directory = prompts_directory or os.path.dirname(__file__)
|
||||
self._registered_prompts: Dict[str, BaseServerPrompt] = {}
|
||||
self._prompt_files: List[str] = []
|
||||
|
||||
@property
|
||||
def directory(self):
|
||||
"""Alias for prompts_directory for backward compatibility"""
|
||||
return self.prompts_directory
|
||||
|
||||
@property
|
||||
def _prompts(self):
|
||||
"""Alias for _registered_prompts for backward compatibility"""
|
||||
return self._registered_prompts
|
||||
|
||||
def discover_prompts(self) -> List[BaseServerPrompt]:
|
||||
"""Discover all prompts in the prompts directory"""
|
||||
prompts = []
|
||||
prompts_dir = Path(self.prompts_directory)
|
||||
|
||||
if not prompts_dir.exists():
|
||||
return prompts
|
||||
|
||||
# Find all Python files in the prompts directory
|
||||
for file_path in prompts_dir.glob("*.py"):
|
||||
if file_path.name.startswith("__"):
|
||||
continue
|
||||
|
||||
module_name = file_path.stem
|
||||
try:
|
||||
# Import the module
|
||||
spec = importlib.util.spec_from_file_location(module_name, file_path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
# Find all BaseServerPrompt subclasses in the module
|
||||
for name, obj in inspect.getmembers(module, inspect.isclass):
|
||||
if (issubclass(obj, BaseServerPrompt) and
|
||||
obj != BaseServerPrompt and
|
||||
not inspect.isabstract(obj)):
|
||||
try:
|
||||
prompt_instance = obj()
|
||||
prompts.append(prompt_instance)
|
||||
self._registered_prompts[prompt_instance.name] = prompt_instance
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not instantiate prompt {name}: {e}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not load prompt file {file_path}: {e}")
|
||||
|
||||
return prompts
|
||||
|
||||
def register_prompt(self, prompt: BaseServerPrompt) -> None:
|
||||
"""Register a single prompt"""
|
||||
self._registered_prompts[prompt.name] = prompt
|
||||
|
||||
def register_prompts(self, prompts: List[BaseServerPrompt]) -> None:
|
||||
"""Register multiple prompts"""
|
||||
for prompt in prompts:
|
||||
self.register_prompt(prompt)
|
||||
|
||||
def get_prompt(self, name: str) -> Optional[BaseServerPrompt]:
|
||||
"""Get a prompt by name"""
|
||||
return self._registered_prompts.get(name)
|
||||
|
||||
def get_all_prompts(self) -> List[BaseServerPrompt]:
|
||||
"""Get all registered prompts"""
|
||||
return list(self._registered_prompts.values())
|
||||
|
||||
def get_prompt_names(self) -> List[str]:
|
||||
"""Get all registered prompt names"""
|
||||
return list(self._registered_prompts.keys())
|
||||
|
||||
def register_prompts_with_server(self, mcp_server) -> None:
|
||||
"""Register all discovered prompts with a FastMCP server"""
|
||||
# First discover prompts if not already done
|
||||
if not self._registered_prompts:
|
||||
self.discover_prompts()
|
||||
|
||||
# Register each prompt with the server
|
||||
for prompt in self._registered_prompts.values():
|
||||
prompt.create_fastmcp_prompt(mcp_server)
|
||||
|
||||
def clear_prompts(self) -> None:
|
||||
"""Clear all registered prompts"""
|
||||
self._registered_prompts.clear()
|
||||
@@ -0,0 +1,7 @@
|
||||
"""
|
||||
Server Resources Module
|
||||
"""
|
||||
from .base_resource import BaseServerResource
|
||||
from .resource_registry import ServerResourceRegistry
|
||||
|
||||
__all__ = ['BaseServerResource', 'ServerResourceRegistry']
|
||||
@@ -0,0 +1,41 @@
|
||||
"""
|
||||
Base Server Resource Class
|
||||
"""
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Any, Dict, Optional, Union
|
||||
|
||||
|
||||
class BaseServerResource(ABC):
|
||||
"""Base class for server resources that can be registered with FastMCP"""
|
||||
|
||||
def __init__(self, uri: str, name: str, description: str, mime_type: str = "text/plain"):
|
||||
self.uri = uri
|
||||
self.name = name
|
||||
self.description = description
|
||||
self.mime_type = mime_type
|
||||
|
||||
@abstractmethod
|
||||
async def get_content(self, **kwargs) -> Union[str, bytes]:
|
||||
"""Get the resource content with the provided arguments"""
|
||||
pass
|
||||
|
||||
def get_resource_definition(self) -> Dict[str, Any]:
|
||||
"""Get the resource definition for FastMCP registration"""
|
||||
return {
|
||||
"uri": self.uri,
|
||||
"name": self.name,
|
||||
"description": self.description,
|
||||
"mime_type": self.mime_type
|
||||
}
|
||||
|
||||
def create_fastmcp_resource(self, mcp_server):
|
||||
"""Create a FastMCP resource decorator for this resource"""
|
||||
@mcp_server.resource(self.uri)
|
||||
async def resource_wrapper(**kwargs):
|
||||
return await self.get_content(**kwargs)
|
||||
|
||||
# Set metadata
|
||||
resource_wrapper.__name__ = self.name
|
||||
resource_wrapper.__doc__ = self.description
|
||||
|
||||
return resource_wrapper
|
||||
@@ -0,0 +1,37 @@
|
||||
"""
|
||||
Configuration Resource Example
|
||||
"""
|
||||
from .base_resource import BaseServerResource
|
||||
|
||||
|
||||
class ConfigResource(BaseServerResource):
|
||||
"""A configuration resource that provides server settings"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
uri="config://settings",
|
||||
name="Server Configuration",
|
||||
description="Get the current server configuration settings",
|
||||
mime_type="application/json"
|
||||
)
|
||||
|
||||
async def get_content(self) -> str:
|
||||
"""Get the configuration content"""
|
||||
import json
|
||||
config = {
|
||||
"server_name": "MCP Template Server",
|
||||
"version": "1.0.0",
|
||||
"features": [
|
||||
"tools",
|
||||
"prompts",
|
||||
"resources"
|
||||
],
|
||||
"transport": {
|
||||
"supported": ["stdio", "sse"],
|
||||
"default": "stdio"
|
||||
},
|
||||
"tools_count": 2,
|
||||
"prompts_count": 1,
|
||||
"resources_count": 1
|
||||
}
|
||||
return json.dumps(config, indent=2)
|
||||
@@ -0,0 +1,28 @@
|
||||
"""
|
||||
Dynamic Resource Example
|
||||
"""
|
||||
from .base_resource import BaseServerResource
|
||||
|
||||
|
||||
class DynamicResource(BaseServerResource):
|
||||
"""A dynamic resource that accepts parameters"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
uri="dynamic://greeting/{name}",
|
||||
name="Dynamic Greeting",
|
||||
description="Get a personalized greeting resource",
|
||||
mime_type="text/plain"
|
||||
)
|
||||
|
||||
async def get_content(self, name: str) -> str:
|
||||
"""Get the dynamic greeting content"""
|
||||
return f"Hello, {name}! This is a dynamic resource that was generated just for you.\n\n" \
|
||||
f"Resource URI: dynamic://greeting/{name}\n" \
|
||||
f"Generated at: {self._get_timestamp()}\n" \
|
||||
f"Personalized for: {name}"
|
||||
|
||||
def _get_timestamp(self) -> str:
|
||||
"""Get current timestamp"""
|
||||
from datetime import datetime
|
||||
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
@@ -0,0 +1,100 @@
|
||||
"""
|
||||
Server Resource Registry for dynamic resource registration
|
||||
"""
|
||||
import os
|
||||
import importlib
|
||||
import inspect
|
||||
from typing import List, Dict, Any, Type,Optional
|
||||
from pathlib import Path
|
||||
from .base_resource import BaseServerResource
|
||||
|
||||
|
||||
class ServerResourceRegistry:
|
||||
"""Registry for managing server resources from files"""
|
||||
|
||||
def __init__(self, resources_directory: str = None):
|
||||
self.resources_directory = resources_directory or os.path.dirname(__file__)
|
||||
self._registered_resources: Dict[str, BaseServerResource] = {}
|
||||
self._resource_files: List[str] = []
|
||||
|
||||
@property
|
||||
def directory(self):
|
||||
"""Alias for resources_directory for backward compatibility"""
|
||||
return self.resources_directory
|
||||
|
||||
@property
|
||||
def _resources(self):
|
||||
"""Alias for _registered_resources for backward compatibility"""
|
||||
return self._registered_resources
|
||||
|
||||
def discover_resources(self) -> List[BaseServerResource]:
|
||||
"""Discover all resources in the resources directory"""
|
||||
resources = []
|
||||
resources_dir = Path(self.resources_directory)
|
||||
|
||||
if not resources_dir.exists():
|
||||
return resources
|
||||
|
||||
# Find all Python files in the resources directory
|
||||
for file_path in resources_dir.glob("*.py"):
|
||||
if file_path.name.startswith("__"):
|
||||
continue
|
||||
|
||||
module_name = file_path.stem
|
||||
try:
|
||||
# Import the module
|
||||
spec = importlib.util.spec_from_file_location(module_name, file_path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
# Find all BaseServerResource subclasses in the module
|
||||
for name, obj in inspect.getmembers(module, inspect.isclass):
|
||||
if (issubclass(obj, BaseServerResource) and
|
||||
obj != BaseServerResource and
|
||||
not inspect.isabstract(obj)):
|
||||
try:
|
||||
resource_instance = obj()
|
||||
resources.append(resource_instance)
|
||||
self._registered_resources[resource_instance.uri] = resource_instance
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not instantiate resource {name}: {e}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not load resource file {file_path}: {e}")
|
||||
|
||||
return resources
|
||||
|
||||
def register_resource(self, resource: BaseServerResource) -> None:
|
||||
"""Register a single resource"""
|
||||
self._registered_resources[resource.uri] = resource
|
||||
|
||||
def register_resources(self, resources: List[BaseServerResource]) -> None:
|
||||
"""Register multiple resources"""
|
||||
for resource in resources:
|
||||
self.register_resource(resource)
|
||||
|
||||
def get_resource(self, uri: str) -> Optional[BaseServerResource]:
|
||||
"""Get a resource by URI"""
|
||||
return self._registered_resources.get(uri)
|
||||
|
||||
def get_all_resources(self) -> List[BaseServerResource]:
|
||||
"""Get all registered resources"""
|
||||
return list(self._registered_resources.values())
|
||||
|
||||
def get_resource_uris(self) -> List[str]:
|
||||
"""Get all registered resource URIs"""
|
||||
return list(self._registered_resources.keys())
|
||||
|
||||
def register_resources_with_server(self, mcp_server) -> None:
|
||||
"""Register all discovered resources with a FastMCP server"""
|
||||
# First discover resources if not already done
|
||||
if not self._registered_resources:
|
||||
self.discover_resources()
|
||||
|
||||
# Register each resource with the server
|
||||
for resource in self._registered_resources.values():
|
||||
resource.create_fastmcp_resource(mcp_server)
|
||||
|
||||
def clear_resources(self) -> None:
|
||||
"""Clear all registered resources"""
|
||||
self._registered_resources.clear()
|
||||
@@ -0,0 +1,258 @@
|
||||
"""
|
||||
MCP Server Factory for easy server creation and configuration
|
||||
"""
|
||||
from typing import List, Optional, Union
|
||||
from ..core.types import MCPServerConfig, MCPTool, MCPResource, MCPPrompt, TransportType
|
||||
from .modular_server import ModularMCPServer
|
||||
from ..core.interfaces import IMCPServer
|
||||
from ..tools.tool_registry import ToolRegistry
|
||||
|
||||
|
||||
class MCPServerFactory:
|
||||
"""Factory class for creating MCP servers with different configurations"""
|
||||
|
||||
@staticmethod
|
||||
def create_server(
|
||||
name: str,
|
||||
version: str = "1.0.0",
|
||||
transport: Union[str, TransportType] = TransportType.STDIO,
|
||||
host: str = "0.0.0.0",
|
||||
port: int = 8050,
|
||||
stateless_http: bool = True,
|
||||
tools: Optional[List[MCPTool]] = None,
|
||||
resources: Optional[List[MCPResource]] = None,
|
||||
prompts: Optional[List[MCPPrompt]] = None,
|
||||
) -> IMCPServer:
|
||||
"""Create an MCP server with the specified configuration"""
|
||||
|
||||
# Convert string transport to enum if needed
|
||||
if isinstance(transport, str):
|
||||
transport = TransportType(transport.lower())
|
||||
|
||||
config = MCPServerConfig(
|
||||
name=name,
|
||||
version=version,
|
||||
transport=transport,
|
||||
host=host,
|
||||
port=port,
|
||||
stateless_http=stateless_http,
|
||||
tools=tools or [],
|
||||
resources=resources or [],
|
||||
prompts=prompts or [],
|
||||
)
|
||||
|
||||
return ModularMCPServer(
|
||||
name=config.name,
|
||||
host=config.host,
|
||||
port=config.port,
|
||||
stateless_http=config.stateless_http
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def create_basic_calculator_server(
|
||||
name: str = "CalculatorServer",
|
||||
transport: Union[str, TransportType] = TransportType.STDIO,
|
||||
host: str = "0.0.0.0",
|
||||
port: int = 8050,
|
||||
) -> IMCPServer:
|
||||
"""Create a basic calculator server with common math tools"""
|
||||
|
||||
async def add(a: float, b: float) -> float:
|
||||
"""Add two numbers together"""
|
||||
return a + b
|
||||
|
||||
async def subtract(a: float, b: float) -> float:
|
||||
"""Subtract b from a"""
|
||||
return a - b
|
||||
|
||||
async def multiply(a: float, b: float) -> float:
|
||||
"""Multiply two numbers"""
|
||||
return a * b
|
||||
|
||||
async def divide(a: float, b: float) -> float:
|
||||
"""Divide a by b"""
|
||||
if b == 0:
|
||||
raise ValueError("Cannot divide by zero")
|
||||
return a / b
|
||||
|
||||
tools = [
|
||||
MCPTool(
|
||||
name="add",
|
||||
description="Add two numbers together",
|
||||
input_schema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"a": {"type": "number", "description": "First number"},
|
||||
"b": {"type": "number", "description": "Second number"},
|
||||
},
|
||||
"required": ["a", "b"],
|
||||
},
|
||||
handler=add,
|
||||
),
|
||||
MCPTool(
|
||||
name="subtract",
|
||||
description="Subtract second number from first",
|
||||
input_schema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"a": {"type": "number", "description": "First number"},
|
||||
"b": {"type": "number", "description": "Number to subtract"},
|
||||
},
|
||||
"required": ["a", "b"],
|
||||
},
|
||||
handler=subtract,
|
||||
),
|
||||
MCPTool(
|
||||
name="multiply",
|
||||
description="Multiply two numbers",
|
||||
input_schema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"a": {"type": "number", "description": "First number"},
|
||||
"b": {"type": "number", "description": "Second number"},
|
||||
},
|
||||
"required": ["a", "b"],
|
||||
},
|
||||
handler=multiply,
|
||||
),
|
||||
MCPTool(
|
||||
name="divide",
|
||||
description="Divide first number by second",
|
||||
input_schema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"a": {"type": "number", "description": "Dividend"},
|
||||
"b": {"type": "number", "description": "Divisor (cannot be zero)"},
|
||||
},
|
||||
"required": ["a", "b"],
|
||||
},
|
||||
handler=divide,
|
||||
),
|
||||
]
|
||||
|
||||
return MCPServerFactory.create_server(
|
||||
name=name,
|
||||
transport=transport,
|
||||
host=host,
|
||||
port=port,
|
||||
tools=tools,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def create_knowledge_base_server(
|
||||
name: str = "KnowledgeBaseServer",
|
||||
transport: Union[str, TransportType] = TransportType.STDIO,
|
||||
host: str = "0.0.0.0",
|
||||
port: int = 8050,
|
||||
kb_data: Optional[dict] = None,
|
||||
) -> IMCPServer:
|
||||
"""Create a knowledge base server with configurable data"""
|
||||
|
||||
if kb_data is None:
|
||||
kb_data = {
|
||||
"company_policy": "Default company policy information...",
|
||||
"faq": "Frequently asked questions and answers...",
|
||||
}
|
||||
|
||||
async def get_knowledge_base() -> str:
|
||||
"""Retrieve the entire knowledge base as formatted string"""
|
||||
formatted = "Knowledge Base:\n\n"
|
||||
for key, value in kb_data.items():
|
||||
formatted += f"**{key.replace('_', ' ').title()}:**\n{value}\n\n"
|
||||
return formatted
|
||||
|
||||
async def search_kb(query: str) -> str:
|
||||
"""Search the knowledge base for relevant information"""
|
||||
query_lower = query.lower()
|
||||
results = []
|
||||
|
||||
for key, value in kb_data.items():
|
||||
if query_lower in key.lower() or query_lower in value.lower():
|
||||
results.append(f"**{key.replace('_', ' ').title()}:**\n{value}")
|
||||
|
||||
if not results:
|
||||
return f"No information found for query: {query}"
|
||||
|
||||
return "\n\n".join(results)
|
||||
|
||||
tools = [
|
||||
MCPTool(
|
||||
name="get_knowledge_base",
|
||||
description="Retrieve the entire knowledge base as a formatted string",
|
||||
input_schema={"type": "object", "properties": {}},
|
||||
handler=get_knowledge_base,
|
||||
),
|
||||
MCPTool(
|
||||
name="search_kb",
|
||||
description="Search the knowledge base for relevant information",
|
||||
input_schema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {"type": "string", "description": "Search query"},
|
||||
},
|
||||
"required": ["query"],
|
||||
},
|
||||
handler=search_kb,
|
||||
),
|
||||
]
|
||||
|
||||
return MCPServerFactory.create_server(
|
||||
name=name,
|
||||
transport=transport,
|
||||
host=host,
|
||||
port=port,
|
||||
tools=tools,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def create_server_from_categories(
|
||||
name: str,
|
||||
categories: List[str],
|
||||
transport: Union[str, TransportType] = TransportType.STDIO,
|
||||
host: str = "0.0.0.0",
|
||||
port: int = 8050,
|
||||
custom_tools: Optional[List[MCPTool]] = None,
|
||||
resources: Optional[List[MCPResource]] = None,
|
||||
prompts: Optional[List[MCPPrompt]] = None,
|
||||
) -> IMCPServer:
|
||||
"""Create a server using tool categories"""
|
||||
|
||||
# Get tools from categories
|
||||
registry = ToolRegistry()
|
||||
category_tools = registry.get_tools_by_categories(categories)
|
||||
|
||||
# Combine with custom tools
|
||||
all_tools = category_tools
|
||||
if custom_tools:
|
||||
all_tools.extend(custom_tools)
|
||||
|
||||
return MCPServerFactory.create_server(
|
||||
name=name,
|
||||
transport=transport,
|
||||
host=host,
|
||||
port=port,
|
||||
tools=all_tools,
|
||||
resources=resources,
|
||||
prompts=prompts,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def create_modular_server(
|
||||
name: str = "ModularServer",
|
||||
host: str = "0.0.0.0",
|
||||
port: int = 8050,
|
||||
stateless_http: bool = True,
|
||||
tools_directory: Optional[str] = None,
|
||||
prompts_directory: Optional[str] = None,
|
||||
resources_directory: Optional[str] = None,
|
||||
) -> ModularMCPServer:
|
||||
"""Create a modular server that auto-discovers tools, prompts, and resources"""
|
||||
return ModularMCPServer(
|
||||
name=name,
|
||||
host=host,
|
||||
port=port,
|
||||
stateless_http=stateless_http,
|
||||
tools_directory=tools_directory,
|
||||
prompts_directory=prompts_directory,
|
||||
resources_directory=resources_directory,
|
||||
)
|
||||
@@ -0,0 +1,7 @@
|
||||
"""
|
||||
Server Tools Module
|
||||
"""
|
||||
from .base_tool import BaseServerTool
|
||||
from .tool_registry import ServerToolRegistry
|
||||
|
||||
__all__ = ['BaseServerTool', 'ServerToolRegistry']
|
||||
@@ -0,0 +1,67 @@
|
||||
"""
|
||||
Base Server Tool Class
|
||||
"""
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Any, Dict, Optional
|
||||
from mcp.server.fastmcp import Context
|
||||
from mcp.server.session import ServerSession
|
||||
|
||||
|
||||
class BaseServerTool(ABC):
|
||||
"""Base class for server tools that can be registered with FastMCP"""
|
||||
|
||||
def __init__(self, name: str, description: str, input_schema: Dict[str, Any]):
|
||||
self.name = name
|
||||
self.description = description
|
||||
self.input_schema = input_schema
|
||||
|
||||
@abstractmethod
|
||||
async def execute(self, **kwargs) -> Any:
|
||||
"""Execute the tool with the provided arguments"""
|
||||
pass
|
||||
|
||||
def get_tool_definition(self) -> Dict[str, Any]:
|
||||
"""Get the tool definition for FastMCP registration"""
|
||||
return {
|
||||
"name": self.name,
|
||||
"description": self.description,
|
||||
"input_schema": self.input_schema
|
||||
}
|
||||
|
||||
def create_fastmcp_tool(self, mcp_server):
|
||||
"""Create a FastMCP tool decorator for this tool"""
|
||||
@mcp_server.tool()
|
||||
async def tool_wrapper(**kwargs):
|
||||
return await self.execute(**kwargs)
|
||||
|
||||
# Set metadata
|
||||
tool_wrapper.__name__ = self.name
|
||||
tool_wrapper.__doc__ = self.description
|
||||
|
||||
return tool_wrapper
|
||||
|
||||
|
||||
class ContextAwareTool(BaseServerTool):
|
||||
"""Base class for tools that need access to MCP context"""
|
||||
|
||||
@abstractmethod
|
||||
async def execute_with_context(self, ctx: Context[ServerSession, None], **kwargs) -> Any:
|
||||
"""Execute the tool with MCP context"""
|
||||
pass
|
||||
|
||||
async def execute(self, **kwargs) -> Any:
|
||||
"""Default implementation that doesn't use context"""
|
||||
# This will be overridden by the FastMCP wrapper
|
||||
raise NotImplementedError("Use execute_with_context for context-aware tools")
|
||||
|
||||
def create_fastmcp_tool(self, mcp_server):
|
||||
"""Create a FastMCP tool decorator for this context-aware tool"""
|
||||
@mcp_server.tool()
|
||||
async def tool_wrapper(ctx: Context[ServerSession, None], **kwargs):
|
||||
return await self.execute_with_context(ctx, **kwargs)
|
||||
|
||||
# Set metadata
|
||||
tool_wrapper.__name__ = self.name
|
||||
tool_wrapper.__doc__ = self.description
|
||||
|
||||
return tool_wrapper
|
||||
@@ -0,0 +1,48 @@
|
||||
"""
|
||||
Calculator Tool Example
|
||||
"""
|
||||
from .base_tool import BaseServerTool
|
||||
|
||||
|
||||
class CalculatorTool(BaseServerTool):
|
||||
"""A simple calculator tool"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
name="calculator",
|
||||
description="Perform basic mathematical operations",
|
||||
input_schema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"operation": {
|
||||
"type": "string",
|
||||
"enum": ["add", "subtract", "multiply", "divide"],
|
||||
"description": "The mathematical operation to perform"
|
||||
},
|
||||
"a": {
|
||||
"type": "number",
|
||||
"description": "First number"
|
||||
},
|
||||
"b": {
|
||||
"type": "number",
|
||||
"description": "Second number"
|
||||
}
|
||||
},
|
||||
"required": ["operation", "a", "b"]
|
||||
}
|
||||
)
|
||||
|
||||
async def execute(self, operation: str, a: float, b: float) -> float:
|
||||
"""Execute the calculator operation"""
|
||||
if operation == "add":
|
||||
return a + b
|
||||
elif operation == "subtract":
|
||||
return a - b
|
||||
elif operation == "multiply":
|
||||
return a * b
|
||||
elif operation == "divide":
|
||||
if b == 0:
|
||||
raise ValueError("Cannot divide by zero")
|
||||
return a / b
|
||||
else:
|
||||
raise ValueError(f"Unknown operation: {operation}")
|
||||
@@ -0,0 +1,54 @@
|
||||
"""
|
||||
Greeting Tool Example
|
||||
"""
|
||||
from .base_tool import ContextAwareTool
|
||||
from mcp.server.fastmcp import Context
|
||||
from mcp.server.session import ServerSession
|
||||
|
||||
|
||||
class GreetingTool(ContextAwareTool):
|
||||
"""A greeting tool that uses MCP context"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
name="greeting",
|
||||
description="Generate personalized greetings with progress updates",
|
||||
input_schema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Name of the person to greet"
|
||||
},
|
||||
"style": {
|
||||
"type": "string",
|
||||
"enum": ["friendly", "formal", "casual"],
|
||||
"description": "Style of greeting",
|
||||
"default": "friendly"
|
||||
}
|
||||
},
|
||||
"required": ["name"]
|
||||
}
|
||||
)
|
||||
|
||||
async def execute_with_context(self, ctx: Context[ServerSession, None], name: str, style: str = "friendly") -> str:
|
||||
"""Generate a greeting with progress updates"""
|
||||
await ctx.info(f"Generating {style} greeting for {name}")
|
||||
|
||||
# Simulate some work with progress updates
|
||||
await ctx.report_progress(progress=0.3, total=1.0, message="Preparing greeting...")
|
||||
|
||||
styles = {
|
||||
"friendly": f"Hello there, {name}! Great to see you!",
|
||||
"formal": f"Good day, {name}. I hope you are well.",
|
||||
"casual": f"Hey {name}! What's up?"
|
||||
}
|
||||
|
||||
await ctx.report_progress(progress=0.7, total=1.0, message="Generating message...")
|
||||
|
||||
greeting = styles.get(style, styles["friendly"])
|
||||
|
||||
await ctx.report_progress(progress=1.0, total=1.0, message="Greeting complete!")
|
||||
await ctx.debug(f"Generated greeting: {greeting}")
|
||||
|
||||
return greeting
|
||||
@@ -0,0 +1,100 @@
|
||||
"""
|
||||
Server Tool Registry for dynamic tool registration
|
||||
"""
|
||||
import os
|
||||
import importlib
|
||||
import inspect
|
||||
from typing import List, Dict, Any, Type,Optional
|
||||
from pathlib import Path
|
||||
from .base_tool import BaseServerTool
|
||||
|
||||
|
||||
class ServerToolRegistry:
|
||||
"""Registry for managing server tools from files"""
|
||||
|
||||
def __init__(self, tools_directory: str = None):
|
||||
self.tools_directory = tools_directory or os.path.dirname(__file__)
|
||||
self._registered_tools: Dict[str, BaseServerTool] = {}
|
||||
self._tool_files: List[str] = []
|
||||
|
||||
@property
|
||||
def directory(self):
|
||||
"""Alias for tools_directory for backward compatibility"""
|
||||
return self.tools_directory
|
||||
|
||||
@property
|
||||
def _tools(self):
|
||||
"""Alias for _registered_tools for backward compatibility"""
|
||||
return self._registered_tools
|
||||
|
||||
def discover_tools(self) -> List[BaseServerTool]:
|
||||
"""Discover all tools in the tools directory"""
|
||||
tools = []
|
||||
tools_dir = Path(self.tools_directory)
|
||||
|
||||
if not tools_dir.exists():
|
||||
return tools
|
||||
|
||||
# Find all Python files in the tools directory
|
||||
for file_path in tools_dir.glob("*.py"):
|
||||
if file_path.name.startswith("__"):
|
||||
continue
|
||||
|
||||
module_name = file_path.stem
|
||||
try:
|
||||
# Import the module
|
||||
spec = importlib.util.spec_from_file_location(module_name, file_path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
# Find all BaseServerTool subclasses in the module
|
||||
for name, obj in inspect.getmembers(module, inspect.isclass):
|
||||
if (issubclass(obj, BaseServerTool) and
|
||||
obj != BaseServerTool and
|
||||
not inspect.isabstract(obj)):
|
||||
try:
|
||||
tool_instance = obj()
|
||||
tools.append(tool_instance)
|
||||
self._registered_tools[tool_instance.name] = tool_instance
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not instantiate tool {name}: {e}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not load tool file {file_path}: {e}")
|
||||
|
||||
return tools
|
||||
|
||||
def register_tool(self, tool: BaseServerTool) -> None:
|
||||
"""Register a single tool"""
|
||||
self._registered_tools[tool.name] = tool
|
||||
|
||||
def register_tools(self, tools: List[BaseServerTool]) -> None:
|
||||
"""Register multiple tools"""
|
||||
for tool in tools:
|
||||
self.register_tool(tool)
|
||||
|
||||
def get_tool(self, name: str) -> Optional[BaseServerTool]:
|
||||
"""Get a tool by name"""
|
||||
return self._registered_tools.get(name)
|
||||
|
||||
def get_all_tools(self) -> List[BaseServerTool]:
|
||||
"""Get all registered tools"""
|
||||
return list(self._registered_tools.values())
|
||||
|
||||
def get_tool_names(self) -> List[str]:
|
||||
"""Get all registered tool names"""
|
||||
return list(self._registered_tools.keys())
|
||||
|
||||
def register_tools_with_server(self, mcp_server) -> None:
|
||||
"""Register all discovered tools with a FastMCP server"""
|
||||
# First discover tools if not already done
|
||||
if not self._registered_tools:
|
||||
self.discover_tools()
|
||||
|
||||
# Register each tool with the server
|
||||
for tool in self._registered_tools.values():
|
||||
tool.create_fastmcp_tool(mcp_server)
|
||||
|
||||
def clear_tools(self) -> None:
|
||||
"""Clear all registered tools"""
|
||||
self._registered_tools.clear()
|
||||
Reference in New Issue
Block a user