initial mcp server setup

This commit is contained in:
OwusuBlessing
2025-09-11 23:13:58 +01:00
commit 20f96c0f30
141 changed files with 14444 additions and 0 deletions
+150
View File
@@ -0,0 +1,150 @@
"""
Base AI Client with common MCP integration functionality
"""
import json
import os
import sys
from typing import Any, Dict, List, Optional
from abc import ABC
# Add the project root to the path to import config
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
if project_root not in sys.path:
sys.path.insert(0, project_root)
try:
from config import Config
CONFIG_AVAILABLE = True
except ImportError:
CONFIG_AVAILABLE = False
from ..core.interfaces import IAIClient, IMCPClient
class BaseAIClient(IAIClient, ABC):
"""Base class for AI clients with MCP integration"""
def __init__(self, model_name: str, provider: str, api_key: Optional[str] = None, **kwargs):
self._model_name = model_name
self._provider = provider
self._client = None
self._initialized = False
self._extra_config = kwargs
# Get API key from config if not provided
if api_key is None and CONFIG_AVAILABLE:
api_key = self._get_api_key_from_config()
if not api_key:
raise ValueError(f"API key not provided and could not be loaded from config for provider: {provider}")
self._api_key = api_key
def _get_api_key_from_config(self) -> Optional[str]:
"""Get API key from config based on provider"""
if not CONFIG_AVAILABLE:
return None
provider_key_map = {
"openai": Config.OPENAI_API_KEY,
"claude": Config.CLAUDE_API_KEY,
"grok": Config.GROK_API_KEY
}
return provider_key_map.get(self._provider)
@property
def model_name(self) -> str:
"""Get the AI model name"""
return self._model_name
async def initialize(self) -> None:
"""Initialize the AI client - to be implemented by subclasses"""
if self._initialized:
return
await self._initialize_client()
self._initialized = True
async def _initialize_client(self) -> None:
"""Initialize the specific AI client - to be implemented by subclasses"""
pass
async def process_with_tools(
self,
query: str,
available_tools: List[Dict[str, Any]],
mcp_client: IMCPClient
) -> str:
"""Process a query with MCP tools using a common pattern"""
# Format tools for the specific AI provider
formatted_tools = self._format_tools_for_provider(available_tools)
# Create initial messages
messages = [{"role": "user", "content": query}]
# Get AI response with tool calling
response = await self.chat_completion(
messages=messages,
tools=formatted_tools,
tool_choice="auto"
)
# Extract assistant message
assistant_message = response["choices"][0]["message"]
# Check if tools were called
if "tool_calls" in assistant_message and assistant_message["tool_calls"]:
# Add assistant message to conversation
messages.append(assistant_message)
# Process each tool call
for tool_call in assistant_message["tool_calls"]:
try:
# Extract tool call details
tool_name = tool_call["function"]["name"]
tool_args = json.loads(tool_call["function"]["arguments"])
# Call the tool via MCP client
tool_result = await mcp_client.call_tool(tool_name, tool_args)
# Add tool response to conversation
messages.append({
"role": "tool",
"tool_call_id": tool_call["id"],
"content": str(tool_result),
})
except Exception as e:
# Handle tool call errors
messages.append({
"role": "tool",
"tool_call_id": tool_call["id"],
"content": f"Error calling tool: {str(e)}",
})
# Get final response from AI with tool results
final_response = await self.chat_completion(
messages=messages,
tools=formatted_tools,
tool_choice="none" # Don't allow more tool calls
)
return final_response["choices"][0]["message"]["content"]
# No tools called, return direct response
return assistant_message["content"]
def _format_tools_for_provider(self, tools: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""Format tools for the specific AI provider - to be implemented by subclasses"""
return tools
async def cleanup(self) -> None:
"""Clean up resources"""
if self._client:
await self._cleanup_client()
self._initialized = False
async def _cleanup_client(self) -> None:
"""Clean up the specific AI client - to be implemented by subclasses"""
pass
+5
View File
@@ -0,0 +1,5 @@
"""
MCP Template Source Package
"""
__version__ = "0.1.0"
@@ -0,0 +1,7 @@
# Configuration management
from .config_manager import ConfigManager
from .server_config import ServerConfig
from .client_config import ClientConfig
from .transport_config import TransportConfig
__all__ = ['ConfigManager', 'ServerConfig', 'ClientConfig', 'TransportConfig']
@@ -0,0 +1,70 @@
"""
Client Configuration
"""
from typing import Dict, Any, Optional
from dataclasses import dataclass
@dataclass
class ClientConfig:
"""Configuration class for MCP clients"""
provider: str = "openai"
model: str = "gpt-4o"
api_key: Optional[str] = None
# Model parameters
temperature: float = 0.7
max_tokens: int = 1000
top_p: float = 1.0
# Connection settings
timeout: int = 30
max_retries: int = 3
retry_delay: float = 1.0
# MCP-specific settings
enable_tool_calling: bool = True
enable_resource_access: bool = True
enable_prompts: bool = True
# Transport settings
transport_host: str = "localhost"
transport_port: int = 8050
transport_endpoint: str = "/sse"
@classmethod
def from_dict(cls, config_dict: Dict[str, Any]) -> 'ClientConfig':
"""Create ClientConfig from dictionary"""
return cls(**config_dict)
def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary"""
return self.__dict__.copy()
def get_provider_config(self) -> Dict[str, Any]:
"""Get provider-specific configuration"""
return {
"model_name": self.model,
"api_key": self.api_key,
"temperature": self.temperature,
"max_tokens": self.max_tokens,
"top_p": self.top_p,
"timeout": self.timeout,
"max_retries": self.max_retries,
"retry_delay": self.retry_delay,
}
def validate(self) -> bool:
"""Validate configuration"""
if not self.provider:
return False
if not self.model:
return False
if not self.api_key:
return False
if self.temperature < 0 or self.temperature > 2:
return False
if self.max_tokens < 1:
return False
return True
@@ -0,0 +1,133 @@
"""
Centralized Configuration Manager
"""
import os
import json
from typing import Dict, Any, Optional
from pathlib import Path
try:
from dotenv import load_dotenv
DOTENV_AVAILABLE = True
except ImportError:
DOTENV_AVAILABLE = False
class ConfigManager:
"""Centralized configuration manager for MCP components"""
def __init__(self, config_file: Optional[str] = None, env_file: Optional[str] = None):
self._config_file = config_file or "config.json"
self._env_file = env_file or ".env"
self._config: Dict[str, Any] = {}
self._loaded = False
async def load_config(self) -> Dict[str, Any]:
"""Load configuration from all sources"""
if self._loaded:
return self._config
# Load environment variables
await self._load_env_vars()
# Load JSON configuration file
await self._load_json_config()
# Merge configurations
self._config = await self._merge_configs()
self._loaded = True
return self._config
async def _load_env_vars(self) -> None:
"""Load environment variables"""
if DOTENV_AVAILABLE and Path(self._env_file).exists():
load_dotenv(self._env_file)
async def _load_json_config(self) -> Dict[str, Any]:
"""Load JSON configuration file"""
config_path = Path(self._config_file)
if config_path.exists():
with open(config_path, 'r') as f:
return json.load(f)
return {}
async def _merge_configs(self) -> Dict[str, Any]:
"""Merge all configuration sources"""
merged = {}
# Start with JSON config as base
json_config = await self._load_json_config()
merged.update(json_config)
# Override with environment variables
env_config = await self._get_env_config()
self._deep_update(merged, env_config)
return merged
async def _get_env_config(self) -> Dict[str, Any]:
"""Get configuration from environment variables"""
env_config = {}
# Server configuration
if os.getenv("MCP_SERVER_NAME"):
env_config.setdefault("server", {})["name"] = os.getenv("MCP_SERVER_NAME")
if os.getenv("MCP_SERVER_HOST"):
env_config.setdefault("server", {})["host"] = os.getenv("MCP_SERVER_HOST")
if os.getenv("MCP_SERVER_PORT"):
env_config.setdefault("server", {})["port"] = int(os.getenv("MCP_SERVER_PORT"))
if os.getenv("MCP_TRANSPORT"):
env_config.setdefault("server", {})["transport"] = os.getenv("MCP_TRANSPORT")
# Client configuration
if os.getenv("MCP_AI_PROVIDER"):
env_config.setdefault("client", {})["provider"] = os.getenv("MCP_AI_PROVIDER")
if os.getenv("MCP_AI_MODEL"):
env_config.setdefault("client", {})["model"] = os.getenv("MCP_AI_MODEL")
# API keys
for provider in ["OPENAI", "CLAUDE", "GROK"]:
api_key = os.getenv(f"{provider}_API_KEY")
if api_key:
env_config.setdefault("api_keys", {})[provider.lower()] = api_key
return env_config
def _deep_update(self, base_dict: Dict[str, Any], update_dict: Dict[str, Any]) -> None:
"""Deep update dictionary"""
for key, value in update_dict.items():
if isinstance(value, dict) and key in base_dict and isinstance(base_dict[key], dict):
self._deep_update(base_dict[key], value)
else:
base_dict[key] = value
async def get_server_config(self) -> Dict[str, Any]:
"""Get server-specific configuration"""
config = await self.load_config()
return config.get("server", {})
async def get_client_config(self) -> Dict[str, Any]:
"""Get client-specific configuration"""
config = await self.load_config()
return config.get("client", {})
async def get_api_key(self, provider: str) -> Optional[str]:
"""Get API key for a specific provider"""
config = await self.load_config()
api_keys = config.get("api_keys", {})
return api_keys.get(provider.lower())
async def save_config(self, config: Dict[str, Any]) -> None:
"""Save configuration to file"""
with open(self._config_file, 'w') as f:
json.dump(config, f, indent=2)
async def update_config(self, updates: Dict[str, Any]) -> None:
"""Update configuration and save"""
config = await self.load_config()
self._deep_update(config, updates)
await self.save_config(config)
self._loaded = False # Force reload on next access
@@ -0,0 +1,72 @@
"""
Server Configuration
"""
from typing import Dict, Any, Optional
from dataclasses import dataclass
from ..core.types import TransportType
@dataclass
class ServerConfig:
"""Configuration class for MCP servers"""
name: str = "MCP Server"
version: str = "1.0.0"
transport: TransportType = TransportType.STDIO
host: str = "0.0.0.0"
port: int = 8050
stateless_http: bool = True
# Tool configurations
enable_default_tools: bool = True
custom_tools: Optional[Dict[str, Any]] = None
# Resource configurations
enable_file_resources: bool = False
resource_paths: Optional[list[str]] = None
# Prompt configurations
enable_default_prompts: bool = False
custom_prompts: Optional[Dict[str, Any]] = None
# Performance settings
max_concurrent_requests: int = 10
request_timeout: int = 30
def __post_init__(self):
"""Initialize optional fields"""
if self.custom_tools is None:
self.custom_tools = {}
if self.resource_paths is None:
self.resource_paths = []
if self.custom_prompts is None:
self.custom_prompts = {}
@classmethod
def from_dict(cls, config_dict: Dict[str, Any]) -> 'ServerConfig':
"""Create ServerConfig from dictionary"""
# Convert transport string to enum
if 'transport' in config_dict and isinstance(config_dict['transport'], str):
config_dict['transport'] = TransportType(config_dict['transport'].lower())
return cls(**config_dict)
def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary"""
result = self.__dict__.copy()
result['transport'] = self.transport.value
return result
def get_transport_config(self) -> Dict[str, Any]:
"""Get transport-specific configuration"""
if self.transport == TransportType.SSE:
return {
"host": self.host,
"port": self.port,
"endpoint": "/sse",
}
elif self.transport == TransportType.STDIO:
return {}
else:
return {}
@@ -0,0 +1,89 @@
"""
Transport Configuration
"""
from typing import Dict, Any, Optional
from dataclasses import dataclass
from ..core.types import TransportType
@dataclass
class TransportConfig:
"""Configuration class for MCP transport"""
transport_type: TransportType = TransportType.STDIO
# SSE-specific settings
sse_host: str = "localhost"
sse_port: int = 8050
sse_endpoint: str = "/sse"
sse_timeout: int = 30
sse_reconnect_delay: float = 1.0
sse_max_reconnects: int = 5
# STDIO-specific settings
stdio_command: Optional[str] = None
stdio_args: Optional[list[str]] = None
stdio_env: Optional[Dict[str, str]] = None
stdio_cwd: Optional[str] = None
# General transport settings
buffer_size: int = 8192
encoding: str = "utf-8"
enable_compression: bool = False
enable_ssl: bool = False
@classmethod
def from_dict(cls, config_dict: Dict[str, Any]) -> 'TransportConfig':
"""Create TransportConfig from dictionary"""
# Convert transport string to enum
if 'transport_type' in config_dict and isinstance(config_dict['transport_type'], str):
config_dict['transport_type'] = TransportType(config_dict['transport_type'].lower())
return cls(**config_dict)
def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary"""
result = self.__dict__.copy()
result['transport_type'] = self.transport_type.value
return result
def get_sse_config(self) -> Dict[str, Any]:
"""Get SSE-specific configuration"""
return {
"host": self.sse_host,
"port": self.sse_port,
"endpoint": self.sse_endpoint,
"timeout": self.sse_timeout,
"reconnect_delay": self.sse_reconnect_delay,
"max_reconnects": self.sse_max_reconnects,
}
def get_stdio_config(self) -> Dict[str, Any]:
"""Get STDIO-specific configuration"""
config = {}
if self.stdio_command:
config["command"] = self.stdio_command
if self.stdio_args:
config["args"] = self.stdio_args
if self.stdio_env:
config["env"] = self.stdio_env
if self.stdio_cwd:
config["cwd"] = self.stdio_cwd
return config
def get_transport_config(self) -> Dict[str, Any]:
"""Get transport configuration based on type"""
config = {
"buffer_size": self.buffer_size,
"encoding": self.encoding,
"enable_compression": self.enable_compression,
"enable_ssl": self.enable_ssl,
}
if self.transport_type == TransportType.SSE:
config.update(self.get_sse_config())
elif self.transport_type == TransportType.STDIO:
config.update(self.get_stdio_config())
return config
+13
View File
@@ -0,0 +1,13 @@
# Core MCP abstractions and base classes
from .types import MCPTool, MCPResource, MCPPrompt, MCPServerConfig
from .interfaces import IMCPServer, IMCPClient, IMCPTransport
__all__ = [
'MCPTool',
'MCPResource',
'MCPPrompt',
'MCPServerConfig',
'IMCPServer',
'IMCPClient',
'IMCPTransport'
]
+181
View File
@@ -0,0 +1,181 @@
"""
Core MCP Interfaces and Abstract Base Classes
"""
from abc import ABC, abstractmethod
from typing import Any, Dict, List, Optional, Protocol
from contextlib import asynccontextmanager
from .types import MCPTool, MCPResource, MCPPrompt, MCPServerConfig
class IMCPServer(ABC):
"""Abstract base class for MCP servers"""
@abstractmethod
async def initialize(self) -> None:
"""Initialize the MCP server"""
pass
@abstractmethod
async def register_tool(self, tool: MCPTool) -> None:
"""Register a tool with the server"""
pass
@abstractmethod
async def register_resource(self, resource: MCPResource) -> None:
"""Register a resource with the server"""
pass
@abstractmethod
async def register_prompt(self, prompt: MCPPrompt) -> None:
"""Register a prompt with the server"""
pass
@abstractmethod
async def list_tools(self) -> List[Dict[str, Any]]:
"""List all available tools"""
pass
@abstractmethod
async def list_resources(self) -> List[Dict[str, Any]]:
"""List all available resources"""
pass
@abstractmethod
async def list_prompts(self) -> List[Dict[str, Any]]:
"""List all available prompts"""
pass
@abstractmethod
async def call_tool(self, name: str, arguments: Dict[str, Any]) -> Any:
"""Call a tool by name with arguments"""
pass
@abstractmethod
async def read_resource(self, uri: str) -> Any:
"""Read a resource by URI"""
pass
@abstractmethod
async def get_prompt(self, name: str, arguments: Optional[Dict[str, Any]] = None) -> str:
"""Get a prompt by name with optional arguments"""
pass
@abstractmethod
async def start(self) -> None:
"""Start the MCP server"""
pass
@abstractmethod
async def stop(self) -> None:
"""Stop the MCP server"""
pass
class IMCPClient(ABC):
"""Abstract base class for MCP clients"""
@abstractmethod
async def connect(self) -> None:
"""Connect to MCP server"""
pass
@abstractmethod
async def disconnect(self) -> None:
"""Disconnect from MCP server"""
pass
@abstractmethod
async def list_tools(self) -> List[Dict[str, Any]]:
"""List available tools from server"""
pass
@abstractmethod
async def list_resources(self) -> List[Dict[str, Any]]:
"""List available resources from server"""
pass
@abstractmethod
async def list_prompts(self) -> List[Dict[str, Any]]:
"""List available prompts from server"""
pass
@abstractmethod
async def call_tool(self, name: str, arguments: Dict[str, Any]) -> Any:
"""Call a tool on the server"""
pass
@abstractmethod
async def read_resource(self, uri: str) -> Any:
"""Read a resource from the server"""
pass
@abstractmethod
async def get_prompt(self, name: str, arguments: Optional[Dict[str, Any]] = None) -> str:
"""Get a prompt from the server"""
pass
class IMCPTransport(ABC):
"""Abstract base class for MCP transport mechanisms"""
@abstractmethod
@asynccontextmanager
async def connect(self):
"""Establish transport connection"""
pass
@abstractmethod
async def send_message(self, message: Dict[str, Any]) -> None:
"""Send a message through the transport"""
pass
@abstractmethod
async def receive_message(self) -> Dict[str, Any]:
"""Receive a message through the transport"""
pass
@abstractmethod
async def close(self) -> None:
"""Close the transport connection"""
pass
class IAIClient(ABC):
"""Abstract base class for AI model clients"""
@property
@abstractmethod
def model_name(self) -> str:
"""Get the AI model name"""
pass
@abstractmethod
async def initialize(self) -> None:
"""Initialize the AI client"""
pass
@abstractmethod
async def chat_completion(
self,
messages: List[Dict[str, Any]],
tools: Optional[List[Dict[str, Any]]] = None,
**kwargs
) -> Dict[str, Any]:
"""Perform a chat completion with optional tools"""
pass
@abstractmethod
async def process_with_tools(
self,
query: str,
available_tools: List[Dict[str, Any]],
mcp_client: IMCPClient
) -> str:
"""Process a query with MCP tools"""
pass
@abstractmethod
async def cleanup(self) -> None:
"""Clean up resources"""
pass
+79
View File
@@ -0,0 +1,79 @@
"""
Core MCP Types and Data Structures
"""
from typing import Any, Dict, List, Optional, Protocol, Union
from abc import ABC, abstractmethod
from dataclasses import dataclass
from enum import Enum
class TransportType(Enum):
"""Supported MCP transport types"""
SSE = "sse"
STDIO = "stdio"
@dataclass
class MCPTool:
"""Represents an MCP tool that can be called by AI models"""
name: str
description: str
input_schema: Dict[str, Any]
handler: callable
def __post_init__(self):
"""Validate tool configuration"""
if not self.name or not self.description:
raise ValueError("Tool name and description are required")
@dataclass
class MCPResource:
"""Represents an MCP resource that can be read by clients"""
uri: str
name: str
description: str
mime_type: str
content: Union[str, bytes]
def __post_init__(self):
"""Validate resource configuration"""
if not self.uri or not self.name:
raise ValueError("Resource URI and name are required")
@dataclass
class MCPPrompt:
"""Represents an MCP prompt template"""
name: str
description: str
template: str
arguments: Optional[Dict[str, Any]] = None
def __post_init__(self):
"""Validate prompt configuration"""
if not self.name or not self.template:
raise ValueError("Prompt name and template are required")
@dataclass
class MCPServerConfig:
"""Configuration for MCP server"""
name: str
version: str = "1.0.0"
transport: TransportType = TransportType.STDIO
host: str = "0.0.0.0"
port: int = 8050
tools: List[MCPTool] = None
resources: List[MCPResource] = None
prompts: List[MCPPrompt] = None
stateless_http: bool = True
def __post_init__(self):
"""Initialize empty lists if not provided"""
if self.tools is None:
self.tools = []
if self.resources is None:
self.resources = []
if self.prompts is None:
self.prompts = []
@@ -0,0 +1,6 @@
# MCP Template Examples
from .server_examples import ServerExamples
from .client_examples import ClientExamples
from .integration_examples import IntegrationExamples
__all__ = ['ServerExamples', 'ClientExamples', 'IntegrationExamples']
@@ -0,0 +1,174 @@
"""
Server Examples showing how to use the modular MCP template
"""
import asyncio
from typing import List
from ..core.types import TransportType
from ..server.server_factory import MCPServerFactory
from ..tools.tool_registry import ToolRegistry
from ..resources.data_resources import DataResources
class ServerExamples:
"""Examples of creating different types of MCP servers"""
@staticmethod
async def create_math_server():
"""Create a server with only math tools"""
print("🧮 Creating Math Server...")
# Use tool registry to get math tools
registry = ToolRegistry()
math_tools = registry.get_tools_by_category('math')
server = MCPServerFactory.create_server(
name="Math Server",
transport=TransportType.STDIO,
tools=math_tools
)
print(f"✅ Math Server created with {len(math_tools)} tools")
return server
@staticmethod
async def create_developer_server():
"""Create a comprehensive developer server"""
print("👨‍💻 Creating Developer Server...")
registry = ToolRegistry()
tools = registry.get_tools_by_categories(['math', 'text', 'system'])
# Add data resources
resources = DataResources.get_resources()
server = MCPServerFactory.create_server(
name="Developer Server",
transport=TransportType.SSE,
host="localhost",
port=8050,
tools=tools,
resources=resources
)
print(f"✅ Developer Server created with {len(tools)} tools and {len(resources)} resources")
return server
@staticmethod
async def create_business_server():
"""Create a business-focused server"""
print("💼 Creating Business Server...")
registry = ToolRegistry()
# Add custom business tools
async def calculate_roi(initial_investment: float, final_value: float) -> str:
"""Calculate Return on Investment"""
if initial_investment <= 0:
raise ValueError("Initial investment must be positive")
roi = ((final_value - initial_investment) / initial_investment) * 100
return ".2f"
async def format_currency(amount: float, currency: str = "USD") -> str:
"""Format amount as currency"""
return ",.2f"
from ..core.types import MCPTool
business_tools = [
MCPTool(
name="calculate_roi",
description="Calculate Return on Investment percentage",
input_schema={
"type": "object",
"properties": {
"initial_investment": {"type": "number", "description": "Initial investment amount"},
"final_value": {"type": "number", "description": "Final value amount"},
},
"required": ["initial_investment", "final_value"],
},
handler=calculate_roi,
),
MCPTool(
name="format_currency",
description="Format a number as currency",
input_schema={
"type": "object",
"properties": {
"amount": {"type": "number", "description": "Amount to format"},
"currency": {"type": "string", "description": "Currency code", "default": "USD"},
},
"required": ["amount"],
},
handler=format_currency,
),
]
# Add to registry
registry.add_custom_tools(business_tools)
# Get business-relevant tools
all_tools = registry.get_tools_by_categories(['math', 'text'])
all_tools.extend(business_tools)
server = MCPServerFactory.create_server(
name="Business Server",
transport=TransportType.STDIO,
tools=all_tools
)
print(f"✅ Business Server created with {len(all_tools)} tools")
return server
@staticmethod
async def create_custom_server_with_config():
"""Create a server using configuration"""
print("⚙️ Creating Custom Server with Configuration...")
config = {
"name": "Custom Config Server",
"transport": "sse",
"host": "localhost",
"port": 8080,
"tools": ["math", "text"], # Tool categories to include
"enable_resources": True,
}
registry = ToolRegistry()
tools = registry.get_tools_by_categories(config["tools"])
resources = DataResources.get_resources() if config.get("enable_resources") else []
server = MCPServerFactory.create_server(
name=config["name"],
transport=config["transport"],
host=config["host"],
port=config["port"],
tools=tools,
resources=resources
)
print(f"✅ Custom Server created with configuration")
print(f" - Tools: {len(tools)}")
print(f" - Resources: {len(resources)}")
print(f" - Transport: {config['transport']}")
return server
@staticmethod
async def demo_server_lifecycle():
"""Demonstrate complete server lifecycle"""
print("🔄 Server Lifecycle Demo")
# Create server
server = await ServerExamples.create_math_server()
# List available tools
tools = await server.list_tools()
print(f"📋 Available tools: {[t['name'] for t in tools]}")
# Test a tool
try:
result = await server.call_tool("add", {"a": 10, "b": 5})
print(f"🧮 Tool test - add(10, 5) = {result}")
except Exception as e:
print(f"❌ Tool test failed: {e}")
print("✅ Server lifecycle demo completed")
return server
@@ -0,0 +1,14 @@
# AI Client implementations
from .base_client import BaseAIClient
from .openai_client import OpenAIClient
from .claude_client import ClaudeClient
from .grok_client import GrokClient
from .client_factory import AIClientFactory
__all__ = [
'BaseAIClient',
'OpenAIClient',
'ClaudeClient',
'GrokClient',
'AIClientFactory'
]
@@ -0,0 +1,150 @@
"""
Base AI Client with common MCP integration functionality
"""
import json
import os
import sys
from typing import Any, Dict, List, Optional
from abc import ABC
# Add the project root to the path to import config
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
if project_root not in sys.path:
sys.path.insert(0, project_root)
try:
from config import Config
CONFIG_AVAILABLE = True
except ImportError:
CONFIG_AVAILABLE = False
from ..core.interfaces import IAIClient, IMCPClient
class BaseAIClient(IAIClient, ABC):
"""Base class for AI clients with MCP integration"""
def __init__(self, model_name: str, provider: str, api_key: Optional[str] = None, **kwargs):
self._model_name = model_name
self._provider = provider
self._client = None
self._initialized = False
self._extra_config = kwargs
# Get API key from config if not provided
if api_key is None and CONFIG_AVAILABLE:
api_key = self._get_api_key_from_config()
if not api_key:
raise ValueError(f"API key not provided and could not be loaded from config for provider: {provider}")
self._api_key = api_key
def _get_api_key_from_config(self) -> Optional[str]:
"""Get API key from config based on provider"""
if not CONFIG_AVAILABLE:
return None
provider_key_map = {
"openai": Config.OPENAI_API_KEY,
"claude": Config.CLAUDE_API_KEY,
"grok": Config.GROK_API_KEY
}
return provider_key_map.get(self._provider)
@property
def model_name(self) -> str:
"""Get the AI model name"""
return self._model_name
async def initialize(self) -> None:
"""Initialize the AI client - to be implemented by subclasses"""
if self._initialized:
return
await self._initialize_client()
self._initialized = True
async def _initialize_client(self) -> None:
"""Initialize the specific AI client - to be implemented by subclasses"""
pass
async def process_with_tools(
self,
query: str,
available_tools: List[Dict[str, Any]],
mcp_client: IMCPClient
) -> str:
"""Process a query with MCP tools using a common pattern"""
# Format tools for the specific AI provider
formatted_tools = self._format_tools_for_provider(available_tools)
# Create initial messages
messages = [{"role": "user", "content": query}]
# Get AI response with tool calling
response = await self.chat_completion(
messages=messages,
tools=formatted_tools,
tool_choice="auto"
)
# Extract assistant message
assistant_message = response["choices"][0]["message"]
# Check if tools were called
if "tool_calls" in assistant_message and assistant_message["tool_calls"]:
# Add assistant message to conversation
messages.append(assistant_message)
# Process each tool call
for tool_call in assistant_message["tool_calls"]:
try:
# Extract tool call details
tool_name = tool_call["function"]["name"]
tool_args = json.loads(tool_call["function"]["arguments"])
# Call the tool via MCP client
tool_result = await mcp_client.call_tool(tool_name, tool_args)
# Add tool response to conversation
messages.append({
"role": "tool",
"tool_call_id": tool_call["id"],
"content": str(tool_result),
})
except Exception as e:
# Handle tool call errors
messages.append({
"role": "tool",
"tool_call_id": tool_call["id"],
"content": f"Error calling tool: {str(e)}",
})
# Get final response from AI with tool results
final_response = await self.chat_completion(
messages=messages,
tools=formatted_tools,
tool_choice="none" # Don't allow more tool calls
)
return final_response["choices"][0]["message"]["content"]
# No tools called, return direct response
return assistant_message["content"]
def _format_tools_for_provider(self, tools: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""Format tools for the specific AI provider - to be implemented by subclasses"""
return tools
async def cleanup(self) -> None:
"""Clean up resources"""
if self._client:
await self._cleanup_client()
self._initialized = False
async def _cleanup_client(self) -> None:
"""Clean up the specific AI client - to be implemented by subclasses"""
pass
@@ -0,0 +1,56 @@
"""
Claude Client Implementation (Placeholder)
"""
from typing import Any, Dict, List, Optional
# Placeholder for Claude/Anthropic client
# This would need the actual Anthropic SDK when implemented
from .base_client import BaseAIClient
class ClaudeClient(BaseAIClient):
"""Claude client with MCP integration (Placeholder Implementation)"""
def __init__(
self,
model_name: str = "claude-3-opus-20240229",
api_key: Optional[str] = None,
**kwargs
):
# Note: This is a placeholder. You'll need to install the Anthropic SDK
# pip install anthropic
super().__init__(model_name, "claude", api_key, **kwargs)
# Claude specific configuration
self._temperature = kwargs.get("temperature", 0.7)
self._max_tokens = kwargs.get("max_tokens", 1000)
async def _initialize_client(self) -> None:
"""Initialize the Claude client"""
# TODO: Implement with actual Anthropic SDK
# self._client = Anthropic(api_key=self._api_key)
raise NotImplementedError("Claude client not yet implemented. Install Anthropic SDK and implement.")
async def chat_completion(
self,
messages: List[Dict[str, Any]],
tools: Optional[List[Dict[str, Any]]] = None,
**kwargs
) -> Dict[str, Any]:
"""Perform Claude chat completion"""
if not self._initialized:
await self.initialize()
# TODO: Implement Claude API call
raise NotImplementedError("Claude chat completion not yet implemented")
def _format_tools_for_provider(self, tools: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""Format tools for Claude's expected format"""
# TODO: Implement Claude tool formatting
return tools
async def _cleanup_client(self) -> None:
"""Clean up Claude client"""
# TODO: Implement cleanup
pass
@@ -0,0 +1,104 @@
"""
AI Client Factory for easy client creation and management
"""
import os
import sys
from typing import Optional, Dict, Any
# Add the project root to the path to import config
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
if project_root not in sys.path:
sys.path.insert(0, project_root)
try:
from config import Config
CONFIG_AVAILABLE = True
except ImportError:
CONFIG_AVAILABLE = False
from .base_client import BaseAIClient
from .openai_client import OpenAIClient
from .claude_client import ClaudeClient
from .grok_client import GrokClient
class AIClientFactory:
"""Factory class for creating AI clients with different providers"""
@staticmethod
def create_client(
provider: str,
model_name: Optional[str] = None,
api_key: Optional[str] = None,
**kwargs
) -> BaseAIClient:
"""Create an AI client for the specified provider"""
# Set default model names if not provided
if model_name is None:
if provider.lower() == "openai":
model_name = "gpt-4o"
elif provider.lower() == "claude":
model_name = "claude-3-opus-20240229"
elif provider.lower() == "grok":
model_name = "grok-1"
# Get API key from config if not provided
if api_key is None and CONFIG_AVAILABLE:
provider_key_map = {
"openai": Config.OPENAI_API_KEY,
"claude": Config.CLAUDE_API_KEY,
"grok": Config.GROK_API_KEY
}
api_key = provider_key_map.get(provider.lower())
if not api_key:
raise ValueError(f"API key not provided and could not be loaded from config for provider: {provider}")
# Create the appropriate client
provider_lower = provider.lower()
if provider_lower == "openai":
return OpenAIClient(model_name, api_key, **kwargs)
elif provider_lower == "claude":
return ClaudeClient(model_name, api_key, **kwargs)
elif provider_lower == "grok":
return GrokClient(model_name, api_key, **kwargs)
else:
raise ValueError(f"Unsupported AI provider: {provider}")
@staticmethod
def create_openai_client(
model_name: str = "gpt-4o",
api_key: Optional[str] = None,
**kwargs
) -> OpenAIClient:
"""Create an OpenAI client"""
return AIClientFactory.create_client("openai", model_name, api_key, **kwargs)
@staticmethod
def create_claude_client(
model_name: str = "claude-3-opus-20240229",
api_key: Optional[str] = None,
**kwargs
) -> ClaudeClient:
"""Create a Claude client"""
return AIClientFactory.create_client("claude", model_name, api_key, **kwargs)
@staticmethod
def create_grok_client(
model_name: str = "grok-1",
api_key: Optional[str] = None,
**kwargs
) -> GrokClient:
"""Create a Grok client"""
return AIClientFactory.create_client("grok", model_name, api_key, **kwargs)
@staticmethod
def get_available_providers() -> list[str]:
"""Get list of available AI providers"""
return ["openai", "claude", "grok"]
@staticmethod
def validate_provider(provider: str) -> bool:
"""Validate if a provider is supported"""
return provider.lower() in AIClientFactory.get_available_providers()
@@ -0,0 +1,55 @@
"""
Grok Client Implementation (Placeholder)
"""
from typing import Any, Dict, List, Optional
# Placeholder for Grok/xAI client
# This would need the xAI SDK or direct API integration when implemented
from .base_client import BaseAIClient
class GrokClient(BaseAIClient):
"""Grok client with MCP integration (Placeholder Implementation)"""
def __init__(
self,
model_name: str = "grok-1",
api_key: Optional[str] = None,
**kwargs
):
# Note: This is a placeholder. You'll need xAI API integration
super().__init__(model_name, "grok", api_key, **kwargs)
# Grok specific configuration
self._temperature = kwargs.get("temperature", 0.7)
self._max_tokens = kwargs.get("max_tokens", 1000)
async def _initialize_client(self) -> None:
"""Initialize the Grok client"""
# TODO: Implement with xAI API or SDK
# This might require direct HTTP calls to xAI API
raise NotImplementedError("Grok client not yet implemented. Implement xAI API integration.")
async def chat_completion(
self,
messages: List[Dict[str, Any]],
tools: Optional[List[Dict[str, Any]]] = None,
**kwargs
) -> Dict[str, Any]:
"""Perform Grok chat completion"""
if not self._initialized:
await self.initialize()
# TODO: Implement Grok API call
raise NotImplementedError("Grok chat completion not yet implemented")
def _format_tools_for_provider(self, tools: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""Format tools for Grok's expected format"""
# TODO: Implement Grok tool formatting
return tools
async def _cleanup_client(self) -> None:
"""Clean up Grok client"""
# TODO: Implement cleanup
pass
@@ -0,0 +1,106 @@
"""
OpenAI Client Implementation
"""
from typing import Any, Dict, List, Optional
try:
from openai import AsyncOpenAI
OPENAI_AVAILABLE = True
except ImportError:
OPENAI_AVAILABLE = False
from .base_client import BaseAIClient
from config import Config
class OpenAIClient(BaseAIClient):
"""OpenAI client with MCP integration"""
def __init__(
self,
model_name: str = "gpt-4o",
api_key: Optional[str] = None,
**kwargs
):
if not OPENAI_AVAILABLE:
raise ImportError("OpenAI package not installed. Install with: pip install openai")
super().__init__(model_name, "openai", api_key, **kwargs)
# OpenAI specific configuration
self._temperature = kwargs.get("temperature", 0.7)
self._max_tokens = kwargs.get("max_tokens", 1000)
self._api_key = api_key or Config.OPENAI_API_KEY
async def _initialize_client(self) -> None:
"""Initialize the OpenAI client"""
self._client = AsyncOpenAI(api_key=self._api_key)
async def chat_completion(
self,
messages: List[Dict[str, Any]],
tools: Optional[List[Dict[str, Any]]] = None,
**kwargs
) -> Dict[str, Any]:
"""Perform OpenAI chat completion"""
if not self._initialized:
await self.initialize()
# Prepare request parameters
request_params = {
"model": self._model_name,
"messages": messages,
"temperature": self._temperature,
"max_tokens": self._max_tokens,
}
# Add tools if provided
if tools:
request_params["tools"] = tools
request_params["tool_choice"] = kwargs.get("tool_choice", "auto")
# Make the API call
response = await self._client.chat.completions.create(**request_params)
# Convert to standard format
return {
"choices": [
{
"message": {
"role": choice.message.role,
"content": choice.message.content,
"tool_calls": [
{
"id": tool_call.id,
"type": "function",
"function": {
"name": tool_call.function.name,
"arguments": tool_call.function.arguments,
}
}
for tool_call in (choice.message.tool_calls or [])
] if choice.message.tool_calls else None,
}
}
for choice in response.choices
]
}
def _format_tools_for_provider(self, tools: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""Format tools for OpenAI's expected format"""
formatted_tools = []
for tool in tools:
formatted_tool = {
"type": "function",
"function": {
"name": tool["name"],
"description": tool["description"],
"parameters": tool["inputSchema"],
}
}
formatted_tools.append(formatted_tool)
return formatted_tools
async def _cleanup_client(self) -> None:
"""Clean up OpenAI client"""
if self._client:
await self._client.close()
@@ -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()
+8
View File
@@ -0,0 +1,8 @@
# MCP Tools Collection
from .math_tools import MathTools
from .text_tools import TextTools
from .system_tools import SystemTools
from .web_tools import WebTools
from .tool_registry import ToolRegistry
__all__ = ['MathTools', 'TextTools', 'SystemTools', 'WebTools', 'ToolRegistry']
+186
View File
@@ -0,0 +1,186 @@
"""
Mathematical Tools for MCP
"""
from typing import List
from ..core.types import MCPTool
class MathTools:
"""Collection of mathematical tools"""
@staticmethod
def get_tools() -> List[MCPTool]:
"""Get all math tools"""
return [
MathTools._create_add_tool(),
MathTools._create_subtract_tool(),
MathTools._create_multiply_tool(),
MathTools._create_divide_tool(),
MathTools._create_power_tool(),
MathTools._create_square_root_tool(),
MathTools._create_calculate_bmi_tool(),
]
@staticmethod
def _create_add_tool() -> MCPTool:
"""Create addition tool"""
async def add(a: float, b: float) -> float:
"""Add two numbers together"""
return a + b
return 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,
)
@staticmethod
def _create_subtract_tool() -> MCPTool:
"""Create subtraction tool"""
async def subtract(a: float, b: float) -> float:
"""Subtract second number from first"""
return a - b
return 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,
)
@staticmethod
def _create_multiply_tool() -> MCPTool:
"""Create multiplication tool"""
async def multiply(a: float, b: float) -> float:
"""Multiply two numbers"""
return a * b
return 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,
)
@staticmethod
def _create_divide_tool() -> MCPTool:
"""Create division tool"""
async def divide(a: float, b: float) -> float:
"""Divide first number by second"""
if b == 0:
raise ValueError("Cannot divide by zero")
return a / b
return 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,
)
@staticmethod
def _create_power_tool() -> MCPTool:
"""Create power tool"""
async def power(base: float, exponent: float) -> float:
"""Calculate base raised to the power of exponent"""
return base ** exponent
return MCPTool(
name="power",
description="Calculate base raised to the power of exponent",
input_schema={
"type": "object",
"properties": {
"base": {"type": "number", "description": "Base number"},
"exponent": {"type": "number", "description": "Exponent"},
},
"required": ["base", "exponent"],
},
handler=power,
)
@staticmethod
def _create_square_root_tool() -> MCPTool:
"""Create square root tool"""
async def square_root(number: float) -> float:
"""Calculate square root of a number"""
if number < 0:
raise ValueError("Cannot calculate square root of negative number")
return number ** 0.5
return MCPTool(
name="square_root",
description="Calculate square root of a number",
input_schema={
"type": "object",
"properties": {
"number": {"type": "number", "description": "Number to find square root of (must be non-negative)"},
},
"required": ["number"],
},
handler=square_root,
)
@staticmethod
def _create_calculate_bmi_tool() -> MCPTool:
"""Create BMI calculation tool"""
async def calculate_bmi(weight_kg: float, height_m: float) -> str:
"""Calculate BMI and provide health category"""
if weight_kg <= 0 or height_m <= 0:
raise ValueError("Weight and height must be positive numbers")
bmi = weight_kg / (height_m ** 2)
if bmi < 18.5:
category = "Underweight"
elif bmi < 25:
category = "Normal weight"
elif bmi < 30:
category = "Overweight"
else:
category = "Obese"
return ".1f"
return MCPTool(
name="calculate_bmi",
description="Calculate BMI and provide health assessment",
input_schema={
"type": "object",
"properties": {
"weight_kg": {"type": "number", "description": "Weight in kilograms"},
"height_m": {"type": "number", "description": "Height in meters"},
},
"required": ["weight_kg", "height_m"],
},
handler=calculate_bmi,
)
@@ -0,0 +1,157 @@
"""
System Tools for MCP
"""
import os
import platform
import psutil
from datetime import datetime
from typing import Dict, Any, List
from ..core.types import MCPTool
class SystemTools:
"""Collection of system-related tools"""
@staticmethod
def get_tools() -> List[MCPTool]:
"""Get all system tools"""
return [
SystemTools._create_get_system_info_tool(),
SystemTools._create_get_current_time_tool(),
SystemTools._create_list_directory_tool(),
SystemTools._create_get_file_info_tool(),
SystemTools._create_get_environment_variable_tool(),
]
@staticmethod
def _create_get_system_info_tool() -> MCPTool:
"""Create system info tool"""
async def get_system_info() -> Dict[str, Any]:
"""Get basic system information"""
try:
return {
"platform": platform.system(),
"platform_version": platform.version(),
"architecture": platform.machine(),
"processor": platform.processor(),
"python_version": platform.python_version(),
"cpu_count": os.cpu_count(),
"memory_total": psutil.virtual_memory().total if psutil else "psutil not available",
"memory_available": psutil.virtual_memory().available if psutil else "psutil not available",
}
except Exception as e:
return {"error": f"Could not retrieve system info: {str(e)}"}
return MCPTool(
name="get_system_info",
description="Get basic system information including OS, CPU, and memory details",
input_schema={"type": "object", "properties": {}},
handler=get_system_info,
)
@staticmethod
def _create_get_current_time_tool() -> MCPTool:
"""Create current time tool"""
async def get_current_time() -> str:
"""Get the current date and time"""
now = datetime.now()
return now.strftime("%Y-%m-%d %H:%M:%S")
return MCPTool(
name="get_current_time",
description="Get the current date and time in YYYY-MM-DD HH:MM:SS format",
input_schema={"type": "object", "properties": {}},
handler=get_current_time,
)
@staticmethod
def _create_list_directory_tool() -> MCPTool:
"""Create directory listing tool"""
async def list_directory(path: str = ".") -> List[str]:
"""List contents of a directory"""
try:
if not os.path.exists(path):
raise ValueError(f"Path does not exist: {path}")
if not os.path.isdir(path):
raise ValueError(f"Path is not a directory: {path}")
return os.listdir(path)
except Exception as e:
raise ValueError(f"Could not list directory: {str(e)}")
return MCPTool(
name="list_directory",
description="List the contents of a directory",
input_schema={
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Directory path to list",
"default": "."
},
},
},
handler=list_directory,
)
@staticmethod
def _create_get_file_info_tool() -> MCPTool:
"""Create file info tool"""
async def get_file_info(file_path: str) -> Dict[str, Any]:
"""Get information about a file"""
try:
if not os.path.exists(file_path):
raise ValueError(f"File does not exist: {file_path}")
stat = os.stat(file_path)
return {
"name": os.path.basename(file_path),
"path": os.path.abspath(file_path),
"size": stat.st_size,
"is_file": os.path.isfile(file_path),
"is_directory": os.path.isdir(file_path),
"modified_time": datetime.fromtimestamp(stat.st_mtime).isoformat(),
"created_time": datetime.fromtimestamp(stat.st_ctime).isoformat(),
}
except Exception as e:
raise ValueError(f"Could not get file info: {str(e)}")
return MCPTool(
name="get_file_info",
description="Get detailed information about a file or directory",
input_schema={
"type": "object",
"properties": {
"file_path": {"type": "string", "description": "Path to the file or directory"},
},
"required": ["file_path"],
},
handler=get_file_info,
)
@staticmethod
def _create_get_environment_variable_tool() -> MCPTool:
"""Create environment variable tool"""
async def get_environment_variable(name: str, default_value: str = "") -> str:
"""Get the value of an environment variable"""
return os.getenv(name, default_value)
return MCPTool(
name="get_environment_variable",
description="Get the value of an environment variable",
input_schema={
"type": "object",
"properties": {
"name": {"type": "string", "description": "Name of the environment variable"},
"default_value": {
"type": "string",
"description": "Default value if variable is not set",
"default": ""
},
},
"required": ["name"],
},
handler=get_environment_variable,
)
+187
View File
@@ -0,0 +1,187 @@
"""
Text Processing Tools for MCP
"""
import re
from typing import List
from ..core.types import MCPTool
class TextTools:
"""Collection of text processing tools"""
@staticmethod
def get_tools() -> List[MCPTool]:
"""Get all text tools"""
return [
TextTools._create_word_count_tool(),
TextTools._create_text_search_tool(),
TextTools._create_text_replace_tool(),
TextTools._create_text_uppercase_tool(),
TextTools._create_text_lowercase_tool(),
TextTools._create_text_length_tool(),
TextTools._create_generate_greeting_tool(),
]
@staticmethod
def _create_word_count_tool() -> MCPTool:
"""Create word count tool"""
async def count_words(text: str) -> int:
"""Count the number of words in a text"""
words = text.strip().split()
return len(words)
return MCPTool(
name="count_words",
description="Count the number of words in a text",
input_schema={
"type": "object",
"properties": {
"text": {"type": "string", "description": "Text to count words in"},
},
"required": ["text"],
},
handler=count_words,
)
@staticmethod
def _create_text_search_tool() -> MCPTool:
"""Create text search tool"""
async def search_text(text: str, pattern: str, case_sensitive: bool = False) -> List[str]:
"""Search for a pattern in text and return all matches"""
flags = 0 if case_sensitive else re.IGNORECASE
matches = re.findall(pattern, text, flags)
return matches
return MCPTool(
name="search_text",
description="Search for a pattern in text and return all matches",
input_schema={
"type": "object",
"properties": {
"text": {"type": "string", "description": "Text to search in"},
"pattern": {"type": "string", "description": "Regular expression pattern to search for"},
"case_sensitive": {"type": "boolean", "description": "Whether search should be case sensitive", "default": False},
},
"required": ["text", "pattern"],
},
handler=search_text,
)
@staticmethod
def _create_text_replace_tool() -> MCPTool:
"""Create text replace tool"""
async def replace_text(text: str, old_pattern: str, new_text: str) -> str:
"""Replace all occurrences of a pattern in text"""
return text.replace(old_pattern, new_text)
return MCPTool(
name="replace_text",
description="Replace all occurrences of a pattern in text",
input_schema={
"type": "object",
"properties": {
"text": {"type": "string", "description": "Original text"},
"old_pattern": {"type": "string", "description": "Text to replace"},
"new_text": {"type": "string", "description": "Replacement text"},
},
"required": ["text", "old_pattern", "new_text"],
},
handler=replace_text,
)
@staticmethod
def _create_text_uppercase_tool() -> MCPTool:
"""Create uppercase tool"""
async def to_uppercase(text: str) -> str:
"""Convert text to uppercase"""
return text.upper()
return MCPTool(
name="to_uppercase",
description="Convert text to uppercase",
input_schema={
"type": "object",
"properties": {
"text": {"type": "string", "description": "Text to convert to uppercase"},
},
"required": ["text"],
},
handler=to_uppercase,
)
@staticmethod
def _create_text_lowercase_tool() -> MCPTool:
"""Create lowercase tool"""
async def to_lowercase(text: str) -> str:
"""Convert text to lowercase"""
return text.lower()
return MCPTool(
name="to_lowercase",
description="Convert text to lowercase",
input_schema={
"type": "object",
"properties": {
"text": {"type": "string", "description": "Text to convert to lowercase"},
},
"required": ["text"],
},
handler=to_lowercase,
)
@staticmethod
def _create_text_length_tool() -> MCPTool:
"""Create text length tool"""
async def text_length(text: str) -> int:
"""Get the length of text"""
return len(text)
return MCPTool(
name="text_length",
description="Get the length of text (number of characters)",
input_schema={
"type": "object",
"properties": {
"text": {"type": "string", "description": "Text to measure length of"},
},
"required": ["text"],
},
handler=text_length,
)
@staticmethod
def _create_generate_greeting_tool() -> MCPTool:
"""Create greeting generation tool"""
async def generate_greeting(name: str, style: str = "casual") -> str:
"""Generate a personalized greeting"""
name = name.strip()
if not name:
raise ValueError("Name cannot be empty")
if style == "casual":
return f"Hey {name}! Welcome! 👋"
elif style == "formal":
return f"Good day, {name}. Welcome to our platform."
elif style == "professional":
return f"Hello, {name}. Thank you for joining us."
else:
return f"Hi {name}! Welcome!"
return MCPTool(
name="generate_greeting",
description="Generate a personalized greeting with different styles",
input_schema={
"type": "object",
"properties": {
"name": {"type": "string", "description": "Person's name"},
"style": {
"type": "string",
"enum": ["casual", "formal", "professional"],
"description": "Greeting style",
"default": "casual"
},
},
"required": ["name"],
},
handler=generate_greeting,
)
@@ -0,0 +1,80 @@
"""
Tool Registry for easy tool management and combination
"""
from typing import List, Dict, Any, Optional
from ..core.types import MCPTool
from .math_tools import MathTools
from .text_tools import TextTools
from .system_tools import SystemTools
from .web_tools import WebTools
class ToolRegistry:
"""Registry for managing and combining MCP tools from different categories"""
def __init__(self):
self._tool_categories = {
'math': MathTools,
'text': TextTools,
'system': SystemTools,
'web': WebTools,
}
self._custom_tools: List[MCPTool] = []
def get_tools_by_category(self, category: str) -> List[MCPTool]:
"""Get all tools from a specific category"""
if category not in self._tool_categories:
raise ValueError(f"Unknown tool category: {category}")
return self._tool_categories[category].get_tools()
def get_tools_by_categories(self, categories: List[str]) -> List[MCPTool]:
"""Get tools from multiple categories"""
tools = []
for category in categories:
tools.extend(self.get_tools_by_category(category))
return tools
def get_all_tools(self) -> List[MCPTool]:
"""Get all tools from all categories"""
tools = []
for category in self._tool_categories.values():
tools.extend(category.get_tools())
tools.extend(self._custom_tools)
return tools
def add_custom_tool(self, tool: MCPTool) -> None:
"""Add a custom tool to the registry"""
self._custom_tools.append(tool)
def add_custom_tools(self, tools: List[MCPTool]) -> None:
"""Add multiple custom tools to the registry"""
self._custom_tools.extend(tools)
def get_available_categories(self) -> List[str]:
"""Get list of available tool categories"""
return list(self._tool_categories.keys())
def get_category_info(self) -> Dict[str, Dict[str, Any]]:
"""Get information about each category"""
info = {}
for category_name, category_class in self._tool_categories.items():
tools = category_class.get_tools()
info[category_name] = {
'tool_count': len(tools),
'tool_names': [tool.name for tool in tools]
}
return info
def create_server_config(self, categories: Optional[List[str]] = None) -> Dict[str, Any]:
"""Create a server configuration with tools from specified categories"""
if categories is None:
tools = self.get_all_tools()
else:
tools = self.get_tools_by_categories(categories)
return {
'tools': tools,
'tool_count': len(tools),
'categories': categories or self.get_available_categories()
}
+161
View File
@@ -0,0 +1,161 @@
"""
Web Tools for MCP
"""
import urllib.parse
from typing import Dict, Any, List, Optional
from ..core.types import MCPTool
class WebTools:
"""Collection of web-related tools"""
@staticmethod
def get_tools() -> List[MCPTool]:
"""Get all web tools"""
return [
WebTools._create_url_encode_tool(),
WebTools._create_url_decode_tool(),
WebTools._create_parse_url_tool(),
WebTools._create_validate_email_tool(),
WebTools._create_extract_domain_tool(),
]
@staticmethod
def _create_url_encode_tool() -> MCPTool:
"""Create URL encode tool"""
async def url_encode(text: str) -> str:
"""URL encode a string"""
return urllib.parse.quote(text)
return MCPTool(
name="url_encode",
description="URL encode a string for safe transmission",
input_schema={
"type": "object",
"properties": {
"text": {"type": "string", "description": "Text to URL encode"},
},
"required": ["text"],
},
handler=url_encode,
)
@staticmethod
def _create_url_decode_tool() -> MCPTool:
"""Create URL decode tool"""
async def url_decode(encoded_text: str) -> str:
"""URL decode a string"""
try:
return urllib.parse.unquote(encoded_text)
except Exception:
raise ValueError("Invalid URL encoded string")
return MCPTool(
name="url_decode",
description="URL decode a previously encoded string",
input_schema={
"type": "object",
"properties": {
"encoded_text": {"type": "string", "description": "URL encoded text to decode"},
},
"required": ["encoded_text"],
},
handler=url_decode,
)
@staticmethod
def _create_parse_url_tool() -> MCPTool:
"""Create URL parsing tool"""
async def parse_url(url: str) -> Dict[str, str]:
"""Parse a URL and return its components"""
try:
parsed = urllib.parse.urlparse(url)
return {
"scheme": parsed.scheme,
"netloc": parsed.netloc,
"hostname": parsed.hostname,
"port": str(parsed.port) if parsed.port else "",
"path": parsed.path,
"query": parsed.query,
"fragment": parsed.fragment,
}
except Exception:
raise ValueError("Invalid URL format")
return MCPTool(
name="parse_url",
description="Parse a URL and return its components (scheme, host, path, etc.)",
input_schema={
"type": "object",
"properties": {
"url": {"type": "string", "description": "URL to parse"},
},
"required": ["url"],
},
handler=parse_url,
)
@staticmethod
def _create_validate_email_tool() -> MCPTool:
"""Create email validation tool"""
async def validate_email(email: str) -> Dict[str, Any]:
"""Validate an email address format"""
import re
email_pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
is_valid = bool(re.match(email_pattern, email))
# Extract domain
if is_valid and '@' in email:
domain = email.split('@')[1]
else:
domain = ""
return {
"email": email,
"is_valid": is_valid,
"domain": domain,
}
return MCPTool(
name="validate_email",
description="Validate an email address format and extract domain",
input_schema={
"type": "object",
"properties": {
"email": {"type": "string", "description": "Email address to validate"},
},
"required": ["email"],
},
handler=validate_email,
)
@staticmethod
def _create_extract_domain_tool() -> MCPTool:
"""Create domain extraction tool"""
async def extract_domain(url: str) -> str:
"""Extract the domain from a URL"""
try:
parsed = urllib.parse.urlparse(url)
if parsed.hostname:
return parsed.hostname
else:
# Fallback for URLs without scheme
url_with_scheme = "http://" + url if "://" not in url else url
parsed = urllib.parse.urlparse(url_with_scheme)
return parsed.hostname or ""
except Exception:
return ""
return MCPTool(
name="extract_domain",
description="Extract the domain name from a URL",
input_schema={
"type": "object",
"properties": {
"url": {"type": "string", "description": "URL to extract domain from"},
},
"required": ["url"],
},
handler=extract_domain,
)
@@ -0,0 +1,6 @@
# Transport layer implementations
from .transport_manager import TransportManager
from .sse_transport import SSETransport
from .stdio_transport import STDIOTransport
__all__ = ['TransportManager', 'SSETransport', 'STDIOTransport']
@@ -0,0 +1,101 @@
"""
SSE (Server-Sent Events) Transport Implementation
"""
import asyncio
import json
from typing import Dict, Any, Optional
from contextlib import asynccontextmanager
try:
import aiohttp
AIOHTTP_AVAILABLE = True
except ImportError:
AIOHTTP_AVAILABLE = False
from ..core.interfaces import IMCPTransport
class SSETransport(IMCPTransport):
"""SSE transport for MCP communication"""
def __init__(
self,
host: str = "localhost",
port: int = 8050,
endpoint: str = "/sse",
**kwargs
):
if not AIOHTTP_AVAILABLE:
raise ImportError("aiohttp package not installed. Install with: pip install aiohttp")
self.host = host
self.port = port
self.endpoint = endpoint
self._session: Optional[aiohttp.ClientSession] = None
self._response: Optional[aiohttp.ClientResponse] = None
self._connected = False
@asynccontextmanager
async def connect(self):
"""Establish SSE connection"""
try:
self._session = aiohttp.ClientSession()
url = f"http://{self.host}:{self.port}{self.endpoint}"
print(f"Connecting to SSE endpoint: {url}")
self._response = await self._session.get(url)
if self._response.status != 200:
raise ConnectionError(f"SSE connection failed with status {self._response.status}")
self._connected = True
yield self
except Exception as e:
print(f"SSE connection error: {e}")
raise
finally:
await self.close()
async def send_message(self, message: Dict[str, Any]) -> None:
"""Send a message through SSE transport"""
if not self._connected:
raise ConnectionError("SSE transport not connected")
# SSE is typically unidirectional from server to client
# For bidirectional communication, you might need to use a different approach
# or combine SSE with HTTP POST requests
print(f"SSE Transport: Sending message: {message}")
# TODO: Implement actual message sending logic
# This might require HTTP POST to a separate endpoint
# or WebSocket upgrade, depending on server implementation
async def receive_message(self) -> Dict[str, Any]:
"""Receive a message through SSE transport"""
if not self._connected or not self._response:
raise ConnectionError("SSE transport not connected")
# Read SSE data
async for line in self._response.content:
line_str = line.decode('utf-8').strip()
if line_str.startswith('data: '):
data = line_str[6:] # Remove 'data: ' prefix
if data:
try:
return json.loads(data)
except json.JSONDecodeError:
continue
return {}
async def close(self) -> None:
"""Close the SSE connection"""
if self._response:
self._response.close()
if self._session:
await self._session.close()
self._connected = False
print("SSE transport closed")
@@ -0,0 +1,81 @@
"""
STDIO Transport Implementation
"""
import asyncio
import json
import sys
from typing import Dict, Any, Optional
from contextlib import asynccontextmanager
from ..core.interfaces import IMCPTransport
class STDIOTransport(IMCPTransport):
"""STDIO transport for MCP communication"""
def __init__(self, **kwargs):
self._connected = False
self._reader: Optional[asyncio.StreamReader] = None
self._writer: Optional[asyncio.StreamWriter] = None
@asynccontextmanager
async def connect(self):
"""Establish STDIO connection"""
try:
# Use stdin/stdout for communication
self._reader = asyncio.StreamReader()
reader_protocol = asyncio.StreamReaderProtocol(self._reader)
await asyncio.get_event_loop().connect_read_pipe(
lambda: reader_protocol, sys.stdin
)
# For writing, we'll use stdout
self._writer = None # We'll write directly to stdout
self._connected = True
print("STDIO transport connected", file=sys.stderr)
yield self
except Exception as e:
print(f"STDIO connection error: {e}", file=sys.stderr)
raise
finally:
await self.close()
async def send_message(self, message: Dict[str, Any]) -> None:
"""Send a message through STDIO"""
if not self._connected:
raise ConnectionError("STDIO transport not connected")
# Convert message to JSON and send to stdout
message_json = json.dumps(message, separators=(',', ':'))
print(message_json, flush=True)
async def receive_message(self) -> Dict[str, Any]:
"""Receive a message through STDIO"""
if not self._connected or not self._reader:
raise ConnectionError("STDIO transport not connected")
try:
# Read line from stdin
line = await self._reader.readline()
if not line:
return {} # EOF
line_str = line.decode('utf-8').strip()
if line_str:
return json.loads(line_str)
except json.JSONDecodeError as e:
print(f"Invalid JSON received: {e}", file=sys.stderr)
except Exception as e:
print(f"Error receiving message: {e}", file=sys.stderr)
return {}
async def close(self) -> None:
"""Close the STDIO connection"""
self._connected = False
if self._writer:
self._writer.close()
print("STDIO transport closed", file=sys.stderr)
@@ -0,0 +1,81 @@
"""
Transport Manager for easy switching between transport methods
"""
from typing import Optional, Union
from contextlib import asynccontextmanager
from ..core.types import TransportType
from ..core.interfaces import IMCPTransport
from .sse_transport import SSETransport
from .stdio_transport import STDIOTransport
class TransportManager:
"""Manager class for handling different MCP transport methods"""
def __init__(self):
self._current_transport: Optional[IMCPTransport] = None
@asynccontextmanager
async def get_transport(
self,
transport_type: Union[str, TransportType],
**kwargs
):
"""Get a transport instance for the specified type"""
# Convert string to enum if needed
if isinstance(transport_type, str):
transport_type = TransportType(transport_type.lower())
# Create the appropriate transport
if transport_type == TransportType.SSE:
transport = SSETransport(**kwargs)
elif transport_type == TransportType.STDIO:
transport = STDIOTransport(**kwargs)
else:
raise ValueError(f"Unsupported transport type: {transport_type}")
self._current_transport = transport
try:
async with transport.connect():
yield transport
finally:
self._current_transport = None
async def switch_transport(
self,
new_transport_type: Union[str, TransportType],
**kwargs
) -> IMCPTransport:
"""Switch to a different transport type"""
# Close current transport if exists
if self._current_transport:
await self._current_transport.close()
# Convert string to enum if needed
if isinstance(new_transport_type, str):
new_transport_type = TransportType(new_transport_type.lower())
# Create new transport
if new_transport_type == TransportType.SSE:
self._current_transport = SSETransport(**kwargs)
elif new_transport_type == TransportType.STDIO:
self._current_transport = STDIOTransport(**kwargs)
else:
raise ValueError(f"Unsupported transport type: {new_transport_type}")
return self._current_transport
@property
def current_transport(self) -> Optional[IMCPTransport]:
"""Get the currently active transport"""
return self._current_transport
async def close_current_transport(self) -> None:
"""Close the currently active transport"""
if self._current_transport:
await self._current_transport.close()
self._current_transport = None