initial mcp server setup

This commit is contained in:
OwusuBlessing
2025-09-11 23:13:58 +01:00
commit 20f96c0f30
141 changed files with 14444 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
OPENAI_API_KEY=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
+44
View File
@@ -0,0 +1,44 @@
# Ignore virtual environment
venv/
groq_keys.json
cerebras_keys.json
# Ignore Python cache files
__pycache__/
# Ignore Jupyter Notebook checkpoints
.ipynb_checkpoints/
# Ignore log files
*.log
test.py
# Ignore data directory
data/
# Ignore all .docx and .pdf files
*.docx
*.pdf
test_keys.py
# Ignore all files in specs/ directory
specs/*
chronobid/*
# Except for scp_engr_doc.json in specs/ directory
!specs/scp_engr_doc.json
!specs/Aienergy.discipline.json
!specs/Aienergy.discipline.criteria.json
db.sqlite3
services/lab.html
env.json
api.json
.DS_Store
_venv
testing_grounds.py
claude_messages.json
.env.production
docs/flask_apispec
+1
View File
@@ -0,0 +1 @@
3.13
+7
View File
@@ -0,0 +1,7 @@
{
"python.testing.pytestArgs": [
"tests"
],
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true
}
+97
View File
@@ -0,0 +1,97 @@
# MCP Development Setup
This document explains how to use the MCP development server with different transport methods.
## Quick Start with MCP Dev Command
The easiest way to run the MCP server in development mode is using the dedicated dev file:
```bash
mcp dev dev_run.py
```
This will:
- ✅ Automatically start the server with SSE transport
- ✅ Run on port 3000 (standard dev port)
- ✅ Show the server URL: `http://0.0.0.0:3000/sse`
- ✅ Auto-discover and register tools, prompts, and resources
## Alternative Development Methods
### Using the Main Server Script
```bash
# Development mode (SSE on port 3000)
python run_mcp_server.py --dev
# Custom development setup
python run_mcp_server.py --transport sse --port 8080
```
### Manual Server Control
```bash
# Default stdio transport
python run_mcp_server.py
# SSE transport
python run_mcp_server.py --transport sse
# Custom port
python run_mcp_server.py --transport sse --port 8080
# Streamable HTTP
python run_mcp_server.py --transport streamable-http --port 9000
```
## Transport Methods
### 1. STDIO Transport (Default)
- **Use case**: Command-line tools, testing
- **Port**: Not applicable (uses stdio)
- **Command**: `python run_mcp_server.py --transport stdio`
### 2. SSE Transport
- **Use case**: Web applications, development
- **Default port**: 8050 (3000 in dev mode)
- **Command**: `python run_mcp_server.py --transport sse --port 8080`
### 3. Streamable HTTP Transport
- **Use case**: Production HTTP applications
- **Default port**: 8050
- **Command**: `python run_mcp_server.py --transport streamable-http --port 9000`
## MCP Dev Command Usage
The `dev_run.py` file is optimized for the MCP dev command:
1. **Automatic Discovery**: MCP dev finds the server object automatically
2. **Standard Port**: Uses port 3000 (industry standard for dev)
3. **SSE Transport**: Optimized for web development
4. **Clean Output**: Shows clear status messages and URLs
## Server Features
The MCP server automatically:
- 🔍 Discovers tools from `src/mcp_template/tools/`
- 📝 Discovers prompts from `src/mcp_template/server/prompts/`
- 📁 Discovers resources from `src/mcp_template/server/resources/`
- 🚀 Registers all components with the MCP server
- 🌐 Provides appropriate transport endpoints
## Troubleshooting
### MCP Dev Command Issues
- Make sure `dev_run.py` is in the project root
- Ensure the MCP CLI is properly installed
- Check that the `mcp` object is properly exposed
### Port Conflicts
- Change ports using `--port` parameter
- Common dev ports: 3000, 8080, 9000
- Check for running processes: `lsof -i :PORT`
### Import Errors
- Ensure you're running from the project root
- Check that all dependencies are installed
- Verify Python path includes the `src` directory
+221
View File
@@ -0,0 +1,221 @@
# MCP OpenAI Client with Flexible Transport
A powerful Python client that integrates OpenAI models with MCP (Model Context Protocol) servers, supporting both SSE and stdio transport methods.
## 🌟 Features
- **Flexible Transport**: Choose between SSE and stdio transport methods
- **OpenAI Integration**: Seamlessly use GPT models with MCP tools
- **Interactive Mode**: Command-line interface for natural conversation
- **Tool Execution**: Automatic tool calling based on user queries
- **Resource Support**: Access to MCP resources and prompts
- **Error Handling**: Robust error handling and connection management
## 🚀 Quick Start
### Prerequisites
1. **OpenAI API Key**: Set your OpenAI API key as an environment variable:
```bash
export OPENAI_API_KEY="your-api-key-here"
```
2. **MCP Server Running**: Make sure your MCP server is running:
```bash
# For SSE transport (recommended)
python run_mcp_server.py --transport sse
# For stdio transport
python run_mcp_server.py --transport stdio
```
### Basic Usage
#### Interactive Mode (Recommended)
```bash
# Default: SSE transport, GPT-4o model
python mcp_openai_client.py
# Stdio transport
python mcp_openai_client.py --transport stdio
# Different model
python mcp_openai_client.py --model gpt-3.5-turbo
```
#### Single Query Mode
```bash
# Process a single query and exit
python mcp_openai_client.py --query "Calculate 15 + 27"
# With different transport
python mcp_openai_client.py --transport stdio --query "What's the square root of 144?"
```
#### Examples and Demos
```bash
# Run example scripts
python example_usage.py
```
## 📖 Usage Examples
### SSE Transport (Default)
```bash
python mcp_openai_client.py
```
This will connect to `http://localhost:8050/sse` by default.
### Custom Server URL
```bash
python mcp_openai_client.py --server-url http://localhost:3000/sse
```
### Stdio Transport
```bash
python mcp_openai_client.py --transport stdio --server-script run_mcp_server.py
```
### Different OpenAI Models
```bash
# GPT-3.5 Turbo (faster, cheaper)
python mcp_openai_client.py --model gpt-3.5-turbo
# GPT-4 (more capable)
python mcp_openai_client.py --model gpt-4
# GPT-4o (recommended default)
python mcp_openai_client.py --model gpt-4o
```
## 🔧 Available Tools
The client automatically discovers and uses all available MCP tools. Current tools include:
### Math Tools
- `add` - Add two numbers
- `subtract` - Subtract numbers
- `multiply` - Multiply numbers
- `divide` - Divide numbers
- `power` - Calculate power
- `square_root` - Calculate square root
- `calculate_bmi` - Calculate BMI
### Text Tools
- `count_words` - Count words in text
- `search_text` - Search for patterns
- `replace_text` - Replace text patterns
- `to_uppercase` - Convert to uppercase
- `to_lowercase` - Convert to lowercase
- `text_length` - Get text length
### System Tools
- `get_system_info` - Get system information
- `get_current_time` - Get current time
- `list_directory` - List directory contents
- `get_file_info` - Get file information
- `get_environment_variable` - Get environment variables
### Web Tools
- `url_encode` - URL encode strings
- `url_decode` - URL decode strings
- `parse_url` - Parse URLs
- `validate_email` - Validate email addresses
- `extract_domain` - Extract domains
### Utility Tools
- `generate_greeting` - Generate personalized greetings
## 📋 Available Resources
- `config://settings` - Server configuration
- `dynamic://greeting` - Dynamic greeting resource
## 💬 Example Queries
Try these example queries to see the system in action:
1. **Math**: "What is 123 multiplied by 456?"
2. **Text**: "Count the words in this sentence and convert it to uppercase"
3. **System**: "What time is it right now?"
4. **File**: "List the contents of the current directory"
5. **Web**: "Validate if 'user@example.com' is a valid email address"
6. **Combined**: "Calculate the BMI for someone who is 5'10\" tall and weighs 160 pounds, then tell me what time it is"
## 🏗️ Architecture
The client follows this workflow:
1. **Connection**: Connects to MCP server using chosen transport (SSE/stdio)
2. **Discovery**: Discovers available tools, prompts, and resources
3. **Query Processing**: Sends user query to OpenAI with tool descriptions
4. **Tool Execution**: If OpenAI requests tool use, executes tools via MCP
5. **Response**: Returns final response incorporating tool results
## 🔧 Configuration
### Environment Variables
- `OPENAI_API_KEY` - Your OpenAI API key (required)
### Command Line Options
- `--transport` - Transport type: `sse` (default) or `stdio`
- `--model` - OpenAI model: `gpt-4o` (default), `gpt-4`, `gpt-3.5-turbo`
- `--server-url` - Server URL for SSE transport
- `--server-script` - Server script path for stdio transport
- `--query` - Single query to process
- `--verbose` - Enable verbose output
## 🚨 Troubleshooting
### Connection Issues
- **SSE Connection Failed**: Make sure the MCP server is running with SSE transport
- **Stdio Connection Failed**: Check that the server script path is correct
- **Port Issues**: Ensure the server is running on the expected port (8050)
### API Key Issues
- **Missing API Key**: Set the `OPENAI_API_KEY` environment variable
- **Invalid API Key**: Check that your OpenAI API key is valid and has credits
### Tool Execution Issues
- **Tool Not Found**: The requested tool might not be available on the server
- **Tool Execution Failed**: Check the server logs for detailed error information
## 📝 Development
### Adding New Tools
1. Add your tool to the appropriate tool file in `src/mcp_template/tools/`
2. Follow the existing pattern with proper error handling
3. Test the tool individually before integration
### Modifying Transport Logic
The client supports both SSE and stdio transports. To add new transports:
1. Add the transport type to the `TransportType` enum
2. Implement the connection method in `MCPOpenAIClient`
3. Update the `connect_to_server` method
## 📄 License
This project is part of the MCP Template system. See the main README for licensing information.
---
## 🎯 Next Steps
1. **Start the MCP Server**:
```bash
python run_mcp_server.py --transport sse
```
2. **Set your OpenAI API Key**:
```bash
export OPENAI_API_KEY="your-key-here"
```
3. **Run the Client**:
```bash
python mcp_openai_client.py
```
4. **Ask Questions**: Try queries like "Calculate 25 * 16" or "What tools do you have available?"
Enjoy using your MCP-powered OpenAI assistant! 🤖✨
+591
View File
@@ -0,0 +1,591 @@
# 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*
View File
+150
View File
@@ -0,0 +1,150 @@
"""
Base AI Client with common MCP integration functionality
"""
import json
import os
import sys
from typing import Any, Dict, List, Optional
from abc import ABC
# Add the project root to the path to import config
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
if project_root not in sys.path:
sys.path.insert(0, project_root)
try:
from config import Config
CONFIG_AVAILABLE = True
except ImportError:
CONFIG_AVAILABLE = False
from ..core.interfaces import IAIClient, IMCPClient
class BaseAIClient(IAIClient, ABC):
"""Base class for AI clients with MCP integration"""
def __init__(self, model_name: str, provider: str, api_key: Optional[str] = None, **kwargs):
self._model_name = model_name
self._provider = provider
self._client = None
self._initialized = False
self._extra_config = kwargs
# Get API key from config if not provided
if api_key is None and CONFIG_AVAILABLE:
api_key = self._get_api_key_from_config()
if not api_key:
raise ValueError(f"API key not provided and could not be loaded from config for provider: {provider}")
self._api_key = api_key
def _get_api_key_from_config(self) -> Optional[str]:
"""Get API key from config based on provider"""
if not CONFIG_AVAILABLE:
return None
provider_key_map = {
"openai": Config.OPENAI_API_KEY,
"claude": Config.CLAUDE_API_KEY,
"grok": Config.GROK_API_KEY
}
return provider_key_map.get(self._provider)
@property
def model_name(self) -> str:
"""Get the AI model name"""
return self._model_name
async def initialize(self) -> None:
"""Initialize the AI client - to be implemented by subclasses"""
if self._initialized:
return
await self._initialize_client()
self._initialized = True
async def _initialize_client(self) -> None:
"""Initialize the specific AI client - to be implemented by subclasses"""
pass
async def process_with_tools(
self,
query: str,
available_tools: List[Dict[str, Any]],
mcp_client: IMCPClient
) -> str:
"""Process a query with MCP tools using a common pattern"""
# Format tools for the specific AI provider
formatted_tools = self._format_tools_for_provider(available_tools)
# Create initial messages
messages = [{"role": "user", "content": query}]
# Get AI response with tool calling
response = await self.chat_completion(
messages=messages,
tools=formatted_tools,
tool_choice="auto"
)
# Extract assistant message
assistant_message = response["choices"][0]["message"]
# Check if tools were called
if "tool_calls" in assistant_message and assistant_message["tool_calls"]:
# Add assistant message to conversation
messages.append(assistant_message)
# Process each tool call
for tool_call in assistant_message["tool_calls"]:
try:
# Extract tool call details
tool_name = tool_call["function"]["name"]
tool_args = json.loads(tool_call["function"]["arguments"])
# Call the tool via MCP client
tool_result = await mcp_client.call_tool(tool_name, tool_args)
# Add tool response to conversation
messages.append({
"role": "tool",
"tool_call_id": tool_call["id"],
"content": str(tool_result),
})
except Exception as e:
# Handle tool call errors
messages.append({
"role": "tool",
"tool_call_id": tool_call["id"],
"content": f"Error calling tool: {str(e)}",
})
# Get final response from AI with tool results
final_response = await self.chat_completion(
messages=messages,
tools=formatted_tools,
tool_choice="none" # Don't allow more tool calls
)
return final_response["choices"][0]["message"]["content"]
# No tools called, return direct response
return assistant_message["content"]
def _format_tools_for_provider(self, tools: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""Format tools for the specific AI provider - to be implemented by subclasses"""
return tools
async def cleanup(self) -> None:
"""Clean up resources"""
if self._client:
await self._cleanup_client()
self._initialized = False
async def _cleanup_client(self) -> None:
"""Clean up the specific AI client - to be implemented by subclasses"""
pass
+5
View File
@@ -0,0 +1,5 @@
"""
MCP Template Source Package
"""
__version__ = "0.1.0"
@@ -0,0 +1,7 @@
# Configuration management
from .config_manager import ConfigManager
from .server_config import ServerConfig
from .client_config import ClientConfig
from .transport_config import TransportConfig
__all__ = ['ConfigManager', 'ServerConfig', 'ClientConfig', 'TransportConfig']
@@ -0,0 +1,70 @@
"""
Client Configuration
"""
from typing import Dict, Any, Optional
from dataclasses import dataclass
@dataclass
class ClientConfig:
"""Configuration class for MCP clients"""
provider: str = "openai"
model: str = "gpt-4o"
api_key: Optional[str] = None
# Model parameters
temperature: float = 0.7
max_tokens: int = 1000
top_p: float = 1.0
# Connection settings
timeout: int = 30
max_retries: int = 3
retry_delay: float = 1.0
# MCP-specific settings
enable_tool_calling: bool = True
enable_resource_access: bool = True
enable_prompts: bool = True
# Transport settings
transport_host: str = "localhost"
transport_port: int = 8050
transport_endpoint: str = "/sse"
@classmethod
def from_dict(cls, config_dict: Dict[str, Any]) -> 'ClientConfig':
"""Create ClientConfig from dictionary"""
return cls(**config_dict)
def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary"""
return self.__dict__.copy()
def get_provider_config(self) -> Dict[str, Any]:
"""Get provider-specific configuration"""
return {
"model_name": self.model,
"api_key": self.api_key,
"temperature": self.temperature,
"max_tokens": self.max_tokens,
"top_p": self.top_p,
"timeout": self.timeout,
"max_retries": self.max_retries,
"retry_delay": self.retry_delay,
}
def validate(self) -> bool:
"""Validate configuration"""
if not self.provider:
return False
if not self.model:
return False
if not self.api_key:
return False
if self.temperature < 0 or self.temperature > 2:
return False
if self.max_tokens < 1:
return False
return True
@@ -0,0 +1,133 @@
"""
Centralized Configuration Manager
"""
import os
import json
from typing import Dict, Any, Optional
from pathlib import Path
try:
from dotenv import load_dotenv
DOTENV_AVAILABLE = True
except ImportError:
DOTENV_AVAILABLE = False
class ConfigManager:
"""Centralized configuration manager for MCP components"""
def __init__(self, config_file: Optional[str] = None, env_file: Optional[str] = None):
self._config_file = config_file or "config.json"
self._env_file = env_file or ".env"
self._config: Dict[str, Any] = {}
self._loaded = False
async def load_config(self) -> Dict[str, Any]:
"""Load configuration from all sources"""
if self._loaded:
return self._config
# Load environment variables
await self._load_env_vars()
# Load JSON configuration file
await self._load_json_config()
# Merge configurations
self._config = await self._merge_configs()
self._loaded = True
return self._config
async def _load_env_vars(self) -> None:
"""Load environment variables"""
if DOTENV_AVAILABLE and Path(self._env_file).exists():
load_dotenv(self._env_file)
async def _load_json_config(self) -> Dict[str, Any]:
"""Load JSON configuration file"""
config_path = Path(self._config_file)
if config_path.exists():
with open(config_path, 'r') as f:
return json.load(f)
return {}
async def _merge_configs(self) -> Dict[str, Any]:
"""Merge all configuration sources"""
merged = {}
# Start with JSON config as base
json_config = await self._load_json_config()
merged.update(json_config)
# Override with environment variables
env_config = await self._get_env_config()
self._deep_update(merged, env_config)
return merged
async def _get_env_config(self) -> Dict[str, Any]:
"""Get configuration from environment variables"""
env_config = {}
# Server configuration
if os.getenv("MCP_SERVER_NAME"):
env_config.setdefault("server", {})["name"] = os.getenv("MCP_SERVER_NAME")
if os.getenv("MCP_SERVER_HOST"):
env_config.setdefault("server", {})["host"] = os.getenv("MCP_SERVER_HOST")
if os.getenv("MCP_SERVER_PORT"):
env_config.setdefault("server", {})["port"] = int(os.getenv("MCP_SERVER_PORT"))
if os.getenv("MCP_TRANSPORT"):
env_config.setdefault("server", {})["transport"] = os.getenv("MCP_TRANSPORT")
# Client configuration
if os.getenv("MCP_AI_PROVIDER"):
env_config.setdefault("client", {})["provider"] = os.getenv("MCP_AI_PROVIDER")
if os.getenv("MCP_AI_MODEL"):
env_config.setdefault("client", {})["model"] = os.getenv("MCP_AI_MODEL")
# API keys
for provider in ["OPENAI", "CLAUDE", "GROK"]:
api_key = os.getenv(f"{provider}_API_KEY")
if api_key:
env_config.setdefault("api_keys", {})[provider.lower()] = api_key
return env_config
def _deep_update(self, base_dict: Dict[str, Any], update_dict: Dict[str, Any]) -> None:
"""Deep update dictionary"""
for key, value in update_dict.items():
if isinstance(value, dict) and key in base_dict and isinstance(base_dict[key], dict):
self._deep_update(base_dict[key], value)
else:
base_dict[key] = value
async def get_server_config(self) -> Dict[str, Any]:
"""Get server-specific configuration"""
config = await self.load_config()
return config.get("server", {})
async def get_client_config(self) -> Dict[str, Any]:
"""Get client-specific configuration"""
config = await self.load_config()
return config.get("client", {})
async def get_api_key(self, provider: str) -> Optional[str]:
"""Get API key for a specific provider"""
config = await self.load_config()
api_keys = config.get("api_keys", {})
return api_keys.get(provider.lower())
async def save_config(self, config: Dict[str, Any]) -> None:
"""Save configuration to file"""
with open(self._config_file, 'w') as f:
json.dump(config, f, indent=2)
async def update_config(self, updates: Dict[str, Any]) -> None:
"""Update configuration and save"""
config = await self.load_config()
self._deep_update(config, updates)
await self.save_config(config)
self._loaded = False # Force reload on next access
@@ -0,0 +1,72 @@
"""
Server Configuration
"""
from typing import Dict, Any, Optional
from dataclasses import dataclass
from ..core.types import TransportType
@dataclass
class ServerConfig:
"""Configuration class for MCP servers"""
name: str = "MCP Server"
version: str = "1.0.0"
transport: TransportType = TransportType.STDIO
host: str = "0.0.0.0"
port: int = 8050
stateless_http: bool = True
# Tool configurations
enable_default_tools: bool = True
custom_tools: Optional[Dict[str, Any]] = None
# Resource configurations
enable_file_resources: bool = False
resource_paths: Optional[list[str]] = None
# Prompt configurations
enable_default_prompts: bool = False
custom_prompts: Optional[Dict[str, Any]] = None
# Performance settings
max_concurrent_requests: int = 10
request_timeout: int = 30
def __post_init__(self):
"""Initialize optional fields"""
if self.custom_tools is None:
self.custom_tools = {}
if self.resource_paths is None:
self.resource_paths = []
if self.custom_prompts is None:
self.custom_prompts = {}
@classmethod
def from_dict(cls, config_dict: Dict[str, Any]) -> 'ServerConfig':
"""Create ServerConfig from dictionary"""
# Convert transport string to enum
if 'transport' in config_dict and isinstance(config_dict['transport'], str):
config_dict['transport'] = TransportType(config_dict['transport'].lower())
return cls(**config_dict)
def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary"""
result = self.__dict__.copy()
result['transport'] = self.transport.value
return result
def get_transport_config(self) -> Dict[str, Any]:
"""Get transport-specific configuration"""
if self.transport == TransportType.SSE:
return {
"host": self.host,
"port": self.port,
"endpoint": "/sse",
}
elif self.transport == TransportType.STDIO:
return {}
else:
return {}
@@ -0,0 +1,89 @@
"""
Transport Configuration
"""
from typing import Dict, Any, Optional
from dataclasses import dataclass
from ..core.types import TransportType
@dataclass
class TransportConfig:
"""Configuration class for MCP transport"""
transport_type: TransportType = TransportType.STDIO
# SSE-specific settings
sse_host: str = "localhost"
sse_port: int = 8050
sse_endpoint: str = "/sse"
sse_timeout: int = 30
sse_reconnect_delay: float = 1.0
sse_max_reconnects: int = 5
# STDIO-specific settings
stdio_command: Optional[str] = None
stdio_args: Optional[list[str]] = None
stdio_env: Optional[Dict[str, str]] = None
stdio_cwd: Optional[str] = None
# General transport settings
buffer_size: int = 8192
encoding: str = "utf-8"
enable_compression: bool = False
enable_ssl: bool = False
@classmethod
def from_dict(cls, config_dict: Dict[str, Any]) -> 'TransportConfig':
"""Create TransportConfig from dictionary"""
# Convert transport string to enum
if 'transport_type' in config_dict and isinstance(config_dict['transport_type'], str):
config_dict['transport_type'] = TransportType(config_dict['transport_type'].lower())
return cls(**config_dict)
def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary"""
result = self.__dict__.copy()
result['transport_type'] = self.transport_type.value
return result
def get_sse_config(self) -> Dict[str, Any]:
"""Get SSE-specific configuration"""
return {
"host": self.sse_host,
"port": self.sse_port,
"endpoint": self.sse_endpoint,
"timeout": self.sse_timeout,
"reconnect_delay": self.sse_reconnect_delay,
"max_reconnects": self.sse_max_reconnects,
}
def get_stdio_config(self) -> Dict[str, Any]:
"""Get STDIO-specific configuration"""
config = {}
if self.stdio_command:
config["command"] = self.stdio_command
if self.stdio_args:
config["args"] = self.stdio_args
if self.stdio_env:
config["env"] = self.stdio_env
if self.stdio_cwd:
config["cwd"] = self.stdio_cwd
return config
def get_transport_config(self) -> Dict[str, Any]:
"""Get transport configuration based on type"""
config = {
"buffer_size": self.buffer_size,
"encoding": self.encoding,
"enable_compression": self.enable_compression,
"enable_ssl": self.enable_ssl,
}
if self.transport_type == TransportType.SSE:
config.update(self.get_sse_config())
elif self.transport_type == TransportType.STDIO:
config.update(self.get_stdio_config())
return config
+13
View File
@@ -0,0 +1,13 @@
# Core MCP abstractions and base classes
from .types import MCPTool, MCPResource, MCPPrompt, MCPServerConfig
from .interfaces import IMCPServer, IMCPClient, IMCPTransport
__all__ = [
'MCPTool',
'MCPResource',
'MCPPrompt',
'MCPServerConfig',
'IMCPServer',
'IMCPClient',
'IMCPTransport'
]
+181
View File
@@ -0,0 +1,181 @@
"""
Core MCP Interfaces and Abstract Base Classes
"""
from abc import ABC, abstractmethod
from typing import Any, Dict, List, Optional, Protocol
from contextlib import asynccontextmanager
from .types import MCPTool, MCPResource, MCPPrompt, MCPServerConfig
class IMCPServer(ABC):
"""Abstract base class for MCP servers"""
@abstractmethod
async def initialize(self) -> None:
"""Initialize the MCP server"""
pass
@abstractmethod
async def register_tool(self, tool: MCPTool) -> None:
"""Register a tool with the server"""
pass
@abstractmethod
async def register_resource(self, resource: MCPResource) -> None:
"""Register a resource with the server"""
pass
@abstractmethod
async def register_prompt(self, prompt: MCPPrompt) -> None:
"""Register a prompt with the server"""
pass
@abstractmethod
async def list_tools(self) -> List[Dict[str, Any]]:
"""List all available tools"""
pass
@abstractmethod
async def list_resources(self) -> List[Dict[str, Any]]:
"""List all available resources"""
pass
@abstractmethod
async def list_prompts(self) -> List[Dict[str, Any]]:
"""List all available prompts"""
pass
@abstractmethod
async def call_tool(self, name: str, arguments: Dict[str, Any]) -> Any:
"""Call a tool by name with arguments"""
pass
@abstractmethod
async def read_resource(self, uri: str) -> Any:
"""Read a resource by URI"""
pass
@abstractmethod
async def get_prompt(self, name: str, arguments: Optional[Dict[str, Any]] = None) -> str:
"""Get a prompt by name with optional arguments"""
pass
@abstractmethod
async def start(self) -> None:
"""Start the MCP server"""
pass
@abstractmethod
async def stop(self) -> None:
"""Stop the MCP server"""
pass
class IMCPClient(ABC):
"""Abstract base class for MCP clients"""
@abstractmethod
async def connect(self) -> None:
"""Connect to MCP server"""
pass
@abstractmethod
async def disconnect(self) -> None:
"""Disconnect from MCP server"""
pass
@abstractmethod
async def list_tools(self) -> List[Dict[str, Any]]:
"""List available tools from server"""
pass
@abstractmethod
async def list_resources(self) -> List[Dict[str, Any]]:
"""List available resources from server"""
pass
@abstractmethod
async def list_prompts(self) -> List[Dict[str, Any]]:
"""List available prompts from server"""
pass
@abstractmethod
async def call_tool(self, name: str, arguments: Dict[str, Any]) -> Any:
"""Call a tool on the server"""
pass
@abstractmethod
async def read_resource(self, uri: str) -> Any:
"""Read a resource from the server"""
pass
@abstractmethod
async def get_prompt(self, name: str, arguments: Optional[Dict[str, Any]] = None) -> str:
"""Get a prompt from the server"""
pass
class IMCPTransport(ABC):
"""Abstract base class for MCP transport mechanisms"""
@abstractmethod
@asynccontextmanager
async def connect(self):
"""Establish transport connection"""
pass
@abstractmethod
async def send_message(self, message: Dict[str, Any]) -> None:
"""Send a message through the transport"""
pass
@abstractmethod
async def receive_message(self) -> Dict[str, Any]:
"""Receive a message through the transport"""
pass
@abstractmethod
async def close(self) -> None:
"""Close the transport connection"""
pass
class IAIClient(ABC):
"""Abstract base class for AI model clients"""
@property
@abstractmethod
def model_name(self) -> str:
"""Get the AI model name"""
pass
@abstractmethod
async def initialize(self) -> None:
"""Initialize the AI client"""
pass
@abstractmethod
async def chat_completion(
self,
messages: List[Dict[str, Any]],
tools: Optional[List[Dict[str, Any]]] = None,
**kwargs
) -> Dict[str, Any]:
"""Perform a chat completion with optional tools"""
pass
@abstractmethod
async def process_with_tools(
self,
query: str,
available_tools: List[Dict[str, Any]],
mcp_client: IMCPClient
) -> str:
"""Process a query with MCP tools"""
pass
@abstractmethod
async def cleanup(self) -> None:
"""Clean up resources"""
pass
+79
View File
@@ -0,0 +1,79 @@
"""
Core MCP Types and Data Structures
"""
from typing import Any, Dict, List, Optional, Protocol, Union
from abc import ABC, abstractmethod
from dataclasses import dataclass
from enum import Enum
class TransportType(Enum):
"""Supported MCP transport types"""
SSE = "sse"
STDIO = "stdio"
@dataclass
class MCPTool:
"""Represents an MCP tool that can be called by AI models"""
name: str
description: str
input_schema: Dict[str, Any]
handler: callable
def __post_init__(self):
"""Validate tool configuration"""
if not self.name or not self.description:
raise ValueError("Tool name and description are required")
@dataclass
class MCPResource:
"""Represents an MCP resource that can be read by clients"""
uri: str
name: str
description: str
mime_type: str
content: Union[str, bytes]
def __post_init__(self):
"""Validate resource configuration"""
if not self.uri or not self.name:
raise ValueError("Resource URI and name are required")
@dataclass
class MCPPrompt:
"""Represents an MCP prompt template"""
name: str
description: str
template: str
arguments: Optional[Dict[str, Any]] = None
def __post_init__(self):
"""Validate prompt configuration"""
if not self.name or not self.template:
raise ValueError("Prompt name and template are required")
@dataclass
class MCPServerConfig:
"""Configuration for MCP server"""
name: str
version: str = "1.0.0"
transport: TransportType = TransportType.STDIO
host: str = "0.0.0.0"
port: int = 8050
tools: List[MCPTool] = None
resources: List[MCPResource] = None
prompts: List[MCPPrompt] = None
stateless_http: bool = True
def __post_init__(self):
"""Initialize empty lists if not provided"""
if self.tools is None:
self.tools = []
if self.resources is None:
self.resources = []
if self.prompts is None:
self.prompts = []
@@ -0,0 +1,6 @@
# MCP Template Examples
from .server_examples import ServerExamples
from .client_examples import ClientExamples
from .integration_examples import IntegrationExamples
__all__ = ['ServerExamples', 'ClientExamples', 'IntegrationExamples']
@@ -0,0 +1,174 @@
"""
Server Examples showing how to use the modular MCP template
"""
import asyncio
from typing import List
from ..core.types import TransportType
from ..server.server_factory import MCPServerFactory
from ..tools.tool_registry import ToolRegistry
from ..resources.data_resources import DataResources
class ServerExamples:
"""Examples of creating different types of MCP servers"""
@staticmethod
async def create_math_server():
"""Create a server with only math tools"""
print("🧮 Creating Math Server...")
# Use tool registry to get math tools
registry = ToolRegistry()
math_tools = registry.get_tools_by_category('math')
server = MCPServerFactory.create_server(
name="Math Server",
transport=TransportType.STDIO,
tools=math_tools
)
print(f"✅ Math Server created with {len(math_tools)} tools")
return server
@staticmethod
async def create_developer_server():
"""Create a comprehensive developer server"""
print("👨‍💻 Creating Developer Server...")
registry = ToolRegistry()
tools = registry.get_tools_by_categories(['math', 'text', 'system'])
# Add data resources
resources = DataResources.get_resources()
server = MCPServerFactory.create_server(
name="Developer Server",
transport=TransportType.SSE,
host="localhost",
port=8050,
tools=tools,
resources=resources
)
print(f"✅ Developer Server created with {len(tools)} tools and {len(resources)} resources")
return server
@staticmethod
async def create_business_server():
"""Create a business-focused server"""
print("💼 Creating Business Server...")
registry = ToolRegistry()
# Add custom business tools
async def calculate_roi(initial_investment: float, final_value: float) -> str:
"""Calculate Return on Investment"""
if initial_investment <= 0:
raise ValueError("Initial investment must be positive")
roi = ((final_value - initial_investment) / initial_investment) * 100
return ".2f"
async def format_currency(amount: float, currency: str = "USD") -> str:
"""Format amount as currency"""
return ",.2f"
from ..core.types import MCPTool
business_tools = [
MCPTool(
name="calculate_roi",
description="Calculate Return on Investment percentage",
input_schema={
"type": "object",
"properties": {
"initial_investment": {"type": "number", "description": "Initial investment amount"},
"final_value": {"type": "number", "description": "Final value amount"},
},
"required": ["initial_investment", "final_value"],
},
handler=calculate_roi,
),
MCPTool(
name="format_currency",
description="Format a number as currency",
input_schema={
"type": "object",
"properties": {
"amount": {"type": "number", "description": "Amount to format"},
"currency": {"type": "string", "description": "Currency code", "default": "USD"},
},
"required": ["amount"],
},
handler=format_currency,
),
]
# Add to registry
registry.add_custom_tools(business_tools)
# Get business-relevant tools
all_tools = registry.get_tools_by_categories(['math', 'text'])
all_tools.extend(business_tools)
server = MCPServerFactory.create_server(
name="Business Server",
transport=TransportType.STDIO,
tools=all_tools
)
print(f"✅ Business Server created with {len(all_tools)} tools")
return server
@staticmethod
async def create_custom_server_with_config():
"""Create a server using configuration"""
print("⚙️ Creating Custom Server with Configuration...")
config = {
"name": "Custom Config Server",
"transport": "sse",
"host": "localhost",
"port": 8080,
"tools": ["math", "text"], # Tool categories to include
"enable_resources": True,
}
registry = ToolRegistry()
tools = registry.get_tools_by_categories(config["tools"])
resources = DataResources.get_resources() if config.get("enable_resources") else []
server = MCPServerFactory.create_server(
name=config["name"],
transport=config["transport"],
host=config["host"],
port=config["port"],
tools=tools,
resources=resources
)
print(f"✅ Custom Server created with configuration")
print(f" - Tools: {len(tools)}")
print(f" - Resources: {len(resources)}")
print(f" - Transport: {config['transport']}")
return server
@staticmethod
async def demo_server_lifecycle():
"""Demonstrate complete server lifecycle"""
print("🔄 Server Lifecycle Demo")
# Create server
server = await ServerExamples.create_math_server()
# List available tools
tools = await server.list_tools()
print(f"📋 Available tools: {[t['name'] for t in tools]}")
# Test a tool
try:
result = await server.call_tool("add", {"a": 10, "b": 5})
print(f"🧮 Tool test - add(10, 5) = {result}")
except Exception as e:
print(f"❌ Tool test failed: {e}")
print("✅ Server lifecycle demo completed")
return server
@@ -0,0 +1,14 @@
# AI Client implementations
from .base_client import BaseAIClient
from .openai_client import OpenAIClient
from .claude_client import ClaudeClient
from .grok_client import GrokClient
from .client_factory import AIClientFactory
__all__ = [
'BaseAIClient',
'OpenAIClient',
'ClaudeClient',
'GrokClient',
'AIClientFactory'
]
@@ -0,0 +1,150 @@
"""
Base AI Client with common MCP integration functionality
"""
import json
import os
import sys
from typing import Any, Dict, List, Optional
from abc import ABC
# Add the project root to the path to import config
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
if project_root not in sys.path:
sys.path.insert(0, project_root)
try:
from config import Config
CONFIG_AVAILABLE = True
except ImportError:
CONFIG_AVAILABLE = False
from ..core.interfaces import IAIClient, IMCPClient
class BaseAIClient(IAIClient, ABC):
"""Base class for AI clients with MCP integration"""
def __init__(self, model_name: str, provider: str, api_key: Optional[str] = None, **kwargs):
self._model_name = model_name
self._provider = provider
self._client = None
self._initialized = False
self._extra_config = kwargs
# Get API key from config if not provided
if api_key is None and CONFIG_AVAILABLE:
api_key = self._get_api_key_from_config()
if not api_key:
raise ValueError(f"API key not provided and could not be loaded from config for provider: {provider}")
self._api_key = api_key
def _get_api_key_from_config(self) -> Optional[str]:
"""Get API key from config based on provider"""
if not CONFIG_AVAILABLE:
return None
provider_key_map = {
"openai": Config.OPENAI_API_KEY,
"claude": Config.CLAUDE_API_KEY,
"grok": Config.GROK_API_KEY
}
return provider_key_map.get(self._provider)
@property
def model_name(self) -> str:
"""Get the AI model name"""
return self._model_name
async def initialize(self) -> None:
"""Initialize the AI client - to be implemented by subclasses"""
if self._initialized:
return
await self._initialize_client()
self._initialized = True
async def _initialize_client(self) -> None:
"""Initialize the specific AI client - to be implemented by subclasses"""
pass
async def process_with_tools(
self,
query: str,
available_tools: List[Dict[str, Any]],
mcp_client: IMCPClient
) -> str:
"""Process a query with MCP tools using a common pattern"""
# Format tools for the specific AI provider
formatted_tools = self._format_tools_for_provider(available_tools)
# Create initial messages
messages = [{"role": "user", "content": query}]
# Get AI response with tool calling
response = await self.chat_completion(
messages=messages,
tools=formatted_tools,
tool_choice="auto"
)
# Extract assistant message
assistant_message = response["choices"][0]["message"]
# Check if tools were called
if "tool_calls" in assistant_message and assistant_message["tool_calls"]:
# Add assistant message to conversation
messages.append(assistant_message)
# Process each tool call
for tool_call in assistant_message["tool_calls"]:
try:
# Extract tool call details
tool_name = tool_call["function"]["name"]
tool_args = json.loads(tool_call["function"]["arguments"])
# Call the tool via MCP client
tool_result = await mcp_client.call_tool(tool_name, tool_args)
# Add tool response to conversation
messages.append({
"role": "tool",
"tool_call_id": tool_call["id"],
"content": str(tool_result),
})
except Exception as e:
# Handle tool call errors
messages.append({
"role": "tool",
"tool_call_id": tool_call["id"],
"content": f"Error calling tool: {str(e)}",
})
# Get final response from AI with tool results
final_response = await self.chat_completion(
messages=messages,
tools=formatted_tools,
tool_choice="none" # Don't allow more tool calls
)
return final_response["choices"][0]["message"]["content"]
# No tools called, return direct response
return assistant_message["content"]
def _format_tools_for_provider(self, tools: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""Format tools for the specific AI provider - to be implemented by subclasses"""
return tools
async def cleanup(self) -> None:
"""Clean up resources"""
if self._client:
await self._cleanup_client()
self._initialized = False
async def _cleanup_client(self) -> None:
"""Clean up the specific AI client - to be implemented by subclasses"""
pass
@@ -0,0 +1,56 @@
"""
Claude Client Implementation (Placeholder)
"""
from typing import Any, Dict, List, Optional
# Placeholder for Claude/Anthropic client
# This would need the actual Anthropic SDK when implemented
from .base_client import BaseAIClient
class ClaudeClient(BaseAIClient):
"""Claude client with MCP integration (Placeholder Implementation)"""
def __init__(
self,
model_name: str = "claude-3-opus-20240229",
api_key: Optional[str] = None,
**kwargs
):
# Note: This is a placeholder. You'll need to install the Anthropic SDK
# pip install anthropic
super().__init__(model_name, "claude", api_key, **kwargs)
# Claude specific configuration
self._temperature = kwargs.get("temperature", 0.7)
self._max_tokens = kwargs.get("max_tokens", 1000)
async def _initialize_client(self) -> None:
"""Initialize the Claude client"""
# TODO: Implement with actual Anthropic SDK
# self._client = Anthropic(api_key=self._api_key)
raise NotImplementedError("Claude client not yet implemented. Install Anthropic SDK and implement.")
async def chat_completion(
self,
messages: List[Dict[str, Any]],
tools: Optional[List[Dict[str, Any]]] = None,
**kwargs
) -> Dict[str, Any]:
"""Perform Claude chat completion"""
if not self._initialized:
await self.initialize()
# TODO: Implement Claude API call
raise NotImplementedError("Claude chat completion not yet implemented")
def _format_tools_for_provider(self, tools: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""Format tools for Claude's expected format"""
# TODO: Implement Claude tool formatting
return tools
async def _cleanup_client(self) -> None:
"""Clean up Claude client"""
# TODO: Implement cleanup
pass
@@ -0,0 +1,104 @@
"""
AI Client Factory for easy client creation and management
"""
import os
import sys
from typing import Optional, Dict, Any
# Add the project root to the path to import config
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
if project_root not in sys.path:
sys.path.insert(0, project_root)
try:
from config import Config
CONFIG_AVAILABLE = True
except ImportError:
CONFIG_AVAILABLE = False
from .base_client import BaseAIClient
from .openai_client import OpenAIClient
from .claude_client import ClaudeClient
from .grok_client import GrokClient
class AIClientFactory:
"""Factory class for creating AI clients with different providers"""
@staticmethod
def create_client(
provider: str,
model_name: Optional[str] = None,
api_key: Optional[str] = None,
**kwargs
) -> BaseAIClient:
"""Create an AI client for the specified provider"""
# Set default model names if not provided
if model_name is None:
if provider.lower() == "openai":
model_name = "gpt-4o"
elif provider.lower() == "claude":
model_name = "claude-3-opus-20240229"
elif provider.lower() == "grok":
model_name = "grok-1"
# Get API key from config if not provided
if api_key is None and CONFIG_AVAILABLE:
provider_key_map = {
"openai": Config.OPENAI_API_KEY,
"claude": Config.CLAUDE_API_KEY,
"grok": Config.GROK_API_KEY
}
api_key = provider_key_map.get(provider.lower())
if not api_key:
raise ValueError(f"API key not provided and could not be loaded from config for provider: {provider}")
# Create the appropriate client
provider_lower = provider.lower()
if provider_lower == "openai":
return OpenAIClient(model_name, api_key, **kwargs)
elif provider_lower == "claude":
return ClaudeClient(model_name, api_key, **kwargs)
elif provider_lower == "grok":
return GrokClient(model_name, api_key, **kwargs)
else:
raise ValueError(f"Unsupported AI provider: {provider}")
@staticmethod
def create_openai_client(
model_name: str = "gpt-4o",
api_key: Optional[str] = None,
**kwargs
) -> OpenAIClient:
"""Create an OpenAI client"""
return AIClientFactory.create_client("openai", model_name, api_key, **kwargs)
@staticmethod
def create_claude_client(
model_name: str = "claude-3-opus-20240229",
api_key: Optional[str] = None,
**kwargs
) -> ClaudeClient:
"""Create a Claude client"""
return AIClientFactory.create_client("claude", model_name, api_key, **kwargs)
@staticmethod
def create_grok_client(
model_name: str = "grok-1",
api_key: Optional[str] = None,
**kwargs
) -> GrokClient:
"""Create a Grok client"""
return AIClientFactory.create_client("grok", model_name, api_key, **kwargs)
@staticmethod
def get_available_providers() -> list[str]:
"""Get list of available AI providers"""
return ["openai", "claude", "grok"]
@staticmethod
def validate_provider(provider: str) -> bool:
"""Validate if a provider is supported"""
return provider.lower() in AIClientFactory.get_available_providers()
@@ -0,0 +1,55 @@
"""
Grok Client Implementation (Placeholder)
"""
from typing import Any, Dict, List, Optional
# Placeholder for Grok/xAI client
# This would need the xAI SDK or direct API integration when implemented
from .base_client import BaseAIClient
class GrokClient(BaseAIClient):
"""Grok client with MCP integration (Placeholder Implementation)"""
def __init__(
self,
model_name: str = "grok-1",
api_key: Optional[str] = None,
**kwargs
):
# Note: This is a placeholder. You'll need xAI API integration
super().__init__(model_name, "grok", api_key, **kwargs)
# Grok specific configuration
self._temperature = kwargs.get("temperature", 0.7)
self._max_tokens = kwargs.get("max_tokens", 1000)
async def _initialize_client(self) -> None:
"""Initialize the Grok client"""
# TODO: Implement with xAI API or SDK
# This might require direct HTTP calls to xAI API
raise NotImplementedError("Grok client not yet implemented. Implement xAI API integration.")
async def chat_completion(
self,
messages: List[Dict[str, Any]],
tools: Optional[List[Dict[str, Any]]] = None,
**kwargs
) -> Dict[str, Any]:
"""Perform Grok chat completion"""
if not self._initialized:
await self.initialize()
# TODO: Implement Grok API call
raise NotImplementedError("Grok chat completion not yet implemented")
def _format_tools_for_provider(self, tools: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""Format tools for Grok's expected format"""
# TODO: Implement Grok tool formatting
return tools
async def _cleanup_client(self) -> None:
"""Clean up Grok client"""
# TODO: Implement cleanup
pass
@@ -0,0 +1,106 @@
"""
OpenAI Client Implementation
"""
from typing import Any, Dict, List, Optional
try:
from openai import AsyncOpenAI
OPENAI_AVAILABLE = True
except ImportError:
OPENAI_AVAILABLE = False
from .base_client import BaseAIClient
from config import Config
class OpenAIClient(BaseAIClient):
"""OpenAI client with MCP integration"""
def __init__(
self,
model_name: str = "gpt-4o",
api_key: Optional[str] = None,
**kwargs
):
if not OPENAI_AVAILABLE:
raise ImportError("OpenAI package not installed. Install with: pip install openai")
super().__init__(model_name, "openai", api_key, **kwargs)
# OpenAI specific configuration
self._temperature = kwargs.get("temperature", 0.7)
self._max_tokens = kwargs.get("max_tokens", 1000)
self._api_key = api_key or Config.OPENAI_API_KEY
async def _initialize_client(self) -> None:
"""Initialize the OpenAI client"""
self._client = AsyncOpenAI(api_key=self._api_key)
async def chat_completion(
self,
messages: List[Dict[str, Any]],
tools: Optional[List[Dict[str, Any]]] = None,
**kwargs
) -> Dict[str, Any]:
"""Perform OpenAI chat completion"""
if not self._initialized:
await self.initialize()
# Prepare request parameters
request_params = {
"model": self._model_name,
"messages": messages,
"temperature": self._temperature,
"max_tokens": self._max_tokens,
}
# Add tools if provided
if tools:
request_params["tools"] = tools
request_params["tool_choice"] = kwargs.get("tool_choice", "auto")
# Make the API call
response = await self._client.chat.completions.create(**request_params)
# Convert to standard format
return {
"choices": [
{
"message": {
"role": choice.message.role,
"content": choice.message.content,
"tool_calls": [
{
"id": tool_call.id,
"type": "function",
"function": {
"name": tool_call.function.name,
"arguments": tool_call.function.arguments,
}
}
for tool_call in (choice.message.tool_calls or [])
] if choice.message.tool_calls else None,
}
}
for choice in response.choices
]
}
def _format_tools_for_provider(self, tools: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""Format tools for OpenAI's expected format"""
formatted_tools = []
for tool in tools:
formatted_tool = {
"type": "function",
"function": {
"name": tool["name"],
"description": tool["description"],
"parameters": tool["inputSchema"],
}
}
formatted_tools.append(formatted_tool)
return formatted_tools
async def _cleanup_client(self) -> None:
"""Clean up OpenAI client"""
if self._client:
await self._client.close()
@@ -0,0 +1,5 @@
# MCP Server implementations
from .modular_server import ModularMCPServer
from .server_factory import MCPServerFactory
__all__ = ['ModularMCPServer', 'MCPServerFactory']
@@ -0,0 +1,99 @@
"""
Modular MCP Server Implementation
"""
from mcp.server.fastmcp import FastMCP
from typing import Optional, List
from .tools.tool_registry import ServerToolRegistry
from .prompts.prompt_registry import ServerPromptRegistry
from .resources.resource_registry import ServerResourceRegistry
class ModularMCPServer:
"""Modular MCP Server that automatically discovers and registers tools, prompts, and resources"""
def __init__(
self,
name: str,
host: str = "0.0.0.0",
port: int = 8050,
stateless_http: bool = True,
tools_directory: Optional[str] = None,
prompts_directory: Optional[str] = None,
resources_directory: Optional[str] = None
):
self.name = name
self.host = host
self.port = port
self.stateless_http = stateless_http
# Initialize registries
self.tool_registry = ServerToolRegistry(tools_directory)
self.prompt_registry = ServerPromptRegistry(prompts_directory)
self.resource_registry = ServerResourceRegistry(resources_directory)
# Create FastMCP server
self.mcp = FastMCP(
name=name,
host=host,
port=port,
stateless_http=stateless_http
)
self._initialized = False
async def initialize(self) -> None:
"""Initialize the server and register all components"""
if self._initialized:
return
print(f"Initializing {self.name} server...")
# Discover and register tools
print("Discovering tools...")
self.tool_registry.register_tools_with_server(self.mcp)
tool_count = len(self.tool_registry.get_all_tools())
print(f"Registered {tool_count} tools")
# Discover and register prompts
print("Discovering prompts...")
self.prompt_registry.register_prompts_with_server(self.mcp)
prompt_count = len(self.prompt_registry.get_all_prompts())
print(f"Registered {prompt_count} prompts")
# Discover and register resources
print("Discovering resources...")
self.resource_registry.register_resources_with_server(self.mcp)
resource_count = len(self.resource_registry.get_all_resources())
print(f"Registered {resource_count} resources")
self._initialized = True
print(f"Server initialization complete!")
def run(self, transport: str = "stdio") -> None:
"""Run the server with the specified transport"""
if not self._initialized:
import asyncio
asyncio.run(self.initialize())
print(f"Starting {self.name} server with {transport} transport...")
self.mcp.run(transport=transport)
def get_server_info(self) -> dict:
"""Get information about the server and its components"""
return {
"name": self.name,
"host": self.host,
"port": self.port,
"tools": {
"count": len(self.tool_registry.get_all_tools()),
"names": self.tool_registry.get_tool_names()
},
"prompts": {
"count": len(self.prompt_registry.get_all_prompts()),
"names": self.prompt_registry.get_prompt_names()
},
"resources": {
"count": len(self.resource_registry.get_all_resources()),
"uris": self.resource_registry.get_resource_uris()
}
}
@@ -0,0 +1,7 @@
"""
Server Prompts Module
"""
from .base_prompt import BaseServerPrompt
from .prompt_registry import ServerPromptRegistry
__all__ = ['BaseServerPrompt', 'ServerPromptRegistry']
@@ -0,0 +1,51 @@
"""
Base Server Prompt Class
"""
from abc import ABC, abstractmethod
from typing import Any, Dict, Optional
class BaseServerPrompt(ABC):
"""Base class for server prompts that can be registered with FastMCP"""
def __init__(self, name: str, description: str, template: str, arguments: Optional[Dict[str, Any]] = None):
self.name = name
self.description = description
self.template = template
self.arguments = arguments or {}
@abstractmethod
async def generate(self, **kwargs) -> str:
"""Generate the prompt with the provided arguments"""
pass
def get_prompt_definition(self) -> Dict[str, Any]:
"""Get the prompt definition for FastMCP registration"""
return {
"name": self.name,
"description": self.description,
"arguments": self.arguments
}
def create_fastmcp_prompt(self, mcp_server):
"""Create a FastMCP prompt decorator for this prompt"""
@mcp_server.prompt()
async def prompt_wrapper(**kwargs):
return await self.generate(**kwargs)
# Set metadata
prompt_wrapper.__name__ = self.name
prompt_wrapper.__doc__ = self.description
return prompt_wrapper
def _substitute_template(self, **kwargs) -> str:
"""Helper method to substitute variables in template"""
result = self.template
# Apply provided arguments
for key, value in kwargs.items():
placeholder = f"{{{key}}}"
result = result.replace(placeholder, str(value))
return result
@@ -0,0 +1,44 @@
"""
Greeting Prompt Example
"""
from .base_prompt import BaseServerPrompt
class GreetingPrompt(BaseServerPrompt):
"""A greeting prompt template"""
def __init__(self):
super().__init__(
name="greeting_prompt",
description="Generate a greeting prompt for AI models",
template="Please write a {style} greeting for someone named {name}. The greeting should be {tone} and include a {element}.",
arguments={
"style": {
"type": "string",
"enum": ["friendly", "formal", "casual"],
"description": "Style of greeting",
"default": "friendly"
},
"tone": {
"type": "string",
"enum": ["warm", "professional", "relaxed"],
"description": "Tone of the greeting",
"default": "warm"
},
"element": {
"type": "string",
"enum": ["compliment", "question", "observation"],
"description": "Element to include in greeting",
"default": "compliment"
}
}
)
async def generate(self, name: str, style: str = "friendly", tone: str = "warm", element: str = "compliment") -> str:
"""Generate the greeting prompt"""
return self._substitute_template(
name=name,
style=style,
tone=tone,
element=element
)
@@ -0,0 +1,100 @@
"""
Server Prompt Registry for dynamic prompt registration
"""
import os
import importlib
import inspect
from typing import List, Dict, Any, Type, Optional
from pathlib import Path
from .base_prompt import BaseServerPrompt
class ServerPromptRegistry:
"""Registry for managing server prompts from files"""
def __init__(self, prompts_directory: str = None):
self.prompts_directory = prompts_directory or os.path.dirname(__file__)
self._registered_prompts: Dict[str, BaseServerPrompt] = {}
self._prompt_files: List[str] = []
@property
def directory(self):
"""Alias for prompts_directory for backward compatibility"""
return self.prompts_directory
@property
def _prompts(self):
"""Alias for _registered_prompts for backward compatibility"""
return self._registered_prompts
def discover_prompts(self) -> List[BaseServerPrompt]:
"""Discover all prompts in the prompts directory"""
prompts = []
prompts_dir = Path(self.prompts_directory)
if not prompts_dir.exists():
return prompts
# Find all Python files in the prompts directory
for file_path in prompts_dir.glob("*.py"):
if file_path.name.startswith("__"):
continue
module_name = file_path.stem
try:
# Import the module
spec = importlib.util.spec_from_file_location(module_name, file_path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
# Find all BaseServerPrompt subclasses in the module
for name, obj in inspect.getmembers(module, inspect.isclass):
if (issubclass(obj, BaseServerPrompt) and
obj != BaseServerPrompt and
not inspect.isabstract(obj)):
try:
prompt_instance = obj()
prompts.append(prompt_instance)
self._registered_prompts[prompt_instance.name] = prompt_instance
except Exception as e:
print(f"Warning: Could not instantiate prompt {name}: {e}")
except Exception as e:
print(f"Warning: Could not load prompt file {file_path}: {e}")
return prompts
def register_prompt(self, prompt: BaseServerPrompt) -> None:
"""Register a single prompt"""
self._registered_prompts[prompt.name] = prompt
def register_prompts(self, prompts: List[BaseServerPrompt]) -> None:
"""Register multiple prompts"""
for prompt in prompts:
self.register_prompt(prompt)
def get_prompt(self, name: str) -> Optional[BaseServerPrompt]:
"""Get a prompt by name"""
return self._registered_prompts.get(name)
def get_all_prompts(self) -> List[BaseServerPrompt]:
"""Get all registered prompts"""
return list(self._registered_prompts.values())
def get_prompt_names(self) -> List[str]:
"""Get all registered prompt names"""
return list(self._registered_prompts.keys())
def register_prompts_with_server(self, mcp_server) -> None:
"""Register all discovered prompts with a FastMCP server"""
# First discover prompts if not already done
if not self._registered_prompts:
self.discover_prompts()
# Register each prompt with the server
for prompt in self._registered_prompts.values():
prompt.create_fastmcp_prompt(mcp_server)
def clear_prompts(self) -> None:
"""Clear all registered prompts"""
self._registered_prompts.clear()
@@ -0,0 +1,7 @@
"""
Server Resources Module
"""
from .base_resource import BaseServerResource
from .resource_registry import ServerResourceRegistry
__all__ = ['BaseServerResource', 'ServerResourceRegistry']
@@ -0,0 +1,41 @@
"""
Base Server Resource Class
"""
from abc import ABC, abstractmethod
from typing import Any, Dict, Optional, Union
class BaseServerResource(ABC):
"""Base class for server resources that can be registered with FastMCP"""
def __init__(self, uri: str, name: str, description: str, mime_type: str = "text/plain"):
self.uri = uri
self.name = name
self.description = description
self.mime_type = mime_type
@abstractmethod
async def get_content(self, **kwargs) -> Union[str, bytes]:
"""Get the resource content with the provided arguments"""
pass
def get_resource_definition(self) -> Dict[str, Any]:
"""Get the resource definition for FastMCP registration"""
return {
"uri": self.uri,
"name": self.name,
"description": self.description,
"mime_type": self.mime_type
}
def create_fastmcp_resource(self, mcp_server):
"""Create a FastMCP resource decorator for this resource"""
@mcp_server.resource(self.uri)
async def resource_wrapper(**kwargs):
return await self.get_content(**kwargs)
# Set metadata
resource_wrapper.__name__ = self.name
resource_wrapper.__doc__ = self.description
return resource_wrapper
@@ -0,0 +1,37 @@
"""
Configuration Resource Example
"""
from .base_resource import BaseServerResource
class ConfigResource(BaseServerResource):
"""A configuration resource that provides server settings"""
def __init__(self):
super().__init__(
uri="config://settings",
name="Server Configuration",
description="Get the current server configuration settings",
mime_type="application/json"
)
async def get_content(self) -> str:
"""Get the configuration content"""
import json
config = {
"server_name": "MCP Template Server",
"version": "1.0.0",
"features": [
"tools",
"prompts",
"resources"
],
"transport": {
"supported": ["stdio", "sse"],
"default": "stdio"
},
"tools_count": 2,
"prompts_count": 1,
"resources_count": 1
}
return json.dumps(config, indent=2)
@@ -0,0 +1,28 @@
"""
Dynamic Resource Example
"""
from .base_resource import BaseServerResource
class DynamicResource(BaseServerResource):
"""A dynamic resource that accepts parameters"""
def __init__(self):
super().__init__(
uri="dynamic://greeting/{name}",
name="Dynamic Greeting",
description="Get a personalized greeting resource",
mime_type="text/plain"
)
async def get_content(self, name: str) -> str:
"""Get the dynamic greeting content"""
return f"Hello, {name}! This is a dynamic resource that was generated just for you.\n\n" \
f"Resource URI: dynamic://greeting/{name}\n" \
f"Generated at: {self._get_timestamp()}\n" \
f"Personalized for: {name}"
def _get_timestamp(self) -> str:
"""Get current timestamp"""
from datetime import datetime
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
@@ -0,0 +1,100 @@
"""
Server Resource Registry for dynamic resource registration
"""
import os
import importlib
import inspect
from typing import List, Dict, Any, Type,Optional
from pathlib import Path
from .base_resource import BaseServerResource
class ServerResourceRegistry:
"""Registry for managing server resources from files"""
def __init__(self, resources_directory: str = None):
self.resources_directory = resources_directory or os.path.dirname(__file__)
self._registered_resources: Dict[str, BaseServerResource] = {}
self._resource_files: List[str] = []
@property
def directory(self):
"""Alias for resources_directory for backward compatibility"""
return self.resources_directory
@property
def _resources(self):
"""Alias for _registered_resources for backward compatibility"""
return self._registered_resources
def discover_resources(self) -> List[BaseServerResource]:
"""Discover all resources in the resources directory"""
resources = []
resources_dir = Path(self.resources_directory)
if not resources_dir.exists():
return resources
# Find all Python files in the resources directory
for file_path in resources_dir.glob("*.py"):
if file_path.name.startswith("__"):
continue
module_name = file_path.stem
try:
# Import the module
spec = importlib.util.spec_from_file_location(module_name, file_path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
# Find all BaseServerResource subclasses in the module
for name, obj in inspect.getmembers(module, inspect.isclass):
if (issubclass(obj, BaseServerResource) and
obj != BaseServerResource and
not inspect.isabstract(obj)):
try:
resource_instance = obj()
resources.append(resource_instance)
self._registered_resources[resource_instance.uri] = resource_instance
except Exception as e:
print(f"Warning: Could not instantiate resource {name}: {e}")
except Exception as e:
print(f"Warning: Could not load resource file {file_path}: {e}")
return resources
def register_resource(self, resource: BaseServerResource) -> None:
"""Register a single resource"""
self._registered_resources[resource.uri] = resource
def register_resources(self, resources: List[BaseServerResource]) -> None:
"""Register multiple resources"""
for resource in resources:
self.register_resource(resource)
def get_resource(self, uri: str) -> Optional[BaseServerResource]:
"""Get a resource by URI"""
return self._registered_resources.get(uri)
def get_all_resources(self) -> List[BaseServerResource]:
"""Get all registered resources"""
return list(self._registered_resources.values())
def get_resource_uris(self) -> List[str]:
"""Get all registered resource URIs"""
return list(self._registered_resources.keys())
def register_resources_with_server(self, mcp_server) -> None:
"""Register all discovered resources with a FastMCP server"""
# First discover resources if not already done
if not self._registered_resources:
self.discover_resources()
# Register each resource with the server
for resource in self._registered_resources.values():
resource.create_fastmcp_resource(mcp_server)
def clear_resources(self) -> None:
"""Clear all registered resources"""
self._registered_resources.clear()
@@ -0,0 +1,258 @@
"""
MCP Server Factory for easy server creation and configuration
"""
from typing import List, Optional, Union
from ..core.types import MCPServerConfig, MCPTool, MCPResource, MCPPrompt, TransportType
from .modular_server import ModularMCPServer
from ..core.interfaces import IMCPServer
from ..tools.tool_registry import ToolRegistry
class MCPServerFactory:
"""Factory class for creating MCP servers with different configurations"""
@staticmethod
def create_server(
name: str,
version: str = "1.0.0",
transport: Union[str, TransportType] = TransportType.STDIO,
host: str = "0.0.0.0",
port: int = 8050,
stateless_http: bool = True,
tools: Optional[List[MCPTool]] = None,
resources: Optional[List[MCPResource]] = None,
prompts: Optional[List[MCPPrompt]] = None,
) -> IMCPServer:
"""Create an MCP server with the specified configuration"""
# Convert string transport to enum if needed
if isinstance(transport, str):
transport = TransportType(transport.lower())
config = MCPServerConfig(
name=name,
version=version,
transport=transport,
host=host,
port=port,
stateless_http=stateless_http,
tools=tools or [],
resources=resources or [],
prompts=prompts or [],
)
return ModularMCPServer(
name=config.name,
host=config.host,
port=config.port,
stateless_http=config.stateless_http
)
@staticmethod
def create_basic_calculator_server(
name: str = "CalculatorServer",
transport: Union[str, TransportType] = TransportType.STDIO,
host: str = "0.0.0.0",
port: int = 8050,
) -> IMCPServer:
"""Create a basic calculator server with common math tools"""
async def add(a: float, b: float) -> float:
"""Add two numbers together"""
return a + b
async def subtract(a: float, b: float) -> float:
"""Subtract b from a"""
return a - b
async def multiply(a: float, b: float) -> float:
"""Multiply two numbers"""
return a * b
async def divide(a: float, b: float) -> float:
"""Divide a by b"""
if b == 0:
raise ValueError("Cannot divide by zero")
return a / b
tools = [
MCPTool(
name="add",
description="Add two numbers together",
input_schema={
"type": "object",
"properties": {
"a": {"type": "number", "description": "First number"},
"b": {"type": "number", "description": "Second number"},
},
"required": ["a", "b"],
},
handler=add,
),
MCPTool(
name="subtract",
description="Subtract second number from first",
input_schema={
"type": "object",
"properties": {
"a": {"type": "number", "description": "First number"},
"b": {"type": "number", "description": "Number to subtract"},
},
"required": ["a", "b"],
},
handler=subtract,
),
MCPTool(
name="multiply",
description="Multiply two numbers",
input_schema={
"type": "object",
"properties": {
"a": {"type": "number", "description": "First number"},
"b": {"type": "number", "description": "Second number"},
},
"required": ["a", "b"],
},
handler=multiply,
),
MCPTool(
name="divide",
description="Divide first number by second",
input_schema={
"type": "object",
"properties": {
"a": {"type": "number", "description": "Dividend"},
"b": {"type": "number", "description": "Divisor (cannot be zero)"},
},
"required": ["a", "b"],
},
handler=divide,
),
]
return MCPServerFactory.create_server(
name=name,
transport=transport,
host=host,
port=port,
tools=tools,
)
@staticmethod
def create_knowledge_base_server(
name: str = "KnowledgeBaseServer",
transport: Union[str, TransportType] = TransportType.STDIO,
host: str = "0.0.0.0",
port: int = 8050,
kb_data: Optional[dict] = None,
) -> IMCPServer:
"""Create a knowledge base server with configurable data"""
if kb_data is None:
kb_data = {
"company_policy": "Default company policy information...",
"faq": "Frequently asked questions and answers...",
}
async def get_knowledge_base() -> str:
"""Retrieve the entire knowledge base as formatted string"""
formatted = "Knowledge Base:\n\n"
for key, value in kb_data.items():
formatted += f"**{key.replace('_', ' ').title()}:**\n{value}\n\n"
return formatted
async def search_kb(query: str) -> str:
"""Search the knowledge base for relevant information"""
query_lower = query.lower()
results = []
for key, value in kb_data.items():
if query_lower in key.lower() or query_lower in value.lower():
results.append(f"**{key.replace('_', ' ').title()}:**\n{value}")
if not results:
return f"No information found for query: {query}"
return "\n\n".join(results)
tools = [
MCPTool(
name="get_knowledge_base",
description="Retrieve the entire knowledge base as a formatted string",
input_schema={"type": "object", "properties": {}},
handler=get_knowledge_base,
),
MCPTool(
name="search_kb",
description="Search the knowledge base for relevant information",
input_schema={
"type": "object",
"properties": {
"query": {"type": "string", "description": "Search query"},
},
"required": ["query"],
},
handler=search_kb,
),
]
return MCPServerFactory.create_server(
name=name,
transport=transport,
host=host,
port=port,
tools=tools,
)
@staticmethod
def create_server_from_categories(
name: str,
categories: List[str],
transport: Union[str, TransportType] = TransportType.STDIO,
host: str = "0.0.0.0",
port: int = 8050,
custom_tools: Optional[List[MCPTool]] = None,
resources: Optional[List[MCPResource]] = None,
prompts: Optional[List[MCPPrompt]] = None,
) -> IMCPServer:
"""Create a server using tool categories"""
# Get tools from categories
registry = ToolRegistry()
category_tools = registry.get_tools_by_categories(categories)
# Combine with custom tools
all_tools = category_tools
if custom_tools:
all_tools.extend(custom_tools)
return MCPServerFactory.create_server(
name=name,
transport=transport,
host=host,
port=port,
tools=all_tools,
resources=resources,
prompts=prompts,
)
@staticmethod
def create_modular_server(
name: str = "ModularServer",
host: str = "0.0.0.0",
port: int = 8050,
stateless_http: bool = True,
tools_directory: Optional[str] = None,
prompts_directory: Optional[str] = None,
resources_directory: Optional[str] = None,
) -> ModularMCPServer:
"""Create a modular server that auto-discovers tools, prompts, and resources"""
return ModularMCPServer(
name=name,
host=host,
port=port,
stateless_http=stateless_http,
tools_directory=tools_directory,
prompts_directory=prompts_directory,
resources_directory=resources_directory,
)
@@ -0,0 +1,7 @@
"""
Server Tools Module
"""
from .base_tool import BaseServerTool
from .tool_registry import ServerToolRegistry
__all__ = ['BaseServerTool', 'ServerToolRegistry']
@@ -0,0 +1,67 @@
"""
Base Server Tool Class
"""
from abc import ABC, abstractmethod
from typing import Any, Dict, Optional
from mcp.server.fastmcp import Context
from mcp.server.session import ServerSession
class BaseServerTool(ABC):
"""Base class for server tools that can be registered with FastMCP"""
def __init__(self, name: str, description: str, input_schema: Dict[str, Any]):
self.name = name
self.description = description
self.input_schema = input_schema
@abstractmethod
async def execute(self, **kwargs) -> Any:
"""Execute the tool with the provided arguments"""
pass
def get_tool_definition(self) -> Dict[str, Any]:
"""Get the tool definition for FastMCP registration"""
return {
"name": self.name,
"description": self.description,
"input_schema": self.input_schema
}
def create_fastmcp_tool(self, mcp_server):
"""Create a FastMCP tool decorator for this tool"""
@mcp_server.tool()
async def tool_wrapper(**kwargs):
return await self.execute(**kwargs)
# Set metadata
tool_wrapper.__name__ = self.name
tool_wrapper.__doc__ = self.description
return tool_wrapper
class ContextAwareTool(BaseServerTool):
"""Base class for tools that need access to MCP context"""
@abstractmethod
async def execute_with_context(self, ctx: Context[ServerSession, None], **kwargs) -> Any:
"""Execute the tool with MCP context"""
pass
async def execute(self, **kwargs) -> Any:
"""Default implementation that doesn't use context"""
# This will be overridden by the FastMCP wrapper
raise NotImplementedError("Use execute_with_context for context-aware tools")
def create_fastmcp_tool(self, mcp_server):
"""Create a FastMCP tool decorator for this context-aware tool"""
@mcp_server.tool()
async def tool_wrapper(ctx: Context[ServerSession, None], **kwargs):
return await self.execute_with_context(ctx, **kwargs)
# Set metadata
tool_wrapper.__name__ = self.name
tool_wrapper.__doc__ = self.description
return tool_wrapper
@@ -0,0 +1,48 @@
"""
Calculator Tool Example
"""
from .base_tool import BaseServerTool
class CalculatorTool(BaseServerTool):
"""A simple calculator tool"""
def __init__(self):
super().__init__(
name="calculator",
description="Perform basic mathematical operations",
input_schema={
"type": "object",
"properties": {
"operation": {
"type": "string",
"enum": ["add", "subtract", "multiply", "divide"],
"description": "The mathematical operation to perform"
},
"a": {
"type": "number",
"description": "First number"
},
"b": {
"type": "number",
"description": "Second number"
}
},
"required": ["operation", "a", "b"]
}
)
async def execute(self, operation: str, a: float, b: float) -> float:
"""Execute the calculator operation"""
if operation == "add":
return a + b
elif operation == "subtract":
return a - b
elif operation == "multiply":
return a * b
elif operation == "divide":
if b == 0:
raise ValueError("Cannot divide by zero")
return a / b
else:
raise ValueError(f"Unknown operation: {operation}")
@@ -0,0 +1,54 @@
"""
Greeting Tool Example
"""
from .base_tool import ContextAwareTool
from mcp.server.fastmcp import Context
from mcp.server.session import ServerSession
class GreetingTool(ContextAwareTool):
"""A greeting tool that uses MCP context"""
def __init__(self):
super().__init__(
name="greeting",
description="Generate personalized greetings with progress updates",
input_schema={
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Name of the person to greet"
},
"style": {
"type": "string",
"enum": ["friendly", "formal", "casual"],
"description": "Style of greeting",
"default": "friendly"
}
},
"required": ["name"]
}
)
async def execute_with_context(self, ctx: Context[ServerSession, None], name: str, style: str = "friendly") -> str:
"""Generate a greeting with progress updates"""
await ctx.info(f"Generating {style} greeting for {name}")
# Simulate some work with progress updates
await ctx.report_progress(progress=0.3, total=1.0, message="Preparing greeting...")
styles = {
"friendly": f"Hello there, {name}! Great to see you!",
"formal": f"Good day, {name}. I hope you are well.",
"casual": f"Hey {name}! What's up?"
}
await ctx.report_progress(progress=0.7, total=1.0, message="Generating message...")
greeting = styles.get(style, styles["friendly"])
await ctx.report_progress(progress=1.0, total=1.0, message="Greeting complete!")
await ctx.debug(f"Generated greeting: {greeting}")
return greeting
@@ -0,0 +1,100 @@
"""
Server Tool Registry for dynamic tool registration
"""
import os
import importlib
import inspect
from typing import List, Dict, Any, Type,Optional
from pathlib import Path
from .base_tool import BaseServerTool
class ServerToolRegistry:
"""Registry for managing server tools from files"""
def __init__(self, tools_directory: str = None):
self.tools_directory = tools_directory or os.path.dirname(__file__)
self._registered_tools: Dict[str, BaseServerTool] = {}
self._tool_files: List[str] = []
@property
def directory(self):
"""Alias for tools_directory for backward compatibility"""
return self.tools_directory
@property
def _tools(self):
"""Alias for _registered_tools for backward compatibility"""
return self._registered_tools
def discover_tools(self) -> List[BaseServerTool]:
"""Discover all tools in the tools directory"""
tools = []
tools_dir = Path(self.tools_directory)
if not tools_dir.exists():
return tools
# Find all Python files in the tools directory
for file_path in tools_dir.glob("*.py"):
if file_path.name.startswith("__"):
continue
module_name = file_path.stem
try:
# Import the module
spec = importlib.util.spec_from_file_location(module_name, file_path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
# Find all BaseServerTool subclasses in the module
for name, obj in inspect.getmembers(module, inspect.isclass):
if (issubclass(obj, BaseServerTool) and
obj != BaseServerTool and
not inspect.isabstract(obj)):
try:
tool_instance = obj()
tools.append(tool_instance)
self._registered_tools[tool_instance.name] = tool_instance
except Exception as e:
print(f"Warning: Could not instantiate tool {name}: {e}")
except Exception as e:
print(f"Warning: Could not load tool file {file_path}: {e}")
return tools
def register_tool(self, tool: BaseServerTool) -> None:
"""Register a single tool"""
self._registered_tools[tool.name] = tool
def register_tools(self, tools: List[BaseServerTool]) -> None:
"""Register multiple tools"""
for tool in tools:
self.register_tool(tool)
def get_tool(self, name: str) -> Optional[BaseServerTool]:
"""Get a tool by name"""
return self._registered_tools.get(name)
def get_all_tools(self) -> List[BaseServerTool]:
"""Get all registered tools"""
return list(self._registered_tools.values())
def get_tool_names(self) -> List[str]:
"""Get all registered tool names"""
return list(self._registered_tools.keys())
def register_tools_with_server(self, mcp_server) -> None:
"""Register all discovered tools with a FastMCP server"""
# First discover tools if not already done
if not self._registered_tools:
self.discover_tools()
# Register each tool with the server
for tool in self._registered_tools.values():
tool.create_fastmcp_tool(mcp_server)
def clear_tools(self) -> None:
"""Clear all registered tools"""
self._registered_tools.clear()
+8
View File
@@ -0,0 +1,8 @@
# MCP Tools Collection
from .math_tools import MathTools
from .text_tools import TextTools
from .system_tools import SystemTools
from .web_tools import WebTools
from .tool_registry import ToolRegistry
__all__ = ['MathTools', 'TextTools', 'SystemTools', 'WebTools', 'ToolRegistry']
+186
View File
@@ -0,0 +1,186 @@
"""
Mathematical Tools for MCP
"""
from typing import List
from ..core.types import MCPTool
class MathTools:
"""Collection of mathematical tools"""
@staticmethod
def get_tools() -> List[MCPTool]:
"""Get all math tools"""
return [
MathTools._create_add_tool(),
MathTools._create_subtract_tool(),
MathTools._create_multiply_tool(),
MathTools._create_divide_tool(),
MathTools._create_power_tool(),
MathTools._create_square_root_tool(),
MathTools._create_calculate_bmi_tool(),
]
@staticmethod
def _create_add_tool() -> MCPTool:
"""Create addition tool"""
async def add(a: float, b: float) -> float:
"""Add two numbers together"""
return a + b
return MCPTool(
name="add",
description="Add two numbers together",
input_schema={
"type": "object",
"properties": {
"a": {"type": "number", "description": "First number"},
"b": {"type": "number", "description": "Second number"},
},
"required": ["a", "b"],
},
handler=add,
)
@staticmethod
def _create_subtract_tool() -> MCPTool:
"""Create subtraction tool"""
async def subtract(a: float, b: float) -> float:
"""Subtract second number from first"""
return a - b
return MCPTool(
name="subtract",
description="Subtract second number from first",
input_schema={
"type": "object",
"properties": {
"a": {"type": "number", "description": "First number"},
"b": {"type": "number", "description": "Number to subtract"},
},
"required": ["a", "b"],
},
handler=subtract,
)
@staticmethod
def _create_multiply_tool() -> MCPTool:
"""Create multiplication tool"""
async def multiply(a: float, b: float) -> float:
"""Multiply two numbers"""
return a * b
return MCPTool(
name="multiply",
description="Multiply two numbers",
input_schema={
"type": "object",
"properties": {
"a": {"type": "number", "description": "First number"},
"b": {"type": "number", "description": "Second number"},
},
"required": ["a", "b"],
},
handler=multiply,
)
@staticmethod
def _create_divide_tool() -> MCPTool:
"""Create division tool"""
async def divide(a: float, b: float) -> float:
"""Divide first number by second"""
if b == 0:
raise ValueError("Cannot divide by zero")
return a / b
return MCPTool(
name="divide",
description="Divide first number by second",
input_schema={
"type": "object",
"properties": {
"a": {"type": "number", "description": "Dividend"},
"b": {"type": "number", "description": "Divisor (cannot be zero)"},
},
"required": ["a", "b"],
},
handler=divide,
)
@staticmethod
def _create_power_tool() -> MCPTool:
"""Create power tool"""
async def power(base: float, exponent: float) -> float:
"""Calculate base raised to the power of exponent"""
return base ** exponent
return MCPTool(
name="power",
description="Calculate base raised to the power of exponent",
input_schema={
"type": "object",
"properties": {
"base": {"type": "number", "description": "Base number"},
"exponent": {"type": "number", "description": "Exponent"},
},
"required": ["base", "exponent"],
},
handler=power,
)
@staticmethod
def _create_square_root_tool() -> MCPTool:
"""Create square root tool"""
async def square_root(number: float) -> float:
"""Calculate square root of a number"""
if number < 0:
raise ValueError("Cannot calculate square root of negative number")
return number ** 0.5
return MCPTool(
name="square_root",
description="Calculate square root of a number",
input_schema={
"type": "object",
"properties": {
"number": {"type": "number", "description": "Number to find square root of (must be non-negative)"},
},
"required": ["number"],
},
handler=square_root,
)
@staticmethod
def _create_calculate_bmi_tool() -> MCPTool:
"""Create BMI calculation tool"""
async def calculate_bmi(weight_kg: float, height_m: float) -> str:
"""Calculate BMI and provide health category"""
if weight_kg <= 0 or height_m <= 0:
raise ValueError("Weight and height must be positive numbers")
bmi = weight_kg / (height_m ** 2)
if bmi < 18.5:
category = "Underweight"
elif bmi < 25:
category = "Normal weight"
elif bmi < 30:
category = "Overweight"
else:
category = "Obese"
return ".1f"
return MCPTool(
name="calculate_bmi",
description="Calculate BMI and provide health assessment",
input_schema={
"type": "object",
"properties": {
"weight_kg": {"type": "number", "description": "Weight in kilograms"},
"height_m": {"type": "number", "description": "Height in meters"},
},
"required": ["weight_kg", "height_m"],
},
handler=calculate_bmi,
)
@@ -0,0 +1,157 @@
"""
System Tools for MCP
"""
import os
import platform
import psutil
from datetime import datetime
from typing import Dict, Any, List
from ..core.types import MCPTool
class SystemTools:
"""Collection of system-related tools"""
@staticmethod
def get_tools() -> List[MCPTool]:
"""Get all system tools"""
return [
SystemTools._create_get_system_info_tool(),
SystemTools._create_get_current_time_tool(),
SystemTools._create_list_directory_tool(),
SystemTools._create_get_file_info_tool(),
SystemTools._create_get_environment_variable_tool(),
]
@staticmethod
def _create_get_system_info_tool() -> MCPTool:
"""Create system info tool"""
async def get_system_info() -> Dict[str, Any]:
"""Get basic system information"""
try:
return {
"platform": platform.system(),
"platform_version": platform.version(),
"architecture": platform.machine(),
"processor": platform.processor(),
"python_version": platform.python_version(),
"cpu_count": os.cpu_count(),
"memory_total": psutil.virtual_memory().total if psutil else "psutil not available",
"memory_available": psutil.virtual_memory().available if psutil else "psutil not available",
}
except Exception as e:
return {"error": f"Could not retrieve system info: {str(e)}"}
return MCPTool(
name="get_system_info",
description="Get basic system information including OS, CPU, and memory details",
input_schema={"type": "object", "properties": {}},
handler=get_system_info,
)
@staticmethod
def _create_get_current_time_tool() -> MCPTool:
"""Create current time tool"""
async def get_current_time() -> str:
"""Get the current date and time"""
now = datetime.now()
return now.strftime("%Y-%m-%d %H:%M:%S")
return MCPTool(
name="get_current_time",
description="Get the current date and time in YYYY-MM-DD HH:MM:SS format",
input_schema={"type": "object", "properties": {}},
handler=get_current_time,
)
@staticmethod
def _create_list_directory_tool() -> MCPTool:
"""Create directory listing tool"""
async def list_directory(path: str = ".") -> List[str]:
"""List contents of a directory"""
try:
if not os.path.exists(path):
raise ValueError(f"Path does not exist: {path}")
if not os.path.isdir(path):
raise ValueError(f"Path is not a directory: {path}")
return os.listdir(path)
except Exception as e:
raise ValueError(f"Could not list directory: {str(e)}")
return MCPTool(
name="list_directory",
description="List the contents of a directory",
input_schema={
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Directory path to list",
"default": "."
},
},
},
handler=list_directory,
)
@staticmethod
def _create_get_file_info_tool() -> MCPTool:
"""Create file info tool"""
async def get_file_info(file_path: str) -> Dict[str, Any]:
"""Get information about a file"""
try:
if not os.path.exists(file_path):
raise ValueError(f"File does not exist: {file_path}")
stat = os.stat(file_path)
return {
"name": os.path.basename(file_path),
"path": os.path.abspath(file_path),
"size": stat.st_size,
"is_file": os.path.isfile(file_path),
"is_directory": os.path.isdir(file_path),
"modified_time": datetime.fromtimestamp(stat.st_mtime).isoformat(),
"created_time": datetime.fromtimestamp(stat.st_ctime).isoformat(),
}
except Exception as e:
raise ValueError(f"Could not get file info: {str(e)}")
return MCPTool(
name="get_file_info",
description="Get detailed information about a file or directory",
input_schema={
"type": "object",
"properties": {
"file_path": {"type": "string", "description": "Path to the file or directory"},
},
"required": ["file_path"],
},
handler=get_file_info,
)
@staticmethod
def _create_get_environment_variable_tool() -> MCPTool:
"""Create environment variable tool"""
async def get_environment_variable(name: str, default_value: str = "") -> str:
"""Get the value of an environment variable"""
return os.getenv(name, default_value)
return MCPTool(
name="get_environment_variable",
description="Get the value of an environment variable",
input_schema={
"type": "object",
"properties": {
"name": {"type": "string", "description": "Name of the environment variable"},
"default_value": {
"type": "string",
"description": "Default value if variable is not set",
"default": ""
},
},
"required": ["name"],
},
handler=get_environment_variable,
)
+187
View File
@@ -0,0 +1,187 @@
"""
Text Processing Tools for MCP
"""
import re
from typing import List
from ..core.types import MCPTool
class TextTools:
"""Collection of text processing tools"""
@staticmethod
def get_tools() -> List[MCPTool]:
"""Get all text tools"""
return [
TextTools._create_word_count_tool(),
TextTools._create_text_search_tool(),
TextTools._create_text_replace_tool(),
TextTools._create_text_uppercase_tool(),
TextTools._create_text_lowercase_tool(),
TextTools._create_text_length_tool(),
TextTools._create_generate_greeting_tool(),
]
@staticmethod
def _create_word_count_tool() -> MCPTool:
"""Create word count tool"""
async def count_words(text: str) -> int:
"""Count the number of words in a text"""
words = text.strip().split()
return len(words)
return MCPTool(
name="count_words",
description="Count the number of words in a text",
input_schema={
"type": "object",
"properties": {
"text": {"type": "string", "description": "Text to count words in"},
},
"required": ["text"],
},
handler=count_words,
)
@staticmethod
def _create_text_search_tool() -> MCPTool:
"""Create text search tool"""
async def search_text(text: str, pattern: str, case_sensitive: bool = False) -> List[str]:
"""Search for a pattern in text and return all matches"""
flags = 0 if case_sensitive else re.IGNORECASE
matches = re.findall(pattern, text, flags)
return matches
return MCPTool(
name="search_text",
description="Search for a pattern in text and return all matches",
input_schema={
"type": "object",
"properties": {
"text": {"type": "string", "description": "Text to search in"},
"pattern": {"type": "string", "description": "Regular expression pattern to search for"},
"case_sensitive": {"type": "boolean", "description": "Whether search should be case sensitive", "default": False},
},
"required": ["text", "pattern"],
},
handler=search_text,
)
@staticmethod
def _create_text_replace_tool() -> MCPTool:
"""Create text replace tool"""
async def replace_text(text: str, old_pattern: str, new_text: str) -> str:
"""Replace all occurrences of a pattern in text"""
return text.replace(old_pattern, new_text)
return MCPTool(
name="replace_text",
description="Replace all occurrences of a pattern in text",
input_schema={
"type": "object",
"properties": {
"text": {"type": "string", "description": "Original text"},
"old_pattern": {"type": "string", "description": "Text to replace"},
"new_text": {"type": "string", "description": "Replacement text"},
},
"required": ["text", "old_pattern", "new_text"],
},
handler=replace_text,
)
@staticmethod
def _create_text_uppercase_tool() -> MCPTool:
"""Create uppercase tool"""
async def to_uppercase(text: str) -> str:
"""Convert text to uppercase"""
return text.upper()
return MCPTool(
name="to_uppercase",
description="Convert text to uppercase",
input_schema={
"type": "object",
"properties": {
"text": {"type": "string", "description": "Text to convert to uppercase"},
},
"required": ["text"],
},
handler=to_uppercase,
)
@staticmethod
def _create_text_lowercase_tool() -> MCPTool:
"""Create lowercase tool"""
async def to_lowercase(text: str) -> str:
"""Convert text to lowercase"""
return text.lower()
return MCPTool(
name="to_lowercase",
description="Convert text to lowercase",
input_schema={
"type": "object",
"properties": {
"text": {"type": "string", "description": "Text to convert to lowercase"},
},
"required": ["text"],
},
handler=to_lowercase,
)
@staticmethod
def _create_text_length_tool() -> MCPTool:
"""Create text length tool"""
async def text_length(text: str) -> int:
"""Get the length of text"""
return len(text)
return MCPTool(
name="text_length",
description="Get the length of text (number of characters)",
input_schema={
"type": "object",
"properties": {
"text": {"type": "string", "description": "Text to measure length of"},
},
"required": ["text"],
},
handler=text_length,
)
@staticmethod
def _create_generate_greeting_tool() -> MCPTool:
"""Create greeting generation tool"""
async def generate_greeting(name: str, style: str = "casual") -> str:
"""Generate a personalized greeting"""
name = name.strip()
if not name:
raise ValueError("Name cannot be empty")
if style == "casual":
return f"Hey {name}! Welcome! 👋"
elif style == "formal":
return f"Good day, {name}. Welcome to our platform."
elif style == "professional":
return f"Hello, {name}. Thank you for joining us."
else:
return f"Hi {name}! Welcome!"
return MCPTool(
name="generate_greeting",
description="Generate a personalized greeting with different styles",
input_schema={
"type": "object",
"properties": {
"name": {"type": "string", "description": "Person's name"},
"style": {
"type": "string",
"enum": ["casual", "formal", "professional"],
"description": "Greeting style",
"default": "casual"
},
},
"required": ["name"],
},
handler=generate_greeting,
)
@@ -0,0 +1,80 @@
"""
Tool Registry for easy tool management and combination
"""
from typing import List, Dict, Any, Optional
from ..core.types import MCPTool
from .math_tools import MathTools
from .text_tools import TextTools
from .system_tools import SystemTools
from .web_tools import WebTools
class ToolRegistry:
"""Registry for managing and combining MCP tools from different categories"""
def __init__(self):
self._tool_categories = {
'math': MathTools,
'text': TextTools,
'system': SystemTools,
'web': WebTools,
}
self._custom_tools: List[MCPTool] = []
def get_tools_by_category(self, category: str) -> List[MCPTool]:
"""Get all tools from a specific category"""
if category not in self._tool_categories:
raise ValueError(f"Unknown tool category: {category}")
return self._tool_categories[category].get_tools()
def get_tools_by_categories(self, categories: List[str]) -> List[MCPTool]:
"""Get tools from multiple categories"""
tools = []
for category in categories:
tools.extend(self.get_tools_by_category(category))
return tools
def get_all_tools(self) -> List[MCPTool]:
"""Get all tools from all categories"""
tools = []
for category in self._tool_categories.values():
tools.extend(category.get_tools())
tools.extend(self._custom_tools)
return tools
def add_custom_tool(self, tool: MCPTool) -> None:
"""Add a custom tool to the registry"""
self._custom_tools.append(tool)
def add_custom_tools(self, tools: List[MCPTool]) -> None:
"""Add multiple custom tools to the registry"""
self._custom_tools.extend(tools)
def get_available_categories(self) -> List[str]:
"""Get list of available tool categories"""
return list(self._tool_categories.keys())
def get_category_info(self) -> Dict[str, Dict[str, Any]]:
"""Get information about each category"""
info = {}
for category_name, category_class in self._tool_categories.items():
tools = category_class.get_tools()
info[category_name] = {
'tool_count': len(tools),
'tool_names': [tool.name for tool in tools]
}
return info
def create_server_config(self, categories: Optional[List[str]] = None) -> Dict[str, Any]:
"""Create a server configuration with tools from specified categories"""
if categories is None:
tools = self.get_all_tools()
else:
tools = self.get_tools_by_categories(categories)
return {
'tools': tools,
'tool_count': len(tools),
'categories': categories or self.get_available_categories()
}
+161
View File
@@ -0,0 +1,161 @@
"""
Web Tools for MCP
"""
import urllib.parse
from typing import Dict, Any, List, Optional
from ..core.types import MCPTool
class WebTools:
"""Collection of web-related tools"""
@staticmethod
def get_tools() -> List[MCPTool]:
"""Get all web tools"""
return [
WebTools._create_url_encode_tool(),
WebTools._create_url_decode_tool(),
WebTools._create_parse_url_tool(),
WebTools._create_validate_email_tool(),
WebTools._create_extract_domain_tool(),
]
@staticmethod
def _create_url_encode_tool() -> MCPTool:
"""Create URL encode tool"""
async def url_encode(text: str) -> str:
"""URL encode a string"""
return urllib.parse.quote(text)
return MCPTool(
name="url_encode",
description="URL encode a string for safe transmission",
input_schema={
"type": "object",
"properties": {
"text": {"type": "string", "description": "Text to URL encode"},
},
"required": ["text"],
},
handler=url_encode,
)
@staticmethod
def _create_url_decode_tool() -> MCPTool:
"""Create URL decode tool"""
async def url_decode(encoded_text: str) -> str:
"""URL decode a string"""
try:
return urllib.parse.unquote(encoded_text)
except Exception:
raise ValueError("Invalid URL encoded string")
return MCPTool(
name="url_decode",
description="URL decode a previously encoded string",
input_schema={
"type": "object",
"properties": {
"encoded_text": {"type": "string", "description": "URL encoded text to decode"},
},
"required": ["encoded_text"],
},
handler=url_decode,
)
@staticmethod
def _create_parse_url_tool() -> MCPTool:
"""Create URL parsing tool"""
async def parse_url(url: str) -> Dict[str, str]:
"""Parse a URL and return its components"""
try:
parsed = urllib.parse.urlparse(url)
return {
"scheme": parsed.scheme,
"netloc": parsed.netloc,
"hostname": parsed.hostname,
"port": str(parsed.port) if parsed.port else "",
"path": parsed.path,
"query": parsed.query,
"fragment": parsed.fragment,
}
except Exception:
raise ValueError("Invalid URL format")
return MCPTool(
name="parse_url",
description="Parse a URL and return its components (scheme, host, path, etc.)",
input_schema={
"type": "object",
"properties": {
"url": {"type": "string", "description": "URL to parse"},
},
"required": ["url"],
},
handler=parse_url,
)
@staticmethod
def _create_validate_email_tool() -> MCPTool:
"""Create email validation tool"""
async def validate_email(email: str) -> Dict[str, Any]:
"""Validate an email address format"""
import re
email_pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
is_valid = bool(re.match(email_pattern, email))
# Extract domain
if is_valid and '@' in email:
domain = email.split('@')[1]
else:
domain = ""
return {
"email": email,
"is_valid": is_valid,
"domain": domain,
}
return MCPTool(
name="validate_email",
description="Validate an email address format and extract domain",
input_schema={
"type": "object",
"properties": {
"email": {"type": "string", "description": "Email address to validate"},
},
"required": ["email"],
},
handler=validate_email,
)
@staticmethod
def _create_extract_domain_tool() -> MCPTool:
"""Create domain extraction tool"""
async def extract_domain(url: str) -> str:
"""Extract the domain from a URL"""
try:
parsed = urllib.parse.urlparse(url)
if parsed.hostname:
return parsed.hostname
else:
# Fallback for URLs without scheme
url_with_scheme = "http://" + url if "://" not in url else url
parsed = urllib.parse.urlparse(url_with_scheme)
return parsed.hostname or ""
except Exception:
return ""
return MCPTool(
name="extract_domain",
description="Extract the domain name from a URL",
input_schema={
"type": "object",
"properties": {
"url": {"type": "string", "description": "URL to extract domain from"},
},
"required": ["url"],
},
handler=extract_domain,
)
@@ -0,0 +1,6 @@
# Transport layer implementations
from .transport_manager import TransportManager
from .sse_transport import SSETransport
from .stdio_transport import STDIOTransport
__all__ = ['TransportManager', 'SSETransport', 'STDIOTransport']
@@ -0,0 +1,101 @@
"""
SSE (Server-Sent Events) Transport Implementation
"""
import asyncio
import json
from typing import Dict, Any, Optional
from contextlib import asynccontextmanager
try:
import aiohttp
AIOHTTP_AVAILABLE = True
except ImportError:
AIOHTTP_AVAILABLE = False
from ..core.interfaces import IMCPTransport
class SSETransport(IMCPTransport):
"""SSE transport for MCP communication"""
def __init__(
self,
host: str = "localhost",
port: int = 8050,
endpoint: str = "/sse",
**kwargs
):
if not AIOHTTP_AVAILABLE:
raise ImportError("aiohttp package not installed. Install with: pip install aiohttp")
self.host = host
self.port = port
self.endpoint = endpoint
self._session: Optional[aiohttp.ClientSession] = None
self._response: Optional[aiohttp.ClientResponse] = None
self._connected = False
@asynccontextmanager
async def connect(self):
"""Establish SSE connection"""
try:
self._session = aiohttp.ClientSession()
url = f"http://{self.host}:{self.port}{self.endpoint}"
print(f"Connecting to SSE endpoint: {url}")
self._response = await self._session.get(url)
if self._response.status != 200:
raise ConnectionError(f"SSE connection failed with status {self._response.status}")
self._connected = True
yield self
except Exception as e:
print(f"SSE connection error: {e}")
raise
finally:
await self.close()
async def send_message(self, message: Dict[str, Any]) -> None:
"""Send a message through SSE transport"""
if not self._connected:
raise ConnectionError("SSE transport not connected")
# SSE is typically unidirectional from server to client
# For bidirectional communication, you might need to use a different approach
# or combine SSE with HTTP POST requests
print(f"SSE Transport: Sending message: {message}")
# TODO: Implement actual message sending logic
# This might require HTTP POST to a separate endpoint
# or WebSocket upgrade, depending on server implementation
async def receive_message(self) -> Dict[str, Any]:
"""Receive a message through SSE transport"""
if not self._connected or not self._response:
raise ConnectionError("SSE transport not connected")
# Read SSE data
async for line in self._response.content:
line_str = line.decode('utf-8').strip()
if line_str.startswith('data: '):
data = line_str[6:] # Remove 'data: ' prefix
if data:
try:
return json.loads(data)
except json.JSONDecodeError:
continue
return {}
async def close(self) -> None:
"""Close the SSE connection"""
if self._response:
self._response.close()
if self._session:
await self._session.close()
self._connected = False
print("SSE transport closed")
@@ -0,0 +1,81 @@
"""
STDIO Transport Implementation
"""
import asyncio
import json
import sys
from typing import Dict, Any, Optional
from contextlib import asynccontextmanager
from ..core.interfaces import IMCPTransport
class STDIOTransport(IMCPTransport):
"""STDIO transport for MCP communication"""
def __init__(self, **kwargs):
self._connected = False
self._reader: Optional[asyncio.StreamReader] = None
self._writer: Optional[asyncio.StreamWriter] = None
@asynccontextmanager
async def connect(self):
"""Establish STDIO connection"""
try:
# Use stdin/stdout for communication
self._reader = asyncio.StreamReader()
reader_protocol = asyncio.StreamReaderProtocol(self._reader)
await asyncio.get_event_loop().connect_read_pipe(
lambda: reader_protocol, sys.stdin
)
# For writing, we'll use stdout
self._writer = None # We'll write directly to stdout
self._connected = True
print("STDIO transport connected", file=sys.stderr)
yield self
except Exception as e:
print(f"STDIO connection error: {e}", file=sys.stderr)
raise
finally:
await self.close()
async def send_message(self, message: Dict[str, Any]) -> None:
"""Send a message through STDIO"""
if not self._connected:
raise ConnectionError("STDIO transport not connected")
# Convert message to JSON and send to stdout
message_json = json.dumps(message, separators=(',', ':'))
print(message_json, flush=True)
async def receive_message(self) -> Dict[str, Any]:
"""Receive a message through STDIO"""
if not self._connected or not self._reader:
raise ConnectionError("STDIO transport not connected")
try:
# Read line from stdin
line = await self._reader.readline()
if not line:
return {} # EOF
line_str = line.decode('utf-8').strip()
if line_str:
return json.loads(line_str)
except json.JSONDecodeError as e:
print(f"Invalid JSON received: {e}", file=sys.stderr)
except Exception as e:
print(f"Error receiving message: {e}", file=sys.stderr)
return {}
async def close(self) -> None:
"""Close the STDIO connection"""
self._connected = False
if self._writer:
self._writer.close()
print("STDIO transport closed", file=sys.stderr)
@@ -0,0 +1,81 @@
"""
Transport Manager for easy switching between transport methods
"""
from typing import Optional, Union
from contextlib import asynccontextmanager
from ..core.types import TransportType
from ..core.interfaces import IMCPTransport
from .sse_transport import SSETransport
from .stdio_transport import STDIOTransport
class TransportManager:
"""Manager class for handling different MCP transport methods"""
def __init__(self):
self._current_transport: Optional[IMCPTransport] = None
@asynccontextmanager
async def get_transport(
self,
transport_type: Union[str, TransportType],
**kwargs
):
"""Get a transport instance for the specified type"""
# Convert string to enum if needed
if isinstance(transport_type, str):
transport_type = TransportType(transport_type.lower())
# Create the appropriate transport
if transport_type == TransportType.SSE:
transport = SSETransport(**kwargs)
elif transport_type == TransportType.STDIO:
transport = STDIOTransport(**kwargs)
else:
raise ValueError(f"Unsupported transport type: {transport_type}")
self._current_transport = transport
try:
async with transport.connect():
yield transport
finally:
self._current_transport = None
async def switch_transport(
self,
new_transport_type: Union[str, TransportType],
**kwargs
) -> IMCPTransport:
"""Switch to a different transport type"""
# Close current transport if exists
if self._current_transport:
await self._current_transport.close()
# Convert string to enum if needed
if isinstance(new_transport_type, str):
new_transport_type = TransportType(new_transport_type.lower())
# Create new transport
if new_transport_type == TransportType.SSE:
self._current_transport = SSETransport(**kwargs)
elif new_transport_type == TransportType.STDIO:
self._current_transport = STDIOTransport(**kwargs)
else:
raise ValueError(f"Unsupported transport type: {new_transport_type}")
return self._current_transport
@property
def current_transport(self) -> Optional[IMCPTransport]:
"""Get the currently active transport"""
return self._current_transport
async def close_current_transport(self) -> None:
"""Close the currently active transport"""
if self._current_transport:
await self._current_transport.close()
self._current_transport = None
+9
View File
@@ -0,0 +1,9 @@
from dotenv import load_dotenv
import os
load_dotenv()
class Config:
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
CLAUDE_API_KEY = os.getenv("CLAUDE_API_KEY")
GROK_API_KEY = os.getenv("GROK_API_KEY")
ANTHROPIC_API_KEY = os.getenv("ANTHROPIC_API_KEY")
GROQ_API_KEY = os.getenv("GROQ_API_KEY")
Executable
+44
View File
@@ -0,0 +1,44 @@
#!/usr/bin/env python3
"""
MCP Development Server Runner
Optimized for use with: mcp dev dev_run.py
This file is specifically designed for MCP development workflow.
It automatically starts the server with SSE transport on port 3000.
"""
import sys
import os
import logging
# Configure logging to reduce noise
logging.getLogger("mcp").setLevel(logging.WARNING)
logging.getLogger("uvicorn").setLevel(logging.WARNING)
logging.getLogger("uvicorn.access").setLevel(logging.WARNING)
# Add the src directory to the Python path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
from mcp_template.server.modular_server import create_default_server
# Create and expose the MCP server for dev mode
# This is what mcp dev command looks for
print("Initializing MCP Development Server...")
print("SSE Transport on port 3000")
# Create the server with dev settings
server = create_default_server()
# Extract the FastMCP instance for MCP dev command compatibility
mcp = server.mcp
if __name__ == "__main__":
mcp.run()
# print(" MCP server ready for development!")
# print("Use: mcp dev dev_run.py")
# print("Server will be available at: http://0.0.0.0:3000/sse")
#uv run mcp dev dev_run.py
# The MCP dev command will handle running the server
# We just need to make the mcp object available
+179
View File
@@ -0,0 +1,179 @@
#!/usr/bin/env python3
"""
MCP Template Demo Client
Demonstrates how to connect to and interact with MCP servers
"""
import asyncio
import sys
import os
# Add src to path for imports
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))
from clients.client_factory import AIClientFactory
from transport.transport_manager import TransportManager
from core.types import TransportType
class MCPDemoClient:
"""Demo client for interacting with MCP servers"""
def __init__(self, ai_provider="openai", model="gpt-4o"):
self.ai_provider = ai_provider
self.model = model
self.ai_client = None
self.transport_manager = TransportManager()
async def initialize(self):
"""Initialize the demo client"""
print(f"🤖 Initializing {self.ai_provider} client with {self.model}...")
# Initialize AI client
self.ai_client = AIClientFactory.create_client(
provider=self.ai_provider,
model_name=self.model
)
await self.ai_client.initialize()
print("✅ AI client initialized")
async def demo_stdio_interaction(self):
"""Demo direct STDIO interaction with MCP server"""
print("\n🔧 Demo: Direct STDIO Interaction")
print("This demonstrates direct tool calls without AI involvement")
# For this demo, we'll simulate MCP server responses
# In a real scenario, you'd connect to an actual MCP server
print("📋 Available tools (simulated):")
print(" • add: Add two numbers together")
print(" • multiply: Multiply two numbers")
print(" • greet_user: Generate personalized greeting")
print(" • calculate_bmi: Calculate BMI and health category")
# Simulate some tool calls
print("\n🧮 Simulating tool calls:")
# Simulate add tool
print(" add(5, 3) = 8")
# Simulate greet tool
print(" greet_user('Alice', 'casual') = 'Hey Alice! Welcome aboard! 🎉'")
# Simulate BMI tool
print(" calculate_bmi(70, 1.75) = 'Your BMI is 22.9 (Normal weight)'")
async def demo_ai_with_tools(self):
"""Demo AI client with MCP tool integration"""
print("\n🧠 Demo: AI Client with MCP Tools")
print("This demonstrates how AI can use MCP tools to perform tasks")
# Sample queries that would benefit from MCP tools
queries = [
"What is 15 + 27?",
"Can you greet Sarah in a professional manner?",
"What's the BMI for someone who weighs 80kg and is 1.8m tall?",
"Calculate 12 * 8 for me"
]
print("💭 Sample queries for AI + MCP integration:")
for i, query in enumerate(queries, 1):
print(f" {i}. {query}")
print("\n📝 Note: To run actual AI+MCP integration, you need:")
print(" 1. A running MCP server (see demo_server.py)")
print(" 2. Proper API keys in environment variables")
print(" 3. Network connection for AI provider")
async def demo_transport_switching(self):
"""Demo transport layer switching capabilities"""
print("\n🔄 Demo: Transport Layer Switching")
print("The MCP template supports easy switching between transports:")
print("📡 SSE (Server-Sent Events):")
print(" • Best for: Web applications, remote connections")
print(" • Protocol: HTTP-based, real-time communication")
print(" • Use case: Browser-based MCP clients")
print("\n💻 STDIO (Standard Input/Output):")
print(" • Best for: Local applications, direct process communication")
print(" • Protocol: Direct pipes between processes")
print(" • Use case: CLI tools, local development")
print("\n🔧 Easy switching example:")
print(" # Switch to SSE transport")
print(" transport_manager.switch_transport('sse', host='localhost', port=8050)")
print(" ")
print(" # Switch to STDIO transport")
print(" transport_manager.switch_transport('stdio')")
async def demo_configuration(self):
"""Demo configuration management"""
print("\n⚙️ Demo: Configuration Management")
print("The MCP template supports flexible configuration:")
print("📄 Configuration sources (in order of priority):")
print(" 1. Environment variables")
print(" 2. JSON configuration files")
print(" 3. Default values")
print("\n🌍 Environment variables example:")
print(" MCP_SERVER_NAME=MyServer")
print(" MCP_TRANSPORT=sse")
print(" MCP_AI_PROVIDER=openai")
print(" OPENAI_API_KEY=your_key_here")
print("\n📋 JSON config example:")
print(" {")
print(' "server": {"name": "MyServer", "port": 8080},')
print(' "client": {"provider": "openai", "model": "gpt-4o"}')
print(" }")
async def run_demo(self):
"""Run the complete demo"""
print("🎭 MCP Template Demo Client")
print("=" * 50)
await self.initialize()
await self.demo_stdio_interaction()
await self.demo_ai_with_tools()
await self.demo_transport_switching()
await self.demo_configuration()
print("\n🎉 Demo completed!")
print("\n📚 Next steps:")
print(" • Run 'python examples/demo_server.py calculator' for a calculator server")
print(" • Run 'python examples/demo_server.py knowledge' for a knowledge base server")
print(" • Check the tests/ directory for comprehensive test examples")
print(" • Read docs in README.md for detailed usage instructions")
async def main():
"""Main demo function"""
if len(sys.argv) > 1:
ai_provider = sys.argv[1]
model = sys.argv[2] if len(sys.argv) > 2 else None
else:
ai_provider = "openai"
model = "gpt-4o"
print(f"🤖 Using AI provider: {ai_provider}")
if model:
print(f"🧠 Using model: {model}")
client = MCPDemoClient(ai_provider, model)
try:
await client.run_demo()
except KeyboardInterrupt:
print("\n👋 Demo interrupted by user")
except Exception as e:
print(f"❌ Error running demo: {e}")
print("💡 Make sure you have the required dependencies installed:")
print(" pip install openai aiohttp python-dotenv")
sys.exit(1)
if __name__ == "__main__":
asyncio.run(main())
+180
View File
@@ -0,0 +1,180 @@
#!/usr/bin/env python3
"""
MCP Template Demo Server
Demonstrates how to create and run different types of MCP servers
"""
import asyncio
import sys
import os
# Add src to path for imports
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))
from server.server_factory import MCPServerFactory
async def demo_calculator_server():
"""Demo: Create and run a calculator server with SSE transport"""
print("🚀 Starting Calculator Server with SSE transport...")
server = MCPServerFactory.create_basic_calculator_server(
name="Demo Calculator Server",
transport="sse",
host="localhost",
port=8050
)
print("📊 Available tools:")
tools = await server.list_tools()
for tool in tools:
print(f"{tool['name']}: {tool['description']}")
print("🌐 Server will be available at: http://localhost:8050/sse")
print("💡 To test: Run 'python examples/demo_client.py' in another terminal")
await server.start()
async def demo_knowledge_base_server():
"""Demo: Create and run a knowledge base server with STDIO transport"""
print("🚀 Starting Knowledge Base Server with STDIO transport...")
# Custom knowledge base data
kb_data = {
"company_info": "TechCorp is a leading AI solutions provider founded in 2020.",
"mission": "Our mission is to democratize AI through open-source tools and education.",
"values": "Innovation, transparency, collaboration, and ethical AI development.",
"contact": "Email: info@techcorp.com | Phone: (555) 123-4567"
}
server = MCPServerFactory.create_knowledge_base_server(
name="Demo Knowledge Base Server",
transport="stdio",
kb_data=kb_data
)
print("📚 Available tools:")
tools = await server.list_tools()
for tool in tools:
print(f"{tool['name']}: {tool['description']}")
print("💻 Server running in STDIO mode")
print("💡 To test: Run 'python examples/demo_client.py' in another terminal")
await server.start()
async def demo_custom_server():
"""Demo: Create a custom server with mixed tools and resources"""
print("🚀 Starting Custom Server with mixed capabilities...")
# Custom tools
async def greet_user(name: str, style: str = "formal") -> str:
"""Generate a personalized greeting"""
if style == "casual":
return f"Hey {name}! Welcome aboard! 🎉"
elif style == "professional":
return f"Good day, {name}. Welcome to our platform."
else:
return f"Hello, {name}. Welcome!"
async def calculate_bmi(weight_kg: float, height_m: float) -> str:
"""Calculate BMI and provide health category"""
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 f"Your BMI is {bmi:.1f} ({category})"
from core.types import MCPTool
tools = [
MCPTool(
name="greet_user",
description="Generate a personalized greeting with different styles",
input_schema={
"type": "object",
"properties": {
"name": {"type": "string", "description": "User's name"},
"style": {
"type": "string",
"enum": ["casual", "formal", "professional"],
"description": "Greeting style"
}
},
"required": ["name"]
},
handler=greet_user
),
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
)
]
server = MCPServerFactory.create_server(
name="Demo Custom Server",
transport="stdio",
tools=tools
)
print("🛠️ Available tools:")
tools_list = await server.list_tools()
for tool in tools_list:
print(f"{tool['name']}: {tool['description']}")
print("💻 Server running in STDIO mode")
print("💡 To test: Run 'python examples/demo_client.py' in another terminal")
await server.start()
async def main():
"""Main demo function"""
if len(sys.argv) != 2:
print("Usage: python demo_server.py <server_type>")
print("Available server types:")
print(" calculator - Basic calculator with math operations")
print(" knowledge - Knowledge base with searchable content")
print(" custom - Custom server with mixed capabilities")
sys.exit(1)
server_type = sys.argv[1].lower()
try:
if server_type == "calculator":
await demo_calculator_server()
elif server_type == "knowledge":
await demo_knowledge_base_server()
elif server_type == "custom":
await demo_custom_server()
else:
print(f"Unknown server type: {server_type}")
sys.exit(1)
except KeyboardInterrupt:
print("\n👋 Server stopped by user")
except Exception as e:
print(f"❌ Error running server: {e}")
sys.exit(1)
if __name__ == "__main__":
asyncio.run(main())
+195
View File
@@ -0,0 +1,195 @@
#!/usr/bin/env python3
"""
Modular MCP Template Demo
Demonstrates the new modular architecture with separate tool, prompt, and resource folders
"""
import asyncio
import sys
import os
# Add src to path for imports
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))
from server.server_factory import MCPServerFactory
from tools.tool_registry import ToolRegistry
from resources.data_resources import DataResources
from clients.client_factory import AIClientFactory
async def demo_modular_server_creation():
"""Demo: Creating servers using the modular architecture"""
print("🏗️ Modular MCP Server Creation Demo")
print("=" * 50)
# 1. Using Tool Registry for category-based tool selection
print("\n1️⃣ Tool Registry - Category-based Tool Selection")
registry = ToolRegistry()
print("Available tool categories:")
categories = registry.get_available_categories()
for category in categories:
tools = registry.get_tools_by_category(category)
print(f"{category}: {len(tools)} tools")
# Create a math-focused server
math_tools = registry.get_tools_by_category('math')
math_server = MCPServerFactory.create_server(
name="Math Server",
tools=math_tools,
transport="stdio"
)
print(f"\n✅ Created Math Server with {len(math_tools)} tools")
# 2. Using Server Factory with categories
print("\n2️⃣ Server Factory - Direct Category Selection")
dev_server = MCPServerFactory.create_server_from_categories(
name="Developer Server",
categories=["math", "text", "system"],
transport="stdio"
)
print("✅ Created Developer Server with math, text, and system tools")
# 3. Adding custom tools to registry
print("\n3️⃣ Custom Tools - Extending the Registry")
async def generate_password(length: int = 12) -> str:
"""Generate a secure random password"""
import secrets
import string
chars = string.ascii_letters + string.digits + "!@#$%^&*"
return ''.join(secrets.choice(chars) for _ in range(length))
from core.types import MCPTool
custom_tool = MCPTool(
name="generate_password",
description="Generate a secure random password",
input_schema={
"type": "object",
"properties": {
"length": {"type": "integer", "description": "Password length", "default": 12}
}
},
handler=generate_password,
)
registry.add_custom_tool(custom_tool)
print("✅ Added custom password generation tool")
# 4. Resources integration
print("\n4️⃣ Resources - Static Data Integration")
resources = DataResources.get_resources()
print(f"Available data resources: {len(resources)}")
for resource in resources:
print(f"{resource.name}: {resource.uri}")
# Create server with resources
resource_server = MCPServerFactory.create_server(
name="Resource Server",
tools=math_tools,
resources=resources,
transport="stdio"
)
print("✅ Created server with both tools and resources")
# 5. Test the servers
print("\n5️⃣ Server Testing")
print("Testing Math Server:")
# List tools
tools = await math_server.list_tools()
print(f" 📋 Available tools: {[t['name'] for t in tools]}")
# Test a tool
try:
result = await math_server.call_tool("add", {"a": 15, "b": 27})
print(f" 🧮 add(15, 27) = {result}")
except Exception as e:
print(f" ❌ Tool test failed: {e}")
# Test resources
print("\nTesting Resource Server:")
resources_list = await resource_server.list_resources()
print(f" 📄 Available resources: {len(resources_list)}")
if resources_list:
# Read a resource
resource_content = await resource_server.read_resource(resources_list[0]["uri"])
print(f" 📖 Read resource: {resources_list[0]['name']} ({len(resource_content)} chars)")
print("\n✅ Modular server creation demo completed!")
async def demo_ai_client_integration():
"""Demo: AI client integration with modular servers"""
print("\n🤖 AI Client Integration Demo")
print("=" * 40)
try:
# Note: This demo requires API keys to be set
print("Note: This demo requires OPENAI_API_KEY environment variable")
# Create AI client (would work with proper API key)
ai_client = AIClientFactory.create_openai_client(
model_name="gpt-4o",
api_key=os.getenv("OPENAI_API_KEY", "demo-key")
)
print("✅ Created OpenAI client (requires valid API key for actual use)")
# Create a server for AI integration
registry = ToolRegistry()
tools = registry.get_tools_by_categories(["math", "text"])
server = MCPServerFactory.create_server(
name="AI Integration Server",
tools=tools,
transport="stdio"
)
print(f"✅ Created server with {len(tools)} tools for AI integration")
print("\nExample AI queries that would work:")
print("'What is 25 * 18?'")
print("'Count the words in: The quick brown fox jumps over the lazy dog'")
print("'Convert this to uppercase: hello world'")
print("'Calculate BMI for 70kg and 1.75m height'")
except Exception as e:
print(f"❌ AI Client demo setup failed: {e}")
print("This is expected without proper API key configuration")
async def main():
"""Main demo function"""
print("🎭 Modular MCP Template Demo")
print("Demonstrating the new modular architecture with separate folders")
print("=" * 70)
try:
await demo_modular_server_creation()
await demo_ai_client_integration()
print("\n🎉 All demos completed successfully!")
print("\n📚 Key Features Demonstrated:")
print(" • 🔧 Modular tool organization by category")
print(" • 📄 Separate resource management")
print(" • 🏗️ Flexible server creation with tool registry")
print(" • 🤖 AI client abstraction and integration")
print(" • ⚙️ Easy configuration and customization")
print("\n🚀 Next Steps:")
print(" • Add your API keys to .env file")
print(" • Create custom tools in src/tools/")
print(" • Add custom resources in src/resources/")
print(" • Create custom prompts in src/prompts/")
print(" • Run the examples with: python examples/modular_demo.py")
except KeyboardInterrupt:
print("\n👋 Demo interrupted by user")
except Exception as e:
print(f"❌ Error running demo: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
if __name__ == "__main__":
asyncio.run(main())
+47
View File
@@ -0,0 +1,47 @@
"""
Modular Server Example
"""
import sys
import os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))
from mcp_template.server.modular_server import ModularMCPServer
def main():
"""Run the modular server example"""
# Create a modular server
server = ModularMCPServer(
name="Modular Demo Server",
host="0.0.0.0",
port=8050,
stateless_http=True
)
# Print server info
print("Server Information:")
print("=" * 50)
info = server.get_server_info()
print(f"Name: {info['name']}")
print(f"Host: {info['host']}:{info['port']}")
print(f"Tools: {info['tools']['count']} ({', '.join(info['tools']['names'])})")
print(f"Prompts: {info['prompts']['count']} ({', '.join(info['prompts']['names'])})")
print(f"Resources: {info['resources']['count']} ({', '.join(info['resources']['uris'])})")
print("=" * 50)
# Choose transport
transport = "stdio" # Change to "sse" for HTTP transport
if transport == "stdio":
print("Running server with STDIO transport")
print("Connect using MCP client with stdio transport")
elif transport == "sse":
print(f"Running server with SSE transport on http://{server.host}:{server.port}")
print("Connect using MCP client with SSE transport")
# Run the server
server.run(transport=transport)
if __name__ == "__main__":
main()
+94
View File
@@ -0,0 +1,94 @@
#!/usr/bin/env python3
"""
Simple test script for LLM clients using config.py
"""
import asyncio
import sys
import os
# Add the src directory to the path so we can import our modules
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))
from config import Config
from llm_client.client_factory import AIClientFactory
async def test_openai_client():
"""Test OpenAI client with config.py"""
print("Testing OpenAI client with config.py...")
if not Config.OPENAI_API_KEY:
print("⚠️ OPENAI_API_KEY not found in environment variables")
return
try:
# Create OpenAI client using the factory (API key automatically loaded from config)
client = AIClientFactory.create_openai_client(
model_name="gpt-3.5-turbo" # Use cheaper model for testing
)
# Test basic chat completion
messages = [{"role": "user", "content": "Hello! Please respond with just 'Hello from OpenAI!'"}]
response = await client.chat_completion(messages)
print("✅ OpenAI client test successful!")
print(f"Response: {response['choices'][0]['message']['content']}")
await client.cleanup()
except Exception as e:
print(f"❌ OpenAI client test failed: {e}")
async def test_client_factory():
"""Test client factory functionality"""
print("\nTesting client factory...")
# Test available providers
providers = AIClientFactory.get_available_providers()
print(f"Available providers: {providers}")
# Test provider validation
print(f"OpenAI valid: {AIClientFactory.validate_provider('openai')}")
print(f"Invalid provider valid: {AIClientFactory.validate_provider('invalid')}")
# Test creating client by provider (API key loaded automatically)
if Config.OPENAI_API_KEY:
try:
client = AIClientFactory.create_client("openai", "gpt-3.5-turbo")
print("✅ Client creation by provider successful!")
await client.cleanup()
except Exception as e:
print(f"❌ Client creation by provider failed: {e}")
else:
print("⚠️ Skipping client creation test - no OpenAI API key")
print("✅ Client factory test completed!")
async def test_config_loading():
"""Test config.py loading"""
print("\nTesting config.py loading...")
print(f"OpenAI API Key loaded: {'Yes' if Config.OPENAI_API_KEY else 'No'}")
print(f"Claude API Key loaded: {'Yes' if Config.CLAUDE_API_KEY else 'No'}")
print(f"Grok API Key loaded: {'Yes' if Config.GROK_API_KEY else 'No'}")
print("✅ Config loading test completed!")
async def main():
"""Run all tests"""
print("🚀 Starting LLM Client Tests with config.py")
print("=" * 50)
await test_config_loading()
await test_client_factory()
await test_openai_client()
print("\n" + "=" * 50)
print("🏁 All tests completed!")
if __name__ == "__main__":
asyncio.run(main())
Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 KiB

+1
View File
@@ -0,0 +1 @@
OPENAI_API_KEY=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
+10
View File
@@ -0,0 +1,10 @@
# Python-generated files
__pycache__/
*.py[oc]
build/
dist/
wheels/
*.egg-info
# Virtual environments
.venv
+1
View File
@@ -0,0 +1 @@
3.13
+5
View File
@@ -0,0 +1,5 @@
from dotenv import load_dotenv
import os
load_dotenv()
class Config:
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
+295
View File
@@ -0,0 +1,295 @@
# Model Context Protocol (MCP) Developer Guide
## What is MCP?
MCP (Model Context Protocol) is an open-source standard for connecting AI applications to external systems. Using MCP, AI applications like Claude or ChatGPT can connect to data sources (local files, databases), tools (search engines, calculators), and workflows (specialized prompts)—enabling them to access key information and perform tasks.
Think of MCP like a USB-C port for AI applications. Just as USB-C provides a standardized way to connect electronic devices, MCP provides a standardized way to connect AI applications to external systems.
## MCP Overview
![MCP Introduction](images/mcp_intro.png)
The diagram above illustrates how MCP creates a standardized interface between AI models and external tools/data sources through MCP servers that act as intermediaries.
## Core MCP Concepts
MCP servers can provide three main types of capabilities that enable AI applications to interact with external systems:
### 🔧 Tools
**Functions that can be called by the LLM (with user approval)**
Tools are the primary way AI models perform actions and computations. They represent executable functions that the AI can invoke to:
- **Perform calculations** (like our calculator example)
- **Execute system commands**
- **Query databases**
- **Interact with APIs**
- **Process data**
**Example Implementation:**
```python
@mcp.tool()
def add(a: int, b: int) -> int:
"""Add two numbers together"""
return a + b
```
**Key Characteristics:**
- Require explicit user approval before execution
- Can have complex input/output schemas
- Enable AI to perform real-world actions
- Primary focus of this tutorial
### 📄 Resources
**File-like data that can be read by clients**
Resources represent data sources that AI can access and read, similar to files or API responses:
- **File contents** (local or remote files)
- **Database records**
- **API responses**
- **Configuration data**
- **Log files**
**Example Use Cases:**
- Reading documentation files
- Accessing configuration settings
- Querying data from databases
- Retrieving API responses
### 📝 Prompts
**Pre-written templates that help users accomplish specific tasks**
Prompts are reusable templates that guide AI behavior for specific scenarios:
- **Task-specific instructions**
- **Workflow templates**
- **Specialized conversation starters**
- **Domain-specific guidance**
**Example Applications:**
- Code review templates
- Customer service response formats
- Technical writing guidelines
- Analysis frameworks
## Transport Protocols
MCP supports multiple transport protocols for communication between clients and servers:
### 1. Server-Sent Events (SSE)
- **Use Case**: Web-based applications, remote connections
- **Communication**: HTTP-based, unidirectional from server to client
- **Implementation**: Uses HTTP endpoints for real-time communication
- **Port**: Configurable (default: 8050)
### 2. Standard Input/Output (STDIO)
- **Use Case**: Local applications, direct process communication
- **Communication**: Direct process pipes, bidirectional
- **Implementation**: Launches server as subprocess with stdin/stdout pipes
- **Setup**: No network configuration required
![Setup Mechanism](images/steup_mechanism.png)
The setup mechanism diagram shows how different transport protocols handle the connection establishment between MCP clients and servers.
## Project Structure
This repository contains example implementations demonstrating MCP concepts:
### `intro_test/` - Basic MCP Implementation
This folder provides fundamental MCP server and client examples:
- **`server.py`** - MCP server with calculator tool using FastMCP
- **`client_sse.py`** - Client connecting via SSE transport
- **`client_stdio.py`** - Client connecting via STDIO transport
- **`main.py`** - Main entry point for testing
- **`Config.py`** - Configuration management
**Key Features:**
- Simple calculator tool (`add` function)
- Support for both SSE and STDIO transports
- Environment variable configuration
- FastMCP framework usage
### `intro_test/openai_test/` - OpenAI Integration
This folder demonstrates MCP integration with OpenAI:
- **`client.py`** - MCP-OpenAI client class with tool calling
- **`server.py`** - MCP server for the integration example
- **`data/kb.json`** - Knowledge base data for AI responses
**Key Features:**
- ✅ Seamless OpenAI API integration
- ✅ Automatic tool discovery and execution
- ✅ Conversation management with tool results
- ✅ Error handling and cleanup
- ✅ Knowledge base tool with JSON data integration
## Quick Start
### Prerequisites
- Python 3.13+
- OpenAI API key (for OpenAI integration examples)
### Installation
```bash
# Clone the repository
git clone <repository-url>
cd mcp_template
# Navigate to intro_test directory
cd intro_test
# Install dependencies using uv (recommended)
uv sync
# Or using pip (if you prefer)
pip install -e .
```
### Environment Setup
Create a `.env` file in the project root:
```bash
# Create .env file
touch .env
```
Then add your OpenAI API key to the `.env` file:
```env
OPENAI_API_KEY=your_openai_api_key_here
```
**Note:** Get your API key from [OpenAI Platform](https://platform.openai.com/api-keys)
### Running Basic Examples
1. **Start the MCP Server (SSE Transport):**
```bash
cd intro_test
uv run server.py
```
2. **Run the SSE Client:**
```bash
uv run client_sse.py
```
3. **Run the STDIO Client:**
```bash
uv run client_stdio.py
```
### OpenAI Integration Example
1. **Start the MCP Server:**
```bash
cd intro_test
uv run python openai_test/server.py
```
2. **Run the OpenAI Client:**
```bash
cd intro_test
uv run python openai_test/client.py
```
**Note:** The OpenAI integration examples must be run from the `intro_test` directory due to relative import dependencies.
## Understanding the Code
### Server Implementation
The server uses FastMCP framework to create MCP-compatible servers:
```python
from mcp.server.fastmcp import FastMCP
mcp = FastMCP(
name="Calculator",
host="0.0.0.0",
port=8050,
stateless_http=True,
)
@mcp.tool()
def add(a: int, b: int) -> int:
"""Add two numbers together"""
return a + b
# Run with different transports
mcp.run(transport="sse") # or "stdio" or "streamable-http"
```
### Client Implementation
Clients connect to servers using different transport methods:
**SSE Client:**
```python
async with sse_client("http://localhost:8050/sse") as (read_stream, write_stream):
async with ClientSession(read_stream, write_stream) as session:
await session.initialize()
# Use tools...
```
**STDIO Client:**
```python
server_params = StdioServerParameters(
command="python",
args=["server.py"],
)
async with stdio_client(server_params) as (read_stream, write_stream):
async with ClientSession(read_stream, write_stream) as session:
# Use tools...
```
### OpenAI Integration
The OpenAI client bridges MCP tools with OpenAI's function calling:
```python
# Get MCP tools in OpenAI format
tools = await self.get_mcp_tools()
# Make OpenAI API call with tools
response = await self.openai_client.chat.completions.create(
model=self.model,
messages=[{"role": "user", "content": query}],
tools=tools,
tool_choice="auto",
)
```
## Next Steps
This guide covers the fundamentals of MCP development. The examples in this repository demonstrate:
- ✅ Basic server and client setup
- ✅ Multiple transport protocol implementations
- ✅ OpenAI integration patterns
- ✅ Tool definition and execution
- ✅ Error handling and cleanup
Future enhancements may include:
- Additional transport protocols
- Advanced tool implementations
- Database integrations
- Webhook support
- Authentication mechanisms
---
*This documentation will be updated as new features and examples are added to the repository.*
URL: https://modelcontextprotocol.io/docs/learn/architecture
+38
View File
@@ -0,0 +1,38 @@
import asyncio
import nest_asyncio
from mcp import ClientSession
from mcp.client.sse import sse_client
nest_asyncio.apply() # Needed to run interactive python
"""
Make sure:
1. The server is running before running this script.
2. The server is configured to use SSE transport.
3. The server is listening on port 8050.
To run the server:
uv run server.py
"""
async def main():
# Connect to the server using SSE
async with sse_client("http://localhost:8050/sse") as (read_stream, write_stream):
async with ClientSession(read_stream, write_stream) as session:
# Initialize the connection
await session.initialize()
# List available tools
tools_result = await session.list_tools()
print("Available tools:")
for tool in tools_result.tools:
print(f" - {tool.name}: {tool.description}")
# Call our calculator tool
result = await session.call_tool("add", arguments={"a": 2, "b": 3})
print(f"2 + 3 = {result.content[0].text}")
if __name__ == "__main__":
asyncio.run(main())
+34
View File
@@ -0,0 +1,34 @@
import asyncio
import nest_asyncio
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
nest_asyncio.apply() # Needed to run interactive python
async def main():
# Define server parameters
server_params = StdioServerParameters(
command="python", # The command to run your server
args=["server.py"], # Arguments to the command
)
# Connect to the server
async with stdio_client(server_params) as (read_stream, write_stream):
async with ClientSession(read_stream, write_stream) as session:
# Initialize the connection
await session.initialize()
# List available tools
tools_result = await session.list_tools()
print("Available tools:")
for tool in tools_result.tools:
print(f" - {tool.name}: {tool.description}")
# Call our calculator tool
result = await session.call_tool("add", arguments={"a": 2, "b": 3})
print(f"2 + 3 = {result.content[0].text}")
if __name__ == "__main__":
asyncio.run(main())
+6
View File
@@ -0,0 +1,6 @@
def main():
print("Hello from intro-test!")
if __name__ == "__main__":
main()
+176
View File
@@ -0,0 +1,176 @@
import asyncio
import json
import sys
import os
from contextlib import AsyncExitStack
from typing import Any, Dict, List, Optional
import nest_asyncio
from dotenv import load_dotenv
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
from openai import AsyncOpenAI
# Add parent directory to path to import Config
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from Config import Config
# Apply nest_asyncio to allow nested event loops (needed for Jupyter/IPython)
nest_asyncio.apply()
class MCPOpenAIClient:
"""Client for interacting with OpenAI models using MCP tools."""
def __init__(self, model: str = "gpt-4o"):
"""Initialize the OpenAI MCP client.
Args:
model: The OpenAI model to use.
"""
# Initialize session and client objects
self.session: Optional[ClientSession] = None
self.exit_stack = AsyncExitStack()
self.openai_client = AsyncOpenAI(api_key=Config.OPENAI_API_KEY)
self.model = model
self.stdio: Optional[Any] = None
self.write: Optional[Any] = None
async def connect_to_server(self, server_script_path: str = "server.py"):
"""Connect to an MCP server.
Args:
server_script_path: Path to the server script.
"""
# Server configuration
server_params = StdioServerParameters(
command="python",
args=[server_script_path],
)
# Connect to the server
stdio_transport = await self.exit_stack.enter_async_context(
stdio_client(server_params)
)
self.stdio, self.write = stdio_transport
self.session = await self.exit_stack.enter_async_context(
ClientSession(self.stdio, self.write)
)
# Initialize the connection
await self.session.initialize()
# List available tools
tools_result = await self.session.list_tools()
print("\nConnected to server with tools:")
for tool in tools_result.tools:
print(f" - {tool.name}: {tool.description}")
async def get_mcp_tools(self) -> List[Dict[str, Any]]:
"""Get available tools from the MCP server in OpenAI format.
Returns:
A list of tools in OpenAI format.
"""
tools_result = await self.session.list_tools()
return [
{
"type": "function",
"function": {
"name": tool.name,
"description": tool.description,
"parameters": tool.inputSchema,
},
}
for tool in tools_result.tools
]
async def process_query(self, query: str) -> str:
"""Process a query using OpenAI and available MCP tools.
Args:
query: The user query.
Returns:
The response from OpenAI.
"""
# Get available tools
tools = await self.get_mcp_tools()
# Initial OpenAI API call
response = await self.openai_client.chat.completions.create(
model=self.model,
messages=[{"role": "user", "content": query}],
tools=tools,
tool_choice="auto",
)
# Get assistant's response
assistant_message = response.choices[0].message
# Initialize conversation with user query and assistant response
messages = [
{"role": "user", "content": query},
assistant_message,
]
# Handle tool calls if present
if assistant_message.tool_calls:
# Process each tool call
for tool_call in assistant_message.tool_calls:
# Execute tool call
result = await self.session.call_tool(
tool_call.function.name,
arguments=json.loads(tool_call.function.arguments),
)
# Add tool response to conversation
messages.append(
{
"role": "tool",
"tool_call_id": tool_call.id,
"content": result.content[0].text,
}
)
# Get final response from OpenAI with tool results
final_response = await self.openai_client.chat.completions.create(
model=self.model,
messages=messages,
tools=tools,
tool_choice="none", # Don't allow more tool calls
)
return final_response.choices[0].message.content
# No tool calls, just return the direct response
return assistant_message.content
async def cleanup(self):
"""Clean up resources."""
try:
await self.exit_stack.aclose()
except Exception as e:
# Ignore cleanup errors that don't affect functionality
pass
async def main():
"""Main entry point for the client."""
client = MCPOpenAIClient()
try:
await client.connect_to_server("server.py")
# Example: Ask about company vacation policy
query = "How many employees do we have?"
print(f"\nQuery: {query}")
response = await client.process_query(query)
print(f"\nResponse: {response}")
finally:
await client.cleanup()
if __name__ == "__main__":
asyncio.run(main())
+53
View File
@@ -0,0 +1,53 @@
import os
import json
from mcp.server.fastmcp import FastMCP
# Create an MCP server
mcp = FastMCP(
name="Knowledge Base",
host="0.0.0.0", # only used for SSE transport (localhost)
port=8050, # only used for SSE transport (set this to any port)
)
@mcp.tool()
def get_knowledge_base() -> str:
"""Retrieve the entire knowledge base as a formatted string.
Returns:
A formatted string containing all Q&A pairs from the knowledge base.
"""
try:
kb_path = os.path.join(os.path.dirname(__file__), "data", "kb.json")
with open(kb_path, "r") as f:
kb_data = json.load(f)
# Format the knowledge base as a string
kb_text = "Here is the retrieved knowledge base:\n\n"
if isinstance(kb_data, list):
for i, item in enumerate(kb_data, 1):
if isinstance(item, dict):
question = item.get("question", "Unknown question")
answer = item.get("answer", "Unknown answer")
else:
question = f"Item {i}"
answer = str(item)
kb_text += f"Q{i}: {question}\n"
kb_text += f"A{i}: {answer}\n\n"
else:
kb_text += f"Knowledge base content: {json.dumps(kb_data, indent=2)}\n\n"
return kb_text
except FileNotFoundError:
return "Error: Knowledge base file not found"
except json.JSONDecodeError:
return "Error: Invalid JSON in knowledge base file"
except Exception as e:
return f"Error: {str(e)}"
# Run the server
if __name__ == "__main__":
mcp.run(transport="stdio")
+13
View File
@@ -0,0 +1,13 @@
[project]
name = "intro-test"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
"mcp[cli]>=1.13.1",
"nest-asyncio",
"openai>=1.0.0",
"pytest>=8.4.2",
"python-dotenv",
]
+35
View File
@@ -0,0 +1,35 @@
from mcp.server.fastmcp import FastMCP
from dotenv import load_dotenv
load_dotenv("../.env")
# Create an MCP server
mcp = FastMCP(
name="Calculator",
host="0.0.0.0", # only used for SSE transport (localhost)
port=8050, # only used for SSE transport (set this to any port)
stateless_http=True,
)
# Add a simple calculator tool
@mcp.tool()
def add(a: int, b: int) -> int:
"""Add two numbers together"""
return a + b
# Run the server
if __name__ == "__main__":
transport = "sse"
if transport == "stdio":
print("Running server with stdio transport")
mcp.run(transport="stdio")
elif transport == "sse":
print("Running server with SSE transport")
mcp.run(transport="sse")
elif transport == "streamable-http":
print("Running server with Streamable HTTP transport")
mcp.run(transport="streamable-http")
else:
raise ValueError(f"Unknown transport: {transport}")
+624
View File
@@ -0,0 +1,624 @@
version = 1
revision = 2
requires-python = ">=3.13"
[[package]]
name = "annotated-types"
version = "0.7.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
]
[[package]]
name = "anyio"
version = "4.10.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "idna" },
{ name = "sniffio" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f1/b4/636b3b65173d3ce9a38ef5f0522789614e590dab6a8d505340a4efe4c567/anyio-4.10.0.tar.gz", hash = "sha256:3f3fae35c96039744587aa5b8371e7e8e603c0702999535961dd336026973ba6", size = 213252, upload-time = "2025-08-04T08:54:26.451Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6f/12/e5e0282d673bb9746bacfb6e2dba8719989d3660cdb2ea79aee9a9651afb/anyio-4.10.0-py3-none-any.whl", hash = "sha256:60e474ac86736bbfd6f210f7a61218939c318f43f9972497381f1c5e930ed3d1", size = 107213, upload-time = "2025-08-04T08:54:24.882Z" },
]
[[package]]
name = "attrs"
version = "25.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032, upload-time = "2025-03-13T11:10:22.779Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload-time = "2025-03-13T11:10:21.14Z" },
]
[[package]]
name = "certifi"
version = "2025.8.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload-time = "2025-08-03T03:07:47.08Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" },
]
[[package]]
name = "click"
version = "8.2.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" },
]
[[package]]
name = "colorama"
version = "0.4.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
]
[[package]]
name = "distro"
version = "1.9.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" },
]
[[package]]
name = "h11"
version = "0.16.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
]
[[package]]
name = "httpcore"
version = "1.0.9"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "h11" },
]
sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
]
[[package]]
name = "httpx"
version = "0.28.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "certifi" },
{ name = "httpcore" },
{ name = "idna" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
]
[[package]]
name = "httpx-sse"
version = "0.4.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/6e/fa/66bd985dd0b7c109a3bcb89272ee0bfb7e2b4d06309ad7b38ff866734b2a/httpx_sse-0.4.1.tar.gz", hash = "sha256:8f44d34414bc7b21bf3602713005c5df4917884f76072479b21f68befa4ea26e", size = 12998, upload-time = "2025-06-24T13:21:05.71Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/25/0a/6269e3473b09aed2dab8aa1a600c70f31f00ae1349bee30658f7e358a159/httpx_sse-0.4.1-py3-none-any.whl", hash = "sha256:cba42174344c3a5b06f255ce65b350880f962d99ead85e776f23c6618a377a37", size = 8054, upload-time = "2025-06-24T13:21:04.772Z" },
]
[[package]]
name = "idna"
version = "3.10"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
]
[[package]]
name = "iniconfig"
version = "2.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" },
]
[[package]]
name = "intro-test"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "mcp", extra = ["cli"] },
{ name = "nest-asyncio" },
{ name = "openai" },
{ name = "pytest" },
{ name = "python-dotenv" },
]
[package.metadata]
requires-dist = [
{ name = "mcp", extras = ["cli"], specifier = ">=1.13.1" },
{ name = "nest-asyncio" },
{ name = "openai", specifier = ">=1.0.0" },
{ name = "pytest", specifier = ">=8.4.2" },
{ name = "python-dotenv" },
]
[[package]]
name = "jiter"
version = "0.10.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ee/9d/ae7ddb4b8ab3fb1b51faf4deb36cb48a4fbbd7cb36bad6a5fca4741306f7/jiter-0.10.0.tar.gz", hash = "sha256:07a7142c38aacc85194391108dc91b5b57093c978a9932bd86a36862759d9500", size = 162759, upload-time = "2025-05-18T19:04:59.73Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2e/b0/279597e7a270e8d22623fea6c5d4eeac328e7d95c236ed51a2b884c54f70/jiter-0.10.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e0588107ec8e11b6f5ef0e0d656fb2803ac6cf94a96b2b9fc675c0e3ab5e8644", size = 311617, upload-time = "2025-05-18T19:04:02.078Z" },
{ url = "https://files.pythonhosted.org/packages/91/e3/0916334936f356d605f54cc164af4060e3e7094364add445a3bc79335d46/jiter-0.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cafc4628b616dc32530c20ee53d71589816cf385dd9449633e910d596b1f5c8a", size = 318947, upload-time = "2025-05-18T19:04:03.347Z" },
{ url = "https://files.pythonhosted.org/packages/6a/8e/fd94e8c02d0e94539b7d669a7ebbd2776e51f329bb2c84d4385e8063a2ad/jiter-0.10.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:520ef6d981172693786a49ff5b09eda72a42e539f14788124a07530f785c3ad6", size = 344618, upload-time = "2025-05-18T19:04:04.709Z" },
{ url = "https://files.pythonhosted.org/packages/6f/b0/f9f0a2ec42c6e9c2e61c327824687f1e2415b767e1089c1d9135f43816bd/jiter-0.10.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:554dedfd05937f8fc45d17ebdf298fe7e0c77458232bcb73d9fbbf4c6455f5b3", size = 368829, upload-time = "2025-05-18T19:04:06.912Z" },
{ url = "https://files.pythonhosted.org/packages/e8/57/5bbcd5331910595ad53b9fd0c610392ac68692176f05ae48d6ce5c852967/jiter-0.10.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5bc299da7789deacf95f64052d97f75c16d4fc8c4c214a22bf8d859a4288a1c2", size = 491034, upload-time = "2025-05-18T19:04:08.222Z" },
{ url = "https://files.pythonhosted.org/packages/9b/be/c393df00e6e6e9e623a73551774449f2f23b6ec6a502a3297aeeece2c65a/jiter-0.10.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5161e201172de298a8a1baad95eb85db4fb90e902353b1f6a41d64ea64644e25", size = 388529, upload-time = "2025-05-18T19:04:09.566Z" },
{ url = "https://files.pythonhosted.org/packages/42/3e/df2235c54d365434c7f150b986a6e35f41ebdc2f95acea3036d99613025d/jiter-0.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e2227db6ba93cb3e2bf67c87e594adde0609f146344e8207e8730364db27041", size = 350671, upload-time = "2025-05-18T19:04:10.98Z" },
{ url = "https://files.pythonhosted.org/packages/c6/77/71b0b24cbcc28f55ab4dbfe029f9a5b73aeadaba677843fc6dc9ed2b1d0a/jiter-0.10.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:15acb267ea5e2c64515574b06a8bf393fbfee6a50eb1673614aa45f4613c0cca", size = 390864, upload-time = "2025-05-18T19:04:12.722Z" },
{ url = "https://files.pythonhosted.org/packages/6a/d3/ef774b6969b9b6178e1d1e7a89a3bd37d241f3d3ec5f8deb37bbd203714a/jiter-0.10.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:901b92f2e2947dc6dfcb52fd624453862e16665ea909a08398dde19c0731b7f4", size = 522989, upload-time = "2025-05-18T19:04:14.261Z" },
{ url = "https://files.pythonhosted.org/packages/0c/41/9becdb1d8dd5d854142f45a9d71949ed7e87a8e312b0bede2de849388cb9/jiter-0.10.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d0cb9a125d5a3ec971a094a845eadde2db0de85b33c9f13eb94a0c63d463879e", size = 513495, upload-time = "2025-05-18T19:04:15.603Z" },
{ url = "https://files.pythonhosted.org/packages/9c/36/3468e5a18238bdedae7c4d19461265b5e9b8e288d3f86cd89d00cbb48686/jiter-0.10.0-cp313-cp313-win32.whl", hash = "sha256:48a403277ad1ee208fb930bdf91745e4d2d6e47253eedc96e2559d1e6527006d", size = 211289, upload-time = "2025-05-18T19:04:17.541Z" },
{ url = "https://files.pythonhosted.org/packages/7e/07/1c96b623128bcb913706e294adb5f768fb7baf8db5e1338ce7b4ee8c78ef/jiter-0.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:75f9eb72ecb640619c29bf714e78c9c46c9c4eaafd644bf78577ede459f330d4", size = 205074, upload-time = "2025-05-18T19:04:19.21Z" },
{ url = "https://files.pythonhosted.org/packages/54/46/caa2c1342655f57d8f0f2519774c6d67132205909c65e9aa8255e1d7b4f4/jiter-0.10.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:28ed2a4c05a1f32ef0e1d24c2611330219fed727dae01789f4a335617634b1ca", size = 318225, upload-time = "2025-05-18T19:04:20.583Z" },
{ url = "https://files.pythonhosted.org/packages/43/84/c7d44c75767e18946219ba2d703a5a32ab37b0bc21886a97bc6062e4da42/jiter-0.10.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14a4c418b1ec86a195f1ca69da8b23e8926c752b685af665ce30777233dfe070", size = 350235, upload-time = "2025-05-18T19:04:22.363Z" },
{ url = "https://files.pythonhosted.org/packages/01/16/f5a0135ccd968b480daad0e6ab34b0c7c5ba3bc447e5088152696140dcb3/jiter-0.10.0-cp313-cp313t-win_amd64.whl", hash = "sha256:d7bfed2fe1fe0e4dda6ef682cee888ba444b21e7a6553e03252e4feb6cf0adca", size = 207278, upload-time = "2025-05-18T19:04:23.627Z" },
{ url = "https://files.pythonhosted.org/packages/1c/9b/1d646da42c3de6c2188fdaa15bce8ecb22b635904fc68be025e21249ba44/jiter-0.10.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:5e9251a5e83fab8d87799d3e1a46cb4b7f2919b895c6f4483629ed2446f66522", size = 310866, upload-time = "2025-05-18T19:04:24.891Z" },
{ url = "https://files.pythonhosted.org/packages/ad/0e/26538b158e8a7c7987e94e7aeb2999e2e82b1f9d2e1f6e9874ddf71ebda0/jiter-0.10.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:023aa0204126fe5b87ccbcd75c8a0d0261b9abdbbf46d55e7ae9f8e22424eeb8", size = 318772, upload-time = "2025-05-18T19:04:26.161Z" },
{ url = "https://files.pythonhosted.org/packages/7b/fb/d302893151caa1c2636d6574d213e4b34e31fd077af6050a9c5cbb42f6fb/jiter-0.10.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c189c4f1779c05f75fc17c0c1267594ed918996a231593a21a5ca5438445216", size = 344534, upload-time = "2025-05-18T19:04:27.495Z" },
{ url = "https://files.pythonhosted.org/packages/01/d8/5780b64a149d74e347c5128d82176eb1e3241b1391ac07935693466d6219/jiter-0.10.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:15720084d90d1098ca0229352607cd68256c76991f6b374af96f36920eae13c4", size = 369087, upload-time = "2025-05-18T19:04:28.896Z" },
{ url = "https://files.pythonhosted.org/packages/e8/5b/f235a1437445160e777544f3ade57544daf96ba7e96c1a5b24a6f7ac7004/jiter-0.10.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e4f2fb68e5f1cfee30e2b2a09549a00683e0fde4c6a2ab88c94072fc33cb7426", size = 490694, upload-time = "2025-05-18T19:04:30.183Z" },
{ url = "https://files.pythonhosted.org/packages/85/a9/9c3d4617caa2ff89cf61b41e83820c27ebb3f7b5fae8a72901e8cd6ff9be/jiter-0.10.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ce541693355fc6da424c08b7edf39a2895f58d6ea17d92cc2b168d20907dee12", size = 388992, upload-time = "2025-05-18T19:04:32.028Z" },
{ url = "https://files.pythonhosted.org/packages/68/b1/344fd14049ba5c94526540af7eb661871f9c54d5f5601ff41a959b9a0bbd/jiter-0.10.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31c50c40272e189d50006ad5c73883caabb73d4e9748a688b216e85a9a9ca3b9", size = 351723, upload-time = "2025-05-18T19:04:33.467Z" },
{ url = "https://files.pythonhosted.org/packages/41/89/4c0e345041186f82a31aee7b9d4219a910df672b9fef26f129f0cda07a29/jiter-0.10.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fa3402a2ff9815960e0372a47b75c76979d74402448509ccd49a275fa983ef8a", size = 392215, upload-time = "2025-05-18T19:04:34.827Z" },
{ url = "https://files.pythonhosted.org/packages/55/58/ee607863e18d3f895feb802154a2177d7e823a7103f000df182e0f718b38/jiter-0.10.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:1956f934dca32d7bb647ea21d06d93ca40868b505c228556d3373cbd255ce853", size = 522762, upload-time = "2025-05-18T19:04:36.19Z" },
{ url = "https://files.pythonhosted.org/packages/15/d0/9123fb41825490d16929e73c212de9a42913d68324a8ce3c8476cae7ac9d/jiter-0.10.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:fcedb049bdfc555e261d6f65a6abe1d5ad68825b7202ccb9692636c70fcced86", size = 513427, upload-time = "2025-05-18T19:04:37.544Z" },
{ url = "https://files.pythonhosted.org/packages/d8/b3/2bd02071c5a2430d0b70403a34411fc519c2f227da7b03da9ba6a956f931/jiter-0.10.0-cp314-cp314-win32.whl", hash = "sha256:ac509f7eccca54b2a29daeb516fb95b6f0bd0d0d8084efaf8ed5dfc7b9f0b357", size = 210127, upload-time = "2025-05-18T19:04:38.837Z" },
{ url = "https://files.pythonhosted.org/packages/03/0c/5fe86614ea050c3ecd728ab4035534387cd41e7c1855ef6c031f1ca93e3f/jiter-0.10.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5ed975b83a2b8639356151cef5c0d597c68376fc4922b45d0eb384ac058cfa00", size = 318527, upload-time = "2025-05-18T19:04:40.612Z" },
{ url = "https://files.pythonhosted.org/packages/b3/4a/4175a563579e884192ba6e81725fc0448b042024419be8d83aa8a80a3f44/jiter-0.10.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3aa96f2abba33dc77f79b4cf791840230375f9534e5fac927ccceb58c5e604a5", size = 354213, upload-time = "2025-05-18T19:04:41.894Z" },
]
[[package]]
name = "jsonschema"
version = "4.25.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "attrs" },
{ name = "jsonschema-specifications" },
{ name = "referencing" },
{ name = "rpds-py" },
]
sdist = { url = "https://files.pythonhosted.org/packages/74/69/f7185de793a29082a9f3c7728268ffb31cb5095131a9c139a74078e27336/jsonschema-4.25.1.tar.gz", hash = "sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85", size = 357342, upload-time = "2025-08-18T17:03:50.038Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63", size = 90040, upload-time = "2025-08-18T17:03:48.373Z" },
]
[[package]]
name = "jsonschema-specifications"
version = "2025.9.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "referencing" },
]
sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" },
]
[[package]]
name = "markdown-it-py"
version = "4.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "mdurl" },
]
sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" },
]
[[package]]
name = "mcp"
version = "1.13.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "httpx" },
{ name = "httpx-sse" },
{ name = "jsonschema" },
{ name = "pydantic" },
{ name = "pydantic-settings" },
{ name = "python-multipart" },
{ name = "pywin32", marker = "sys_platform == 'win32'" },
{ name = "sse-starlette" },
{ name = "starlette" },
{ name = "uvicorn", marker = "sys_platform != 'emscripten'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/66/3c/82c400c2d50afdac4fbefb5b4031fd327e2ad1f23ccef8eee13c5909aa48/mcp-1.13.1.tar.gz", hash = "sha256:165306a8fd7991dc80334edd2de07798175a56461043b7ae907b279794a834c5", size = 438198, upload-time = "2025-08-22T09:22:16.061Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/19/3f/d085c7f49ade6d273b185d61ec9405e672b6433f710ea64a90135a8dd445/mcp-1.13.1-py3-none-any.whl", hash = "sha256:c314e7c8bd477a23ba3ef472ee5a32880316c42d03e06dcfa31a1cc7a73b65df", size = 161494, upload-time = "2025-08-22T09:22:14.705Z" },
]
[package.optional-dependencies]
cli = [
{ name = "python-dotenv" },
{ name = "typer" },
]
[[package]]
name = "mdurl"
version = "0.1.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
]
[[package]]
name = "nest-asyncio"
version = "1.6.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/83/f8/51569ac65d696c8ecbee95938f89d4abf00f47d58d48f6fbabfe8f0baefe/nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe", size = 7418, upload-time = "2024-01-21T14:25:19.227Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" },
]
[[package]]
name = "openai"
version = "1.107.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "distro" },
{ name = "httpx" },
{ name = "jiter" },
{ name = "pydantic" },
{ name = "sniffio" },
{ name = "tqdm" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f3/e0/a62daa7ff769df969cc1b782852cace79615039630b297005356f5fb46fb/openai-1.107.1.tar.gz", hash = "sha256:7c51b6b8adadfcf5cada08a613423575258b180af5ad4bc2954b36ebc0d3ad48", size = 563671, upload-time = "2025-09-10T15:04:40.288Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d4/12/32c19999a58eec4a695e8ce334442b6135df949f0bb61b2ceaa4fa60d3a9/openai-1.107.1-py3-none-any.whl", hash = "sha256:168f9885b1b70d13ada0868a0d0adfd538c16a02f7fd9fe063851a2c9a025e72", size = 945177, upload-time = "2025-09-10T15:04:37.782Z" },
]
[[package]]
name = "packaging"
version = "25.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
]
[[package]]
name = "pluggy"
version = "1.6.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
]
[[package]]
name = "pydantic"
version = "2.11.7"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "annotated-types" },
{ name = "pydantic-core" },
{ name = "typing-extensions" },
{ name = "typing-inspection" },
]
sdist = { url = "https://files.pythonhosted.org/packages/00/dd/4325abf92c39ba8623b5af936ddb36ffcfe0beae70405d456ab1fb2f5b8c/pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db", size = 788350, upload-time = "2025-06-14T08:33:17.137Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b", size = 444782, upload-time = "2025-06-14T08:33:14.905Z" },
]
[[package]]
name = "pydantic-core"
version = "2.33.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" },
{ url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" },
{ url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" },
{ url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" },
{ url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" },
{ url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" },
{ url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" },
{ url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" },
{ url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" },
{ url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" },
{ url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" },
{ url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" },
{ url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" },
{ url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" },
{ url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" },
{ url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" },
{ url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" },
]
[[package]]
name = "pydantic-settings"
version = "2.10.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pydantic" },
{ name = "python-dotenv" },
{ name = "typing-inspection" },
]
sdist = { url = "https://files.pythonhosted.org/packages/68/85/1ea668bbab3c50071ca613c6ab30047fb36ab0da1b92fa8f17bbc38fd36c/pydantic_settings-2.10.1.tar.gz", hash = "sha256:06f0062169818d0f5524420a360d632d5857b83cffd4d42fe29597807a1614ee", size = 172583, upload-time = "2025-06-24T13:26:46.841Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/58/f0/427018098906416f580e3cf1366d3b1abfb408a0652e9f31600c24a1903c/pydantic_settings-2.10.1-py3-none-any.whl", hash = "sha256:a60952460b99cf661dc25c29c0ef171721f98bfcb52ef8d9ea4c943d7c8cc796", size = 45235, upload-time = "2025-06-24T13:26:45.485Z" },
]
[[package]]
name = "pygments"
version = "2.19.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
]
[[package]]
name = "pytest"
version = "8.4.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "iniconfig" },
{ name = "packaging" },
{ name = "pluggy" },
{ name = "pygments" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" },
]
[[package]]
name = "python-dotenv"
version = "1.1.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f6/b0/4bc07ccd3572a2f9df7e6782f52b0c6c90dcbb803ac4a167702d7d0dfe1e/python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab", size = 41978, upload-time = "2025-06-24T04:21:07.341Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload-time = "2025-06-24T04:21:06.073Z" },
]
[[package]]
name = "python-multipart"
version = "0.0.20"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158, upload-time = "2024-12-16T19:45:46.972Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" },
]
[[package]]
name = "pywin32"
version = "311"
source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" },
{ url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" },
{ url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" },
{ url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" },
{ url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" },
{ url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" },
]
[[package]]
name = "referencing"
version = "0.36.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "attrs" },
{ name = "rpds-py" },
]
sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744, upload-time = "2025-01-25T08:48:16.138Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775, upload-time = "2025-01-25T08:48:14.241Z" },
]
[[package]]
name = "rich"
version = "14.1.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markdown-it-py" },
{ name = "pygments" },
]
sdist = { url = "https://files.pythonhosted.org/packages/fe/75/af448d8e52bf1d8fa6a9d089ca6c07ff4453d86c65c145d0a300bb073b9b/rich-14.1.0.tar.gz", hash = "sha256:e497a48b844b0320d45007cdebfeaeed8db2a4f4bcf49f15e455cfc4af11eaa8", size = 224441, upload-time = "2025-07-25T07:32:58.125Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e3/30/3c4d035596d3cf444529e0b2953ad0466f6049528a879d27534700580395/rich-14.1.0-py3-none-any.whl", hash = "sha256:536f5f1785986d6dbdea3c75205c473f970777b4a0d6c6dd1b696aa05a3fa04f", size = 243368, upload-time = "2025-07-25T07:32:56.73Z" },
]
[[package]]
name = "rpds-py"
version = "0.27.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e9/dd/2c0cbe774744272b0ae725f44032c77bdcab6e8bcf544bffa3b6e70c8dba/rpds_py-0.27.1.tar.gz", hash = "sha256:26a1c73171d10b7acccbded82bf6a586ab8203601e565badc74bbbf8bc5a10f8", size = 27479, upload-time = "2025-08-27T12:16:36.024Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cc/77/610aeee8d41e39080c7e14afa5387138e3c9fa9756ab893d09d99e7d8e98/rpds_py-0.27.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e4b9fcfbc021633863a37e92571d6f91851fa656f0180246e84cbd8b3f6b329b", size = 361741, upload-time = "2025-08-27T12:13:31.039Z" },
{ url = "https://files.pythonhosted.org/packages/3a/fc/c43765f201c6a1c60be2043cbdb664013def52460a4c7adace89d6682bf4/rpds_py-0.27.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1441811a96eadca93c517d08df75de45e5ffe68aa3089924f963c782c4b898cf", size = 345574, upload-time = "2025-08-27T12:13:32.902Z" },
{ url = "https://files.pythonhosted.org/packages/20/42/ee2b2ca114294cd9847d0ef9c26d2b0851b2e7e00bf14cc4c0b581df0fc3/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55266dafa22e672f5a4f65019015f90336ed31c6383bd53f5e7826d21a0e0b83", size = 385051, upload-time = "2025-08-27T12:13:34.228Z" },
{ url = "https://files.pythonhosted.org/packages/fd/e8/1e430fe311e4799e02e2d1af7c765f024e95e17d651612425b226705f910/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d78827d7ac08627ea2c8e02c9e5b41180ea5ea1f747e9db0915e3adf36b62dcf", size = 398395, upload-time = "2025-08-27T12:13:36.132Z" },
{ url = "https://files.pythonhosted.org/packages/82/95/9dc227d441ff2670651c27a739acb2535ccaf8b351a88d78c088965e5996/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae92443798a40a92dc5f0b01d8a7c93adde0c4dc965310a29ae7c64d72b9fad2", size = 524334, upload-time = "2025-08-27T12:13:37.562Z" },
{ url = "https://files.pythonhosted.org/packages/87/01/a670c232f401d9ad461d9a332aa4080cd3cb1d1df18213dbd0d2a6a7ab51/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c46c9dd2403b66a2a3b9720ec4b74d4ab49d4fabf9f03dfdce2d42af913fe8d0", size = 407691, upload-time = "2025-08-27T12:13:38.94Z" },
{ url = "https://files.pythonhosted.org/packages/03/36/0a14aebbaa26fe7fab4780c76f2239e76cc95a0090bdb25e31d95c492fcd/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2efe4eb1d01b7f5f1939f4ef30ecea6c6b3521eec451fb93191bf84b2a522418", size = 386868, upload-time = "2025-08-27T12:13:40.192Z" },
{ url = "https://files.pythonhosted.org/packages/3b/03/8c897fb8b5347ff6c1cc31239b9611c5bf79d78c984430887a353e1409a1/rpds_py-0.27.1-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:15d3b4d83582d10c601f481eca29c3f138d44c92187d197aff663a269197c02d", size = 405469, upload-time = "2025-08-27T12:13:41.496Z" },
{ url = "https://files.pythonhosted.org/packages/da/07/88c60edc2df74850d496d78a1fdcdc7b54360a7f610a4d50008309d41b94/rpds_py-0.27.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4ed2e16abbc982a169d30d1a420274a709949e2cbdef119fe2ec9d870b42f274", size = 422125, upload-time = "2025-08-27T12:13:42.802Z" },
{ url = "https://files.pythonhosted.org/packages/6b/86/5f4c707603e41b05f191a749984f390dabcbc467cf833769b47bf14ba04f/rpds_py-0.27.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a75f305c9b013289121ec0f1181931975df78738cdf650093e6b86d74aa7d8dd", size = 562341, upload-time = "2025-08-27T12:13:44.472Z" },
{ url = "https://files.pythonhosted.org/packages/b2/92/3c0cb2492094e3cd9baf9e49bbb7befeceb584ea0c1a8b5939dca4da12e5/rpds_py-0.27.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:67ce7620704745881a3d4b0ada80ab4d99df390838839921f99e63c474f82cf2", size = 592511, upload-time = "2025-08-27T12:13:45.898Z" },
{ url = "https://files.pythonhosted.org/packages/10/bb/82e64fbb0047c46a168faa28d0d45a7851cd0582f850b966811d30f67ad8/rpds_py-0.27.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9d992ac10eb86d9b6f369647b6a3f412fc0075cfd5d799530e84d335e440a002", size = 557736, upload-time = "2025-08-27T12:13:47.408Z" },
{ url = "https://files.pythonhosted.org/packages/00/95/3c863973d409210da7fb41958172c6b7dbe7fc34e04d3cc1f10bb85e979f/rpds_py-0.27.1-cp313-cp313-win32.whl", hash = "sha256:4f75e4bd8ab8db624e02c8e2fc4063021b58becdbe6df793a8111d9343aec1e3", size = 221462, upload-time = "2025-08-27T12:13:48.742Z" },
{ url = "https://files.pythonhosted.org/packages/ce/2c/5867b14a81dc217b56d95a9f2a40fdbc56a1ab0181b80132beeecbd4b2d6/rpds_py-0.27.1-cp313-cp313-win_amd64.whl", hash = "sha256:f9025faafc62ed0b75a53e541895ca272815bec18abe2249ff6501c8f2e12b83", size = 232034, upload-time = "2025-08-27T12:13:50.11Z" },
{ url = "https://files.pythonhosted.org/packages/c7/78/3958f3f018c01923823f1e47f1cc338e398814b92d83cd278364446fac66/rpds_py-0.27.1-cp313-cp313-win_arm64.whl", hash = "sha256:ed10dc32829e7d222b7d3b93136d25a406ba9788f6a7ebf6809092da1f4d279d", size = 222392, upload-time = "2025-08-27T12:13:52.587Z" },
{ url = "https://files.pythonhosted.org/packages/01/76/1cdf1f91aed5c3a7bf2eba1f1c4e4d6f57832d73003919a20118870ea659/rpds_py-0.27.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:92022bbbad0d4426e616815b16bc4127f83c9a74940e1ccf3cfe0b387aba0228", size = 358355, upload-time = "2025-08-27T12:13:54.012Z" },
{ url = "https://files.pythonhosted.org/packages/c3/6f/bf142541229374287604caf3bb2a4ae17f0a580798fd72d3b009b532db4e/rpds_py-0.27.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:47162fdab9407ec3f160805ac3e154df042e577dd53341745fc7fb3f625e6d92", size = 342138, upload-time = "2025-08-27T12:13:55.791Z" },
{ url = "https://files.pythonhosted.org/packages/1a/77/355b1c041d6be40886c44ff5e798b4e2769e497b790f0f7fd1e78d17e9a8/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb89bec23fddc489e5d78b550a7b773557c9ab58b7946154a10a6f7a214a48b2", size = 380247, upload-time = "2025-08-27T12:13:57.683Z" },
{ url = "https://files.pythonhosted.org/packages/d6/a4/d9cef5c3946ea271ce2243c51481971cd6e34f21925af2783dd17b26e815/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e48af21883ded2b3e9eb48cb7880ad8598b31ab752ff3be6457001d78f416723", size = 390699, upload-time = "2025-08-27T12:13:59.137Z" },
{ url = "https://files.pythonhosted.org/packages/3a/06/005106a7b8c6c1a7e91b73169e49870f4af5256119d34a361ae5240a0c1d/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6f5b7bd8e219ed50299e58551a410b64daafb5017d54bbe822e003856f06a802", size = 521852, upload-time = "2025-08-27T12:14:00.583Z" },
{ url = "https://files.pythonhosted.org/packages/e5/3e/50fb1dac0948e17a02eb05c24510a8fe12d5ce8561c6b7b7d1339ab7ab9c/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08f1e20bccf73b08d12d804d6e1c22ca5530e71659e6673bce31a6bb71c1e73f", size = 402582, upload-time = "2025-08-27T12:14:02.034Z" },
{ url = "https://files.pythonhosted.org/packages/cb/b0/f4e224090dc5b0ec15f31a02d746ab24101dd430847c4d99123798661bfc/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dc5dceeaefcc96dc192e3a80bbe1d6c410c469e97bdd47494a7d930987f18b2", size = 384126, upload-time = "2025-08-27T12:14:03.437Z" },
{ url = "https://files.pythonhosted.org/packages/54/77/ac339d5f82b6afff1df8f0fe0d2145cc827992cb5f8eeb90fc9f31ef7a63/rpds_py-0.27.1-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:d76f9cc8665acdc0c9177043746775aa7babbf479b5520b78ae4002d889f5c21", size = 399486, upload-time = "2025-08-27T12:14:05.443Z" },
{ url = "https://files.pythonhosted.org/packages/d6/29/3e1c255eee6ac358c056a57d6d6869baa00a62fa32eea5ee0632039c50a3/rpds_py-0.27.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:134fae0e36022edad8290a6661edf40c023562964efea0cc0ec7f5d392d2aaef", size = 414832, upload-time = "2025-08-27T12:14:06.902Z" },
{ url = "https://files.pythonhosted.org/packages/3f/db/6d498b844342deb3fa1d030598db93937a9964fcf5cb4da4feb5f17be34b/rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb11a4f1b2b63337cfd3b4d110af778a59aae51c81d195768e353d8b52f88081", size = 557249, upload-time = "2025-08-27T12:14:08.37Z" },
{ url = "https://files.pythonhosted.org/packages/60/f3/690dd38e2310b6f68858a331399b4d6dbb9132c3e8ef8b4333b96caf403d/rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:13e608ac9f50a0ed4faec0e90ece76ae33b34c0e8656e3dceb9a7db994c692cd", size = 587356, upload-time = "2025-08-27T12:14:10.034Z" },
{ url = "https://files.pythonhosted.org/packages/86/e3/84507781cccd0145f35b1dc32c72675200c5ce8d5b30f813e49424ef68fc/rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dd2135527aa40f061350c3f8f89da2644de26cd73e4de458e79606384f4f68e7", size = 555300, upload-time = "2025-08-27T12:14:11.783Z" },
{ url = "https://files.pythonhosted.org/packages/e5/ee/375469849e6b429b3516206b4580a79e9ef3eb12920ddbd4492b56eaacbe/rpds_py-0.27.1-cp313-cp313t-win32.whl", hash = "sha256:3020724ade63fe320a972e2ffd93b5623227e684315adce194941167fee02688", size = 216714, upload-time = "2025-08-27T12:14:13.629Z" },
{ url = "https://files.pythonhosted.org/packages/21/87/3fc94e47c9bd0742660e84706c311a860dcae4374cf4a03c477e23ce605a/rpds_py-0.27.1-cp313-cp313t-win_amd64.whl", hash = "sha256:8ee50c3e41739886606388ba3ab3ee2aae9f35fb23f833091833255a31740797", size = 228943, upload-time = "2025-08-27T12:14:14.937Z" },
{ url = "https://files.pythonhosted.org/packages/70/36/b6e6066520a07cf029d385de869729a895917b411e777ab1cde878100a1d/rpds_py-0.27.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:acb9aafccaae278f449d9c713b64a9e68662e7799dbd5859e2c6b3c67b56d334", size = 362472, upload-time = "2025-08-27T12:14:16.333Z" },
{ url = "https://files.pythonhosted.org/packages/af/07/b4646032e0dcec0df9c73a3bd52f63bc6c5f9cda992f06bd0e73fe3fbebd/rpds_py-0.27.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b7fb801aa7f845ddf601c49630deeeccde7ce10065561d92729bfe81bd21fb33", size = 345676, upload-time = "2025-08-27T12:14:17.764Z" },
{ url = "https://files.pythonhosted.org/packages/b0/16/2f1003ee5d0af4bcb13c0cf894957984c32a6751ed7206db2aee7379a55e/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe0dd05afb46597b9a2e11c351e5e4283c741237e7f617ffb3252780cca9336a", size = 385313, upload-time = "2025-08-27T12:14:19.829Z" },
{ url = "https://files.pythonhosted.org/packages/05/cd/7eb6dd7b232e7f2654d03fa07f1414d7dfc980e82ba71e40a7c46fd95484/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b6dfb0e058adb12d8b1d1b25f686e94ffa65d9995a5157afe99743bf7369d62b", size = 399080, upload-time = "2025-08-27T12:14:21.531Z" },
{ url = "https://files.pythonhosted.org/packages/20/51/5829afd5000ec1cb60f304711f02572d619040aa3ec033d8226817d1e571/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed090ccd235f6fa8bb5861684567f0a83e04f52dfc2e5c05f2e4b1309fcf85e7", size = 523868, upload-time = "2025-08-27T12:14:23.485Z" },
{ url = "https://files.pythonhosted.org/packages/05/2c/30eebca20d5db95720ab4d2faec1b5e4c1025c473f703738c371241476a2/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bf876e79763eecf3e7356f157540d6a093cef395b65514f17a356f62af6cc136", size = 408750, upload-time = "2025-08-27T12:14:24.924Z" },
{ url = "https://files.pythonhosted.org/packages/90/1a/cdb5083f043597c4d4276eae4e4c70c55ab5accec078da8611f24575a367/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:12ed005216a51b1d6e2b02a7bd31885fe317e45897de81d86dcce7d74618ffff", size = 387688, upload-time = "2025-08-27T12:14:27.537Z" },
{ url = "https://files.pythonhosted.org/packages/7c/92/cf786a15320e173f945d205ab31585cc43969743bb1a48b6888f7a2b0a2d/rpds_py-0.27.1-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:ee4308f409a40e50593c7e3bb8cbe0b4d4c66d1674a316324f0c2f5383b486f9", size = 407225, upload-time = "2025-08-27T12:14:28.981Z" },
{ url = "https://files.pythonhosted.org/packages/33/5c/85ee16df5b65063ef26017bef33096557a4c83fbe56218ac7cd8c235f16d/rpds_py-0.27.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0b08d152555acf1f455154d498ca855618c1378ec810646fcd7c76416ac6dc60", size = 423361, upload-time = "2025-08-27T12:14:30.469Z" },
{ url = "https://files.pythonhosted.org/packages/4b/8e/1c2741307fcabd1a334ecf008e92c4f47bb6f848712cf15c923becfe82bb/rpds_py-0.27.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:dce51c828941973a5684d458214d3a36fcd28da3e1875d659388f4f9f12cc33e", size = 562493, upload-time = "2025-08-27T12:14:31.987Z" },
{ url = "https://files.pythonhosted.org/packages/04/03/5159321baae9b2222442a70c1f988cbbd66b9be0675dd3936461269be360/rpds_py-0.27.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:c1476d6f29eb81aa4151c9a31219b03f1f798dc43d8af1250a870735516a1212", size = 592623, upload-time = "2025-08-27T12:14:33.543Z" },
{ url = "https://files.pythonhosted.org/packages/ff/39/c09fd1ad28b85bc1d4554a8710233c9f4cefd03d7717a1b8fbfd171d1167/rpds_py-0.27.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3ce0cac322b0d69b63c9cdb895ee1b65805ec9ffad37639f291dd79467bee675", size = 558800, upload-time = "2025-08-27T12:14:35.436Z" },
{ url = "https://files.pythonhosted.org/packages/c5/d6/99228e6bbcf4baa764b18258f519a9035131d91b538d4e0e294313462a98/rpds_py-0.27.1-cp314-cp314-win32.whl", hash = "sha256:dfbfac137d2a3d0725758cd141f878bf4329ba25e34979797c89474a89a8a3a3", size = 221943, upload-time = "2025-08-27T12:14:36.898Z" },
{ url = "https://files.pythonhosted.org/packages/be/07/c802bc6b8e95be83b79bdf23d1aa61d68324cb1006e245d6c58e959e314d/rpds_py-0.27.1-cp314-cp314-win_amd64.whl", hash = "sha256:a6e57b0abfe7cc513450fcf529eb486b6e4d3f8aee83e92eb5f1ef848218d456", size = 233739, upload-time = "2025-08-27T12:14:38.386Z" },
{ url = "https://files.pythonhosted.org/packages/c8/89/3e1b1c16d4c2d547c5717377a8df99aee8099ff050f87c45cb4d5fa70891/rpds_py-0.27.1-cp314-cp314-win_arm64.whl", hash = "sha256:faf8d146f3d476abfee026c4ae3bdd9ca14236ae4e4c310cbd1cf75ba33d24a3", size = 223120, upload-time = "2025-08-27T12:14:39.82Z" },
{ url = "https://files.pythonhosted.org/packages/62/7e/dc7931dc2fa4a6e46b2a4fa744a9fe5c548efd70e0ba74f40b39fa4a8c10/rpds_py-0.27.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:ba81d2b56b6d4911ce735aad0a1d4495e808b8ee4dc58715998741a26874e7c2", size = 358944, upload-time = "2025-08-27T12:14:41.199Z" },
{ url = "https://files.pythonhosted.org/packages/e6/22/4af76ac4e9f336bfb1a5f240d18a33c6b2fcaadb7472ac7680576512b49a/rpds_py-0.27.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:84f7d509870098de0e864cad0102711c1e24e9b1a50ee713b65928adb22269e4", size = 342283, upload-time = "2025-08-27T12:14:42.699Z" },
{ url = "https://files.pythonhosted.org/packages/1c/15/2a7c619b3c2272ea9feb9ade67a45c40b3eeb500d503ad4c28c395dc51b4/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9e960fc78fecd1100539f14132425e1d5fe44ecb9239f8f27f079962021523e", size = 380320, upload-time = "2025-08-27T12:14:44.157Z" },
{ url = "https://files.pythonhosted.org/packages/a2/7d/4c6d243ba4a3057e994bb5bedd01b5c963c12fe38dde707a52acdb3849e7/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:62f85b665cedab1a503747617393573995dac4600ff51869d69ad2f39eb5e817", size = 391760, upload-time = "2025-08-27T12:14:45.845Z" },
{ url = "https://files.pythonhosted.org/packages/b4/71/b19401a909b83bcd67f90221330bc1ef11bc486fe4e04c24388d28a618ae/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fed467af29776f6556250c9ed85ea5a4dd121ab56a5f8b206e3e7a4c551e48ec", size = 522476, upload-time = "2025-08-27T12:14:47.364Z" },
{ url = "https://files.pythonhosted.org/packages/e4/44/1a3b9715c0455d2e2f0f6df5ee6d6f5afdc423d0773a8a682ed2b43c566c/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2729615f9d430af0ae6b36cf042cb55c0936408d543fb691e1a9e36648fd35a", size = 403418, upload-time = "2025-08-27T12:14:49.991Z" },
{ url = "https://files.pythonhosted.org/packages/1c/4b/fb6c4f14984eb56673bc868a66536f53417ddb13ed44b391998100a06a96/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b207d881a9aef7ba753d69c123a35d96ca7cb808056998f6b9e8747321f03b8", size = 384771, upload-time = "2025-08-27T12:14:52.159Z" },
{ url = "https://files.pythonhosted.org/packages/c0/56/d5265d2d28b7420d7b4d4d85cad8ef891760f5135102e60d5c970b976e41/rpds_py-0.27.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:639fd5efec029f99b79ae47e5d7e00ad8a773da899b6309f6786ecaf22948c48", size = 400022, upload-time = "2025-08-27T12:14:53.859Z" },
{ url = "https://files.pythonhosted.org/packages/8f/e9/9f5fc70164a569bdd6ed9046486c3568d6926e3a49bdefeeccfb18655875/rpds_py-0.27.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fecc80cb2a90e28af8a9b366edacf33d7a91cbfe4c2c4544ea1246e949cfebeb", size = 416787, upload-time = "2025-08-27T12:14:55.673Z" },
{ url = "https://files.pythonhosted.org/packages/d4/64/56dd03430ba491db943a81dcdef115a985aac5f44f565cd39a00c766d45c/rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42a89282d711711d0a62d6f57d81aa43a1368686c45bc1c46b7f079d55692734", size = 557538, upload-time = "2025-08-27T12:14:57.245Z" },
{ url = "https://files.pythonhosted.org/packages/3f/36/92cc885a3129993b1d963a2a42ecf64e6a8e129d2c7cc980dbeba84e55fb/rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:cf9931f14223de59551ab9d38ed18d92f14f055a5f78c1d8ad6493f735021bbb", size = 588512, upload-time = "2025-08-27T12:14:58.728Z" },
{ url = "https://files.pythonhosted.org/packages/dd/10/6b283707780a81919f71625351182b4f98932ac89a09023cb61865136244/rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f39f58a27cc6e59f432b568ed8429c7e1641324fbe38131de852cd77b2d534b0", size = 555813, upload-time = "2025-08-27T12:15:00.334Z" },
{ url = "https://files.pythonhosted.org/packages/04/2e/30b5ea18c01379da6272a92825dd7e53dc9d15c88a19e97932d35d430ef7/rpds_py-0.27.1-cp314-cp314t-win32.whl", hash = "sha256:d5fa0ee122dc09e23607a28e6d7b150da16c662e66409bbe85230e4c85bb528a", size = 217385, upload-time = "2025-08-27T12:15:01.937Z" },
{ url = "https://files.pythonhosted.org/packages/32/7d/97119da51cb1dd3f2f3c0805f155a3aa4a95fa44fe7d78ae15e69edf4f34/rpds_py-0.27.1-cp314-cp314t-win_amd64.whl", hash = "sha256:6567d2bb951e21232c2f660c24cf3470bb96de56cdcb3f071a83feeaff8a2772", size = 230097, upload-time = "2025-08-27T12:15:03.961Z" },
]
[[package]]
name = "shellingham"
version = "1.5.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" },
]
[[package]]
name = "sniffio"
version = "1.3.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
]
[[package]]
name = "sse-starlette"
version = "3.0.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
]
sdist = { url = "https://files.pythonhosted.org/packages/42/6f/22ed6e33f8a9e76ca0a412405f31abb844b779d52c5f96660766edcd737c/sse_starlette-3.0.2.tar.gz", hash = "sha256:ccd60b5765ebb3584d0de2d7a6e4f745672581de4f5005ab31c3a25d10b52b3a", size = 20985, upload-time = "2025-07-27T09:07:44.565Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ef/10/c78f463b4ef22eef8491f218f692be838282cd65480f6e423d7730dfd1fb/sse_starlette-3.0.2-py3-none-any.whl", hash = "sha256:16b7cbfddbcd4eaca11f7b586f3b8a080f1afe952c15813455b162edea619e5a", size = 11297, upload-time = "2025-07-27T09:07:43.268Z" },
]
[[package]]
name = "starlette"
version = "0.47.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
]
sdist = { url = "https://files.pythonhosted.org/packages/15/b9/cc3017f9a9c9b6e27c5106cc10cc7904653c3eec0729793aec10479dd669/starlette-0.47.3.tar.gz", hash = "sha256:6bc94f839cc176c4858894f1f8908f0ab79dfec1a6b8402f6da9be26ebea52e9", size = 2584144, upload-time = "2025-08-24T13:36:42.122Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ce/fd/901cfa59aaa5b30a99e16876f11abe38b59a1a2c51ffb3d7142bb6089069/starlette-0.47.3-py3-none-any.whl", hash = "sha256:89c0778ca62a76b826101e7c709e70680a1699ca7da6b44d38eb0a7e61fe4b51", size = 72991, upload-time = "2025-08-24T13:36:40.887Z" },
]
[[package]]
name = "tqdm"
version = "4.67.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" },
]
[[package]]
name = "typer"
version = "0.17.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "rich" },
{ name = "shellingham" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/92/e8/2a73ccf9874ec4c7638f172efc8972ceab13a0e3480b389d6ed822f7a822/typer-0.17.4.tar.gz", hash = "sha256:b77dc07d849312fd2bb5e7f20a7af8985c7ec360c45b051ed5412f64d8dc1580", size = 103734, upload-time = "2025-09-05T18:14:40.746Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/93/72/6b3e70d32e89a5cbb6a4513726c1ae8762165b027af569289e19ec08edd8/typer-0.17.4-py3-none-any.whl", hash = "sha256:015534a6edaa450e7007eba705d5c18c3349dcea50a6ad79a5ed530967575824", size = 46643, upload-time = "2025-09-05T18:14:39.166Z" },
]
[[package]]
name = "typing-extensions"
version = "4.15.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
]
[[package]]
name = "typing-inspection"
version = "0.4.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" },
]
[[package]]
name = "uvicorn"
version = "0.35.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "h11" },
]
sdist = { url = "https://files.pythonhosted.org/packages/5e/42/e0e305207bb88c6b8d3061399c6a961ffe5fbb7e2aa63c9234df7259e9cd/uvicorn-0.35.0.tar.gz", hash = "sha256:bc662f087f7cf2ce11a1d7fd70b90c9f98ef2e2831556dd078d131b96cc94a01", size = 78473, upload-time = "2025-06-28T16:15:46.058Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d2/e2/dc81b1bd1dcfe91735810265e9d26bc8ec5da45b4c0f6237e286819194c3/uvicorn-0.35.0-py3-none-any.whl", hash = "sha256:197535216b25ff9b785e29a0b79199f55222193d47f820816e7da751e9bc8d4a", size = 66406, upload-time = "2025-06-28T16:15:44.816Z" },
]
+514
View File
@@ -0,0 +1,514 @@
#!/usr/bin/env python3
"""
MCP AI Client with Flexible Transport and LLM Provider Support
This client can connect to MCP servers using either SSE or stdio transport
and use various AI models (OpenAI, Claude, Grok) to process queries with access to MCP tools.
"""
import asyncio
import json
import sys
import os
from contextlib import AsyncExitStack
from typing import Any, Dict, List, Optional
from enum import Enum
import nest_asyncio
from dotenv import load_dotenv
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
from mcp.client.sse import sse_client
# Load environment variables
load_dotenv()
# Apply nest_asyncio to allow nested event loops
nest_asyncio.apply()
# Import LLM client factory
try:
# Try to import from source first
import sys
import os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
from mcp_template.llm_client.client_factory import AIClientFactory
LLM_CLIENT_AVAILABLE = True
except ImportError:
try:
# Fallback to build version
from mcp_template.llm_client.client_factory import AIClientFactory
LLM_CLIENT_AVAILABLE = True
except ImportError:
LLM_CLIENT_AVAILABLE = False
class TransportType(Enum):
"""Supported MCP transport types"""
STDIO = "stdio"
SSE = "sse"
class MCPAIClient:
"""Client for interacting with AI models using MCP tools with flexible transport and provider support."""
def __init__(
self,
model: str = "gpt-4o",
transport: TransportType = TransportType.SSE,
provider: str = "openai",
temperature: float = 0.7,
max_tokens: int = 1000,
top_k: Optional[int] = None,
top_p: Optional[float] = None,
**kwargs
):
"""Initialize the AI MCP client.
Args:
model: The AI model to use.
transport: The MCP transport type to use (stdio or sse).
provider: The AI provider to use (openai, claude, grok).
temperature: Sampling temperature for AI responses.
max_tokens: Maximum tokens for AI responses.
**kwargs: Additional parameters for the AI client.
"""
self.transport = transport
self.model = model
self.provider = provider
self.temperature = temperature
self.max_tokens = max_tokens
# Initialize session and client objects
self.session: Optional[ClientSession] = None
self.exit_stack = AsyncExitStack()
# Initialize AI client using factory
if not LLM_CLIENT_AVAILABLE:
raise ImportError("LLM client not available. Make sure the LLM client modules are properly installed.")
# Prepare additional parameters for AI client
ai_kwargs = kwargs.copy()
if top_k is not None:
ai_kwargs["top_k"] = top_k
if top_p is not None:
ai_kwargs["top_p"] = top_p
self.ai_client = AIClientFactory.create_client(
provider=provider,
model_name=model,
temperature=temperature,
max_tokens=max_tokens,
**ai_kwargs
)
self.stdio: Optional[Any] = None
self.write: Optional[Any] = None
# Transport-specific attributes
self.read_stream: Optional[Any] = None
self.write_stream: Optional[Any] = None
async def connect_stdio(self, server_script_path: str = "run_mcp_server.py", server_args: List[str] = None):
"""Connect to an MCP server using stdio transport.
Args:
server_script_path: Path to the server script.
server_args: Additional arguments for the server script.
"""
if server_args is None:
server_args = ["--transport", "stdio"]
# Server configuration
server_params = StdioServerParameters(
command="python",
args=[server_script_path] + server_args,
)
# Connect to the server
stdio_transport = await self.exit_stack.enter_async_context(
stdio_client(server_params)
)
self.stdio, self.write = stdio_transport
self.session = await self.exit_stack.enter_async_context(
ClientSession(self.stdio, self.write)
)
async def connect_sse(self, server_url: str = "http://localhost:8050/sse"):
"""Connect to an MCP server using SSE transport.
Args:
server_url: The SSE endpoint URL of the server.
"""
# Connect to the server
sse_transport = await self.exit_stack.enter_async_context(
sse_client(server_url)
)
self.read_stream, self.write_stream = sse_transport
self.session = await self.exit_stack.enter_async_context(
ClientSession(self.read_stream, self.write_stream)
)
async def connect_to_server(self, server_url: str = "http://localhost:8050/sse",
server_script_path: str = "run_mcp_server.py"):
"""Connect to an MCP server using the configured transport.
Args:
server_url: The SSE endpoint URL (used for SSE transport).
server_script_path: Path to the server script (used for stdio transport).
"""
if self.transport == TransportType.SSE:
await self.connect_sse(server_url)
elif self.transport == TransportType.STDIO:
await self.connect_stdio(server_script_path)
else:
raise ValueError(f"Unsupported transport type: {self.transport}")
# Initialize the connection
await self.session.initialize()
print(f"✅ Connected to MCP server using {self.transport.value.upper()} transport")
# List available components
await self.list_available_components()
async def list_available_components(self):
"""List all available tools, prompts, and resources."""
print("\n" + "="*60)
print("📋 AVAILABLE MCP COMPONENTS")
print("="*60)
# List available tools
tools_result = await self.session.list_tools()
print(f"\n🔧 Tools ({len(tools_result.tools)}):")
for tool in tools_result.tools:
print(f"{tool.name}: {tool.description}")
# List available prompts
prompts_result = await self.session.list_prompts()
print(f"\n💬 Prompts ({len(prompts_result.prompts)}):")
for prompt in prompts_result.prompts:
print(f"{prompt.name}: {prompt.description}")
# List available resources
resources_result = await self.session.list_resources()
print(f"\n📄 Resources ({len(resources_result.resources)}):")
for resource in resources_result.resources:
print(f"{resource.uri}: {resource.name}")
if resource.description:
print(f" └─ {resource.description}")
print("\n" + "="*60)
async def get_mcp_tools(self) -> List[Dict[str, Any]]:
"""Get available tools from the MCP server in AI provider format.
Returns:
A list of tools formatted for the specific AI provider.
"""
tools_result = await self.session.list_tools()
# Convert MCP tools to standard format
standard_tools = [
{
"name": tool.name,
"description": tool.description,
"inputSchema": tool.inputSchema,
}
for tool in tools_result.tools
]
# Use AI client's formatting method
return self.ai_client._format_tools_for_provider(standard_tools)
async def process_query(self, query: str) -> str:
"""Process a query using AI model and available MCP tools.
Args:
query: The user query.
Returns:
The response from the AI model.
"""
print(f"\n🤔 Processing query: '{query}'")
# Get available tools
tools = await self.get_mcp_tools()
# Initial AI API call
print(f"🧠 Calling {self.provider}/{self.model} with {len(tools)} available tools...")
response = await self.ai_client.chat_completion(
messages=[{"role": "user", "content": query}],
tools=tools,
tool_choice="auto",
)
# Get assistant's response
assistant_message = response["choices"][0]["message"]
# Initialize conversation with user query and assistant response
messages = [
{"role": "user", "content": query},
{
"role": assistant_message["role"],
"content": assistant_message.get("content"),
"tool_calls": [
{
"id": tc["id"],
"type": tc.get("type", "function"),
"function": tc["function"]
}
for tc in assistant_message.get("tool_calls", [])
] if assistant_message.get("tool_calls") else None,
},
]
# Handle tool calls if present
if "tool_calls" in assistant_message and assistant_message["tool_calls"]:
print(f"🔧 Assistant wants to use {len(assistant_message['tool_calls'])} tool(s)")
# Process each tool call
for i, tool_call in enumerate(assistant_message["tool_calls"], 1):
tool_name = tool_call["function"]["name"]
tool_args = json.loads(tool_call["function"]["arguments"])
print(f" {i}. Calling tool: {tool_name}")
print(f" Arguments: {tool_args}")
# In proper MCP, arguments should be passed directly
fastmcp_args = tool_args
# Execute tool call
try:
result = await self.session.call_tool(
tool_name,
arguments=fastmcp_args,
)
tool_result = result.content[0].text if result.content else "No result"
print(f" Result: {tool_result}{'...' if len(tool_result) > 100 else ''}")
# Add tool response to conversation
messages.append(
{
"role": "tool",
"tool_call_id": tool_call["id"],
"content": tool_result,
}
)
except Exception as e:
error_msg = f"Tool execution failed: {e}"
print(f" ❌ Error: {error_msg}")
messages.append(
{
"role": "tool",
"tool_call_id": tool_call["id"],
"content": error_msg,
}
)
# Get final response from AI with tool results
print(f"🧠 Getting final response from {self.provider}/{self.model}...")
final_response = await self.ai_client.chat_completion(
messages=messages,
tools=tools,
tool_choice="none", # Don't allow more tool calls
)
final_answer = final_response["choices"][0]["message"]["content"]
print(f"💡 Final answer: {final_answer[:200]}{'...' if len(final_answer) > 200 else ''}")
return final_answer
# No tool calls, just return the direct response
direct_answer = assistant_message["content"]
print(f"💡 Direct answer: {direct_answer[:200]}{'...' if len(direct_answer) > 200 else ''}")
return direct_answer
async def interactive_session(self):
"""Start an interactive session for querying."""
print("🚀 Starting interactive MCP-AI session")
print(f"📡 Transport: {self.transport.value.upper()}")
print(f"🤖 Model: {self.provider}/{self.model}")
print(f"🌡️ Temperature: {self.temperature}")
print(f"📏 Max Tokens: {self.max_tokens}")
print("💡 Type 'quit' or 'exit' to end the session")
print("-" * 50)
while True:
try:
query = input("\n❓ Your query: ").strip()
if query.lower() in ['quit', 'exit', 'q']:
print("👋 Goodbye!")
break
if not query:
continue
# Process the query
response = await self.process_query(query)
print(f"\n🎯 Response: {response}")
except KeyboardInterrupt:
print("\n👋 Session interrupted. Goodbye!")
break
except Exception as e:
print(f"❌ Error processing query: {e}")
continue
async def cleanup(self):
"""Clean up resources."""
try:
await self.exit_stack.aclose()
print("🧹 Resources cleaned up successfully")
except Exception as e:
print(f"⚠️ Cleanup warning: {e}")
async def main():
"""Main entry point for the client."""
import argparse
parser = argparse.ArgumentParser(description="MCP AI Client with Flexible Transport and LLM Provider Support")
parser.add_argument(
"--transport",
choices=["sse", "stdio"],
default="sse",
help="MCP transport type (default: sse)"
)
parser.add_argument(
"--provider",
choices=["openai", "claude", "grok"],
default="openai",
help="AI provider to use (default: openai)"
)
parser.add_argument(
"--model",
help="AI model to use (defaults based on provider: openai=gpt-4o, claude=claude-3-opus-20240229, grok=grok-1)"
)
parser.add_argument(
"--temperature",
type=float,
default=0.7,
help="Sampling temperature (default: 0.7)"
)
parser.add_argument(
"--max-tokens",
type=int,
default=1000,
help="Maximum tokens for response (default: 1000)"
)
parser.add_argument(
"--top-k",
type=int,
help="Top-k sampling parameter (provider-specific)"
)
parser.add_argument(
"--top-p",
type=float,
help="Top-p sampling parameter (provider-specific)"
)
parser.add_argument(
"--server-url",
default="http://localhost:8050/sse",
help="Server URL for SSE transport (default: http://localhost:8050/sse)"
)
parser.add_argument(
"--server-script",
default="run_mcp_server.py",
help="Server script path for stdio transport (default: run_mcp_server.py)"
)
parser.add_argument(
"--query",
help="Single query to process (if not provided, starts interactive mode)"
)
parser.add_argument(
"--verbose", "-v",
action="store_true",
help="Enable verbose output"
)
args = parser.parse_args()
# Validate API keys based on provider
provider = args.provider
api_key_env_vars = {
"openai": "OPENAI_API_KEY",
"claude": "ANTHROPIC_API_KEY",
"grok": "GROK_API_KEY"
}
api_key_env = api_key_env_vars.get(provider)
if not api_key_env:
print(f"❌ Error: Unknown provider: {provider}")
return
api_key = os.getenv(api_key_env)
if not api_key:
print(f"❌ Error: {api_key_env} environment variable not set")
print(f" Please set your {provider.upper()} API key:")
print(f" export {api_key_env}='your-api-key-here'")
print(f" Or create a .env file with: {api_key_env}=your-api-key-here")
return
# Basic API key validation
if provider == "openai" and (not api_key.startswith("sk-") or len(api_key) < 20):
print("❌ Error: OPENAI_API_KEY appears to be invalid")
print(" API key should start with 'sk-' and be at least 20 characters long")
return
# Set default model if not provided
model = args.model
if not model:
default_models = {
"openai": "gpt-4o",
"claude": "claude-3-opus-20240229",
"grok": "grok-1"
}
model = default_models.get(provider, "gpt-4o")
# Create client
transport = TransportType(args.transport)
client = MCPAIClient(
model=model,
transport=transport,
provider=provider,
temperature=args.temperature,
max_tokens=args.max_tokens,
top_k=args.top_k,
top_p=args.top_p
)
try:
# Connect to server
if transport == TransportType.SSE:
await client.connect_to_server(server_url=args.server_url)
else:
await client.connect_to_server(server_script_path=args.server_script)
# Handle single query or interactive mode
if args.query:
response = await client.process_query(args.query)
print(f"\n🎯 Response: {response}")
else:
await client.interactive_session()
except Exception as e:
print(f"❌ Error: {e}")
if "ConnectionRefusedError" in str(e):
if transport == TransportType.SSE:
print("💡 Make sure the MCP server is running with SSE transport:")
print(f" python {args.server_script} --transport sse")
else:
print("💡 Make sure the server script path is correct")
finally:
await client.cleanup()
# Backward compatibility alias
MCPOpenAIClient = MCPAIClient
if __name__ == "__main__":
asyncio.run(main())
+12
View File
@@ -0,0 +1,12 @@
[project]
name = "mcp-template"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"mcp[cli]>=1.14.0",
"nest-asyncio>=1.6.0",
"openai>=1.107.1",
"psutil",
]
+20
View File
@@ -0,0 +1,20 @@
[tool:pytest]
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
pythonpath = src
addopts =
-v
--tb=short
--strict-markers
--asyncio-mode=auto
--disable-warnings
markers =
integration: marks tests as integration tests (deselect with '-m "not integration"')
unit: marks tests as unit tests
slow: marks tests as slow (deselect with '-m "not slow"')
requires_api_key: marks tests that require API keys
filterwarnings =
ignore::DeprecationWarning
ignore::PendingDeprecationWarning
+12
View File
@@ -0,0 +1,12 @@
anthropic
openai
mcp[cli]
pytest
pytest-asyncio
uvicorn
fastapi
httpx
pydantic
nest-asyncio
python-dotenv
psutil
+145
View File
@@ -0,0 +1,145 @@
#!/usr/bin/env python3
"""
MCP Modular Server Runner
Run the modular MCP server with dynamic transport selection
"""
import argparse
import sys
import os
import logging
# Default logging configuration (can be overridden by --verbose)
logging.getLogger("mcp").setLevel(logging.WARNING)
logging.getLogger("uvicorn").setLevel(logging.WARNING)
logging.getLogger("uvicorn.access").setLevel(logging.WARNING)
# Add the src directory to the Python path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
from mcp_template.server.modular_server import create_default_server
# Global server object for MCP dev command compatibility
mcp_server = None
mcp = None # Global MCP object for `mcp dev` command compatibility
def get_dev_mcp():
"""Get the MCP object for dev mode"""
global mcp
if mcp is None:
try:
# Create a basic server instance for dev mode
dev_server = create_default_server("stdio", 8050)
mcp = dev_server.mcp # Extract the FastMCP instance
except Exception as e:
print(f"Warning: Could not create MCP server for dev mode: {e}")
mcp = None
return mcp
# Initialize MCP object for dev command
mcp = get_dev_mcp()
def get_mcp_server(transport: str = "stdio", port: int = 8050):
"""Get or create the MCP server instance"""
global mcp_server
if mcp_server is None:
mcp_server = create_default_server(transport, port)
return mcp_server
def run_server(transport: str = "stdio", port: int = 8050) -> None:
"""Run the modular MCP server with the specified transport method and port"""
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 on port {port}")
# Create and run the server
server = get_mcp_server(transport, port)
if transport == "stdio":
print("Server ready for stdio communication")
elif transport == "sse":
print(f"Server ready for SSE communication on http://0.0.0.0:{port}/sse")
elif transport == "streamable-http":
print(f"Server ready for Streamable HTTP communication on port {port}")
# Run the server
server.run(transport)
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Run MCP Modular Server",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
python run_mcp_server.py # Run with stdio on port 8050 (default)
python run_mcp_server.py --transport stdio # Run with stdio explicitly
python run_mcp_server.py --transport sse # Run with SSE transport
python run_mcp_server.py --transport sse --port 8080 # Run SSE on custom port
python run_mcp_server.py --transport streamable-http --port 9000 # Run Streamable HTTP on port 9000
python run_mcp_server.py --dev # Run in development mode (SSE on port 3000)
python run_mcp_server.py --dev --verbose # Dev mode with detailed logging
MCP Dev Command:
mcp dev dev_run.py # Use MCP dev command with dedicated dev file
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)"
)
parser.add_argument(
"--port",
type=int,
default=8050,
help="Port to run the server on (default: 8050, only used for HTTP transports)"
)
parser.add_argument(
"--dev",
action="store_true",
help="Run in development mode with SSE transport on port 3000"
)
parser.add_argument(
"--verbose", "-v",
action="store_true",
help="Enable verbose logging (shows all MCP and server messages)"
)
args = parser.parse_args()
# Configure logging based on verbose flag
if args.verbose:
logging.getLogger("mcp").setLevel(logging.INFO)
logging.getLogger("uvicorn").setLevel(logging.INFO)
logging.getLogger("uvicorn.access").setLevel(logging.INFO)
print("Verbose logging enabled")
else:
# Keep the reduced logging for cleaner output
logging.getLogger("mcp").setLevel(logging.WARNING)
logging.getLogger("uvicorn").setLevel(logging.WARNING)
logging.getLogger("uvicorn.access").setLevel(logging.WARNING)
if args.dev:
# In dev mode, run with SSE transport on a standard dev port
# This is what MCP dev command expects
print("Starting MCP server in development mode...")
server = get_mcp_server("sse", 3000) # Standard dev port
print(f"🌐 Server ready for SSE communication on http://0.0.0.0:3000/sse")
server.run("sse")
run_server(args.transport, args.port)
+202
View File
@@ -0,0 +1,202 @@
#!/usr/bin/env python3
"""
Test Runner for MCP Template
This script provides an easy way to run different types of tests
with appropriate configurations and environment setup.
"""
import os
import sys
import argparse
import subprocess
from pathlib import Path
def run_command(cmd, cwd=None, env=None):
"""Run a command and return the result."""
try:
result = subprocess.run(
cmd,
shell=True,
cwd=cwd or os.getcwd(),
env=env or os.environ.copy(),
capture_output=True,
text=True
)
return result.returncode == 0, result.stdout, result.stderr
except Exception as e:
return False, "", str(e)
def setup_environment():
"""Set up environment variables for testing."""
env = os.environ.copy()
# Load .env file if it exists
env_file = Path(".env")
if env_file.exists():
try:
from dotenv import load_dotenv
load_dotenv()
env.update(os.environ.copy())
except ImportError:
print("Warning: python-dotenv not installed, .env file not loaded")
return env
def run_unit_tests(args):
"""Run unit tests."""
print("🧪 Running Unit Tests...")
cmd = ["python", "-m", "pytest", "tests/unit/", "-v"]
if args.coverage:
cmd.extend(["--cov=src", "--cov-report=html", "--cov-report=term"])
success, stdout, stderr = run_command(" ".join(cmd))
print(stdout)
if stderr:
print(stderr)
return success
def run_integration_tests(args):
"""Run integration tests."""
print("🔗 Running Integration Tests...")
# Check for API keys
has_openai = bool(os.getenv("OPENAI_API_KEY"))
has_anthropic = bool(os.getenv("ANTHROPIC_API_KEY"))
has_grok = bool(os.getenv("GROK_API_KEY"))
if not any([has_openai, has_anthropic, has_grok]):
print("⚠️ Warning: No API keys found. Integration tests will be skipped.")
print(" Set OPENAI_API_KEY, ANTHROPIC_API_KEY, or GROK_API_KEY environment variables.")
cmd = ["python", "-m", "pytest", "tests/integration/", "-v", "-s"]
if args.coverage:
cmd.extend(["--cov=src", "--cov-report=html", "--cov-report=term"])
if args.skip_slow:
cmd.append("-m")
cmd.append("not slow")
success, stdout, stderr = run_command(" ".join(cmd))
print(stdout)
if stderr:
print(stderr)
return success
def run_all_tests(args):
"""Run all tests."""
print("🚀 Running All Tests...")
success1 = run_unit_tests(args)
success2 = run_integration_tests(args)
return success1 and success2
def run_specific_test(args):
"""Run a specific test file or test function."""
print(f"🎯 Running Specific Test: {args.test_path}")
cmd = ["python", "-m", "pytest", args.test_path, "-v", "-s"]
if args.coverage:
cmd.extend(["--cov=src", "--cov-report=html", "--cov-report=term"])
success, stdout, stderr = run_command(" ".join(cmd))
print(stdout)
if stderr:
print(stderr)
return success
def main():
"""Main entry point."""
parser = argparse.ArgumentParser(
description="Test Runner for MCP Template",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
# Run all tests
python run_tests.py all
# Run unit tests only
python run_tests.py unit
# Run integration tests only
python run_tests.py integration
# Run specific test file
python run_tests.py specific tests/integration/test_mcp_integration.py
# Run with coverage
python run_tests.py all --coverage
# Run integration tests but skip slow ones
python run_tests.py integration --skip-slow
"""
)
parser.add_argument(
"command",
choices=["unit", "integration", "all", "specific"],
help="Type of tests to run"
)
parser.add_argument(
"test_path",
nargs="?",
help="Path to specific test file or test function (for 'specific' command)"
)
parser.add_argument(
"--coverage",
action="store_true",
help="Generate coverage report"
)
parser.add_argument(
"--skip-slow",
action="store_true",
help="Skip slow tests (integration tests only)"
)
args = parser.parse_args()
# Set up environment
env = setup_environment()
# Change to the correct working directory
os.chdir(Path(__file__).parent)
# Run the appropriate test command
if args.command == "unit":
success = run_unit_tests(args)
elif args.command == "integration":
success = run_integration_tests(args)
elif args.command == "all":
success = run_all_tests(args)
elif args.command == "specific":
if not args.test_path:
print("❌ Error: test_path is required for 'specific' command")
sys.exit(1)
success = run_specific_test(args)
else:
print("❌ Unknown command")
success = False
# Exit with appropriate code
sys.exit(0 if success else 1)
if __name__ == "__main__":
main()
View File
+602
View File
@@ -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*
+53
View File
@@ -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 @@
+4
View File
@@ -0,0 +1,4 @@
mcp[cli]>=1.14.0
nest-asyncio>=1.6.0
openai>=1.107.1
psutil
+2
View File
@@ -0,0 +1,2 @@
__init__
mcp_template
+5
View File
@@ -0,0 +1,5 @@
"""
MCP Template Source Package
"""
__version__ = "0.1.0"
+7
View File
@@ -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']
+70
View File
@@ -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
+133
View File
@@ -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
+72
View File
@@ -0,0 +1,72 @@
"""
Server Configuration
"""
from typing import Dict, Any, Optional
from dataclasses import dataclass
from ..core.types import TransportType
@dataclass
class ServerConfig:
"""Configuration class for MCP servers"""
name: str = "MCP Server"
version: str = "1.0.0"
transport: TransportType = TransportType.STDIO
host: str = "0.0.0.0"
port: int = 8050
stateless_http: bool = True
# Tool configurations
enable_default_tools: bool = True
custom_tools: Optional[Dict[str, Any]] = None
# Resource configurations
enable_file_resources: bool = False
resource_paths: Optional[list[str]] = None
# Prompt configurations
enable_default_prompts: bool = False
custom_prompts: Optional[Dict[str, Any]] = None
# Performance settings
max_concurrent_requests: int = 10
request_timeout: int = 30
def __post_init__(self):
"""Initialize optional fields"""
if self.custom_tools is None:
self.custom_tools = {}
if self.resource_paths is None:
self.resource_paths = []
if self.custom_prompts is None:
self.custom_prompts = {}
@classmethod
def from_dict(cls, config_dict: Dict[str, Any]) -> 'ServerConfig':
"""Create ServerConfig from dictionary"""
# Convert transport string to enum
if 'transport' in config_dict and isinstance(config_dict['transport'], str):
config_dict['transport'] = TransportType(config_dict['transport'].lower())
return cls(**config_dict)
def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary"""
result = self.__dict__.copy()
result['transport'] = self.transport.value
return result
def get_transport_config(self) -> Dict[str, Any]:
"""Get transport-specific configuration"""
if self.transport == TransportType.SSE:
return {
"host": self.host,
"port": self.port,
"endpoint": "/sse",
}
elif self.transport == TransportType.STDIO:
return {}
else:
return {}
@@ -0,0 +1,89 @@
"""
Transport Configuration
"""
from typing import Dict, Any, Optional
from dataclasses import dataclass
from ..core.types import TransportType
@dataclass
class TransportConfig:
"""Configuration class for MCP transport"""
transport_type: TransportType = TransportType.STDIO
# SSE-specific settings
sse_host: str = "localhost"
sse_port: int = 8050
sse_endpoint: str = "/sse"
sse_timeout: int = 30
sse_reconnect_delay: float = 1.0
sse_max_reconnects: int = 5
# STDIO-specific settings
stdio_command: Optional[str] = None
stdio_args: Optional[list[str]] = None
stdio_env: Optional[Dict[str, str]] = None
stdio_cwd: Optional[str] = None
# General transport settings
buffer_size: int = 8192
encoding: str = "utf-8"
enable_compression: bool = False
enable_ssl: bool = False
@classmethod
def from_dict(cls, config_dict: Dict[str, Any]) -> 'TransportConfig':
"""Create TransportConfig from dictionary"""
# Convert transport string to enum
if 'transport_type' in config_dict and isinstance(config_dict['transport_type'], str):
config_dict['transport_type'] = TransportType(config_dict['transport_type'].lower())
return cls(**config_dict)
def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary"""
result = self.__dict__.copy()
result['transport_type'] = self.transport_type.value
return result
def get_sse_config(self) -> Dict[str, Any]:
"""Get SSE-specific configuration"""
return {
"host": self.sse_host,
"port": self.sse_port,
"endpoint": self.sse_endpoint,
"timeout": self.sse_timeout,
"reconnect_delay": self.sse_reconnect_delay,
"max_reconnects": self.sse_max_reconnects,
}
def get_stdio_config(self) -> Dict[str, Any]:
"""Get STDIO-specific configuration"""
config = {}
if self.stdio_command:
config["command"] = self.stdio_command
if self.stdio_args:
config["args"] = self.stdio_args
if self.stdio_env:
config["env"] = self.stdio_env
if self.stdio_cwd:
config["cwd"] = self.stdio_cwd
return config
def get_transport_config(self) -> Dict[str, Any]:
"""Get transport configuration based on type"""
config = {
"buffer_size": self.buffer_size,
"encoding": self.encoding,
"enable_compression": self.enable_compression,
"enable_ssl": self.enable_ssl,
}
if self.transport_type == TransportType.SSE:
config.update(self.get_sse_config())
elif self.transport_type == TransportType.STDIO:
config.update(self.get_stdio_config())
return config
+13
View File
@@ -0,0 +1,13 @@
# Core MCP abstractions and base classes
from .types import MCPTool, MCPResource, MCPPrompt, MCPServerConfig
from .interfaces import IMCPServer, IMCPClient, IMCPTransport
__all__ = [
'MCPTool',
'MCPResource',
'MCPPrompt',
'MCPServerConfig',
'IMCPServer',
'IMCPClient',
'IMCPTransport'
]
+181
View File
@@ -0,0 +1,181 @@
"""
Core MCP Interfaces and Abstract Base Classes
"""
from abc import ABC, abstractmethod
from typing import Any, Dict, List, Optional, Protocol
from contextlib import asynccontextmanager
from .types import MCPTool, MCPResource, MCPPrompt, MCPServerConfig
class IMCPServer(ABC):
"""Abstract base class for MCP servers"""
@abstractmethod
async def initialize(self) -> None:
"""Initialize the MCP server"""
pass
@abstractmethod
async def register_tool(self, tool: MCPTool) -> None:
"""Register a tool with the server"""
pass
@abstractmethod
async def register_resource(self, resource: MCPResource) -> None:
"""Register a resource with the server"""
pass
@abstractmethod
async def register_prompt(self, prompt: MCPPrompt) -> None:
"""Register a prompt with the server"""
pass
@abstractmethod
async def list_tools(self) -> List[Dict[str, Any]]:
"""List all available tools"""
pass
@abstractmethod
async def list_resources(self) -> List[Dict[str, Any]]:
"""List all available resources"""
pass
@abstractmethod
async def list_prompts(self) -> List[Dict[str, Any]]:
"""List all available prompts"""
pass
@abstractmethod
async def call_tool(self, name: str, arguments: Dict[str, Any]) -> Any:
"""Call a tool by name with arguments"""
pass
@abstractmethod
async def read_resource(self, uri: str) -> Any:
"""Read a resource by URI"""
pass
@abstractmethod
async def get_prompt(self, name: str, arguments: Optional[Dict[str, Any]] = None) -> str:
"""Get a prompt by name with optional arguments"""
pass
@abstractmethod
async def start(self) -> None:
"""Start the MCP server"""
pass
@abstractmethod
async def stop(self) -> None:
"""Stop the MCP server"""
pass
class IMCPClient(ABC):
"""Abstract base class for MCP clients"""
@abstractmethod
async def connect(self) -> None:
"""Connect to MCP server"""
pass
@abstractmethod
async def disconnect(self) -> None:
"""Disconnect from MCP server"""
pass
@abstractmethod
async def list_tools(self) -> List[Dict[str, Any]]:
"""List available tools from server"""
pass
@abstractmethod
async def list_resources(self) -> List[Dict[str, Any]]:
"""List available resources from server"""
pass
@abstractmethod
async def list_prompts(self) -> List[Dict[str, Any]]:
"""List available prompts from server"""
pass
@abstractmethod
async def call_tool(self, name: str, arguments: Dict[str, Any]) -> Any:
"""Call a tool on the server"""
pass
@abstractmethod
async def read_resource(self, uri: str) -> Any:
"""Read a resource from the server"""
pass
@abstractmethod
async def get_prompt(self, name: str, arguments: Optional[Dict[str, Any]] = None) -> str:
"""Get a prompt from the server"""
pass
class IMCPTransport(ABC):
"""Abstract base class for MCP transport mechanisms"""
@abstractmethod
@asynccontextmanager
async def connect(self):
"""Establish transport connection"""
pass
@abstractmethod
async def send_message(self, message: Dict[str, Any]) -> None:
"""Send a message through the transport"""
pass
@abstractmethod
async def receive_message(self) -> Dict[str, Any]:
"""Receive a message through the transport"""
pass
@abstractmethod
async def close(self) -> None:
"""Close the transport connection"""
pass
class IAIClient(ABC):
"""Abstract base class for AI model clients"""
@property
@abstractmethod
def model_name(self) -> str:
"""Get the AI model name"""
pass
@abstractmethod
async def initialize(self) -> None:
"""Initialize the AI client"""
pass
@abstractmethod
async def chat_completion(
self,
messages: List[Dict[str, Any]],
tools: Optional[List[Dict[str, Any]]] = None,
**kwargs
) -> Dict[str, Any]:
"""Perform a chat completion with optional tools"""
pass
@abstractmethod
async def process_with_tools(
self,
query: str,
available_tools: List[Dict[str, Any]],
mcp_client: IMCPClient
) -> str:
"""Process a query with MCP tools"""
pass
@abstractmethod
async def cleanup(self) -> None:
"""Clean up resources"""
pass
+79
View File
@@ -0,0 +1,79 @@
"""
Core MCP Types and Data Structures
"""
from typing import Any, Dict, List, Optional, Protocol, Union
from abc import ABC, abstractmethod
from dataclasses import dataclass
from enum import Enum
class TransportType(Enum):
"""Supported MCP transport types"""
SSE = "sse"
STDIO = "stdio"
@dataclass
class MCPTool:
"""Represents an MCP tool that can be called by AI models"""
name: str
description: str
input_schema: Dict[str, Any]
handler: callable
def __post_init__(self):
"""Validate tool configuration"""
if not self.name or not self.description:
raise ValueError("Tool name and description are required")
@dataclass
class MCPResource:
"""Represents an MCP resource that can be read by clients"""
uri: str
name: str
description: str
mime_type: str
content: Union[str, bytes]
def __post_init__(self):
"""Validate resource configuration"""
if not self.uri or not self.name:
raise ValueError("Resource URI and name are required")
@dataclass
class MCPPrompt:
"""Represents an MCP prompt template"""
name: str
description: str
template: str
arguments: Optional[Dict[str, Any]] = None
def __post_init__(self):
"""Validate prompt configuration"""
if not self.name or not self.template:
raise ValueError("Prompt name and template are required")
@dataclass
class MCPServerConfig:
"""Configuration for MCP server"""
name: str
version: str = "1.0.0"
transport: TransportType = TransportType.STDIO
host: str = "0.0.0.0"
port: int = 8050
tools: List[MCPTool] = None
resources: List[MCPResource] = None
prompts: List[MCPPrompt] = None
stateless_http: bool = True
def __post_init__(self):
"""Initialize empty lists if not provided"""
if self.tools is None:
self.tools = []
if self.resources is None:
self.resources = []
if self.prompts is None:
self.prompts = []
+6
View File
@@ -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
+14
View File
@@ -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'
]

Some files were not shown because too many files have changed in this diff Show More