initial mcp server setup
This commit is contained in:
@@ -0,0 +1,602 @@
|
||||
Metadata-Version: 2.4
|
||||
Name: mcp-template
|
||||
Version: 0.1.0
|
||||
Summary: Add your description here
|
||||
Requires-Python: >=3.13
|
||||
Description-Content-Type: text/markdown
|
||||
Requires-Dist: mcp[cli]>=1.14.0
|
||||
Requires-Dist: nest-asyncio>=1.6.0
|
||||
Requires-Dist: openai>=1.107.1
|
||||
Requires-Dist: psutil
|
||||
|
||||
# MCP Template Server & LLM Client
|
||||
|
||||
A comprehensive Python implementation of the **Model Context Protocol (MCP)** with multi-provider LLM support, featuring modular tools, prompts, and resources. This project provides a production-ready MCP server and flexible client that supports OpenAI, Claude, and Grok models.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Quick Start](#quick-start)
|
||||
- [Introduction](#introduction)
|
||||
- [Architecture & Code Structure](#architecture--code-structure)
|
||||
- [Tools](#tools)
|
||||
- [Prompts](#prompts)
|
||||
- [Resources](#resources)
|
||||
- [Test Cases](#test-cases)
|
||||
- [Configuration](#configuration)
|
||||
- [Running the System](#running-the-system)
|
||||
- [Development](#development)
|
||||
- [API Reference](#api-reference)
|
||||
- [Contributing](#contributing)
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Basic Usage (5 minutes)
|
||||
|
||||
```bash
|
||||
# 1. Install dependencies
|
||||
pip install -r requirements.txt
|
||||
|
||||
# 2. Set your API key (choose your provider)
|
||||
export OPENAI_API_KEY="your-openai-key"
|
||||
# OR
|
||||
export ANTHROPIC_API_KEY="your-claude-key"
|
||||
# OR
|
||||
export GROK_API_KEY="your-grok-key"
|
||||
|
||||
# 3. Start the MCP server
|
||||
python run_mcp_server.py --transport sse
|
||||
|
||||
# 4. Run the client (in another terminal)
|
||||
python mcp_llm_client.py --provider openai --query "Calculate 25 squared"
|
||||
```
|
||||
|
||||
### Output
|
||||
```
|
||||
Connected to MCP server using SSE transport
|
||||
Processing query: 'Calculate 25 squared'
|
||||
Calling openai/gpt-4o with 24 available tools...
|
||||
Assistant wants to use 1 tool(s)
|
||||
1. Calling tool: power
|
||||
Arguments: {'kwargs': '{"base": 25, "exponent": 2}'}
|
||||
Result: 625
|
||||
Getting final response from openai/gpt-4o...
|
||||
Final answer: 25 squared is 625.
|
||||
```
|
||||
|
||||
## Introduction
|
||||
|
||||
### What is MCP?
|
||||
|
||||
**MCP (Model Context Protocol)** is an open standard for connecting AI applications to external systems and data sources. It enables AI models to:
|
||||
|
||||
- **Execute tools** (calculations, API calls, system commands)
|
||||
- **Access resources** (files, databases, configurations)
|
||||
- **Use prompts** (reusable conversation templates)
|
||||
|
||||
### Getting Started with MCP
|
||||
|
||||
For a basic introduction to MCP concepts, see the [`intro_test/`](./intro_test/) directory, which contains:
|
||||
|
||||
- **[Complete MCP Guide](./intro_test/INTRO_README.md)** - Comprehensive introduction to MCP concepts
|
||||
- **Basic Server & Client Examples** - Simple implementations using FastMCP
|
||||
- **Transport Protocol Examples** - SSE and STDIO transport implementations
|
||||
- **OpenAI Integration** - Basic MCP-OpenAI client example
|
||||
|
||||
> **New to MCP?** Start with [`intro_test/INTRO_README.md`](./intro_test/INTRO_README.md) for a complete beginner's guide.
|
||||
|
||||
## Architecture & Code Structure
|
||||
|
||||
```
|
||||
mcp_template/
|
||||
├── src/mcp_template/
|
||||
│ ├── core/ # Core MCP types and interfaces
|
||||
│ ├── llm_client/ # Multi-provider LLM client factory
|
||||
│ │ ├── base_client.py # Abstract base client
|
||||
│ │ ├── openai_client.py # OpenAI implementation
|
||||
│ │ ├── claude_client.py # Claude implementation
|
||||
│ │ ├── grok_client.py # Grok implementation
|
||||
│ │ └── client_factory.py # Factory for creating clients
|
||||
│ ├── server/
|
||||
│ │ ├── modular_server.py # Main MCP server
|
||||
│ │ ├── tools/ # Tool registration system
|
||||
│ │ ├── prompts/ # Prompt management
|
||||
│ │ └── resources/ # Resource management
|
||||
│ └── tools/ # Tool implementations
|
||||
│ ├── math_tools.py # Mathematical operations
|
||||
│ ├── text_tools.py # Text processing
|
||||
│ ├── system_tools.py # System utilities
|
||||
│ └── web_tools.py # Web utilities
|
||||
├── intro_test/ # Basic MCP examples
|
||||
├── mcp_llm_client.py # Multi-provider MCP client
|
||||
├── run_mcp_server.py # Server launcher
|
||||
└── config.py # Configuration management
|
||||
```
|
||||
|
||||
### Key Components
|
||||
|
||||
- **`modular_server.py`** - Main MCP server with auto-discovery
|
||||
- **`mcp_llm_client.py`** - Client supporting multiple AI providers
|
||||
- **`client_factory.py`** - Factory pattern for LLM client creation
|
||||
- **Tool Registry** - Automatic tool discovery and registration
|
||||
- **Transport Layer** - SSE and STDIO transport implementations
|
||||
|
||||
## Tools
|
||||
|
||||
Tools are executable functions that AI models can invoke to perform actions. The system includes **24 built-in tools** across multiple categories.
|
||||
|
||||
### Available Tool Categories
|
||||
|
||||
#### Mathematical Tools (`math_tools.py`)
|
||||
```python
|
||||
# Available functions: add, subtract, multiply, divide, power, square_root, calculate_bmi
|
||||
result = await add(a=10, b=5) # Returns: 15
|
||||
result = await power(base=5, exponent=3) # Returns: 125
|
||||
result = await square_root(number=144) # Returns: 12.0
|
||||
```
|
||||
|
||||
#### Text Processing Tools (`text_tools.py`)
|
||||
```python
|
||||
# Available functions: count_words, search_text, replace_text, to_uppercase, to_lowercase, text_length
|
||||
word_count = await count_words(text="Hello world") # Returns: 2
|
||||
uppercase = await to_uppercase(text="hello") # Returns: "HELLO"
|
||||
```
|
||||
|
||||
#### System Tools (`system_tools.py`)
|
||||
```python
|
||||
# Available functions: get_system_info, get_current_time, list_directory, get_file_info, get_environment_variable
|
||||
sys_info = await get_system_info() # Returns system information
|
||||
current_time = await get_current_time() # Returns: "2024-01-15 14:30:25"
|
||||
```
|
||||
|
||||
#### Web Tools (`web_tools.py`)
|
||||
```python
|
||||
# Available functions: url_encode, url_decode, parse_url, validate_email, extract_domain
|
||||
encoded = await url_encode(text="hello world") # Returns: "hello%20world"
|
||||
is_valid = await validate_email(email="user@example.com") # Returns: True
|
||||
```
|
||||
|
||||
### Creating Custom Tools
|
||||
|
||||
#### Method 1: Using MCPTool Class
|
||||
```python
|
||||
from mcp_template.core.types import MCPTool
|
||||
|
||||
async def custom_calculator(x: float, operation: str) -> float:
|
||||
"""Perform custom calculation"""
|
||||
if operation == "double":
|
||||
return x * 2
|
||||
elif operation == "half":
|
||||
return x / 2
|
||||
return x
|
||||
|
||||
tool = MCPTool(
|
||||
name="custom_calculator",
|
||||
description="Perform custom calculations",
|
||||
input_schema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"x": {"type": "number", "description": "Input number"},
|
||||
"operation": {
|
||||
"type": "string",
|
||||
"enum": ["double", "half"],
|
||||
"description": "Operation to perform"
|
||||
}
|
||||
},
|
||||
"required": ["x", "operation"]
|
||||
},
|
||||
handler=custom_calculator
|
||||
)
|
||||
```
|
||||
|
||||
#### Method 2: Using Tool Classes
|
||||
```python
|
||||
from mcp_template.tools.tool_registry import ServerToolRegistry
|
||||
|
||||
class CustomTools:
|
||||
@staticmethod
|
||||
def get_tools():
|
||||
return [
|
||||
CustomTools._create_custom_tool(),
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def _create_custom_tool():
|
||||
async def my_tool(param1: str, param2: int = 0) -> str:
|
||||
"""Description of my tool"""
|
||||
return f"Processed {param1} with {param2}"
|
||||
|
||||
return MCPTool(
|
||||
name="my_custom_tool",
|
||||
description="Custom tool description",
|
||||
input_schema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"param1": {"type": "string", "description": "First parameter"},
|
||||
"param2": {"type": "integer", "description": "Second parameter", "default": 0}
|
||||
},
|
||||
"required": ["param1"]
|
||||
},
|
||||
handler=my_tool
|
||||
)
|
||||
```
|
||||
|
||||
#### Method 3: FastMCP Integration
|
||||
```python
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
|
||||
mcp = FastMCP("MyServer")
|
||||
|
||||
@mcp.tool()
|
||||
async def my_fastmcp_tool(input: str) -> str:
|
||||
"""FastMCP tool with automatic schema generation"""
|
||||
return f"Processed: {input}"
|
||||
|
||||
# Tool is automatically registered with proper schema
|
||||
```
|
||||
|
||||
### Tool Registration
|
||||
|
||||
Tools are automatically discovered and registered:
|
||||
|
||||
```python
|
||||
# Automatic registration (recommended)
|
||||
from mcp_template.server.modular_server import create_default_server
|
||||
|
||||
server = create_default_server()
|
||||
await server.initialize() # Automatically discovers and registers all tools
|
||||
|
||||
# Manual registration
|
||||
from mcp_template.server.tools.tool_registry import ServerToolRegistry
|
||||
|
||||
registry = ServerToolRegistry()
|
||||
registry.register_tools(your_custom_tools)
|
||||
```
|
||||
|
||||
## Prompts
|
||||
|
||||
Prompts are reusable conversation templates that help AI models perform specific tasks.
|
||||
|
||||
### Available Prompts
|
||||
|
||||
#### Greeting Prompt
|
||||
```python
|
||||
from mcp_template.server.prompts.greeting_prompt import GreetingPrompt
|
||||
|
||||
prompt = GreetingPrompt()
|
||||
result = await prompt.generate(
|
||||
name="Alice",
|
||||
style="friendly",
|
||||
tone="warm",
|
||||
element="compliment"
|
||||
)
|
||||
# Returns: Template with substituted variables
|
||||
```
|
||||
|
||||
### Creating Custom Prompts
|
||||
|
||||
```python
|
||||
from mcp_template.server.prompts.base_prompt import BaseServerPrompt
|
||||
|
||||
class CodeReviewPrompt(BaseServerPrompt):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
name="code_review",
|
||||
description="Generate code review feedback",
|
||||
template="""
|
||||
Please review the following {language} code for:
|
||||
- Code quality and best practices
|
||||
- Potential bugs or security issues
|
||||
- Performance optimizations
|
||||
- Readability improvements
|
||||
|
||||
Code to review:
|
||||
{code}
|
||||
|
||||
Focus on: {focus_areas}
|
||||
""",
|
||||
arguments={
|
||||
"language": {
|
||||
"type": "string",
|
||||
"enum": ["python", "javascript", "java", "cpp"],
|
||||
"description": "Programming language"
|
||||
},
|
||||
"focus_areas": {
|
||||
"type": "string",
|
||||
"description": "Areas to focus the review on"
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
async def generate(self, code: str, language: str, focus_areas: str = "all") -> str:
|
||||
return self._substitute_template(
|
||||
code=code,
|
||||
language=language,
|
||||
focus_areas=focus_areas
|
||||
)
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
Resources represent data sources that AI models can access and read.
|
||||
|
||||
### Available Resources
|
||||
|
||||
#### Configuration Resource
|
||||
```python
|
||||
# URI: config://settings
|
||||
# Returns: Server configuration in JSON format
|
||||
{
|
||||
"server_name": "MCP Template Server",
|
||||
"version": "1.0.0",
|
||||
"features": ["tools", "prompts", "resources"],
|
||||
"transport": {"supported": ["stdio", "sse"], "default": "stdio"},
|
||||
"tools_count": 24,
|
||||
"prompts_count": 1,
|
||||
"resources_count": 2
|
||||
}
|
||||
```
|
||||
|
||||
#### Dynamic Greeting Resource
|
||||
```python
|
||||
# URI: dynamic://greeting
|
||||
# Returns: Personalized greeting with current timestamp
|
||||
```
|
||||
|
||||
### Creating Custom Resources
|
||||
|
||||
```python
|
||||
from mcp_template.server.resources.base_resource import BaseServerResource
|
||||
|
||||
class DatabaseResource(BaseServerResource):
|
||||
def __init__(self, db_path: str):
|
||||
super().__init__(
|
||||
uri=f"db://{db_path}",
|
||||
name="Database Connection",
|
||||
description=f"Access to database: {db_path}",
|
||||
mime_type="application/json"
|
||||
)
|
||||
self.db_path = db_path
|
||||
|
||||
async def read(self, query: str = None, **kwargs) -> str:
|
||||
"""Execute database query and return results"""
|
||||
# Implement database logic here
|
||||
import json
|
||||
|
||||
if query:
|
||||
# Execute specific query
|
||||
result = {"query": query, "result": "mock_data"}
|
||||
else:
|
||||
# Return database info
|
||||
result = {"database": self.db_path, "status": "connected"}
|
||||
|
||||
return json.dumps(result, indent=2)
|
||||
```
|
||||
|
||||
## Test Cases
|
||||
|
||||
Test cases should be written for each tool category to ensure proper functionality:
|
||||
|
||||
### Mathematical Tools Tests
|
||||
Should test basic arithmetic, power functions, square root calculations, and BMI calculations.
|
||||
|
||||
### Text Tools Tests
|
||||
Should test word counting, case conversion, text search, and text replacement functionality.
|
||||
|
||||
### System Tools Tests
|
||||
Should test system information retrieval, current time, and environment variable access.
|
||||
|
||||
### Web Tools Tests
|
||||
Should test URL encoding/decoding, email validation, and domain extraction.
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
Create a `.env` file in the project root:
|
||||
|
||||
```bash
|
||||
# OpenAI (default provider)
|
||||
OPENAI_API_KEY=sk-your-openai-key-here
|
||||
|
||||
# Claude (alternative provider)
|
||||
ANTHROPIC_API_KEY=sk-ant-your-claude-key-here
|
||||
|
||||
# Grok (alternative provider)
|
||||
GROK_API_KEY=xai-your-grok-key-here
|
||||
```
|
||||
|
||||
### Server Configuration
|
||||
|
||||
The server automatically discovers and registers:
|
||||
- **Tools**: All classes in `src/mcp_template/tools/`
|
||||
- **Prompts**: All classes in `src/mcp_template/server/prompts/`
|
||||
- **Resources**: All classes in `src/mcp_template/server/resources/`
|
||||
|
||||
## Running the System
|
||||
|
||||
### Transport Methods
|
||||
|
||||
#### 1. Server-Sent Events (SSE) - Recommended
|
||||
|
||||
**Start Server:**
|
||||
```bash
|
||||
python run_mcp_server.py --transport sse --port 8050
|
||||
```
|
||||
|
||||
**Run Client:**
|
||||
```bash
|
||||
# Interactive mode
|
||||
python mcp_llm_client.py --provider openai --model gpt-4o
|
||||
|
||||
# Single query
|
||||
python mcp_llm_client.py --provider openai --query "Calculate 25 + 30"
|
||||
|
||||
# With custom parameters
|
||||
python mcp_llm_client.py --provider openai --model gpt-4o --temperature 0.7 --max-tokens 1000
|
||||
```
|
||||
|
||||
#### 2. Standard Input/Output (STDIO)
|
||||
|
||||
**Note:** STDIO transport doesn't require starting a separate server. The client automatically launches the server process.
|
||||
|
||||
```bash
|
||||
# The client will automatically start the server
|
||||
python mcp_llm_client.py --transport stdio --provider openai --query "Calculate 10 * 5"
|
||||
```
|
||||
|
||||
### Advanced Usage
|
||||
|
||||
#### Multiple Providers
|
||||
```bash
|
||||
# OpenAI (default)
|
||||
python mcp_llm_client.py --provider openai --model gpt-4o
|
||||
|
||||
# Claude
|
||||
python mcp_llm_client.py --provider claude --model claude-3-opus-20240229
|
||||
|
||||
# Grok
|
||||
python mcp_llm_client.py --provider grok --model grok-1
|
||||
```
|
||||
|
||||
#### Custom Parameters
|
||||
```bash
|
||||
python mcp_llm_client.py \
|
||||
--provider openai \
|
||||
--model gpt-4o \
|
||||
--temperature 0.01 \
|
||||
--max-tokens 500 \
|
||||
--top-p 0.9 \
|
||||
--query "Explain quantum computing"
|
||||
```
|
||||
|
||||
#### Interactive Mode
|
||||
```bash
|
||||
python mcp_llm_client.py --provider openai --temperature 0.7
|
||||
|
||||
# Now you can have natural conversations:
|
||||
# Your query: Calculate 15 + 27
|
||||
# Response: 15 + 27 equals 42.
|
||||
#
|
||||
# Your query: What's the square root of 144?
|
||||
# Response: The square root of 144 is 12.
|
||||
#
|
||||
# Your query: Count words in "Hello beautiful world"
|
||||
# Response: The text contains 3 words.
|
||||
```
|
||||
|
||||
### Docker Usage (Future)
|
||||
|
||||
```bash
|
||||
# Build the image
|
||||
docker build -t mcp-template .
|
||||
|
||||
# Run with environment variables
|
||||
docker run -e OPENAI_API_KEY=your-key -p 8050:8050 mcp-template
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
### Adding New Tools
|
||||
|
||||
1. **Create tool file** in `src/mcp_template/tools/`
|
||||
2. **Implement tool class** with `get_tools()` method
|
||||
3. **Return MCPTool objects** with proper schemas
|
||||
4. **Tools are auto-discovered** - no registration needed
|
||||
|
||||
### Adding New Prompts
|
||||
|
||||
1. **Create prompt class** in `src/mcp_template/server/prompts/`
|
||||
2. **Extend BaseServerPrompt**
|
||||
3. **Implement template and arguments**
|
||||
4. **Prompts are auto-discovered**
|
||||
|
||||
### Adding New Resources
|
||||
|
||||
1. **Create resource class** in `src/mcp_template/server/resources/`
|
||||
2. **Extend BaseServerResource**
|
||||
3. **Implement read() method**
|
||||
4. **Resources are auto-discovered**
|
||||
|
||||
### Testing
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
python -m pytest tests/
|
||||
|
||||
# Run specific test file
|
||||
python -m pytest tests/unit/test_tools.py
|
||||
|
||||
# Run with coverage
|
||||
python -m pytest --cov=src tests/
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### MCPAIClient
|
||||
|
||||
```python
|
||||
class MCPAIClient:
|
||||
def __init__(
|
||||
self,
|
||||
model: str = "gpt-4o",
|
||||
transport: TransportType = TransportType.SSE,
|
||||
provider: str = "openai",
|
||||
temperature: float = 0.7,
|
||||
max_tokens: int = 1000,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
async def process_query(self, query: str) -> str
|
||||
async def get_mcp_tools(self) -> List[Dict[str, Any]]
|
||||
async def interactive_session(self)
|
||||
```
|
||||
|
||||
### AIClientFactory
|
||||
|
||||
```python
|
||||
class AIClientFactory:
|
||||
@staticmethod
|
||||
def create_client(provider: str, model_name: str, **kwargs) -> BaseAIClient
|
||||
|
||||
@staticmethod
|
||||
def create_openai_client(model_name: str = "gpt-4o", **kwargs) -> OpenAIClient
|
||||
|
||||
@staticmethod
|
||||
def create_claude_client(model_name: str = "claude-3-opus-20240229", **kwargs) -> ClaudeClient
|
||||
|
||||
@staticmethod
|
||||
def create_grok_client(model_name: str = "grok-1", **kwargs) -> GrokClient
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
1. **Fork the repository**
|
||||
2. **Create feature branch**: `git checkout -b feature/your-feature`
|
||||
3. **Add tests** for new functionality
|
||||
4. **Ensure tests pass**: `python -m pytest`
|
||||
5. **Submit pull request**
|
||||
|
||||
### Code Standards
|
||||
|
||||
- Use type hints for all function parameters and return values
|
||||
- Follow PEP 8 style guidelines
|
||||
- Add docstrings to all classes and methods
|
||||
- Include unit tests for new features
|
||||
- Update documentation for API changes
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
||||
|
||||
## Support
|
||||
|
||||
- **Documentation**: See [`intro_test/INTRO_README.md`](./intro_test/INTRO_README.md) for MCP basics
|
||||
- **Issues**: Report bugs on GitHub Issues
|
||||
- **Discussions**: Join discussions on GitHub Discussions
|
||||
- **Email**: Contact maintainers for support
|
||||
|
||||
---
|
||||
|
||||
**Built with using the Model Context Protocol (MCP)**
|
||||
|
||||
*Transforming AI applications with standardized tool integration*
|
||||
@@ -0,0 +1,53 @@
|
||||
README.md
|
||||
pyproject.toml
|
||||
src/__init__.py
|
||||
src/mcp_template/__init__.py
|
||||
src/mcp_template/mcp_client.py
|
||||
src/mcp_template.egg-info/PKG-INFO
|
||||
src/mcp_template.egg-info/SOURCES.txt
|
||||
src/mcp_template.egg-info/dependency_links.txt
|
||||
src/mcp_template.egg-info/requires.txt
|
||||
src/mcp_template.egg-info/top_level.txt
|
||||
src/mcp_template/config/__init__.py
|
||||
src/mcp_template/config/client_config.py
|
||||
src/mcp_template/config/config_manager.py
|
||||
src/mcp_template/config/server_config.py
|
||||
src/mcp_template/config/transport_config.py
|
||||
src/mcp_template/core/__init__.py
|
||||
src/mcp_template/core/interfaces.py
|
||||
src/mcp_template/core/types.py
|
||||
src/mcp_template/examples/__init__.py
|
||||
src/mcp_template/examples/server_examples.py
|
||||
src/mcp_template/llm_client/__init__.py
|
||||
src/mcp_template/llm_client/base_client.py
|
||||
src/mcp_template/llm_client/claude_client.py
|
||||
src/mcp_template/llm_client/client_factory.py
|
||||
src/mcp_template/llm_client/grok_client.py
|
||||
src/mcp_template/llm_client/openai_client.py
|
||||
src/mcp_template/server/__init__.py
|
||||
src/mcp_template/server/modular_server.py
|
||||
src/mcp_template/server/server_factory.py
|
||||
src/mcp_template/server/prompts/__init__.py
|
||||
src/mcp_template/server/prompts/base_prompt.py
|
||||
src/mcp_template/server/prompts/greeting_prompt.py
|
||||
src/mcp_template/server/prompts/prompt_registry.py
|
||||
src/mcp_template/server/resources/__init__.py
|
||||
src/mcp_template/server/resources/base_resource.py
|
||||
src/mcp_template/server/resources/config_resource.py
|
||||
src/mcp_template/server/resources/dynamic_resource.py
|
||||
src/mcp_template/server/resources/resource_registry.py
|
||||
src/mcp_template/server/tools/__init__.py
|
||||
src/mcp_template/server/tools/base_tool.py
|
||||
src/mcp_template/server/tools/calculator_tool.py
|
||||
src/mcp_template/server/tools/greeting_tool.py
|
||||
src/mcp_template/server/tools/tool_registry.py
|
||||
src/mcp_template/tools/__init__.py
|
||||
src/mcp_template/tools/math_tools.py
|
||||
src/mcp_template/tools/system_tools.py
|
||||
src/mcp_template/tools/text_tools.py
|
||||
src/mcp_template/tools/tool_registry.py
|
||||
src/mcp_template/tools/web_tools.py
|
||||
src/mcp_template/transport/__init__.py
|
||||
src/mcp_template/transport/sse_transport.py
|
||||
src/mcp_template/transport/stdio_transport.py
|
||||
src/mcp_template/transport/transport_manager.py
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
mcp[cli]>=1.14.0
|
||||
nest-asyncio>=1.6.0
|
||||
openai>=1.107.1
|
||||
psutil
|
||||
@@ -0,0 +1,2 @@
|
||||
__init__
|
||||
mcp_template
|
||||
@@ -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
|
||||
@@ -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'
|
||||
]
|
||||
@@ -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
|
||||
@@ -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,105 @@
|
||||
"""
|
||||
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,
|
||||
"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,174 @@
|
||||
"""
|
||||
Modular MCP Server Implementation
|
||||
"""
|
||||
import os
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def create_default_server(transport: str = "stdio", port: int = 8050) -> "ModularMCPServer":
|
||||
"""Create a default modular server with auto-discovery"""
|
||||
# Get the directory paths for tools, prompts, and resources
|
||||
# Start from the mcp_template package directory
|
||||
base_dir = os.path.dirname(os.path.dirname(__file__))
|
||||
tools_dir = os.path.join(base_dir, "tools")
|
||||
prompts_dir = os.path.join(base_dir, "server", "prompts")
|
||||
resources_dir = os.path.join(base_dir, "server", "resources")
|
||||
|
||||
server = ModularMCPServer(
|
||||
name="MCP Modular Server",
|
||||
host="0.0.0.0",
|
||||
port=port,
|
||||
stateless_http=True,
|
||||
tools_directory=tools_dir,
|
||||
prompts_directory=prompts_dir,
|
||||
resources_directory=resources_dir
|
||||
)
|
||||
return server
|
||||
|
||||
|
||||
def run_server(transport: str = "stdio") -> None:
|
||||
"""Run the modular MCP server with the specified transport method"""
|
||||
supported_transports = ["stdio", "sse", "streamable-http"]
|
||||
|
||||
if transport not in supported_transports:
|
||||
raise ValueError(f"Unknown transport: {transport}. Supported: {supported_transports}")
|
||||
|
||||
print(f" Starting MCP Modular Server with {transport.upper()} transport")
|
||||
|
||||
# Create and run the server
|
||||
server = create_default_server(transport)
|
||||
|
||||
if transport == "stdio":
|
||||
print(" Server ready for stdio communication")
|
||||
elif transport == "sse":
|
||||
print("Server ready for SSE communication on http://0.0.0.0:8050/sse")
|
||||
elif transport == "streamable-http":
|
||||
print("Server ready for Streamable HTTP communication")
|
||||
|
||||
# Run the server
|
||||
server.run(transport)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Run MCP Modular Server",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Examples:
|
||||
python modular_server.py # Run with stdio (default)
|
||||
python modular_server.py --transport stdio # Run with stdio explicitly
|
||||
python modular_server.py --transport sse # Run with SSE transport
|
||||
python modular_server.py --transport streamable-http # Run with Streamable HTTP
|
||||
|
||||
The server will automatically discover and register:
|
||||
- Tools from the tools directory
|
||||
- Prompts from the prompts directory
|
||||
- Resources from the resources directory
|
||||
"""
|
||||
)
|
||||
parser.add_argument(
|
||||
"--transport",
|
||||
choices=["stdio", "sse", "streamable-http"],
|
||||
default="stdio",
|
||||
help="Transport method to use (default: stdio)"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
run_server(args.transport)
|
||||
@@ -0,0 +1,7 @@
|
||||
"""
|
||||
Server Prompts Module
|
||||
"""
|
||||
from .base_prompt import BaseServerPrompt
|
||||
from .prompt_registry import ServerPromptRegistry
|
||||
|
||||
__all__ = ['BaseServerPrompt', 'ServerPromptRegistry']
|
||||
@@ -0,0 +1,47 @@
|
||||
"""
|
||||
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(name=self.name, description=self.description)
|
||||
async def prompt_wrapper(**kwargs):
|
||||
return await self.generate(**kwargs)
|
||||
|
||||
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 mcp_template.server.prompts.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,185 @@
|
||||
"""
|
||||
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 mcp_template.server.prompts.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)
|
||||
|
||||
# First, look for BaseServerPrompt subclasses (class-based approach)
|
||||
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}")
|
||||
|
||||
# Second, look for functions decorated with @mcp.prompt or ending with _prompt
|
||||
for name, obj in inspect.getmembers(module):
|
||||
if (inspect.isfunction(obj) and
|
||||
hasattr(obj, '__name__') and
|
||||
not name.startswith('_')):
|
||||
try:
|
||||
# Check if it's decorated with mcp.prompt or follows naming convention
|
||||
if (name.endswith('_prompt') or
|
||||
hasattr(obj, '__mcp_prompt__') or
|
||||
hasattr(obj, '__wrapped__')): # Often indicates decoration
|
||||
# Convert function to BaseServerPrompt wrapper
|
||||
wrapper = self._create_prompt_wrapper(obj, name)
|
||||
prompts.append(wrapper)
|
||||
self._registered_prompts[name] = wrapper
|
||||
except Exception as e:
|
||||
continue
|
||||
|
||||
# Third, look for classes with get_prompts() static method
|
||||
for name, obj in inspect.getmembers(module, inspect.isclass):
|
||||
if hasattr(obj, 'get_prompts') and callable(getattr(obj, 'get_prompts')):
|
||||
try:
|
||||
# Call get_prompts() to get prompt objects
|
||||
prompt_objects = obj.get_prompts()
|
||||
if isinstance(prompt_objects, list):
|
||||
for prompt in prompt_objects:
|
||||
if hasattr(prompt, 'name'):
|
||||
wrapper = self._create_prompt_wrapper_from_object(prompt)
|
||||
prompts.append(wrapper)
|
||||
self._registered_prompts[prompt.name] = wrapper
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not get prompts from {name}: {e}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not load prompt file {file_path}: {e}")
|
||||
|
||||
return prompts
|
||||
|
||||
def _create_prompt_wrapper(self, func, name: str) -> BaseServerPrompt:
|
||||
"""Convert a function to a BaseServerPrompt wrapper"""
|
||||
from mcp_template.server.prompts.base_prompt import BaseServerPrompt
|
||||
|
||||
class PromptWrapper(BaseServerPrompt):
|
||||
def __init__(self, func, name):
|
||||
# Extract arguments from function signature
|
||||
import inspect as insp
|
||||
sig = insp.signature(func)
|
||||
arguments = {}
|
||||
for param_name, param in sig.parameters.items():
|
||||
if param_name != 'self': # Skip self for methods
|
||||
arguments[param_name] = {
|
||||
"type": "string", # Default to string, could be enhanced
|
||||
"description": f"Parameter {param_name}"
|
||||
}
|
||||
|
||||
super().__init__(
|
||||
name=name,
|
||||
description=getattr(func, '__doc__', f'Prompt: {name}'),
|
||||
arguments=arguments
|
||||
)
|
||||
self._func = func
|
||||
|
||||
def get_messages(self, **kwargs):
|
||||
# Call the function with provided arguments
|
||||
return self._func(**kwargs)
|
||||
|
||||
return PromptWrapper(func, name)
|
||||
|
||||
def _create_prompt_wrapper_from_object(self, prompt_obj) -> BaseServerPrompt:
|
||||
"""Convert a prompt object to BaseServerPrompt wrapper"""
|
||||
from mcp_template.server.prompts.base_prompt import BaseServerPrompt
|
||||
|
||||
class PromptObjectWrapper(BaseServerPrompt):
|
||||
def __init__(self, prompt_obj):
|
||||
super().__init__(
|
||||
name=getattr(prompt_obj, 'name', 'unknown'),
|
||||
description=getattr(prompt_obj, 'description', ''),
|
||||
arguments=getattr(prompt_obj, 'arguments', {})
|
||||
)
|
||||
self._prompt_obj = prompt_obj
|
||||
|
||||
def get_messages(self, **kwargs):
|
||||
# Try to call the prompt object
|
||||
if hasattr(self._prompt_obj, 'get_messages'):
|
||||
return self._prompt_obj.get_messages(**kwargs)
|
||||
elif callable(self._prompt_obj):
|
||||
return self._prompt_obj(**kwargs)
|
||||
return []
|
||||
|
||||
return PromptObjectWrapper(prompt_obj)
|
||||
|
||||
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,37 @@
|
||||
"""
|
||||
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 read(self, **kwargs) -> Union[str, bytes]:
|
||||
"""Read the resource content"""
|
||||
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, name=self.name, description=self.description, mime_type=self.mime_type)
|
||||
async def resource_wrapper():
|
||||
return await self.read()
|
||||
|
||||
return resource_wrapper
|
||||
@@ -0,0 +1,37 @@
|
||||
"""
|
||||
Configuration Resource Example
|
||||
"""
|
||||
from mcp_template.server.resources.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 read(self, **kwargs) -> 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 mcp_template.server.resources.base_resource import BaseServerResource
|
||||
|
||||
|
||||
class DynamicResource(BaseServerResource):
|
||||
"""A dynamic resource that accepts parameters"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
uri="dynamic://greeting",
|
||||
name="Dynamic Greeting",
|
||||
description="Get a personalized greeting resource",
|
||||
mime_type="text/plain"
|
||||
)
|
||||
|
||||
async def read(self, name: str = "World") -> 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: {self.uri}\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,179 @@
|
||||
"""
|
||||
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 mcp_template.server.resources.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)
|
||||
|
||||
# First, look for BaseServerResource subclasses (class-based approach)
|
||||
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}")
|
||||
|
||||
# Second, look for functions decorated with @mcp.resource or ending with _resource
|
||||
for name, obj in inspect.getmembers(module):
|
||||
if (inspect.isfunction(obj) and
|
||||
hasattr(obj, '__name__') and
|
||||
not name.startswith('_')):
|
||||
try:
|
||||
# Check if it's decorated with mcp.resource or follows naming convention
|
||||
if (name.endswith('_resource') or
|
||||
hasattr(obj, '__mcp_resource__') or
|
||||
hasattr(obj, '__wrapped__')): # Often indicates decoration
|
||||
# Convert function to BaseServerResource wrapper
|
||||
wrapper = self._create_resource_wrapper(obj, name)
|
||||
resources.append(wrapper)
|
||||
self._registered_resources[wrapper.uri] = wrapper
|
||||
except Exception as e:
|
||||
continue
|
||||
|
||||
# Third, look for classes with get_resources() static method
|
||||
for name, obj in inspect.getmembers(module, inspect.isclass):
|
||||
if hasattr(obj, 'get_resources') and callable(getattr(obj, 'get_resources')):
|
||||
try:
|
||||
# Call get_resources() to get resource objects
|
||||
resource_objects = obj.get_resources()
|
||||
if isinstance(resource_objects, list):
|
||||
for resource in resource_objects:
|
||||
if hasattr(resource, 'uri'):
|
||||
wrapper = self._create_resource_wrapper_from_object(resource)
|
||||
resources.append(wrapper)
|
||||
self._registered_resources[resource.uri] = wrapper
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not get resources from {name}: {e}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not load resource file {file_path}: {e}")
|
||||
|
||||
return resources
|
||||
|
||||
def _create_resource_wrapper(self, func, name: str) -> BaseServerResource:
|
||||
"""Convert a function to a BaseServerResource wrapper"""
|
||||
from mcp_template.server.resources.base_resource import BaseServerResource
|
||||
|
||||
class ResourceWrapper(BaseServerResource):
|
||||
def __init__(self, func, name):
|
||||
# Create a URI pattern from the function name
|
||||
uri = f"resource://{name}"
|
||||
|
||||
super().__init__(
|
||||
uri=uri,
|
||||
name=name,
|
||||
description=getattr(func, '__doc__', f'Resource: {name}'),
|
||||
mime_type="text/plain" # Default, could be enhanced
|
||||
)
|
||||
self._func = func
|
||||
|
||||
async def read(self):
|
||||
# Call the function to get resource content
|
||||
return str(self._func())
|
||||
|
||||
return ResourceWrapper(func, name)
|
||||
|
||||
def _create_resource_wrapper_from_object(self, resource_obj) -> BaseServerResource:
|
||||
"""Convert a resource object to BaseServerResource wrapper"""
|
||||
from mcp_template.server.resources.base_resource import BaseServerResource
|
||||
|
||||
class ResourceObjectWrapper(BaseServerResource):
|
||||
def __init__(self, resource_obj):
|
||||
super().__init__(
|
||||
uri=getattr(resource_obj, 'uri', 'unknown://resource'),
|
||||
name=getattr(resource_obj, 'name', 'unknown'),
|
||||
description=getattr(resource_obj, 'description', ''),
|
||||
mime_type=getattr(resource_obj, 'mime_type', 'text/plain')
|
||||
)
|
||||
self._resource_obj = resource_obj
|
||||
|
||||
async def read(self, **kwargs):
|
||||
# Try to call the resource object's read method
|
||||
if hasattr(self._resource_obj, 'read'):
|
||||
return await self._resource_obj.read(**kwargs)
|
||||
elif callable(self._resource_obj):
|
||||
return str(self._resource_obj())
|
||||
return "Resource content not available"
|
||||
|
||||
return ResourceObjectWrapper(resource_obj)
|
||||
|
||||
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,59 @@
|
||||
"""
|
||||
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(name=self.name, description=self.description)
|
||||
async def tool_wrapper(**kwargs):
|
||||
return await self.execute(**kwargs)
|
||||
|
||||
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(name=self.name, description=self.description)
|
||||
async def tool_wrapper(ctx: Context[ServerSession, None], **kwargs):
|
||||
return await self.execute_with_context(ctx, **kwargs)
|
||||
|
||||
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,198 @@
|
||||
"""
|
||||
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 mcp_template.server.tools.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)
|
||||
|
||||
# First, look for BaseServerTool subclasses (class-based approach)
|
||||
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}")
|
||||
|
||||
# Second, look for static methods that return MCPTool objects (static approach)
|
||||
for name, obj in inspect.getmembers(module):
|
||||
if (inspect.isfunction(obj) and
|
||||
hasattr(obj, '__name__') and
|
||||
not name.startswith('_') and
|
||||
name.endswith('_tool')): # Convention: methods ending with _tool
|
||||
try:
|
||||
# Check if it's a static method by calling it
|
||||
if hasattr(obj, '__self__') and obj.__self__ is not None:
|
||||
continue # Skip instance methods
|
||||
|
||||
# Try to call the method to get MCPTool objects
|
||||
result = obj()
|
||||
if isinstance(result, list):
|
||||
# Method returns a list of MCPTool objects
|
||||
for tool in result:
|
||||
if hasattr(tool, 'name') and hasattr(tool, 'handler'):
|
||||
# Convert MCPTool to BaseServerTool wrapper
|
||||
wrapper = self._create_tool_wrapper(tool)
|
||||
tools.append(wrapper)
|
||||
self._registered_tools[tool.name] = wrapper
|
||||
elif hasattr(result, 'name') and hasattr(result, 'handler'):
|
||||
# Method returns a single MCPTool object
|
||||
wrapper = self._create_tool_wrapper(result)
|
||||
tools.append(wrapper)
|
||||
self._registered_tools[result.name] = wrapper
|
||||
|
||||
except Exception as e:
|
||||
# Skip methods that don't work as expected
|
||||
continue
|
||||
|
||||
# Third, look for classes with get_tools() static method
|
||||
for name, obj in inspect.getmembers(module, inspect.isclass):
|
||||
if hasattr(obj, 'get_tools') and callable(getattr(obj, 'get_tools')):
|
||||
try:
|
||||
# Call get_tools() to get MCPTool objects
|
||||
mcp_tools = obj.get_tools()
|
||||
if isinstance(mcp_tools, list):
|
||||
for tool in mcp_tools:
|
||||
if hasattr(tool, 'name') and hasattr(tool, 'handler'):
|
||||
wrapper = self._create_tool_wrapper(tool)
|
||||
tools.append(wrapper)
|
||||
self._registered_tools[tool.name] = wrapper
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not get tools from {name}: {e}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not load tool file {file_path}: {e}")
|
||||
|
||||
return tools
|
||||
|
||||
def _create_tool_wrapper(self, mcp_tool) -> BaseServerTool:
|
||||
"""Convert an MCPTool to a BaseServerTool wrapper"""
|
||||
from mcp_template.server.tools.base_tool import BaseServerTool
|
||||
|
||||
class MCPToolWrapper(BaseServerTool):
|
||||
def __init__(self, mcp_tool):
|
||||
super().__init__(
|
||||
name=mcp_tool.name,
|
||||
description=getattr(mcp_tool, 'description', ''),
|
||||
input_schema=getattr(mcp_tool, 'input_schema', {})
|
||||
)
|
||||
self._mcp_tool = mcp_tool
|
||||
|
||||
async def execute(self, **kwargs):
|
||||
# Handle different argument formats that OpenAI might send
|
||||
actual_kwargs = kwargs.copy()
|
||||
|
||||
# Check if this is FastMCP's kwargs format
|
||||
if len(kwargs) == 1 and 'kwargs' in kwargs:
|
||||
kwargs_value = kwargs['kwargs']
|
||||
if isinstance(kwargs_value, str):
|
||||
# Try to parse as JSON first
|
||||
try:
|
||||
import json
|
||||
parsed = json.loads(kwargs_value)
|
||||
if isinstance(parsed, dict):
|
||||
actual_kwargs = parsed
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
# If not JSON, try to map to expected parameters
|
||||
input_schema = getattr(self._mcp_tool, 'input_schema', {})
|
||||
properties = input_schema.get('properties', {})
|
||||
if properties and len(properties) == 1:
|
||||
param_name = list(properties.keys())[0]
|
||||
actual_kwargs = {param_name: kwargs_value}
|
||||
elif isinstance(kwargs_value, dict):
|
||||
# kwargs_value is already a dict
|
||||
actual_kwargs = kwargs_value
|
||||
|
||||
# Call the MCPTool handler with the processed arguments
|
||||
if hasattr(self._mcp_tool, 'handler') and callable(self._mcp_tool.handler):
|
||||
try:
|
||||
result = await self._mcp_tool.handler(**actual_kwargs)
|
||||
return result
|
||||
except TypeError as e:
|
||||
print(f"TypeError in tool {self.name}: {e}")
|
||||
print(f"Expected parameters: {list(getattr(self._mcp_tool, 'input_schema', {}).get('properties', {}).keys())}")
|
||||
print(f"Received: {list(actual_kwargs.keys())}")
|
||||
raise
|
||||
return None
|
||||
|
||||
return MCPToolWrapper(mcp_tool)
|
||||
|
||||
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()
|
||||
@@ -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']
|
||||
@@ -0,0 +1,192 @@
|
||||
"""
|
||||
Mathematical Tools for MCP
|
||||
"""
|
||||
from typing import List
|
||||
from mcp_template.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"""
|
||||
# Ensure number is a float
|
||||
try:
|
||||
number = float(number)
|
||||
except (ValueError, TypeError):
|
||||
raise ValueError(f"Invalid number format: {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 mcp_template.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,
|
||||
)
|
||||
@@ -0,0 +1,187 @@
|
||||
"""
|
||||
Text Processing Tools for MCP
|
||||
"""
|
||||
import re
|
||||
from typing import List
|
||||
from mcp_template.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 mcp_template.core.types import MCPTool
|
||||
from mcp_template.tools.math_tools import MathTools
|
||||
from mcp_template.tools.text_tools import TextTools
|
||||
from mcp_template.tools.system_tools import SystemTools
|
||||
from mcp_template.tools.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()
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
"""
|
||||
Web Tools for MCP
|
||||
"""
|
||||
import urllib.parse
|
||||
from typing import Dict, Any, List, Optional
|
||||
from mcp_template.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
|
||||
Reference in New Issue
Block a user