From 20f96c0f308ef1e020df4484d09bc8670ab8e5b6 Mon Sep 17 00:00:00 2001 From: OwusuBlessing Date: Thu, 11 Sep 2025 23:13:58 +0100 Subject: [PATCH] initial mcp server setup --- .env | 1 + .gitignore | 44 ++ .python-version | 1 + .vscode/settings.json | 7 + DEV_README.md | 97 +++ MCP_CLIENT_README.md | 221 +++++++ README.md | 591 +++++++++++++++++ __init__.py | 0 build/lib/llm_client/base_client.py | 150 +++++ build/lib/mcp_template/__init__.py | 5 + build/lib/mcp_template/config/__init__.py | 7 + .../lib/mcp_template/config/client_config.py | 70 ++ .../lib/mcp_template/config/config_manager.py | 133 ++++ .../lib/mcp_template/config/server_config.py | 72 ++ .../mcp_template/config/transport_config.py | 89 +++ build/lib/mcp_template/core/__init__.py | 13 + build/lib/mcp_template/core/interfaces.py | 181 +++++ build/lib/mcp_template/core/types.py | 79 +++ build/lib/mcp_template/examples/__init__.py | 6 + .../mcp_template/examples/server_examples.py | 174 +++++ build/lib/mcp_template/llm_client/__init__.py | 14 + .../mcp_template/llm_client/base_client.py | 150 +++++ .../mcp_template/llm_client/claude_client.py | 56 ++ .../mcp_template/llm_client/client_factory.py | 104 +++ .../mcp_template/llm_client/grok_client.py | 55 ++ .../mcp_template/llm_client/openai_client.py | 106 +++ build/lib/mcp_template/mcp_client.py | 0 build/lib/mcp_template/server/__init__.py | 5 + .../lib/mcp_template/server/modular_server.py | 99 +++ .../mcp_template/server/prompts/__init__.py | 7 + .../server/prompts/base_prompt.py | 51 ++ .../server/prompts/greeting_prompt.py | 44 ++ .../server/prompts/prompt_registry.py | 100 +++ .../mcp_template/server/resources/__init__.py | 7 + .../server/resources/base_resource.py | 41 ++ .../server/resources/config_resource.py | 37 ++ .../server/resources/dynamic_resource.py | 28 + .../server/resources/resource_registry.py | 100 +++ .../lib/mcp_template/server/server_factory.py | 258 ++++++++ .../lib/mcp_template/server/tools/__init__.py | 7 + .../mcp_template/server/tools/base_tool.py | 67 ++ .../server/tools/calculator_tool.py | 48 ++ .../server/tools/greeting_tool.py | 54 ++ .../server/tools/tool_registry.py | 100 +++ build/lib/mcp_template/tools/__init__.py | 8 + build/lib/mcp_template/tools/math_tools.py | 186 ++++++ build/lib/mcp_template/tools/system_tools.py | 157 +++++ build/lib/mcp_template/tools/text_tools.py | 187 ++++++ build/lib/mcp_template/tools/tool_registry.py | 80 +++ build/lib/mcp_template/tools/web_tools.py | 161 +++++ build/lib/mcp_template/transport/__init__.py | 6 + .../mcp_template/transport/sse_transport.py | 101 +++ .../mcp_template/transport/stdio_transport.py | 81 +++ .../transport/transport_manager.py | 81 +++ config.py | 9 + dev_run.py | 44 ++ examples/demo_client.py | 179 +++++ examples/demo_server.py | 180 +++++ examples/modular_demo.py | 195 ++++++ examples/modular_server_example.py | 47 ++ examples/test_llm_config.py | 94 +++ images/mcp_intro.png | Bin 0 -> 165950 bytes images/steup_mechanism.png | Bin 0 -> 211729 bytes intro_test/.env | 1 + intro_test/.gitignore | 10 + intro_test/.python-version | 1 + intro_test/Config.py | 5 + intro_test/INTRO_README.md | 295 +++++++++ intro_test/client_sse.py | 38 ++ intro_test/client_stdio.py | 34 + intro_test/main.py | 6 + intro_test/openai_test/client.py | 176 +++++ intro_test/openai_test/server.py | 53 ++ intro_test/pyproject.toml | 13 + intro_test/server.py | 35 + intro_test/uv.lock | 624 ++++++++++++++++++ mcp_llm_client.py | 514 +++++++++++++++ pyproject.toml | 12 + pytest.ini | 20 + requirements.txt | 12 + run_mcp_server.py | 145 ++++ run_tests.py | 202 ++++++ src/__init__.py | 0 src/mcp_template.egg-info/PKG-INFO | 602 +++++++++++++++++ src/mcp_template.egg-info/SOURCES.txt | 53 ++ .../dependency_links.txt | 1 + src/mcp_template.egg-info/requires.txt | 4 + src/mcp_template.egg-info/top_level.txt | 2 + src/mcp_template/__init__.py | 5 + src/mcp_template/config/__init__.py | 7 + src/mcp_template/config/client_config.py | 70 ++ src/mcp_template/config/config_manager.py | 133 ++++ src/mcp_template/config/server_config.py | 72 ++ src/mcp_template/config/transport_config.py | 89 +++ src/mcp_template/core/__init__.py | 13 + src/mcp_template/core/interfaces.py | 181 +++++ src/mcp_template/core/types.py | 79 +++ src/mcp_template/examples/__init__.py | 6 + src/mcp_template/examples/server_examples.py | 174 +++++ src/mcp_template/llm_client/__init__.py | 14 + src/mcp_template/llm_client/base_client.py | 150 +++++ src/mcp_template/llm_client/claude_client.py | 56 ++ src/mcp_template/llm_client/client_factory.py | 104 +++ src/mcp_template/llm_client/grok_client.py | 55 ++ src/mcp_template/llm_client/openai_client.py | 105 +++ src/mcp_template/mcp_client.py | 0 src/mcp_template/server/__init__.py | 5 + src/mcp_template/server/modular_server.py | 174 +++++ src/mcp_template/server/prompts/__init__.py | 7 + .../server/prompts/base_prompt.py | 47 ++ .../server/prompts/greeting_prompt.py | 44 ++ .../server/prompts/prompt_registry.py | 185 ++++++ src/mcp_template/server/resources/__init__.py | 7 + .../server/resources/base_resource.py | 37 ++ .../server/resources/config_resource.py | 37 ++ .../server/resources/dynamic_resource.py | 28 + .../server/resources/resource_registry.py | 179 +++++ src/mcp_template/server/server_factory.py | 258 ++++++++ src/mcp_template/server/tools/__init__.py | 7 + src/mcp_template/server/tools/base_tool.py | 59 ++ .../server/tools/calculator_tool.py | 48 ++ .../server/tools/greeting_tool.py | 54 ++ .../server/tools/tool_registry.py | 198 ++++++ src/mcp_template/tools/__init__.py | 8 + src/mcp_template/tools/math_tools.py | 192 ++++++ src/mcp_template/tools/system_tools.py | 157 +++++ src/mcp_template/tools/text_tools.py | 187 ++++++ src/mcp_template/tools/tool_registry.py | 80 +++ src/mcp_template/tools/web_tools.py | 161 +++++ src/mcp_template/transport/__init__.py | 6 + src/mcp_template/transport/sse_transport.py | 101 +++ src/mcp_template/transport/stdio_transport.py | 81 +++ .../transport/transport_manager.py | 81 +++ tests/README.md | 198 ++++++ tests/__init__.py | 2 + tests/e2e/test_end_to_end.py | 255 +++++++ tests/integration/test_mcp_integration.py | 611 +++++++++++++++++ tests/unit/test_clients.py | 202 ++++++ tests/unit/test_core_types.py | 213 ++++++ tests/unit/test_server.py | 495 ++++++++++++++ uv.lock | 594 +++++++++++++++++ 141 files changed, 14444 insertions(+) create mode 100644 .env create mode 100644 .gitignore create mode 100644 .python-version create mode 100644 .vscode/settings.json create mode 100644 DEV_README.md create mode 100644 MCP_CLIENT_README.md create mode 100644 README.md create mode 100644 __init__.py create mode 100644 build/lib/llm_client/base_client.py create mode 100644 build/lib/mcp_template/__init__.py create mode 100644 build/lib/mcp_template/config/__init__.py create mode 100644 build/lib/mcp_template/config/client_config.py create mode 100644 build/lib/mcp_template/config/config_manager.py create mode 100644 build/lib/mcp_template/config/server_config.py create mode 100644 build/lib/mcp_template/config/transport_config.py create mode 100644 build/lib/mcp_template/core/__init__.py create mode 100644 build/lib/mcp_template/core/interfaces.py create mode 100644 build/lib/mcp_template/core/types.py create mode 100644 build/lib/mcp_template/examples/__init__.py create mode 100644 build/lib/mcp_template/examples/server_examples.py create mode 100644 build/lib/mcp_template/llm_client/__init__.py create mode 100644 build/lib/mcp_template/llm_client/base_client.py create mode 100644 build/lib/mcp_template/llm_client/claude_client.py create mode 100644 build/lib/mcp_template/llm_client/client_factory.py create mode 100644 build/lib/mcp_template/llm_client/grok_client.py create mode 100644 build/lib/mcp_template/llm_client/openai_client.py create mode 100644 build/lib/mcp_template/mcp_client.py create mode 100644 build/lib/mcp_template/server/__init__.py create mode 100644 build/lib/mcp_template/server/modular_server.py create mode 100644 build/lib/mcp_template/server/prompts/__init__.py create mode 100644 build/lib/mcp_template/server/prompts/base_prompt.py create mode 100644 build/lib/mcp_template/server/prompts/greeting_prompt.py create mode 100644 build/lib/mcp_template/server/prompts/prompt_registry.py create mode 100644 build/lib/mcp_template/server/resources/__init__.py create mode 100644 build/lib/mcp_template/server/resources/base_resource.py create mode 100644 build/lib/mcp_template/server/resources/config_resource.py create mode 100644 build/lib/mcp_template/server/resources/dynamic_resource.py create mode 100644 build/lib/mcp_template/server/resources/resource_registry.py create mode 100644 build/lib/mcp_template/server/server_factory.py create mode 100644 build/lib/mcp_template/server/tools/__init__.py create mode 100644 build/lib/mcp_template/server/tools/base_tool.py create mode 100644 build/lib/mcp_template/server/tools/calculator_tool.py create mode 100644 build/lib/mcp_template/server/tools/greeting_tool.py create mode 100644 build/lib/mcp_template/server/tools/tool_registry.py create mode 100644 build/lib/mcp_template/tools/__init__.py create mode 100644 build/lib/mcp_template/tools/math_tools.py create mode 100644 build/lib/mcp_template/tools/system_tools.py create mode 100644 build/lib/mcp_template/tools/text_tools.py create mode 100644 build/lib/mcp_template/tools/tool_registry.py create mode 100644 build/lib/mcp_template/tools/web_tools.py create mode 100644 build/lib/mcp_template/transport/__init__.py create mode 100644 build/lib/mcp_template/transport/sse_transport.py create mode 100644 build/lib/mcp_template/transport/stdio_transport.py create mode 100644 build/lib/mcp_template/transport/transport_manager.py create mode 100644 config.py create mode 100755 dev_run.py create mode 100644 examples/demo_client.py create mode 100644 examples/demo_server.py create mode 100644 examples/modular_demo.py create mode 100644 examples/modular_server_example.py create mode 100644 examples/test_llm_config.py create mode 100644 images/mcp_intro.png create mode 100644 images/steup_mechanism.png create mode 100644 intro_test/.env create mode 100644 intro_test/.gitignore create mode 100644 intro_test/.python-version create mode 100644 intro_test/Config.py create mode 100644 intro_test/INTRO_README.md create mode 100644 intro_test/client_sse.py create mode 100644 intro_test/client_stdio.py create mode 100644 intro_test/main.py create mode 100644 intro_test/openai_test/client.py create mode 100644 intro_test/openai_test/server.py create mode 100644 intro_test/pyproject.toml create mode 100644 intro_test/server.py create mode 100644 intro_test/uv.lock create mode 100644 mcp_llm_client.py create mode 100644 pyproject.toml create mode 100644 pytest.ini create mode 100644 requirements.txt create mode 100644 run_mcp_server.py create mode 100644 run_tests.py create mode 100644 src/__init__.py create mode 100644 src/mcp_template.egg-info/PKG-INFO create mode 100644 src/mcp_template.egg-info/SOURCES.txt create mode 100644 src/mcp_template.egg-info/dependency_links.txt create mode 100644 src/mcp_template.egg-info/requires.txt create mode 100644 src/mcp_template.egg-info/top_level.txt create mode 100644 src/mcp_template/__init__.py create mode 100644 src/mcp_template/config/__init__.py create mode 100644 src/mcp_template/config/client_config.py create mode 100644 src/mcp_template/config/config_manager.py create mode 100644 src/mcp_template/config/server_config.py create mode 100644 src/mcp_template/config/transport_config.py create mode 100644 src/mcp_template/core/__init__.py create mode 100644 src/mcp_template/core/interfaces.py create mode 100644 src/mcp_template/core/types.py create mode 100644 src/mcp_template/examples/__init__.py create mode 100644 src/mcp_template/examples/server_examples.py create mode 100644 src/mcp_template/llm_client/__init__.py create mode 100644 src/mcp_template/llm_client/base_client.py create mode 100644 src/mcp_template/llm_client/claude_client.py create mode 100644 src/mcp_template/llm_client/client_factory.py create mode 100644 src/mcp_template/llm_client/grok_client.py create mode 100644 src/mcp_template/llm_client/openai_client.py create mode 100644 src/mcp_template/mcp_client.py create mode 100644 src/mcp_template/server/__init__.py create mode 100644 src/mcp_template/server/modular_server.py create mode 100644 src/mcp_template/server/prompts/__init__.py create mode 100644 src/mcp_template/server/prompts/base_prompt.py create mode 100644 src/mcp_template/server/prompts/greeting_prompt.py create mode 100644 src/mcp_template/server/prompts/prompt_registry.py create mode 100644 src/mcp_template/server/resources/__init__.py create mode 100644 src/mcp_template/server/resources/base_resource.py create mode 100644 src/mcp_template/server/resources/config_resource.py create mode 100644 src/mcp_template/server/resources/dynamic_resource.py create mode 100644 src/mcp_template/server/resources/resource_registry.py create mode 100644 src/mcp_template/server/server_factory.py create mode 100644 src/mcp_template/server/tools/__init__.py create mode 100644 src/mcp_template/server/tools/base_tool.py create mode 100644 src/mcp_template/server/tools/calculator_tool.py create mode 100644 src/mcp_template/server/tools/greeting_tool.py create mode 100644 src/mcp_template/server/tools/tool_registry.py create mode 100644 src/mcp_template/tools/__init__.py create mode 100644 src/mcp_template/tools/math_tools.py create mode 100644 src/mcp_template/tools/system_tools.py create mode 100644 src/mcp_template/tools/text_tools.py create mode 100644 src/mcp_template/tools/tool_registry.py create mode 100644 src/mcp_template/tools/web_tools.py create mode 100644 src/mcp_template/transport/__init__.py create mode 100644 src/mcp_template/transport/sse_transport.py create mode 100644 src/mcp_template/transport/stdio_transport.py create mode 100644 src/mcp_template/transport/transport_manager.py create mode 100644 tests/README.md create mode 100644 tests/__init__.py create mode 100644 tests/e2e/test_end_to_end.py create mode 100644 tests/integration/test_mcp_integration.py create mode 100644 tests/unit/test_clients.py create mode 100644 tests/unit/test_core_types.py create mode 100644 tests/unit/test_server.py create mode 100644 uv.lock diff --git a/.env b/.env new file mode 100644 index 0000000..113dd37 --- /dev/null +++ b/.env @@ -0,0 +1 @@ +OPENAI_API_KEY=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 \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..104a94a --- /dev/null +++ b/.gitignore @@ -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 diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..24ee5b1 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..9b38853 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "python.testing.pytestArgs": [ + "tests" + ], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true +} \ No newline at end of file diff --git a/DEV_README.md b/DEV_README.md new file mode 100644 index 0000000..53d77a5 --- /dev/null +++ b/DEV_README.md @@ -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 diff --git a/MCP_CLIENT_README.md b/MCP_CLIENT_README.md new file mode 100644 index 0000000..3fbbcae --- /dev/null +++ b/MCP_CLIENT_README.md @@ -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! 🤖✨ diff --git a/README.md b/README.md new file mode 100644 index 0000000..3e83e36 --- /dev/null +++ b/README.md @@ -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* diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/build/lib/llm_client/base_client.py b/build/lib/llm_client/base_client.py new file mode 100644 index 0000000..0bd7b22 --- /dev/null +++ b/build/lib/llm_client/base_client.py @@ -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 diff --git a/build/lib/mcp_template/__init__.py b/build/lib/mcp_template/__init__.py new file mode 100644 index 0000000..e2d634e --- /dev/null +++ b/build/lib/mcp_template/__init__.py @@ -0,0 +1,5 @@ +""" +MCP Template Source Package +""" + +__version__ = "0.1.0" diff --git a/build/lib/mcp_template/config/__init__.py b/build/lib/mcp_template/config/__init__.py new file mode 100644 index 0000000..bddd5cc --- /dev/null +++ b/build/lib/mcp_template/config/__init__.py @@ -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'] diff --git a/build/lib/mcp_template/config/client_config.py b/build/lib/mcp_template/config/client_config.py new file mode 100644 index 0000000..f87a259 --- /dev/null +++ b/build/lib/mcp_template/config/client_config.py @@ -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 diff --git a/build/lib/mcp_template/config/config_manager.py b/build/lib/mcp_template/config/config_manager.py new file mode 100644 index 0000000..dfbc121 --- /dev/null +++ b/build/lib/mcp_template/config/config_manager.py @@ -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 diff --git a/build/lib/mcp_template/config/server_config.py b/build/lib/mcp_template/config/server_config.py new file mode 100644 index 0000000..2e9aa1a --- /dev/null +++ b/build/lib/mcp_template/config/server_config.py @@ -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 {} diff --git a/build/lib/mcp_template/config/transport_config.py b/build/lib/mcp_template/config/transport_config.py new file mode 100644 index 0000000..0b71545 --- /dev/null +++ b/build/lib/mcp_template/config/transport_config.py @@ -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 diff --git a/build/lib/mcp_template/core/__init__.py b/build/lib/mcp_template/core/__init__.py new file mode 100644 index 0000000..dc7f94c --- /dev/null +++ b/build/lib/mcp_template/core/__init__.py @@ -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' +] diff --git a/build/lib/mcp_template/core/interfaces.py b/build/lib/mcp_template/core/interfaces.py new file mode 100644 index 0000000..d75cbff --- /dev/null +++ b/build/lib/mcp_template/core/interfaces.py @@ -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 diff --git a/build/lib/mcp_template/core/types.py b/build/lib/mcp_template/core/types.py new file mode 100644 index 0000000..84b0d2b --- /dev/null +++ b/build/lib/mcp_template/core/types.py @@ -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 = [] diff --git a/build/lib/mcp_template/examples/__init__.py b/build/lib/mcp_template/examples/__init__.py new file mode 100644 index 0000000..f58010e --- /dev/null +++ b/build/lib/mcp_template/examples/__init__.py @@ -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'] diff --git a/build/lib/mcp_template/examples/server_examples.py b/build/lib/mcp_template/examples/server_examples.py new file mode 100644 index 0000000..c64106e --- /dev/null +++ b/build/lib/mcp_template/examples/server_examples.py @@ -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 diff --git a/build/lib/mcp_template/llm_client/__init__.py b/build/lib/mcp_template/llm_client/__init__.py new file mode 100644 index 0000000..89a0471 --- /dev/null +++ b/build/lib/mcp_template/llm_client/__init__.py @@ -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' +] diff --git a/build/lib/mcp_template/llm_client/base_client.py b/build/lib/mcp_template/llm_client/base_client.py new file mode 100644 index 0000000..0bd7b22 --- /dev/null +++ b/build/lib/mcp_template/llm_client/base_client.py @@ -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 diff --git a/build/lib/mcp_template/llm_client/claude_client.py b/build/lib/mcp_template/llm_client/claude_client.py new file mode 100644 index 0000000..ce5cbb1 --- /dev/null +++ b/build/lib/mcp_template/llm_client/claude_client.py @@ -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 diff --git a/build/lib/mcp_template/llm_client/client_factory.py b/build/lib/mcp_template/llm_client/client_factory.py new file mode 100644 index 0000000..c12521c --- /dev/null +++ b/build/lib/mcp_template/llm_client/client_factory.py @@ -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() diff --git a/build/lib/mcp_template/llm_client/grok_client.py b/build/lib/mcp_template/llm_client/grok_client.py new file mode 100644 index 0000000..0bb1968 --- /dev/null +++ b/build/lib/mcp_template/llm_client/grok_client.py @@ -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 diff --git a/build/lib/mcp_template/llm_client/openai_client.py b/build/lib/mcp_template/llm_client/openai_client.py new file mode 100644 index 0000000..4076e32 --- /dev/null +++ b/build/lib/mcp_template/llm_client/openai_client.py @@ -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() diff --git a/build/lib/mcp_template/mcp_client.py b/build/lib/mcp_template/mcp_client.py new file mode 100644 index 0000000..e69de29 diff --git a/build/lib/mcp_template/server/__init__.py b/build/lib/mcp_template/server/__init__.py new file mode 100644 index 0000000..297e1c8 --- /dev/null +++ b/build/lib/mcp_template/server/__init__.py @@ -0,0 +1,5 @@ +# MCP Server implementations +from .modular_server import ModularMCPServer +from .server_factory import MCPServerFactory + +__all__ = ['ModularMCPServer', 'MCPServerFactory'] diff --git a/build/lib/mcp_template/server/modular_server.py b/build/lib/mcp_template/server/modular_server.py new file mode 100644 index 0000000..c09222a --- /dev/null +++ b/build/lib/mcp_template/server/modular_server.py @@ -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() + } + } diff --git a/build/lib/mcp_template/server/prompts/__init__.py b/build/lib/mcp_template/server/prompts/__init__.py new file mode 100644 index 0000000..de4d4a0 --- /dev/null +++ b/build/lib/mcp_template/server/prompts/__init__.py @@ -0,0 +1,7 @@ +""" +Server Prompts Module +""" +from .base_prompt import BaseServerPrompt +from .prompt_registry import ServerPromptRegistry + +__all__ = ['BaseServerPrompt', 'ServerPromptRegistry'] diff --git a/build/lib/mcp_template/server/prompts/base_prompt.py b/build/lib/mcp_template/server/prompts/base_prompt.py new file mode 100644 index 0000000..f5858ce --- /dev/null +++ b/build/lib/mcp_template/server/prompts/base_prompt.py @@ -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 diff --git a/build/lib/mcp_template/server/prompts/greeting_prompt.py b/build/lib/mcp_template/server/prompts/greeting_prompt.py new file mode 100644 index 0000000..f0f02ba --- /dev/null +++ b/build/lib/mcp_template/server/prompts/greeting_prompt.py @@ -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 + ) diff --git a/build/lib/mcp_template/server/prompts/prompt_registry.py b/build/lib/mcp_template/server/prompts/prompt_registry.py new file mode 100644 index 0000000..92c802a --- /dev/null +++ b/build/lib/mcp_template/server/prompts/prompt_registry.py @@ -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() diff --git a/build/lib/mcp_template/server/resources/__init__.py b/build/lib/mcp_template/server/resources/__init__.py new file mode 100644 index 0000000..d6b9d57 --- /dev/null +++ b/build/lib/mcp_template/server/resources/__init__.py @@ -0,0 +1,7 @@ +""" +Server Resources Module +""" +from .base_resource import BaseServerResource +from .resource_registry import ServerResourceRegistry + +__all__ = ['BaseServerResource', 'ServerResourceRegistry'] diff --git a/build/lib/mcp_template/server/resources/base_resource.py b/build/lib/mcp_template/server/resources/base_resource.py new file mode 100644 index 0000000..64d797e --- /dev/null +++ b/build/lib/mcp_template/server/resources/base_resource.py @@ -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 diff --git a/build/lib/mcp_template/server/resources/config_resource.py b/build/lib/mcp_template/server/resources/config_resource.py new file mode 100644 index 0000000..d2dfc29 --- /dev/null +++ b/build/lib/mcp_template/server/resources/config_resource.py @@ -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) diff --git a/build/lib/mcp_template/server/resources/dynamic_resource.py b/build/lib/mcp_template/server/resources/dynamic_resource.py new file mode 100644 index 0000000..f3ec1f6 --- /dev/null +++ b/build/lib/mcp_template/server/resources/dynamic_resource.py @@ -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") diff --git a/build/lib/mcp_template/server/resources/resource_registry.py b/build/lib/mcp_template/server/resources/resource_registry.py new file mode 100644 index 0000000..1356fc8 --- /dev/null +++ b/build/lib/mcp_template/server/resources/resource_registry.py @@ -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() diff --git a/build/lib/mcp_template/server/server_factory.py b/build/lib/mcp_template/server/server_factory.py new file mode 100644 index 0000000..39ac0b0 --- /dev/null +++ b/build/lib/mcp_template/server/server_factory.py @@ -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, + ) diff --git a/build/lib/mcp_template/server/tools/__init__.py b/build/lib/mcp_template/server/tools/__init__.py new file mode 100644 index 0000000..2480785 --- /dev/null +++ b/build/lib/mcp_template/server/tools/__init__.py @@ -0,0 +1,7 @@ +""" +Server Tools Module +""" +from .base_tool import BaseServerTool +from .tool_registry import ServerToolRegistry + +__all__ = ['BaseServerTool', 'ServerToolRegistry'] diff --git a/build/lib/mcp_template/server/tools/base_tool.py b/build/lib/mcp_template/server/tools/base_tool.py new file mode 100644 index 0000000..a224ef2 --- /dev/null +++ b/build/lib/mcp_template/server/tools/base_tool.py @@ -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 diff --git a/build/lib/mcp_template/server/tools/calculator_tool.py b/build/lib/mcp_template/server/tools/calculator_tool.py new file mode 100644 index 0000000..05fed15 --- /dev/null +++ b/build/lib/mcp_template/server/tools/calculator_tool.py @@ -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}") diff --git a/build/lib/mcp_template/server/tools/greeting_tool.py b/build/lib/mcp_template/server/tools/greeting_tool.py new file mode 100644 index 0000000..3172e49 --- /dev/null +++ b/build/lib/mcp_template/server/tools/greeting_tool.py @@ -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 diff --git a/build/lib/mcp_template/server/tools/tool_registry.py b/build/lib/mcp_template/server/tools/tool_registry.py new file mode 100644 index 0000000..54fb113 --- /dev/null +++ b/build/lib/mcp_template/server/tools/tool_registry.py @@ -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() diff --git a/build/lib/mcp_template/tools/__init__.py b/build/lib/mcp_template/tools/__init__.py new file mode 100644 index 0000000..533ef44 --- /dev/null +++ b/build/lib/mcp_template/tools/__init__.py @@ -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'] diff --git a/build/lib/mcp_template/tools/math_tools.py b/build/lib/mcp_template/tools/math_tools.py new file mode 100644 index 0000000..185c1d7 --- /dev/null +++ b/build/lib/mcp_template/tools/math_tools.py @@ -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, + ) diff --git a/build/lib/mcp_template/tools/system_tools.py b/build/lib/mcp_template/tools/system_tools.py new file mode 100644 index 0000000..de6ee55 --- /dev/null +++ b/build/lib/mcp_template/tools/system_tools.py @@ -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, + ) diff --git a/build/lib/mcp_template/tools/text_tools.py b/build/lib/mcp_template/tools/text_tools.py new file mode 100644 index 0000000..c7ae0ac --- /dev/null +++ b/build/lib/mcp_template/tools/text_tools.py @@ -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, + ) diff --git a/build/lib/mcp_template/tools/tool_registry.py b/build/lib/mcp_template/tools/tool_registry.py new file mode 100644 index 0000000..52af662 --- /dev/null +++ b/build/lib/mcp_template/tools/tool_registry.py @@ -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() + } diff --git a/build/lib/mcp_template/tools/web_tools.py b/build/lib/mcp_template/tools/web_tools.py new file mode 100644 index 0000000..7fd9b75 --- /dev/null +++ b/build/lib/mcp_template/tools/web_tools.py @@ -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, + ) diff --git a/build/lib/mcp_template/transport/__init__.py b/build/lib/mcp_template/transport/__init__.py new file mode 100644 index 0000000..f9885a7 --- /dev/null +++ b/build/lib/mcp_template/transport/__init__.py @@ -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'] diff --git a/build/lib/mcp_template/transport/sse_transport.py b/build/lib/mcp_template/transport/sse_transport.py new file mode 100644 index 0000000..daf0d21 --- /dev/null +++ b/build/lib/mcp_template/transport/sse_transport.py @@ -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") diff --git a/build/lib/mcp_template/transport/stdio_transport.py b/build/lib/mcp_template/transport/stdio_transport.py new file mode 100644 index 0000000..d480f6b --- /dev/null +++ b/build/lib/mcp_template/transport/stdio_transport.py @@ -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) diff --git a/build/lib/mcp_template/transport/transport_manager.py b/build/lib/mcp_template/transport/transport_manager.py new file mode 100644 index 0000000..ff5f051 --- /dev/null +++ b/build/lib/mcp_template/transport/transport_manager.py @@ -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 diff --git a/config.py b/config.py new file mode 100644 index 0000000..0c20744 --- /dev/null +++ b/config.py @@ -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") \ No newline at end of file diff --git a/dev_run.py b/dev_run.py new file mode 100755 index 0000000..40ea589 --- /dev/null +++ b/dev_run.py @@ -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 diff --git a/examples/demo_client.py b/examples/demo_client.py new file mode 100644 index 0000000..6339492 --- /dev/null +++ b/examples/demo_client.py @@ -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()) diff --git a/examples/demo_server.py b/examples/demo_server.py new file mode 100644 index 0000000..4d57690 --- /dev/null +++ b/examples/demo_server.py @@ -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 ") + 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()) diff --git a/examples/modular_demo.py b/examples/modular_demo.py new file mode 100644 index 0000000..4d2e68e --- /dev/null +++ b/examples/modular_demo.py @@ -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()) diff --git a/examples/modular_server_example.py b/examples/modular_server_example.py new file mode 100644 index 0000000..93e34b8 --- /dev/null +++ b/examples/modular_server_example.py @@ -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() diff --git a/examples/test_llm_config.py b/examples/test_llm_config.py new file mode 100644 index 0000000..6c73e11 --- /dev/null +++ b/examples/test_llm_config.py @@ -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()) diff --git a/images/mcp_intro.png b/images/mcp_intro.png new file mode 100644 index 0000000000000000000000000000000000000000..6767f361e9f506c329d55c1c09284c37c00c43c2 GIT binary patch literal 165950 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≀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 literal 0 HcmV?d00001 diff --git a/images/steup_mechanism.png b/images/steup_mechanism.png new file mode 100644 index 0000000000000000000000000000000000000000..09e19a4ac52e452333d4b3f1973ead35c36f0046 GIT binary patch literal 211729 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

    ?tZvBzep$7BzpKkpZsgOjGOR7rUoncCw=m& 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 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 +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 \ No newline at end of file diff --git a/intro_test/client_sse.py b/intro_test/client_sse.py new file mode 100644 index 0000000..9ad5401 --- /dev/null +++ b/intro_test/client_sse.py @@ -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()) \ No newline at end of file diff --git a/intro_test/client_stdio.py b/intro_test/client_stdio.py new file mode 100644 index 0000000..0234417 --- /dev/null +++ b/intro_test/client_stdio.py @@ -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()) \ No newline at end of file diff --git a/intro_test/main.py b/intro_test/main.py new file mode 100644 index 0000000..8b7fb96 --- /dev/null +++ b/intro_test/main.py @@ -0,0 +1,6 @@ +def main(): + print("Hello from intro-test!") + + +if __name__ == "__main__": + main() diff --git a/intro_test/openai_test/client.py b/intro_test/openai_test/client.py new file mode 100644 index 0000000..a46201a --- /dev/null +++ b/intro_test/openai_test/client.py @@ -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()) \ No newline at end of file diff --git a/intro_test/openai_test/server.py b/intro_test/openai_test/server.py new file mode 100644 index 0000000..f647d13 --- /dev/null +++ b/intro_test/openai_test/server.py @@ -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") \ No newline at end of file diff --git a/intro_test/pyproject.toml b/intro_test/pyproject.toml new file mode 100644 index 0000000..e47432f --- /dev/null +++ b/intro_test/pyproject.toml @@ -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", +] diff --git a/intro_test/server.py b/intro_test/server.py new file mode 100644 index 0000000..85dabd0 --- /dev/null +++ b/intro_test/server.py @@ -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}") \ No newline at end of file diff --git a/intro_test/uv.lock b/intro_test/uv.lock new file mode 100644 index 0000000..6107a5f --- /dev/null +++ b/intro_test/uv.lock @@ -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" }, +] diff --git a/mcp_llm_client.py b/mcp_llm_client.py new file mode 100644 index 0000000..9885a4e --- /dev/null +++ b/mcp_llm_client.py @@ -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()) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..f2c9bfa --- /dev/null +++ b/pyproject.toml @@ -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", +] diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..eea2f67 --- /dev/null +++ b/pytest.ini @@ -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 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..0b81610 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,12 @@ +anthropic +openai +mcp[cli] +pytest +pytest-asyncio +uvicorn +fastapi +httpx +pydantic +nest-asyncio +python-dotenv +psutil \ No newline at end of file diff --git a/run_mcp_server.py b/run_mcp_server.py new file mode 100644 index 0000000..b9d4b63 --- /dev/null +++ b/run_mcp_server.py @@ -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) diff --git a/run_tests.py b/run_tests.py new file mode 100644 index 0000000..5229621 --- /dev/null +++ b/run_tests.py @@ -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() diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/mcp_template.egg-info/PKG-INFO b/src/mcp_template.egg-info/PKG-INFO new file mode 100644 index 0000000..c93ef7d --- /dev/null +++ b/src/mcp_template.egg-info/PKG-INFO @@ -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* diff --git a/src/mcp_template.egg-info/SOURCES.txt b/src/mcp_template.egg-info/SOURCES.txt new file mode 100644 index 0000000..5f20167 --- /dev/null +++ b/src/mcp_template.egg-info/SOURCES.txt @@ -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 \ No newline at end of file diff --git a/src/mcp_template.egg-info/dependency_links.txt b/src/mcp_template.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/mcp_template.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/src/mcp_template.egg-info/requires.txt b/src/mcp_template.egg-info/requires.txt new file mode 100644 index 0000000..cfcb767 --- /dev/null +++ b/src/mcp_template.egg-info/requires.txt @@ -0,0 +1,4 @@ +mcp[cli]>=1.14.0 +nest-asyncio>=1.6.0 +openai>=1.107.1 +psutil diff --git a/src/mcp_template.egg-info/top_level.txt b/src/mcp_template.egg-info/top_level.txt new file mode 100644 index 0000000..785cbb7 --- /dev/null +++ b/src/mcp_template.egg-info/top_level.txt @@ -0,0 +1,2 @@ +__init__ +mcp_template diff --git a/src/mcp_template/__init__.py b/src/mcp_template/__init__.py new file mode 100644 index 0000000..e2d634e --- /dev/null +++ b/src/mcp_template/__init__.py @@ -0,0 +1,5 @@ +""" +MCP Template Source Package +""" + +__version__ = "0.1.0" diff --git a/src/mcp_template/config/__init__.py b/src/mcp_template/config/__init__.py new file mode 100644 index 0000000..bddd5cc --- /dev/null +++ b/src/mcp_template/config/__init__.py @@ -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'] diff --git a/src/mcp_template/config/client_config.py b/src/mcp_template/config/client_config.py new file mode 100644 index 0000000..f87a259 --- /dev/null +++ b/src/mcp_template/config/client_config.py @@ -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 diff --git a/src/mcp_template/config/config_manager.py b/src/mcp_template/config/config_manager.py new file mode 100644 index 0000000..dfbc121 --- /dev/null +++ b/src/mcp_template/config/config_manager.py @@ -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 diff --git a/src/mcp_template/config/server_config.py b/src/mcp_template/config/server_config.py new file mode 100644 index 0000000..2e9aa1a --- /dev/null +++ b/src/mcp_template/config/server_config.py @@ -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 {} diff --git a/src/mcp_template/config/transport_config.py b/src/mcp_template/config/transport_config.py new file mode 100644 index 0000000..0b71545 --- /dev/null +++ b/src/mcp_template/config/transport_config.py @@ -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 diff --git a/src/mcp_template/core/__init__.py b/src/mcp_template/core/__init__.py new file mode 100644 index 0000000..dc7f94c --- /dev/null +++ b/src/mcp_template/core/__init__.py @@ -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' +] diff --git a/src/mcp_template/core/interfaces.py b/src/mcp_template/core/interfaces.py new file mode 100644 index 0000000..d75cbff --- /dev/null +++ b/src/mcp_template/core/interfaces.py @@ -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 diff --git a/src/mcp_template/core/types.py b/src/mcp_template/core/types.py new file mode 100644 index 0000000..84b0d2b --- /dev/null +++ b/src/mcp_template/core/types.py @@ -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 = [] diff --git a/src/mcp_template/examples/__init__.py b/src/mcp_template/examples/__init__.py new file mode 100644 index 0000000..f58010e --- /dev/null +++ b/src/mcp_template/examples/__init__.py @@ -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'] diff --git a/src/mcp_template/examples/server_examples.py b/src/mcp_template/examples/server_examples.py new file mode 100644 index 0000000..c64106e --- /dev/null +++ b/src/mcp_template/examples/server_examples.py @@ -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 diff --git a/src/mcp_template/llm_client/__init__.py b/src/mcp_template/llm_client/__init__.py new file mode 100644 index 0000000..89a0471 --- /dev/null +++ b/src/mcp_template/llm_client/__init__.py @@ -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' +] diff --git a/src/mcp_template/llm_client/base_client.py b/src/mcp_template/llm_client/base_client.py new file mode 100644 index 0000000..0bd7b22 --- /dev/null +++ b/src/mcp_template/llm_client/base_client.py @@ -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 diff --git a/src/mcp_template/llm_client/claude_client.py b/src/mcp_template/llm_client/claude_client.py new file mode 100644 index 0000000..ce5cbb1 --- /dev/null +++ b/src/mcp_template/llm_client/claude_client.py @@ -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 diff --git a/src/mcp_template/llm_client/client_factory.py b/src/mcp_template/llm_client/client_factory.py new file mode 100644 index 0000000..c12521c --- /dev/null +++ b/src/mcp_template/llm_client/client_factory.py @@ -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() diff --git a/src/mcp_template/llm_client/grok_client.py b/src/mcp_template/llm_client/grok_client.py new file mode 100644 index 0000000..0bb1968 --- /dev/null +++ b/src/mcp_template/llm_client/grok_client.py @@ -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 diff --git a/src/mcp_template/llm_client/openai_client.py b/src/mcp_template/llm_client/openai_client.py new file mode 100644 index 0000000..a9b9980 --- /dev/null +++ b/src/mcp_template/llm_client/openai_client.py @@ -0,0 +1,105 @@ +""" +OpenAI Client Implementation +""" +from typing import Any, Dict, List, Optional + +try: + from openai import AsyncOpenAI + OPENAI_AVAILABLE = True +except ImportError: + OPENAI_AVAILABLE = False + +from .base_client import BaseAIClient +from config import Config + +class OpenAIClient(BaseAIClient): + """OpenAI client with MCP integration""" + + def __init__( + self, + model_name: str = "gpt-4o", + api_key: Optional[str] = None, + **kwargs + ): + if not OPENAI_AVAILABLE: + raise ImportError("OpenAI package not installed. Install with: pip install openai") + + super().__init__(model_name, "openai", api_key, **kwargs) + + # OpenAI specific configuration + self._temperature = kwargs.get("temperature", 0.7) + self._max_tokens = kwargs.get("max_tokens", 1000) + self._api_key = api_key or Config.OPENAI_API_KEY + + async def _initialize_client(self) -> None: + """Initialize the OpenAI client""" + self._client = AsyncOpenAI(api_key=self._api_key) + + async def chat_completion( + self, + messages: List[Dict[str, Any]], + tools: Optional[List[Dict[str, Any]]] = None, + **kwargs + ) -> Dict[str, Any]: + """Perform OpenAI chat completion""" + if not self._initialized: + await self.initialize() + + # Prepare request parameters + request_params = { + "model": self._model_name, + "messages": messages, + "temperature": self._temperature, + "max_tokens": self._max_tokens, + } + + # Add tools if provided + if tools: + request_params["tools"] = tools + request_params["tool_choice"] = kwargs.get("tool_choice", "auto") + + # Make the API call + response = await self._client.chat.completions.create(**request_params) + + # Convert to standard format + return { + "choices": [ + { + "message": { + "role": choice.message.role, + "content": choice.message.content, + "tool_calls": [ + { + "id": tool_call.id, + "function": { + "name": tool_call.function.name, + "arguments": tool_call.function.arguments, + } + } + for tool_call in (choice.message.tool_calls or []) + ] if choice.message.tool_calls else None, + } + } + for choice in response.choices + ] + } + + def _format_tools_for_provider(self, tools: List[Dict[str, Any]]) -> List[Dict[str, Any]]: + """Format tools for OpenAI's expected format""" + formatted_tools = [] + for tool in tools: + formatted_tool = { + "type": "function", + "function": { + "name": tool["name"], + "description": tool["description"], + "parameters": tool["inputSchema"], + } + } + formatted_tools.append(formatted_tool) + return formatted_tools + + async def _cleanup_client(self) -> None: + """Clean up OpenAI client""" + if self._client: + await self._client.close() diff --git a/src/mcp_template/mcp_client.py b/src/mcp_template/mcp_client.py new file mode 100644 index 0000000..e69de29 diff --git a/src/mcp_template/server/__init__.py b/src/mcp_template/server/__init__.py new file mode 100644 index 0000000..297e1c8 --- /dev/null +++ b/src/mcp_template/server/__init__.py @@ -0,0 +1,5 @@ +# MCP Server implementations +from .modular_server import ModularMCPServer +from .server_factory import MCPServerFactory + +__all__ = ['ModularMCPServer', 'MCPServerFactory'] diff --git a/src/mcp_template/server/modular_server.py b/src/mcp_template/server/modular_server.py new file mode 100644 index 0000000..f606fa6 --- /dev/null +++ b/src/mcp_template/server/modular_server.py @@ -0,0 +1,174 @@ +""" +Modular MCP Server Implementation +""" +import os +from mcp.server.fastmcp import FastMCP +from typing import Optional, List +from .tools.tool_registry import ServerToolRegistry +from .prompts.prompt_registry import ServerPromptRegistry +from .resources.resource_registry import ServerResourceRegistry + + +class ModularMCPServer: + """Modular MCP Server that automatically discovers and registers tools, prompts, and resources""" + + def __init__( + self, + name: str, + host: str = "0.0.0.0", + port: int = 8050, + stateless_http: bool = True, + tools_directory: Optional[str] = None, + prompts_directory: Optional[str] = None, + resources_directory: Optional[str] = None + ): + self.name = name + self.host = host + self.port = port + self.stateless_http = stateless_http + + # Initialize registries + self.tool_registry = ServerToolRegistry(tools_directory) + self.prompt_registry = ServerPromptRegistry(prompts_directory) + self.resource_registry = ServerResourceRegistry(resources_directory) + + # Create FastMCP server + self.mcp = FastMCP( + name=name, + host=host, + port=port, + stateless_http=stateless_http + ) + + self._initialized = False + + async def initialize(self) -> None: + """Initialize the server and register all components""" + if self._initialized: + return + + print(f"Initializing {self.name} server...") + + # Discover and register tools + print("Discovering tools...") + self.tool_registry.register_tools_with_server(self.mcp) + tool_count = len(self.tool_registry.get_all_tools()) + print(f"Registered {tool_count} tools") + + # Discover and register prompts + print("Discovering prompts...") + self.prompt_registry.register_prompts_with_server(self.mcp) + prompt_count = len(self.prompt_registry.get_all_prompts()) + print(f"Registered {prompt_count} prompts") + + # Discover and register resources + print("Discovering resources...") + self.resource_registry.register_resources_with_server(self.mcp) + resource_count = len(self.resource_registry.get_all_resources()) + print(f"Registered {resource_count} resources") + + self._initialized = True + print(f"Server initialization complete!") + + def run(self, transport: str = "stdio") -> None: + """Run the server with the specified transport""" + if not self._initialized: + import asyncio + asyncio.run(self.initialize()) + + print(f"Starting {self.name} server with {transport} transport...") + self.mcp.run(transport=transport) + + def get_server_info(self) -> dict: + """Get information about the server and its components""" + return { + "name": self.name, + "host": self.host, + "port": self.port, + "tools": { + "count": len(self.tool_registry.get_all_tools()), + "names": self.tool_registry.get_tool_names() + }, + "prompts": { + "count": len(self.prompt_registry.get_all_prompts()), + "names": self.prompt_registry.get_prompt_names() + }, + "resources": { + "count": len(self.resource_registry.get_all_resources()), + "uris": self.resource_registry.get_resource_uris() + } + } + + +def create_default_server(transport: str = "stdio", port: int = 8050) -> "ModularMCPServer": + """Create a default modular server with auto-discovery""" + # Get the directory paths for tools, prompts, and resources + # Start from the mcp_template package directory + base_dir = os.path.dirname(os.path.dirname(__file__)) + tools_dir = os.path.join(base_dir, "tools") + prompts_dir = os.path.join(base_dir, "server", "prompts") + resources_dir = os.path.join(base_dir, "server", "resources") + + server = ModularMCPServer( + name="MCP Modular Server", + host="0.0.0.0", + port=port, + stateless_http=True, + tools_directory=tools_dir, + prompts_directory=prompts_dir, + resources_directory=resources_dir + ) + return server + + +def run_server(transport: str = "stdio") -> None: + """Run the modular MCP server with the specified transport method""" + supported_transports = ["stdio", "sse", "streamable-http"] + + if transport not in supported_transports: + raise ValueError(f"Unknown transport: {transport}. Supported: {supported_transports}") + + print(f" Starting MCP Modular Server with {transport.upper()} transport") + + # Create and run the server + server = create_default_server(transport) + + if transport == "stdio": + print(" Server ready for stdio communication") + elif transport == "sse": + print("Server ready for SSE communication on http://0.0.0.0:8050/sse") + elif transport == "streamable-http": + print("Server ready for Streamable HTTP communication") + + # Run the server + server.run(transport) + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser( + description="Run MCP Modular Server", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + python modular_server.py # Run with stdio (default) + python modular_server.py --transport stdio # Run with stdio explicitly + python modular_server.py --transport sse # Run with SSE transport + python modular_server.py --transport streamable-http # Run with Streamable HTTP + +The server will automatically discover and register: +- Tools from the tools directory +- Prompts from the prompts directory +- Resources from the resources directory + """ + ) + parser.add_argument( + "--transport", + choices=["stdio", "sse", "streamable-http"], + default="stdio", + help="Transport method to use (default: stdio)" + ) + + args = parser.parse_args() + run_server(args.transport) diff --git a/src/mcp_template/server/prompts/__init__.py b/src/mcp_template/server/prompts/__init__.py new file mode 100644 index 0000000..de4d4a0 --- /dev/null +++ b/src/mcp_template/server/prompts/__init__.py @@ -0,0 +1,7 @@ +""" +Server Prompts Module +""" +from .base_prompt import BaseServerPrompt +from .prompt_registry import ServerPromptRegistry + +__all__ = ['BaseServerPrompt', 'ServerPromptRegistry'] diff --git a/src/mcp_template/server/prompts/base_prompt.py b/src/mcp_template/server/prompts/base_prompt.py new file mode 100644 index 0000000..9dd96e5 --- /dev/null +++ b/src/mcp_template/server/prompts/base_prompt.py @@ -0,0 +1,47 @@ +""" +Base Server Prompt Class +""" +from abc import ABC, abstractmethod +from typing import Any, Dict, Optional + + +class BaseServerPrompt(ABC): + """Base class for server prompts that can be registered with FastMCP""" + + def __init__(self, name: str, description: str, template: str, arguments: Optional[Dict[str, Any]] = None): + self.name = name + self.description = description + self.template = template + self.arguments = arguments or {} + + @abstractmethod + async def generate(self, **kwargs) -> str: + """Generate the prompt with the provided arguments""" + pass + + def get_prompt_definition(self) -> Dict[str, Any]: + """Get the prompt definition for FastMCP registration""" + return { + "name": self.name, + "description": self.description, + "arguments": self.arguments + } + + def create_fastmcp_prompt(self, mcp_server): + """Create a FastMCP prompt decorator for this prompt""" + @mcp_server.prompt(name=self.name, description=self.description) + async def prompt_wrapper(**kwargs): + return await self.generate(**kwargs) + + return prompt_wrapper + + def _substitute_template(self, **kwargs) -> str: + """Helper method to substitute variables in template""" + result = self.template + + # Apply provided arguments + for key, value in kwargs.items(): + placeholder = f"{{{key}}}" + result = result.replace(placeholder, str(value)) + + return result diff --git a/src/mcp_template/server/prompts/greeting_prompt.py b/src/mcp_template/server/prompts/greeting_prompt.py new file mode 100644 index 0000000..c821c1d --- /dev/null +++ b/src/mcp_template/server/prompts/greeting_prompt.py @@ -0,0 +1,44 @@ +""" +Greeting Prompt Example +""" +from mcp_template.server.prompts.base_prompt import BaseServerPrompt + + +class GreetingPrompt(BaseServerPrompt): + """A greeting prompt template""" + + def __init__(self): + super().__init__( + name="greeting_prompt", + description="Generate a greeting prompt for AI models", + template="Please write a {style} greeting for someone named {name}. The greeting should be {tone} and include a {element}.", + arguments={ + "style": { + "type": "string", + "enum": ["friendly", "formal", "casual"], + "description": "Style of greeting", + "default": "friendly" + }, + "tone": { + "type": "string", + "enum": ["warm", "professional", "relaxed"], + "description": "Tone of the greeting", + "default": "warm" + }, + "element": { + "type": "string", + "enum": ["compliment", "question", "observation"], + "description": "Element to include in greeting", + "default": "compliment" + } + } + ) + + async def generate(self, name: str, style: str = "friendly", tone: str = "warm", element: str = "compliment") -> str: + """Generate the greeting prompt""" + return self._substitute_template( + name=name, + style=style, + tone=tone, + element=element + ) diff --git a/src/mcp_template/server/prompts/prompt_registry.py b/src/mcp_template/server/prompts/prompt_registry.py new file mode 100644 index 0000000..6e49fa9 --- /dev/null +++ b/src/mcp_template/server/prompts/prompt_registry.py @@ -0,0 +1,185 @@ +""" +Server Prompt Registry for dynamic prompt registration +""" +import os +import importlib +import inspect +from typing import List, Dict, Any, Type, Optional +from pathlib import Path +from mcp_template.server.prompts.base_prompt import BaseServerPrompt + + +class ServerPromptRegistry: + """Registry for managing server prompts from files""" + + def __init__(self, prompts_directory: str = None): + self.prompts_directory = prompts_directory or os.path.dirname(__file__) + self._registered_prompts: Dict[str, BaseServerPrompt] = {} + self._prompt_files: List[str] = [] + + @property + def directory(self): + """Alias for prompts_directory for backward compatibility""" + return self.prompts_directory + + @property + def _prompts(self): + """Alias for _registered_prompts for backward compatibility""" + return self._registered_prompts + + def discover_prompts(self) -> List[BaseServerPrompt]: + """Discover all prompts in the prompts directory""" + prompts = [] + prompts_dir = Path(self.prompts_directory) + + if not prompts_dir.exists(): + return prompts + + # Find all Python files in the prompts directory + for file_path in prompts_dir.glob("*.py"): + if file_path.name.startswith("__"): + continue + + module_name = file_path.stem + try: + # Import the module + spec = importlib.util.spec_from_file_location(module_name, file_path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + + # First, look for BaseServerPrompt subclasses (class-based approach) + for name, obj in inspect.getmembers(module, inspect.isclass): + if (issubclass(obj, BaseServerPrompt) and + obj != BaseServerPrompt and + not inspect.isabstract(obj)): + try: + prompt_instance = obj() + prompts.append(prompt_instance) + self._registered_prompts[prompt_instance.name] = prompt_instance + except Exception as e: + print(f"Warning: Could not instantiate prompt {name}: {e}") + + # Second, look for functions decorated with @mcp.prompt or ending with _prompt + for name, obj in inspect.getmembers(module): + if (inspect.isfunction(obj) and + hasattr(obj, '__name__') and + not name.startswith('_')): + try: + # Check if it's decorated with mcp.prompt or follows naming convention + if (name.endswith('_prompt') or + hasattr(obj, '__mcp_prompt__') or + hasattr(obj, '__wrapped__')): # Often indicates decoration + # Convert function to BaseServerPrompt wrapper + wrapper = self._create_prompt_wrapper(obj, name) + prompts.append(wrapper) + self._registered_prompts[name] = wrapper + except Exception as e: + continue + + # Third, look for classes with get_prompts() static method + for name, obj in inspect.getmembers(module, inspect.isclass): + if hasattr(obj, 'get_prompts') and callable(getattr(obj, 'get_prompts')): + try: + # Call get_prompts() to get prompt objects + prompt_objects = obj.get_prompts() + if isinstance(prompt_objects, list): + for prompt in prompt_objects: + if hasattr(prompt, 'name'): + wrapper = self._create_prompt_wrapper_from_object(prompt) + prompts.append(wrapper) + self._registered_prompts[prompt.name] = wrapper + except Exception as e: + print(f"Warning: Could not get prompts from {name}: {e}") + + except Exception as e: + print(f"Warning: Could not load prompt file {file_path}: {e}") + + return prompts + + def _create_prompt_wrapper(self, func, name: str) -> BaseServerPrompt: + """Convert a function to a BaseServerPrompt wrapper""" + from mcp_template.server.prompts.base_prompt import BaseServerPrompt + + class PromptWrapper(BaseServerPrompt): + def __init__(self, func, name): + # Extract arguments from function signature + import inspect as insp + sig = insp.signature(func) + arguments = {} + for param_name, param in sig.parameters.items(): + if param_name != 'self': # Skip self for methods + arguments[param_name] = { + "type": "string", # Default to string, could be enhanced + "description": f"Parameter {param_name}" + } + + super().__init__( + name=name, + description=getattr(func, '__doc__', f'Prompt: {name}'), + arguments=arguments + ) + self._func = func + + def get_messages(self, **kwargs): + # Call the function with provided arguments + return self._func(**kwargs) + + return PromptWrapper(func, name) + + def _create_prompt_wrapper_from_object(self, prompt_obj) -> BaseServerPrompt: + """Convert a prompt object to BaseServerPrompt wrapper""" + from mcp_template.server.prompts.base_prompt import BaseServerPrompt + + class PromptObjectWrapper(BaseServerPrompt): + def __init__(self, prompt_obj): + super().__init__( + name=getattr(prompt_obj, 'name', 'unknown'), + description=getattr(prompt_obj, 'description', ''), + arguments=getattr(prompt_obj, 'arguments', {}) + ) + self._prompt_obj = prompt_obj + + def get_messages(self, **kwargs): + # Try to call the prompt object + if hasattr(self._prompt_obj, 'get_messages'): + return self._prompt_obj.get_messages(**kwargs) + elif callable(self._prompt_obj): + return self._prompt_obj(**kwargs) + return [] + + return PromptObjectWrapper(prompt_obj) + + def register_prompt(self, prompt: BaseServerPrompt) -> None: + """Register a single prompt""" + self._registered_prompts[prompt.name] = prompt + + def register_prompts(self, prompts: List[BaseServerPrompt]) -> None: + """Register multiple prompts""" + for prompt in prompts: + self.register_prompt(prompt) + + def get_prompt(self, name: str) -> Optional[BaseServerPrompt]: + """Get a prompt by name""" + return self._registered_prompts.get(name) + + def get_all_prompts(self) -> List[BaseServerPrompt]: + """Get all registered prompts""" + return list(self._registered_prompts.values()) + + def get_prompt_names(self) -> List[str]: + """Get all registered prompt names""" + return list(self._registered_prompts.keys()) + + def register_prompts_with_server(self, mcp_server) -> None: + """Register all discovered prompts with a FastMCP server""" + # First discover prompts if not already done + if not self._registered_prompts: + self.discover_prompts() + + # Register each prompt with the server + for prompt in self._registered_prompts.values(): + prompt.create_fastmcp_prompt(mcp_server) + + def clear_prompts(self) -> None: + """Clear all registered prompts""" + self._registered_prompts.clear() diff --git a/src/mcp_template/server/resources/__init__.py b/src/mcp_template/server/resources/__init__.py new file mode 100644 index 0000000..d6b9d57 --- /dev/null +++ b/src/mcp_template/server/resources/__init__.py @@ -0,0 +1,7 @@ +""" +Server Resources Module +""" +from .base_resource import BaseServerResource +from .resource_registry import ServerResourceRegistry + +__all__ = ['BaseServerResource', 'ServerResourceRegistry'] diff --git a/src/mcp_template/server/resources/base_resource.py b/src/mcp_template/server/resources/base_resource.py new file mode 100644 index 0000000..37256d7 --- /dev/null +++ b/src/mcp_template/server/resources/base_resource.py @@ -0,0 +1,37 @@ +""" +Base Server Resource Class +""" +from abc import ABC, abstractmethod +from typing import Any, Dict, Optional, Union + + +class BaseServerResource(ABC): + """Base class for server resources that can be registered with FastMCP""" + + def __init__(self, uri: str, name: str, description: str, mime_type: str = "text/plain"): + self.uri = uri + self.name = name + self.description = description + self.mime_type = mime_type + + @abstractmethod + async def read(self, **kwargs) -> Union[str, bytes]: + """Read the resource content""" + pass + + def get_resource_definition(self) -> Dict[str, Any]: + """Get the resource definition for FastMCP registration""" + return { + "uri": self.uri, + "name": self.name, + "description": self.description, + "mime_type": self.mime_type + } + + def create_fastmcp_resource(self, mcp_server): + """Create a FastMCP resource decorator for this resource""" + @mcp_server.resource(self.uri, name=self.name, description=self.description, mime_type=self.mime_type) + async def resource_wrapper(): + return await self.read() + + return resource_wrapper diff --git a/src/mcp_template/server/resources/config_resource.py b/src/mcp_template/server/resources/config_resource.py new file mode 100644 index 0000000..fb4e200 --- /dev/null +++ b/src/mcp_template/server/resources/config_resource.py @@ -0,0 +1,37 @@ +""" +Configuration Resource Example +""" +from mcp_template.server.resources.base_resource import BaseServerResource + + +class ConfigResource(BaseServerResource): + """A configuration resource that provides server settings""" + + def __init__(self): + super().__init__( + uri="config://settings", + name="Server Configuration", + description="Get the current server configuration settings", + mime_type="application/json" + ) + + async def read(self, **kwargs) -> str: + """Get the configuration content""" + import json + config = { + "server_name": "MCP Template Server", + "version": "1.0.0", + "features": [ + "tools", + "prompts", + "resources" + ], + "transport": { + "supported": ["stdio", "sse"], + "default": "stdio" + }, + "tools_count": 2, + "prompts_count": 1, + "resources_count": 1 + } + return json.dumps(config, indent=2) diff --git a/src/mcp_template/server/resources/dynamic_resource.py b/src/mcp_template/server/resources/dynamic_resource.py new file mode 100644 index 0000000..90585da --- /dev/null +++ b/src/mcp_template/server/resources/dynamic_resource.py @@ -0,0 +1,28 @@ +""" +Dynamic Resource Example +""" +from mcp_template.server.resources.base_resource import BaseServerResource + + +class DynamicResource(BaseServerResource): + """A dynamic resource that accepts parameters""" + + def __init__(self): + super().__init__( + uri="dynamic://greeting", + name="Dynamic Greeting", + description="Get a personalized greeting resource", + mime_type="text/plain" + ) + + async def read(self, name: str = "World") -> str: + """Get the dynamic greeting content""" + return f"Hello, {name}! This is a dynamic resource that was generated just for you.\n\n" \ + f"Resource URI: {self.uri}\n" \ + f"Generated at: {self._get_timestamp()}\n" \ + f"Personalized for: {name}" + + def _get_timestamp(self) -> str: + """Get current timestamp""" + from datetime import datetime + return datetime.now().strftime("%Y-%m-%d %H:%M:%S") diff --git a/src/mcp_template/server/resources/resource_registry.py b/src/mcp_template/server/resources/resource_registry.py new file mode 100644 index 0000000..70fed66 --- /dev/null +++ b/src/mcp_template/server/resources/resource_registry.py @@ -0,0 +1,179 @@ +""" +Server Resource Registry for dynamic resource registration +""" +import os +import importlib +import inspect +from typing import List, Dict, Any, Type,Optional +from pathlib import Path +from mcp_template.server.resources.base_resource import BaseServerResource + + +class ServerResourceRegistry: + """Registry for managing server resources from files""" + + def __init__(self, resources_directory: str = None): + self.resources_directory = resources_directory or os.path.dirname(__file__) + self._registered_resources: Dict[str, BaseServerResource] = {} + self._resource_files: List[str] = [] + + @property + def directory(self): + """Alias for resources_directory for backward compatibility""" + return self.resources_directory + + @property + def _resources(self): + """Alias for _registered_resources for backward compatibility""" + return self._registered_resources + + def discover_resources(self) -> List[BaseServerResource]: + """Discover all resources in the resources directory""" + resources = [] + resources_dir = Path(self.resources_directory) + + if not resources_dir.exists(): + return resources + + # Find all Python files in the resources directory + for file_path in resources_dir.glob("*.py"): + if file_path.name.startswith("__"): + continue + + module_name = file_path.stem + try: + # Import the module + spec = importlib.util.spec_from_file_location(module_name, file_path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + + # First, look for BaseServerResource subclasses (class-based approach) + for name, obj in inspect.getmembers(module, inspect.isclass): + if (issubclass(obj, BaseServerResource) and + obj != BaseServerResource and + not inspect.isabstract(obj)): + try: + resource_instance = obj() + resources.append(resource_instance) + self._registered_resources[resource_instance.uri] = resource_instance + except Exception as e: + print(f"Warning: Could not instantiate resource {name}: {e}") + + # Second, look for functions decorated with @mcp.resource or ending with _resource + for name, obj in inspect.getmembers(module): + if (inspect.isfunction(obj) and + hasattr(obj, '__name__') and + not name.startswith('_')): + try: + # Check if it's decorated with mcp.resource or follows naming convention + if (name.endswith('_resource') or + hasattr(obj, '__mcp_resource__') or + hasattr(obj, '__wrapped__')): # Often indicates decoration + # Convert function to BaseServerResource wrapper + wrapper = self._create_resource_wrapper(obj, name) + resources.append(wrapper) + self._registered_resources[wrapper.uri] = wrapper + except Exception as e: + continue + + # Third, look for classes with get_resources() static method + for name, obj in inspect.getmembers(module, inspect.isclass): + if hasattr(obj, 'get_resources') and callable(getattr(obj, 'get_resources')): + try: + # Call get_resources() to get resource objects + resource_objects = obj.get_resources() + if isinstance(resource_objects, list): + for resource in resource_objects: + if hasattr(resource, 'uri'): + wrapper = self._create_resource_wrapper_from_object(resource) + resources.append(wrapper) + self._registered_resources[resource.uri] = wrapper + except Exception as e: + print(f"Warning: Could not get resources from {name}: {e}") + + except Exception as e: + print(f"Warning: Could not load resource file {file_path}: {e}") + + return resources + + def _create_resource_wrapper(self, func, name: str) -> BaseServerResource: + """Convert a function to a BaseServerResource wrapper""" + from mcp_template.server.resources.base_resource import BaseServerResource + + class ResourceWrapper(BaseServerResource): + def __init__(self, func, name): + # Create a URI pattern from the function name + uri = f"resource://{name}" + + super().__init__( + uri=uri, + name=name, + description=getattr(func, '__doc__', f'Resource: {name}'), + mime_type="text/plain" # Default, could be enhanced + ) + self._func = func + + async def read(self): + # Call the function to get resource content + return str(self._func()) + + return ResourceWrapper(func, name) + + def _create_resource_wrapper_from_object(self, resource_obj) -> BaseServerResource: + """Convert a resource object to BaseServerResource wrapper""" + from mcp_template.server.resources.base_resource import BaseServerResource + + class ResourceObjectWrapper(BaseServerResource): + def __init__(self, resource_obj): + super().__init__( + uri=getattr(resource_obj, 'uri', 'unknown://resource'), + name=getattr(resource_obj, 'name', 'unknown'), + description=getattr(resource_obj, 'description', ''), + mime_type=getattr(resource_obj, 'mime_type', 'text/plain') + ) + self._resource_obj = resource_obj + + async def read(self, **kwargs): + # Try to call the resource object's read method + if hasattr(self._resource_obj, 'read'): + return await self._resource_obj.read(**kwargs) + elif callable(self._resource_obj): + return str(self._resource_obj()) + return "Resource content not available" + + return ResourceObjectWrapper(resource_obj) + + def register_resource(self, resource: BaseServerResource) -> None: + """Register a single resource""" + self._registered_resources[resource.uri] = resource + + def register_resources(self, resources: List[BaseServerResource]) -> None: + """Register multiple resources""" + for resource in resources: + self.register_resource(resource) + + def get_resource(self, uri: str) -> Optional[BaseServerResource]: + """Get a resource by URI""" + return self._registered_resources.get(uri) + + def get_all_resources(self) -> List[BaseServerResource]: + """Get all registered resources""" + return list(self._registered_resources.values()) + + def get_resource_uris(self) -> List[str]: + """Get all registered resource URIs""" + return list(self._registered_resources.keys()) + + def register_resources_with_server(self, mcp_server) -> None: + """Register all discovered resources with a FastMCP server""" + # First discover resources if not already done + if not self._registered_resources: + self.discover_resources() + + # Register each resource with the server + for resource in self._registered_resources.values(): + resource.create_fastmcp_resource(mcp_server) + + def clear_resources(self) -> None: + """Clear all registered resources""" + self._registered_resources.clear() diff --git a/src/mcp_template/server/server_factory.py b/src/mcp_template/server/server_factory.py new file mode 100644 index 0000000..39ac0b0 --- /dev/null +++ b/src/mcp_template/server/server_factory.py @@ -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, + ) diff --git a/src/mcp_template/server/tools/__init__.py b/src/mcp_template/server/tools/__init__.py new file mode 100644 index 0000000..2480785 --- /dev/null +++ b/src/mcp_template/server/tools/__init__.py @@ -0,0 +1,7 @@ +""" +Server Tools Module +""" +from .base_tool import BaseServerTool +from .tool_registry import ServerToolRegistry + +__all__ = ['BaseServerTool', 'ServerToolRegistry'] diff --git a/src/mcp_template/server/tools/base_tool.py b/src/mcp_template/server/tools/base_tool.py new file mode 100644 index 0000000..0eb9f5f --- /dev/null +++ b/src/mcp_template/server/tools/base_tool.py @@ -0,0 +1,59 @@ +""" +Base Server Tool Class +""" +from abc import ABC, abstractmethod +from typing import Any, Dict, Optional +from mcp.server.fastmcp import Context +from mcp.server.session import ServerSession + + +class BaseServerTool(ABC): + """Base class for server tools that can be registered with FastMCP""" + + def __init__(self, name: str, description: str, input_schema: Dict[str, Any]): + self.name = name + self.description = description + self.input_schema = input_schema + + @abstractmethod + async def execute(self, **kwargs) -> Any: + """Execute the tool with the provided arguments""" + pass + + def get_tool_definition(self) -> Dict[str, Any]: + """Get the tool definition for FastMCP registration""" + return { + "name": self.name, + "description": self.description, + "input_schema": self.input_schema + } + + def create_fastmcp_tool(self, mcp_server): + """Create a FastMCP tool decorator for this tool""" + @mcp_server.tool(name=self.name, description=self.description) + async def tool_wrapper(**kwargs): + return await self.execute(**kwargs) + + return tool_wrapper + + +class ContextAwareTool(BaseServerTool): + """Base class for tools that need access to MCP context""" + + @abstractmethod + async def execute_with_context(self, ctx: Context[ServerSession, None], **kwargs) -> Any: + """Execute the tool with MCP context""" + pass + + async def execute(self, **kwargs) -> Any: + """Default implementation that doesn't use context""" + # This will be overridden by the FastMCP wrapper + raise NotImplementedError("Use execute_with_context for context-aware tools") + + def create_fastmcp_tool(self, mcp_server): + """Create a FastMCP tool decorator for this context-aware tool""" + @mcp_server.tool(name=self.name, description=self.description) + async def tool_wrapper(ctx: Context[ServerSession, None], **kwargs): + return await self.execute_with_context(ctx, **kwargs) + + return tool_wrapper diff --git a/src/mcp_template/server/tools/calculator_tool.py b/src/mcp_template/server/tools/calculator_tool.py new file mode 100644 index 0000000..05fed15 --- /dev/null +++ b/src/mcp_template/server/tools/calculator_tool.py @@ -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}") diff --git a/src/mcp_template/server/tools/greeting_tool.py b/src/mcp_template/server/tools/greeting_tool.py new file mode 100644 index 0000000..3172e49 --- /dev/null +++ b/src/mcp_template/server/tools/greeting_tool.py @@ -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 diff --git a/src/mcp_template/server/tools/tool_registry.py b/src/mcp_template/server/tools/tool_registry.py new file mode 100644 index 0000000..eceb705 --- /dev/null +++ b/src/mcp_template/server/tools/tool_registry.py @@ -0,0 +1,198 @@ +""" +Server Tool Registry for dynamic tool registration +""" +import os +import importlib +import inspect +from typing import List, Dict, Any, Type,Optional +from pathlib import Path +from mcp_template.server.tools.base_tool import BaseServerTool + + +class ServerToolRegistry: + """Registry for managing server tools from files""" + + def __init__(self, tools_directory: str = None): + self.tools_directory = tools_directory or os.path.dirname(__file__) + self._registered_tools: Dict[str, BaseServerTool] = {} + self._tool_files: List[str] = [] + + @property + def directory(self): + """Alias for tools_directory for backward compatibility""" + return self.tools_directory + + @property + def _tools(self): + """Alias for _registered_tools for backward compatibility""" + return self._registered_tools + + def discover_tools(self) -> List[BaseServerTool]: + """Discover all tools in the tools directory""" + tools = [] + tools_dir = Path(self.tools_directory) + + if not tools_dir.exists(): + return tools + + # Find all Python files in the tools directory + for file_path in tools_dir.glob("*.py"): + if file_path.name.startswith("__"): + continue + + module_name = file_path.stem + try: + # Import the module + spec = importlib.util.spec_from_file_location(module_name, file_path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + + # First, look for BaseServerTool subclasses (class-based approach) + for name, obj in inspect.getmembers(module, inspect.isclass): + if (issubclass(obj, BaseServerTool) and + obj != BaseServerTool and + not inspect.isabstract(obj)): + try: + tool_instance = obj() + tools.append(tool_instance) + self._registered_tools[tool_instance.name] = tool_instance + except Exception as e: + print(f"Warning: Could not instantiate tool {name}: {e}") + + # Second, look for static methods that return MCPTool objects (static approach) + for name, obj in inspect.getmembers(module): + if (inspect.isfunction(obj) and + hasattr(obj, '__name__') and + not name.startswith('_') and + name.endswith('_tool')): # Convention: methods ending with _tool + try: + # Check if it's a static method by calling it + if hasattr(obj, '__self__') and obj.__self__ is not None: + continue # Skip instance methods + + # Try to call the method to get MCPTool objects + result = obj() + if isinstance(result, list): + # Method returns a list of MCPTool objects + for tool in result: + if hasattr(tool, 'name') and hasattr(tool, 'handler'): + # Convert MCPTool to BaseServerTool wrapper + wrapper = self._create_tool_wrapper(tool) + tools.append(wrapper) + self._registered_tools[tool.name] = wrapper + elif hasattr(result, 'name') and hasattr(result, 'handler'): + # Method returns a single MCPTool object + wrapper = self._create_tool_wrapper(result) + tools.append(wrapper) + self._registered_tools[result.name] = wrapper + + except Exception as e: + # Skip methods that don't work as expected + continue + + # Third, look for classes with get_tools() static method + for name, obj in inspect.getmembers(module, inspect.isclass): + if hasattr(obj, 'get_tools') and callable(getattr(obj, 'get_tools')): + try: + # Call get_tools() to get MCPTool objects + mcp_tools = obj.get_tools() + if isinstance(mcp_tools, list): + for tool in mcp_tools: + if hasattr(tool, 'name') and hasattr(tool, 'handler'): + wrapper = self._create_tool_wrapper(tool) + tools.append(wrapper) + self._registered_tools[tool.name] = wrapper + except Exception as e: + print(f"Warning: Could not get tools from {name}: {e}") + + except Exception as e: + print(f"Warning: Could not load tool file {file_path}: {e}") + + return tools + + def _create_tool_wrapper(self, mcp_tool) -> BaseServerTool: + """Convert an MCPTool to a BaseServerTool wrapper""" + from mcp_template.server.tools.base_tool import BaseServerTool + + class MCPToolWrapper(BaseServerTool): + def __init__(self, mcp_tool): + super().__init__( + name=mcp_tool.name, + description=getattr(mcp_tool, 'description', ''), + input_schema=getattr(mcp_tool, 'input_schema', {}) + ) + self._mcp_tool = mcp_tool + + async def execute(self, **kwargs): + # Handle different argument formats that OpenAI might send + actual_kwargs = kwargs.copy() + + # Check if this is FastMCP's kwargs format + if len(kwargs) == 1 and 'kwargs' in kwargs: + kwargs_value = kwargs['kwargs'] + if isinstance(kwargs_value, str): + # Try to parse as JSON first + try: + import json + parsed = json.loads(kwargs_value) + if isinstance(parsed, dict): + actual_kwargs = parsed + except (json.JSONDecodeError, TypeError): + # If not JSON, try to map to expected parameters + input_schema = getattr(self._mcp_tool, 'input_schema', {}) + properties = input_schema.get('properties', {}) + if properties and len(properties) == 1: + param_name = list(properties.keys())[0] + actual_kwargs = {param_name: kwargs_value} + elif isinstance(kwargs_value, dict): + # kwargs_value is already a dict + actual_kwargs = kwargs_value + + # Call the MCPTool handler with the processed arguments + if hasattr(self._mcp_tool, 'handler') and callable(self._mcp_tool.handler): + try: + result = await self._mcp_tool.handler(**actual_kwargs) + return result + except TypeError as e: + print(f"TypeError in tool {self.name}: {e}") + print(f"Expected parameters: {list(getattr(self._mcp_tool, 'input_schema', {}).get('properties', {}).keys())}") + print(f"Received: {list(actual_kwargs.keys())}") + raise + return None + + return MCPToolWrapper(mcp_tool) + + def register_tool(self, tool: BaseServerTool) -> None: + """Register a single tool""" + self._registered_tools[tool.name] = tool + + def register_tools(self, tools: List[BaseServerTool]) -> None: + """Register multiple tools""" + for tool in tools: + self.register_tool(tool) + + def get_tool(self, name: str) -> Optional[BaseServerTool]: + """Get a tool by name""" + return self._registered_tools.get(name) + + def get_all_tools(self) -> List[BaseServerTool]: + """Get all registered tools""" + return list(self._registered_tools.values()) + + def get_tool_names(self) -> List[str]: + """Get all registered tool names""" + return list(self._registered_tools.keys()) + + def register_tools_with_server(self, mcp_server) -> None: + """Register all discovered tools with a FastMCP server""" + # First discover tools if not already done + if not self._registered_tools: + self.discover_tools() + + # Register each tool with the server + for tool in self._registered_tools.values(): + tool.create_fastmcp_tool(mcp_server) + + def clear_tools(self) -> None: + """Clear all registered tools""" + self._registered_tools.clear() diff --git a/src/mcp_template/tools/__init__.py b/src/mcp_template/tools/__init__.py new file mode 100644 index 0000000..533ef44 --- /dev/null +++ b/src/mcp_template/tools/__init__.py @@ -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'] diff --git a/src/mcp_template/tools/math_tools.py b/src/mcp_template/tools/math_tools.py new file mode 100644 index 0000000..d604ed2 --- /dev/null +++ b/src/mcp_template/tools/math_tools.py @@ -0,0 +1,192 @@ +""" +Mathematical Tools for MCP +""" +from typing import List +from mcp_template.core.types import MCPTool + + +class MathTools: + """Collection of mathematical tools""" + + @staticmethod + def get_tools() -> List[MCPTool]: + """Get all math tools""" + return [ + MathTools._create_add_tool(), + MathTools._create_subtract_tool(), + MathTools._create_multiply_tool(), + MathTools._create_divide_tool(), + MathTools._create_power_tool(), + MathTools._create_square_root_tool(), + MathTools._create_calculate_bmi_tool(), + ] + + @staticmethod + def _create_add_tool() -> MCPTool: + """Create addition tool""" + async def add(a: float, b: float) -> float: + """Add two numbers together""" + return a + b + + return MCPTool( + name="add", + description="Add two numbers together", + input_schema={ + "type": "object", + "properties": { + "a": {"type": "number", "description": "First number"}, + "b": {"type": "number", "description": "Second number"}, + }, + "required": ["a", "b"], + }, + handler=add, + ) + + @staticmethod + def _create_subtract_tool() -> MCPTool: + """Create subtraction tool""" + async def subtract(a: float, b: float) -> float: + """Subtract second number from first""" + return a - b + + return MCPTool( + name="subtract", + description="Subtract second number from first", + input_schema={ + "type": "object", + "properties": { + "a": {"type": "number", "description": "First number"}, + "b": {"type": "number", "description": "Number to subtract"}, + }, + "required": ["a", "b"], + }, + handler=subtract, + ) + + @staticmethod + def _create_multiply_tool() -> MCPTool: + """Create multiplication tool""" + async def multiply(a: float, b: float) -> float: + """Multiply two numbers""" + return a * b + + return MCPTool( + name="multiply", + description="Multiply two numbers", + input_schema={ + "type": "object", + "properties": { + "a": {"type": "number", "description": "First number"}, + "b": {"type": "number", "description": "Second number"}, + }, + "required": ["a", "b"], + }, + handler=multiply, + ) + + @staticmethod + def _create_divide_tool() -> MCPTool: + """Create division tool""" + async def divide(a: float, b: float) -> float: + """Divide first number by second""" + if b == 0: + raise ValueError("Cannot divide by zero") + return a / b + + return MCPTool( + name="divide", + description="Divide first number by second", + input_schema={ + "type": "object", + "properties": { + "a": {"type": "number", "description": "Dividend"}, + "b": {"type": "number", "description": "Divisor (cannot be zero)"}, + }, + "required": ["a", "b"], + }, + handler=divide, + ) + + @staticmethod + def _create_power_tool() -> MCPTool: + """Create power tool""" + async def power(base: float, exponent: float) -> float: + """Calculate base raised to the power of exponent""" + return base ** exponent + + return MCPTool( + name="power", + description="Calculate base raised to the power of exponent", + input_schema={ + "type": "object", + "properties": { + "base": {"type": "number", "description": "Base number"}, + "exponent": {"type": "number", "description": "Exponent"}, + }, + "required": ["base", "exponent"], + }, + handler=power, + ) + + @staticmethod + def _create_square_root_tool() -> MCPTool: + """Create square root tool""" + async def square_root(number: float) -> float: + """Calculate square root of a number""" + # Ensure number is a float + try: + number = float(number) + except (ValueError, TypeError): + raise ValueError(f"Invalid number format: {number}") + + if number < 0: + raise ValueError("Cannot calculate square root of negative number") + return number ** 0.5 + + return MCPTool( + name="square_root", + description="Calculate square root of a number", + input_schema={ + "type": "object", + "properties": { + "number": {"type": "number", "description": "Number to find square root of (must be non-negative)"}, + }, + "required": ["number"], + }, + handler=square_root, + ) + + @staticmethod + def _create_calculate_bmi_tool() -> MCPTool: + """Create BMI calculation tool""" + async def calculate_bmi(weight_kg: float, height_m: float) -> str: + """Calculate BMI and provide health category""" + if weight_kg <= 0 or height_m <= 0: + raise ValueError("Weight and height must be positive numbers") + + bmi = weight_kg / (height_m ** 2) + + if bmi < 18.5: + category = "Underweight" + elif bmi < 25: + category = "Normal weight" + elif bmi < 30: + category = "Overweight" + else: + category = "Obese" + + return ".1f" + + return MCPTool( + name="calculate_bmi", + description="Calculate BMI and provide health assessment", + input_schema={ + "type": "object", + "properties": { + "weight_kg": {"type": "number", "description": "Weight in kilograms"}, + "height_m": {"type": "number", "description": "Height in meters"}, + }, + "required": ["weight_kg", "height_m"], + }, + handler=calculate_bmi, + ) diff --git a/src/mcp_template/tools/system_tools.py b/src/mcp_template/tools/system_tools.py new file mode 100644 index 0000000..c588704 --- /dev/null +++ b/src/mcp_template/tools/system_tools.py @@ -0,0 +1,157 @@ +""" +System Tools for MCP +""" +import os +import platform +import psutil +from datetime import datetime +from typing import Dict, Any, List +from mcp_template.core.types import MCPTool + + +class SystemTools: + """Collection of system-related tools""" + + @staticmethod + def get_tools() -> List[MCPTool]: + """Get all system tools""" + return [ + SystemTools._create_get_system_info_tool(), + SystemTools._create_get_current_time_tool(), + SystemTools._create_list_directory_tool(), + SystemTools._create_get_file_info_tool(), + SystemTools._create_get_environment_variable_tool(), + ] + + @staticmethod + def _create_get_system_info_tool() -> MCPTool: + """Create system info tool""" + async def get_system_info() -> Dict[str, Any]: + """Get basic system information""" + try: + return { + "platform": platform.system(), + "platform_version": platform.version(), + "architecture": platform.machine(), + "processor": platform.processor(), + "python_version": platform.python_version(), + "cpu_count": os.cpu_count(), + "memory_total": psutil.virtual_memory().total if psutil else "psutil not available", + "memory_available": psutil.virtual_memory().available if psutil else "psutil not available", + } + except Exception as e: + return {"error": f"Could not retrieve system info: {str(e)}"} + + return MCPTool( + name="get_system_info", + description="Get basic system information including OS, CPU, and memory details", + input_schema={"type": "object", "properties": {}}, + handler=get_system_info, + ) + + @staticmethod + def _create_get_current_time_tool() -> MCPTool: + """Create current time tool""" + async def get_current_time() -> str: + """Get the current date and time""" + now = datetime.now() + return now.strftime("%Y-%m-%d %H:%M:%S") + + return MCPTool( + name="get_current_time", + description="Get the current date and time in YYYY-MM-DD HH:MM:SS format", + input_schema={"type": "object", "properties": {}}, + handler=get_current_time, + ) + + @staticmethod + def _create_list_directory_tool() -> MCPTool: + """Create directory listing tool""" + async def list_directory(path: str = ".") -> List[str]: + """List contents of a directory""" + try: + if not os.path.exists(path): + raise ValueError(f"Path does not exist: {path}") + + if not os.path.isdir(path): + raise ValueError(f"Path is not a directory: {path}") + + return os.listdir(path) + except Exception as e: + raise ValueError(f"Could not list directory: {str(e)}") + + return MCPTool( + name="list_directory", + description="List the contents of a directory", + input_schema={ + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "Directory path to list", + "default": "." + }, + }, + }, + handler=list_directory, + ) + + @staticmethod + def _create_get_file_info_tool() -> MCPTool: + """Create file info tool""" + async def get_file_info(file_path: str) -> Dict[str, Any]: + """Get information about a file""" + try: + if not os.path.exists(file_path): + raise ValueError(f"File does not exist: {file_path}") + + stat = os.stat(file_path) + return { + "name": os.path.basename(file_path), + "path": os.path.abspath(file_path), + "size": stat.st_size, + "is_file": os.path.isfile(file_path), + "is_directory": os.path.isdir(file_path), + "modified_time": datetime.fromtimestamp(stat.st_mtime).isoformat(), + "created_time": datetime.fromtimestamp(stat.st_ctime).isoformat(), + } + except Exception as e: + raise ValueError(f"Could not get file info: {str(e)}") + + return MCPTool( + name="get_file_info", + description="Get detailed information about a file or directory", + input_schema={ + "type": "object", + "properties": { + "file_path": {"type": "string", "description": "Path to the file or directory"}, + }, + "required": ["file_path"], + }, + handler=get_file_info, + ) + + @staticmethod + def _create_get_environment_variable_tool() -> MCPTool: + """Create environment variable tool""" + async def get_environment_variable(name: str, default_value: str = "") -> str: + """Get the value of an environment variable""" + return os.getenv(name, default_value) + + return MCPTool( + name="get_environment_variable", + description="Get the value of an environment variable", + input_schema={ + "type": "object", + "properties": { + "name": {"type": "string", "description": "Name of the environment variable"}, + "default_value": { + "type": "string", + "description": "Default value if variable is not set", + "default": "" + }, + }, + "required": ["name"], + }, + handler=get_environment_variable, + ) diff --git a/src/mcp_template/tools/text_tools.py b/src/mcp_template/tools/text_tools.py new file mode 100644 index 0000000..6f85ff5 --- /dev/null +++ b/src/mcp_template/tools/text_tools.py @@ -0,0 +1,187 @@ +""" +Text Processing Tools for MCP +""" +import re +from typing import List +from mcp_template.core.types import MCPTool + + +class TextTools: + """Collection of text processing tools""" + + @staticmethod + def get_tools() -> List[MCPTool]: + """Get all text tools""" + return [ + TextTools._create_word_count_tool(), + TextTools._create_text_search_tool(), + TextTools._create_text_replace_tool(), + TextTools._create_text_uppercase_tool(), + TextTools._create_text_lowercase_tool(), + TextTools._create_text_length_tool(), + TextTools._create_generate_greeting_tool(), + ] + + @staticmethod + def _create_word_count_tool() -> MCPTool: + """Create word count tool""" + async def count_words(text: str) -> int: + """Count the number of words in a text""" + words = text.strip().split() + return len(words) + + return MCPTool( + name="count_words", + description="Count the number of words in a text", + input_schema={ + "type": "object", + "properties": { + "text": {"type": "string", "description": "Text to count words in"}, + }, + "required": ["text"], + }, + handler=count_words, + ) + + @staticmethod + def _create_text_search_tool() -> MCPTool: + """Create text search tool""" + async def search_text(text: str, pattern: str, case_sensitive: bool = False) -> List[str]: + """Search for a pattern in text and return all matches""" + flags = 0 if case_sensitive else re.IGNORECASE + matches = re.findall(pattern, text, flags) + return matches + + return MCPTool( + name="search_text", + description="Search for a pattern in text and return all matches", + input_schema={ + "type": "object", + "properties": { + "text": {"type": "string", "description": "Text to search in"}, + "pattern": {"type": "string", "description": "Regular expression pattern to search for"}, + "case_sensitive": {"type": "boolean", "description": "Whether search should be case sensitive", "default": False}, + }, + "required": ["text", "pattern"], + }, + handler=search_text, + ) + + @staticmethod + def _create_text_replace_tool() -> MCPTool: + """Create text replace tool""" + async def replace_text(text: str, old_pattern: str, new_text: str) -> str: + """Replace all occurrences of a pattern in text""" + return text.replace(old_pattern, new_text) + + return MCPTool( + name="replace_text", + description="Replace all occurrences of a pattern in text", + input_schema={ + "type": "object", + "properties": { + "text": {"type": "string", "description": "Original text"}, + "old_pattern": {"type": "string", "description": "Text to replace"}, + "new_text": {"type": "string", "description": "Replacement text"}, + }, + "required": ["text", "old_pattern", "new_text"], + }, + handler=replace_text, + ) + + @staticmethod + def _create_text_uppercase_tool() -> MCPTool: + """Create uppercase tool""" + async def to_uppercase(text: str) -> str: + """Convert text to uppercase""" + return text.upper() + + return MCPTool( + name="to_uppercase", + description="Convert text to uppercase", + input_schema={ + "type": "object", + "properties": { + "text": {"type": "string", "description": "Text to convert to uppercase"}, + }, + "required": ["text"], + }, + handler=to_uppercase, + ) + + @staticmethod + def _create_text_lowercase_tool() -> MCPTool: + """Create lowercase tool""" + async def to_lowercase(text: str) -> str: + """Convert text to lowercase""" + return text.lower() + + return MCPTool( + name="to_lowercase", + description="Convert text to lowercase", + input_schema={ + "type": "object", + "properties": { + "text": {"type": "string", "description": "Text to convert to lowercase"}, + }, + "required": ["text"], + }, + handler=to_lowercase, + ) + + @staticmethod + def _create_text_length_tool() -> MCPTool: + """Create text length tool""" + async def text_length(text: str) -> int: + """Get the length of text""" + return len(text) + + return MCPTool( + name="text_length", + description="Get the length of text (number of characters)", + input_schema={ + "type": "object", + "properties": { + "text": {"type": "string", "description": "Text to measure length of"}, + }, + "required": ["text"], + }, + handler=text_length, + ) + + @staticmethod + def _create_generate_greeting_tool() -> MCPTool: + """Create greeting generation tool""" + async def generate_greeting(name: str, style: str = "casual") -> str: + """Generate a personalized greeting""" + name = name.strip() + if not name: + raise ValueError("Name cannot be empty") + + if style == "casual": + return f"Hey {name}! Welcome! 👋" + elif style == "formal": + return f"Good day, {name}. Welcome to our platform." + elif style == "professional": + return f"Hello, {name}. Thank you for joining us." + else: + return f"Hi {name}! Welcome!" + + return MCPTool( + name="generate_greeting", + description="Generate a personalized greeting with different styles", + input_schema={ + "type": "object", + "properties": { + "name": {"type": "string", "description": "Person's name"}, + "style": { + "type": "string", + "enum": ["casual", "formal", "professional"], + "description": "Greeting style", + "default": "casual" + }, + }, + "required": ["name"], + }, + handler=generate_greeting, + ) diff --git a/src/mcp_template/tools/tool_registry.py b/src/mcp_template/tools/tool_registry.py new file mode 100644 index 0000000..898d598 --- /dev/null +++ b/src/mcp_template/tools/tool_registry.py @@ -0,0 +1,80 @@ +""" +Tool Registry for easy tool management and combination +""" +from typing import List, Dict, Any, Optional +from mcp_template.core.types import MCPTool +from mcp_template.tools.math_tools import MathTools +from mcp_template.tools.text_tools import TextTools +from mcp_template.tools.system_tools import SystemTools +from mcp_template.tools.web_tools import WebTools + + +class ToolRegistry: + """Registry for managing and combining MCP tools from different categories""" + + def __init__(self): + self._tool_categories = { + 'math': MathTools, + 'text': TextTools, + 'system': SystemTools, + 'web': WebTools, + } + self._custom_tools: List[MCPTool] = [] + + def get_tools_by_category(self, category: str) -> List[MCPTool]: + """Get all tools from a specific category""" + if category not in self._tool_categories: + raise ValueError(f"Unknown tool category: {category}") + + return self._tool_categories[category].get_tools() + + def get_tools_by_categories(self, categories: List[str]) -> List[MCPTool]: + """Get tools from multiple categories""" + tools = [] + for category in categories: + tools.extend(self.get_tools_by_category(category)) + return tools + + def get_all_tools(self) -> List[MCPTool]: + """Get all tools from all categories""" + tools = [] + for category in self._tool_categories.values(): + tools.extend(category.get_tools()) + tools.extend(self._custom_tools) + return tools + + def add_custom_tool(self, tool: MCPTool) -> None: + """Add a custom tool to the registry""" + self._custom_tools.append(tool) + + def add_custom_tools(self, tools: List[MCPTool]) -> None: + """Add multiple custom tools to the registry""" + self._custom_tools.extend(tools) + + def get_available_categories(self) -> List[str]: + """Get list of available tool categories""" + return list(self._tool_categories.keys()) + + def get_category_info(self) -> Dict[str, Dict[str, Any]]: + """Get information about each category""" + info = {} + for category_name, category_class in self._tool_categories.items(): + tools = category_class.get_tools() + info[category_name] = { + 'tool_count': len(tools), + 'tool_names': [tool.name for tool in tools] + } + return info + + def create_server_config(self, categories: Optional[List[str]] = None) -> Dict[str, Any]: + """Create a server configuration with tools from specified categories""" + if categories is None: + tools = self.get_all_tools() + else: + tools = self.get_tools_by_categories(categories) + + return { + 'tools': tools, + 'tool_count': len(tools), + 'categories': categories or self.get_available_categories() + } diff --git a/src/mcp_template/tools/web_tools.py b/src/mcp_template/tools/web_tools.py new file mode 100644 index 0000000..246d6f5 --- /dev/null +++ b/src/mcp_template/tools/web_tools.py @@ -0,0 +1,161 @@ +""" +Web Tools for MCP +""" +import urllib.parse +from typing import Dict, Any, List, Optional +from mcp_template.core.types import MCPTool + + +class WebTools: + """Collection of web-related tools""" + + @staticmethod + def get_tools() -> List[MCPTool]: + """Get all web tools""" + return [ + WebTools._create_url_encode_tool(), + WebTools._create_url_decode_tool(), + WebTools._create_parse_url_tool(), + WebTools._create_validate_email_tool(), + WebTools._create_extract_domain_tool(), + ] + + @staticmethod + def _create_url_encode_tool() -> MCPTool: + """Create URL encode tool""" + async def url_encode(text: str) -> str: + """URL encode a string""" + return urllib.parse.quote(text) + + return MCPTool( + name="url_encode", + description="URL encode a string for safe transmission", + input_schema={ + "type": "object", + "properties": { + "text": {"type": "string", "description": "Text to URL encode"}, + }, + "required": ["text"], + }, + handler=url_encode, + ) + + @staticmethod + def _create_url_decode_tool() -> MCPTool: + """Create URL decode tool""" + async def url_decode(encoded_text: str) -> str: + """URL decode a string""" + try: + return urllib.parse.unquote(encoded_text) + except Exception: + raise ValueError("Invalid URL encoded string") + + return MCPTool( + name="url_decode", + description="URL decode a previously encoded string", + input_schema={ + "type": "object", + "properties": { + "encoded_text": {"type": "string", "description": "URL encoded text to decode"}, + }, + "required": ["encoded_text"], + }, + handler=url_decode, + ) + + @staticmethod + def _create_parse_url_tool() -> MCPTool: + """Create URL parsing tool""" + async def parse_url(url: str) -> Dict[str, str]: + """Parse a URL and return its components""" + try: + parsed = urllib.parse.urlparse(url) + return { + "scheme": parsed.scheme, + "netloc": parsed.netloc, + "hostname": parsed.hostname, + "port": str(parsed.port) if parsed.port else "", + "path": parsed.path, + "query": parsed.query, + "fragment": parsed.fragment, + } + except Exception: + raise ValueError("Invalid URL format") + + return MCPTool( + name="parse_url", + description="Parse a URL and return its components (scheme, host, path, etc.)", + input_schema={ + "type": "object", + "properties": { + "url": {"type": "string", "description": "URL to parse"}, + }, + "required": ["url"], + }, + handler=parse_url, + ) + + @staticmethod + def _create_validate_email_tool() -> MCPTool: + """Create email validation tool""" + async def validate_email(email: str) -> Dict[str, Any]: + """Validate an email address format""" + import re + + email_pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$' + is_valid = bool(re.match(email_pattern, email)) + + # Extract domain + if is_valid and '@' in email: + domain = email.split('@')[1] + else: + domain = "" + + return { + "email": email, + "is_valid": is_valid, + "domain": domain, + } + + return MCPTool( + name="validate_email", + description="Validate an email address format and extract domain", + input_schema={ + "type": "object", + "properties": { + "email": {"type": "string", "description": "Email address to validate"}, + }, + "required": ["email"], + }, + handler=validate_email, + ) + + @staticmethod + def _create_extract_domain_tool() -> MCPTool: + """Create domain extraction tool""" + async def extract_domain(url: str) -> str: + """Extract the domain from a URL""" + try: + parsed = urllib.parse.urlparse(url) + if parsed.hostname: + return parsed.hostname + else: + # Fallback for URLs without scheme + url_with_scheme = "http://" + url if "://" not in url else url + parsed = urllib.parse.urlparse(url_with_scheme) + return parsed.hostname or "" + except Exception: + return "" + + return MCPTool( + name="extract_domain", + description="Extract the domain name from a URL", + input_schema={ + "type": "object", + "properties": { + "url": {"type": "string", "description": "URL to extract domain from"}, + }, + "required": ["url"], + }, + handler=extract_domain, + ) diff --git a/src/mcp_template/transport/__init__.py b/src/mcp_template/transport/__init__.py new file mode 100644 index 0000000..f9885a7 --- /dev/null +++ b/src/mcp_template/transport/__init__.py @@ -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'] diff --git a/src/mcp_template/transport/sse_transport.py b/src/mcp_template/transport/sse_transport.py new file mode 100644 index 0000000..daf0d21 --- /dev/null +++ b/src/mcp_template/transport/sse_transport.py @@ -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") diff --git a/src/mcp_template/transport/stdio_transport.py b/src/mcp_template/transport/stdio_transport.py new file mode 100644 index 0000000..d480f6b --- /dev/null +++ b/src/mcp_template/transport/stdio_transport.py @@ -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) diff --git a/src/mcp_template/transport/transport_manager.py b/src/mcp_template/transport/transport_manager.py new file mode 100644 index 0000000..ff5f051 --- /dev/null +++ b/src/mcp_template/transport/transport_manager.py @@ -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 diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..9a14bcb --- /dev/null +++ b/tests/README.md @@ -0,0 +1,198 @@ +# Test Suite + +This directory contains comprehensive tests for the MCP Template system. + +## Test Structure + +``` +tests/ +├── unit/ # Unit tests (fast, no external dependencies) +├── integration/ # Integration tests (require real server connections) +├── e2e/ # End-to-end tests +└── README.md # This file +``` + +## Test Types + +### Unit Tests +- **Fast**: Run in milliseconds +- **Isolated**: No external dependencies +- **Focused**: Test individual components +- **Run with**: `python run_tests.py unit` + +### Integration Tests +- **Real connections**: Test with actual MCP server +- **API calls**: May require API keys +- **Slower**: Take longer to execute +- **Run with**: `python run_tests.py integration` + +## Running Tests + +### Quick Start + +```bash +# 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 +``` + +### Advanced Options + +```bash +# Run with coverage report +python run_tests.py all --coverage + +# Skip slow tests +python run_tests.py integration --skip-slow + +# Run specific test markers +pytest -m "integration and not slow" +pytest -m "unit" +pytest -m "requires_api_key" +``` + +## Test Markers + +- **`@pytest.mark.unit`**: Unit tests (fast, isolated) +- **`@pytest.mark.integration`**: Integration tests (require server) +- **`@pytest.mark.requires_api_key`**: Tests requiring API keys +- **`@pytest.mark.slow`**: Slow-running tests +- **`@pytest.mark.asyncio`**: Async tests + +## Environment Setup + +### API Keys (for integration tests) + +Create a `.env` file in the project root: + +```bash +# OpenAI (required for most tests) +OPENAI_API_KEY=your_openai_api_key_here + +# Optional: Other providers +ANTHROPIC_API_KEY=your_anthropic_key_here +GROK_API_KEY=your_grok_key_here +``` + +### Test Configuration + +Tests are configured via `pytest.ini`: +- Async mode enabled +- Custom markers defined +- Test discovery patterns +- Warning filters + +## Integration Test Details + +### MCP Server Management +Integration tests automatically: +1. Start MCP server processes +2. Wait for server readiness +3. Connect test clients +4. Clean up processes after tests + +### Transport Testing +- **SSE Transport**: HTTP-based, tested with real server +- **STDIO Transport**: Direct process communication + +### AI Provider Testing +- **OpenAI**: Primary provider for most tests +- **Claude**: Tested if ANTHROPIC_API_KEY available +- **Grok**: Tested if GROK_API_KEY available + +## Writing Tests + +### Unit Test Example +```python +import pytest +from mcp_template.core.types import TransportType + +@pytest.mark.unit +def test_transport_enum(): + """Test TransportType enum values""" + assert TransportType.SSE.value == "sse" + assert TransportType.STDIO.value == "stdio" +``` + +### Integration Test Example +```python +import pytest +from mcp_llm_client import MCPAIClient, TransportType + +@pytest.mark.integration +@pytest.mark.requires_api_key +@pytest.mark.asyncio +async def test_client_connection(sse_server): + """Test MCP client can connect to server""" + if not os.getenv("OPENAI_API_KEY"): + pytest.skip("OpenAI API key not available") + + client = MCPAIClient(model="gpt-4o", transport=TransportType.SSE) + + try: + await client.connect(sse_server.url) + tools = await client.get_mcp_tools() + assert len(tools) > 0 + finally: + await client.disconnect() +``` + +## Test Coverage + +Run tests with coverage: + +```bash +# Generate HTML coverage report +python run_tests.py all --coverage + +# View coverage report +open htmlcov/index.html +``` + +## Troubleshooting + +### Common Issues + +1. **Server won't start**: Check port availability +2. **API key errors**: Ensure `.env` file exists +3. **Connection timeouts**: Increase timeout values +4. **Process cleanup**: Tests handle cleanup automatically + +### Debug Mode + +```bash +# Run with verbose output +pytest -v -s tests/integration/test_mcp_integration.py::TestMCPClientIntegration::test_sse_transport_connection + +# Run single test with debugging +pytest -v --pdb tests/integration/test_mcp_integration.py -k "test_sse_transport" +``` + +## CI/CD Integration + +Tests are designed to work in CI/CD environments: +- Automatic skipping when API keys unavailable +- Process cleanup on test failure +- No manual server startup required +- Cross-platform compatibility + +## Performance Testing + +Some tests measure performance: +- Connection establishment time +- Query response times +- Concurrent connection handling +- Resource cleanup efficiency + +Run performance tests: +```bash +pytest -m "slow" -v +``` diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e003090 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,2 @@ +# Test suite for MCP Template +# Run tests with: python -m pytest tests/ diff --git a/tests/e2e/test_end_to_end.py b/tests/e2e/test_end_to_end.py new file mode 100644 index 0000000..d6c4ff5 --- /dev/null +++ b/tests/e2e/test_end_to_end.py @@ -0,0 +1,255 @@ +""" +End-to-end tests for complete MCP workflow +""" +import pytest +import asyncio +import tempfile +import os +from pathlib import Path +from unittest.mock import Mock, AsyncMock, patch + +from mcp_template.core.types import MCPTool, MCPServerConfig, TransportType +from mcp_template.server.fastmcp_server import FastMCPServer +from mcp_template.llm_client.openai_client import OpenAIClient +from mcp_template.config.config_manager import ConfigManager + + +class TestEndToEnd: + """End-to-end tests for complete MCP workflow""" + + @pytest.fixture + async def knowledge_base_server(self): + """Create a knowledge base server for e2e testing""" + kb_data = { + "company_policy": "Our company offers 20 days of paid vacation annually.", + "benefits": "We provide health insurance, 401k matching, and flexible work hours.", + "faq": "Common questions about our policies and procedures." + } + + async def get_kb(): + 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): + 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}" + + return "\n\n".join(results) + + tools = [ + MCPTool( + name="get_knowledge_base", + description="Retrieve the entire knowledge base", + input_schema={"type": "object", "properties": {}}, + handler=get_kb + ), + MCPTool( + name="search_kb", + description="Search the knowledge base for specific information", + input_schema={ + "type": "object", + "properties": { + "query": {"type": "string", "description": "Search query"} + }, + "required": ["query"] + }, + handler=search_kb + ) + ] + + config = MCPServerConfig( + name="Knowledge Base Server", + tools=tools + ) + + server = FastMCPServer(config) + await server.initialize() + return server + + @pytest.fixture + def mock_openai_response(self): + """Mock OpenAI API response with tool calls""" + return { + "choices": [ + { + "message": { + "role": "assistant", + "content": "I'll search the knowledge base for vacation policy information.", + "tool_calls": [ + { + "id": "call_123", + "function": { + "name": "search_kb", + "arguments": '{"query": "vacation"}' + } + } + ] + } + } + ] + } + + @patch('src.clients.openai_client.AsyncOpenAI') + @pytest.mark.asyncio + async def test_complete_mcp_workflow(self, mock_openai_class, knowledge_base_server, mock_openai_response): + """Test complete MCP workflow from query to response""" + + # Mock OpenAI client setup + mock_client = Mock() + mock_response = Mock() + mock_response.choices = [ + Mock(message=Mock( + role="assistant", + content="I'll search the knowledge base for vacation policy information.", + tool_calls=[ + Mock( + id="call_123", + function=Mock( + name="search_kb", + arguments='{"query": "vacation"}' + ) + ) + ] + )) + ] + + # Second call response (after tool execution) + mock_final_response = Mock() + mock_final_response.choices = [ + Mock(message=Mock( + content="Based on the knowledge base, our company offers 20 days of paid vacation annually." + )) + ] + + mock_client.chat.completions.create = AsyncMock(side_effect=[mock_response, mock_final_response]) + mock_openai_class.return_value = mock_client + + # Create AI client + ai_client = OpenAIClient("gpt-4o", "test-key") + await ai_client.initialize() + + # Create mock MCP client that uses the real server + class MockMCPClient: + def __init__(self, server): + self.server = server + + async def call_tool(self, name, arguments): + return await self.server.call_tool(name, arguments) + + async def list_tools(self): + return await self.server.list_tools() + + mcp_client = MockMCPClient(knowledge_base_server) + + # Test the complete workflow + query = "What's the company vacation policy?" + response = await ai_client.process_with_tools(query, [], mcp_client) + + # Verify the workflow + assert "vacation" in response.lower() or "20 days" in response + + # Verify OpenAI was called twice (initial + after tool execution) + assert mock_client.chat.completions.create.call_count == 2 + + # Verify the tool was called correctly + tool_result = await mcp_client.call_tool("search_kb", {"query": "vacation"}) + assert "vacation" in tool_result.lower() + + @pytest.mark.asyncio + async def test_configuration_workflow(self, knowledge_base_server): + """Test configuration loading and usage""" + + # Create temporary config file + config_data = { + "server": { + "name": "Test Server", + "port": 8080, + "transport": "sse" + }, + "client": { + "provider": "openai", + "model": "gpt-4o" + }, + "api_keys": { + "openai": "test-key" + } + } + + with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f: + import json + json.dump(config_data, f) + config_file = f.name + + try: + # Test config manager + config_manager = ConfigManager(config_file) + + server_config = await config_manager.get_server_config() + assert server_config["name"] == "Test Server" + assert server_config["port"] == 8080 + + client_config = await config_manager.get_client_config() + assert client_config["provider"] == "openai" + assert client_config["model"] == "gpt-4o" + + api_key = await config_manager.get_api_key("openai") + assert api_key == "test-key" + + finally: + # Clean up + os.unlink(config_file) + + @pytest.mark.asyncio + async def test_error_recovery(self, knowledge_base_server): + """Test error recovery in MCP operations""" + + # Test with invalid tool call + try: + await knowledge_base_server.call_tool("nonexistent_tool", {}) + assert False, "Should have raised ValueError" + except ValueError as e: + assert "not found" in str(e).lower() + + # Test with valid tool but invalid arguments + try: + await knowledge_base_server.call_tool("search_kb", {"invalid": "args"}) + assert False, "Should have raised error" + except Exception: + # Should handle gracefully + pass + + # Verify server still works after errors + result = await knowledge_base_server.call_tool("get_knowledge_base", {}) + assert "Knowledge Base" in result + + @pytest.mark.asyncio + async def test_multiple_queries_workflow(self, knowledge_base_server): + """Test multiple queries in sequence""" + + queries = [ + "vacation policy", + "benefits", + "faq" + ] + + for query in queries: + # Direct tool call (simulating what AI would do) + result = await knowledge_base_server.call_tool("search_kb", {"query": query}) + + # Verify we get relevant results + assert query.lower() in result.lower() or len(result) > 50 + + # Test getting full knowledge base + full_kb = await knowledge_base_server.call_tool("get_knowledge_base", {}) + assert "company_policy" in full_kb + assert "benefits" in full_kb + assert "faq" in full_kb diff --git a/tests/integration/test_mcp_integration.py b/tests/integration/test_mcp_integration.py new file mode 100644 index 0000000..abc5a90 --- /dev/null +++ b/tests/integration/test_mcp_integration.py @@ -0,0 +1,611 @@ +""" +Comprehensive Integration Tests for MCP Client + +This module contains end-to-end integration tests for the MCP client, +testing real server connections, tool calling, and AI provider integration. +""" +import pytest +import asyncio +import subprocess +import time +import signal +import os +import sys +from typing import Optional, Dict, Any, List +from unittest.mock import Mock, patch +import httpx + +# Add src to path for imports +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..', 'src')) + +from mcp_llm_client import MCPAIClient, TransportType +from mcp_template.llm_client.client_factory import AIClientFactory + + +class MCPServerProcess: + """Helper class to manage MCP server process for testing""" + + def __init__(self, transport: str = "sse", port: int = 8051): + self.transport = transport + self.port = port + self.process: Optional[subprocess.Popen] = None + self.url = f"http://localhost:{port}" + + async def start(self): + """Start the MCP server process""" + cmd = [ + sys.executable, + "run_mcp_server.py", + "--transport", self.transport, + "--port", str(self.port) + ] + + self.process = subprocess.Popen( + cmd, + cwd=os.path.join(os.path.dirname(__file__), '..', '..'), + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) + + # Wait for server to be ready + await self._wait_for_server() + return self + + async def _wait_for_server(self, timeout: int = 30): + """Wait for server to be ready""" + start_time = time.time() + + while time.time() - start_time < timeout: + try: + async with httpx.AsyncClient() as client: + if self.transport == "sse": + response = await client.get(f"{self.url}/sse", timeout=5.0) + if response.status_code == 200: + return + else: + # For stdio, just wait a bit + await asyncio.sleep(2) + return + except Exception: + pass + + await asyncio.sleep(1) + + raise TimeoutError(f"Server did not start within {timeout} seconds") + + async def stop(self): + """Stop the MCP server process""" + if self.process: + try: + self.process.terminate() + self.process.wait(timeout=10) + except subprocess.TimeoutExpired: + self.process.kill() + self.process.wait() + + +@pytest.fixture +async def sse_server(): + """Fixture to start and stop SSE MCP server""" + server = MCPServerProcess(transport="sse", port=8051) + await server.start() + yield server + await server.stop() + + + @pytest.fixture +async def stdio_server(): + """Fixture to start and stop STDIO MCP server""" + server = MCPServerProcess(transport="stdio", port=8052) + await server.start() + yield server + await server.stop() + + +class TestMCPClientIntegration: + """Comprehensive integration tests for MCP client""" + + @pytest.mark.integration + @pytest.mark.requires_api_key + @pytest.mark.asyncio + async def test_sse_transport_connection(self, sse_server): + """Test MCP client can connect to server using SSE transport""" + # Skip if no OpenAI API key + if not os.getenv("OPENAI_API_KEY"): + pytest.skip("OpenAI API key not available") + + client = MCPAIClient( + model="gpt-4o", + transport=TransportType.SSE, + provider="openai", + temperature=0.1, + max_tokens=100 + ) + + try: + # Test connection + await client.connect(sse_server.url) + + # Test getting tools + tools = await client.get_mcp_tools() + assert isinstance(tools, list) + assert len(tools) > 0 + + # Verify tool structure + for tool in tools: + assert "name" in tool + assert "description" in tool + assert "inputSchema" in tool + + finally: + await client.disconnect() + + @pytest.mark.integration + @pytest.mark.requires_api_key + @pytest.mark.asyncio + async def test_stdio_transport_connection(self): + """Test MCP client can connect using STDIO transport""" + # Skip if no OpenAI API key + if not os.getenv("OPENAI_API_KEY"): + pytest.skip("OpenAI API key not available") + + client = MCPAIClient( + model="gpt-4o", + transport=TransportType.STDIO, + provider="openai", + temperature=0.1, + max_tokens=100 + ) + + try: + # For STDIO, we need to provide server command + await client.connect_stdio( + server_command=[sys.executable, "run_mcp_server.py", "--transport", "stdio"] + ) + + # Test getting tools + tools = await client.get_mcp_tools() + assert isinstance(tools, list) + assert len(tools) > 0 + + finally: + await client.disconnect() + + @pytest.mark.integration + @pytest.mark.requires_api_key + @pytest.mark.slow + @pytest.mark.asyncio + async def test_end_to_end_tool_calling_sse(self, sse_server): + """Test complete end-to-end tool calling with SSE transport""" + if not os.getenv("OPENAI_API_KEY"): + pytest.skip("OpenAI API key not available") + + client = MCPAIClient( + model="gpt-4o", + transport=TransportType.SSE, + provider="openai", + temperature=0.1, + max_tokens=200 + ) + + try: + await client.connect(sse_server.url) + + # Test a simple mathematical query that should trigger tool calls + query = "Calculate 15 + 27 and then multiply the result by 2" + response = await client.process_query(query) + + assert isinstance(response, str) + assert len(response) > 0 + + # The response should contain the calculation result + # We can't predict exact wording but should contain numbers + assert any(char.isdigit() for char in response) + + finally: + await client.disconnect() + + @pytest.mark.integration + @pytest.mark.requires_api_key + @pytest.mark.asyncio + async def test_multiple_provider_support(self, sse_server): + """Test MCP client works with different AI providers""" + if not os.getenv("OPENAI_API_KEY"): + pytest.skip("OpenAI API key not available") + + providers_to_test = ["openai"] + + for provider in providers_to_test: + client = MCPAIClient( + model="gpt-4o" if provider == "openai" else "claude-3-opus-20240229", + transport=TransportType.SSE, + provider=provider, + temperature=0.1, + max_tokens=100 + ) + + try: + await client.connect(sse_server.url) + + # Test basic tool listing + tools = await client.get_mcp_tools() + assert len(tools) > 0 + + # Test simple query + response = await client.process_query("What tools are available?") + assert isinstance(response, str) + + finally: + await client.disconnect() + + @pytest.mark.integration + @pytest.mark.asyncio + async def test_error_handling_connection_failure(self): + """Test error handling when server connection fails""" + client = MCPAIClient( + model="gpt-4o", + transport=TransportType.SSE, + provider="openai" + ) + + # Try to connect to non-existent server + with pytest.raises(Exception): + await client.connect("http://localhost:9999") + + @pytest.mark.integration + @pytest.mark.requires_api_key + @pytest.mark.asyncio + async def test_error_handling_invalid_query(self, sse_server): + """Test error handling with invalid queries""" + if not os.getenv("OPENAI_API_KEY"): + pytest.skip("OpenAI API key not available") + + client = MCPAIClient( + model="gpt-4o", + transport=TransportType.SSE, + provider="openai", + temperature=0.1 + ) + + try: + await client.connect(sse_server.url) + + # Test with empty query + response = await client.process_query("") + assert isinstance(response, str) + + # Test with very long query + long_query = "test " * 1000 + response = await client.process_query(long_query) + assert isinstance(response, str) + + finally: + await client.disconnect() + + @pytest.mark.integration + @pytest.mark.requires_api_key + @pytest.mark.asyncio + async def test_interactive_session_mode(self, sse_server): + """Test interactive session functionality""" + if not os.getenv("OPENAI_API_KEY"): + pytest.skip("OpenAI API key not available") + + client = MCPAIClient( + model="gpt-4o", + transport=TransportType.SSE, + provider="openai", + temperature=0.1, + max_tokens=100 + ) + + try: + await client.connect(sse_server.url) + + # Mock user inputs for interactive session + inputs = ["Calculate 5 + 3", "quit"] + + with patch('builtins.input', side_effect=inputs): + # This would normally run an interactive loop + # For testing, we'll just verify the client is ready + assert client.session is not None + assert client.ai_client is not None + + finally: + await client.disconnect() + + @pytest.mark.integration + @pytest.mark.requires_api_key + @pytest.mark.asyncio + async def test_tool_schema_validation(self, sse_server): + """Test that tool schemas are properly formatted for AI providers""" + if not os.getenv("OPENAI_API_KEY"): + pytest.skip("OpenAI API key not available") + + client = MCPAIClient( + model="gpt-4o", + transport=TransportType.SSE, + provider="openai" + ) + + try: + await client.connect(sse_server.url) + tools = await client.get_mcp_tools() + + # Verify OpenAI-specific tool formatting + for tool in tools: + assert "type" in tool + assert tool["type"] == "function" + assert "function" in tool + assert "name" in tool["function"] + assert "description" in tool["function"] + assert "parameters" in tool["function"] + + finally: + await client.disconnect() + + @pytest.mark.integration + @pytest.mark.requires_api_key + @pytest.mark.slow + @pytest.mark.asyncio + async def test_concurrent_connections(self, sse_server): + """Test multiple clients can connect simultaneously""" + if not os.getenv("OPENAI_API_KEY"): + pytest.skip("OpenAI API key not available") + + async def create_and_test_client(client_id: int): + client = MCPAIClient( + model="gpt-4o", + transport=TransportType.SSE, + provider="openai", + temperature=0.1 + ) + + try: + await client.connect(sse_server.url) + tools = await client.get_mcp_tools() + return len(tools) + finally: + await client.disconnect() + + # Test 3 concurrent connections + tasks = [create_and_test_client(i) for i in range(3)] + results = await asyncio.gather(*tasks) + + # All should succeed and return same number of tools + assert all(r > 0 for r in results) + assert len(set(results)) == 1 # All should return same count + + @pytest.mark.integration + @pytest.mark.asyncio + async def test_configuration_parameters(self): + """Test various configuration parameters work correctly""" + # Test different temperature settings + for temp in [0.1, 0.5, 0.9]: + client = MCPAIClient( + model="gpt-4o", + transport=TransportType.SSE, + provider="openai", + temperature=temp, + max_tokens=100 + ) + + assert client.temperature == temp + assert client.ai_client.temperature == temp + + @pytest.mark.integration + @pytest.mark.requires_api_key + @pytest.mark.asyncio + async def test_resource_access(self, sse_server): + """Test accessing MCP resources""" + if not os.getenv("OPENAI_API_KEY"): + pytest.skip("OpenAI API key not available") + + client = MCPAIClient( + model="gpt-4o", + transport=TransportType.SSE, + provider="openai" + ) + + try: + await client.connect(sse_server.url) + + # Test listing resources + resources = await client.session.list_resources() + assert isinstance(resources.resources, list) + + # Test reading resources if any exist + if resources.resources: + for resource in resources.resources[:2]: # Test first 2 resources + content = await client.session.read_resource(resource.uri) + assert content is not None + + finally: + await client.disconnect() + + @pytest.mark.integration + @pytest.mark.requires_api_key + @pytest.mark.asyncio + async def test_prompt_access(self, sse_server): + """Test accessing MCP prompts""" + if not os.getenv("OPENAI_API_KEY"): + pytest.skip("OpenAI API key not available") + + client = MCPAIClient( + model="gpt-4o", + transport=TransportType.SSE, + provider="openai" + ) + + try: + await client.connect(sse_server.url) + + # Test listing prompts + prompts = await client.session.list_prompts() + assert isinstance(prompts.prompts, list) + + # Test getting prompts if any exist + if prompts.prompts: + for prompt in prompts.prompts[:2]: # Test first 2 prompts + prompt_content = await client.session.get_prompt(prompt.name) + assert prompt_content is not None + + finally: + await client.disconnect() + + +class TestMCPClientUtilities: + """Test utility functions and edge cases""" + + @pytest.mark.unit + def test_transport_type_enum(self): + """Test TransportType enum values""" + assert TransportType.SSE.value == "sse" + assert TransportType.STDIO.value == "stdio" + + @pytest.mark.unit + def test_client_initialization_validation(self): + """Test client initialization with various parameters""" + # Test with minimal parameters + client = MCPAIClient() + assert client.model == "gpt-4o" + assert client.provider == "openai" + assert client.transport == TransportType.SSE + + # Test with custom parameters + client = MCPAIClient( + model="gpt-4o-mini", + provider="openai", + transport=TransportType.STDIO, + temperature=0.5, + max_tokens=500, + top_p=0.9 + ) + + assert client.model == "gpt-4o-mini" + assert client.provider == "openai" + assert client.transport == TransportType.STDIO + assert client.temperature == 0.5 + assert client.max_tokens == 500 + + @pytest.mark.unit + @patch.dict(os.environ, {"OPENAI_API_KEY": "test-key"}) + def test_ai_client_factory_integration(self): + """Test that AI client factory creates correct client types""" + # Test OpenAI client creation + openai_client = AIClientFactory.create_client( + provider="openai", + model_name="gpt-4o", + temperature=0.5 + ) + + assert openai_client is not None + assert hasattr(openai_client, 'chat_completion') + assert hasattr(openai_client, '_format_tools_for_provider') + + @pytest.mark.unit + def test_missing_llm_client_handling(self): + """Test behavior when LLM client is not available""" + with patch('mcp_llm_client.LLM_CLIENT_AVAILABLE', False): + with pytest.raises(ImportError, match="LLM client not available"): + MCPAIClient() + + +class TestPerformanceAndLoad: + """Performance and load testing for MCP client""" + + @pytest.mark.integration + @pytest.mark.requires_api_key + @pytest.mark.slow + @pytest.mark.asyncio + async def test_multiple_rapid_queries(self, sse_server): + """Test handling multiple rapid queries""" + if not os.getenv("OPENAI_API_KEY"): + pytest.skip("OpenAI API key not available") + + client = MCPAIClient( + model="gpt-4o", + transport=TransportType.SSE, + provider="openai", + temperature=0.1, + max_tokens=50 + ) + + try: + await client.connect(sse_server.url) + + queries = [ + "What is 2 + 2?", + "Calculate 10 * 5", + "What tools do you have?", + "Test query 4" + ] + + # Execute queries concurrently + tasks = [client.process_query(query) for query in queries] + responses = await asyncio.gather(*tasks) + + # Verify all responses + assert len(responses) == len(queries) + assert all(isinstance(r, str) for r in responses) + assert all(len(r) > 0 for r in responses) + + finally: + await client.disconnect() + + @pytest.mark.integration + @pytest.mark.requires_api_key + @pytest.mark.asyncio + async def test_connection_reuse(self, sse_server): + """Test reusing connection for multiple operations""" + if not os.getenv("OPENAI_API_KEY"): + pytest.skip("OpenAI API key not available") + + client = MCPAIClient( + model="gpt-4o", + transport=TransportType.SSE, + provider="openai", + temperature=0.1 + ) + + try: + await client.connect(sse_server.url) + + # Perform multiple operations on same connection + for i in range(5): + tools = await client.get_mcp_tools() + assert len(tools) > 0 + + response = await client.process_query(f"Test query {i}") + assert isinstance(response, str) + + finally: + await client.disconnect() + + +# Cleanup fixture to ensure no processes are left running +@pytest.fixture(scope="session", autouse=True) +async def cleanup_processes(): + """Clean up any remaining MCP server processes""" + yield + + # Kill any remaining MCP server processes + try: + # Find and kill any remaining server processes + result = subprocess.run( + ["pgrep", "-f", "run_mcp_server.py"], + capture_output=True, + text=True + ) + + if result.returncode == 0: + pids = result.stdout.strip().split('\n') + for pid in pids: + if pid: + try: + os.kill(int(pid), signal.SIGTERM) + except (ProcessLookupError, OSError): + pass # Process already dead + + except (subprocess.SubprocessError, FileNotFoundError): + pass # pgrep not available or no processes found diff --git a/tests/unit/test_clients.py b/tests/unit/test_clients.py new file mode 100644 index 0000000..365bc30 --- /dev/null +++ b/tests/unit/test_clients.py @@ -0,0 +1,202 @@ +""" +Unit tests for AI client components +""" +from unittest import async_case +import pytest +from unittest.mock import Mock, AsyncMock, patch +from mcp_template.llm_client.base_client import BaseAIClient +from mcp_template.llm_client.openai_client import OpenAIClient +from mcp_template.llm_client.client_factory import AIClientFactory + + +class TestBaseAIClient: + """Test base AI client functionality""" + + def test_base_client_creation(self): + """Test creating a base client""" + # Create a concrete implementation for testing + class TestAIClient(BaseAIClient): + async def chat_completion(self, messages, tools=None, **kwargs): + return {"choices": [{"message": {"content": "test response"}}]} + + async def _initialize_client(self): + pass + + client = TestAIClient( + model_name="test-model", + provider="test", + api_key="test-key" + ) + + assert client.model_name == "test-model" + assert client._api_key == "test-key" + assert not client._initialized + + def test_abstract_class_cannot_be_instantiated(self): + """Test that abstract BaseAIClient cannot be instantiated directly""" + with pytest.raises(TypeError, match="Can't instantiate abstract class"): + BaseAIClient( + model_name="test-model", + provider="test", + api_key="test-key" + ) + + +class TestOpenAIClient: + """Test OpenAI client implementation""" + + def test_openai_client_creation(self): + """Test creating an OpenAI client""" + client = OpenAIClient( + model_name="gpt-4o", + api_key="test-key", + temperature=0.5, + max_tokens=500 + ) + + assert client.model_name == "gpt-4o" + assert client._temperature == 0.5 + assert client._max_tokens == 500 + + def test_openai_client_defaults(self): + """Test OpenAI client default values""" + client = OpenAIClient( + model_name="gpt-4o", + api_key="test-key" + ) + + assert client._temperature == 0.7 + assert client._max_tokens == 1000 + + @patch('mcp_template.llm_client.openai_client.AsyncOpenAI') + @pytest.mark.asyncio + async def test_openai_initialization(self, mock_openai_class): + """Test OpenAI client initialization""" + mock_client = Mock() + mock_openai_class.return_value = mock_client + + client = OpenAIClient("gpt-4o", "test-key") + await client._initialize_client() + + mock_openai_class.assert_called_once_with(api_key="test-key") + assert client._client == mock_client + + @patch('mcp_template.llm_client.openai_client.AsyncOpenAI') + @pytest.mark.asyncio + async def test_openai_chat_completion(self, mock_openai_class): + """Test OpenAI chat completion""" + # Mock the OpenAI client and response + mock_client = Mock() + mock_response = Mock() + mock_choice = Mock() + mock_message = Mock() + + mock_message.role = "assistant" + mock_message.content = "Test response" + mock_message.tool_calls = None + + mock_choice.message = mock_message + mock_response.choices = [mock_choice] + + mock_client.chat.completions.create = AsyncMock(return_value=mock_response) + mock_openai_class.return_value = mock_client + + client = OpenAIClient("gpt-4o", "test-key") + await client.initialize() + + messages = [{"role": "user", "content": "Hello"}] + result = await client.chat_completion(messages) + + # Verify the call was made correctly + mock_client.chat.completions.create.assert_called_once() + call_args = mock_client.chat.completions.create.call_args[1] + + assert call_args["model"] == "gpt-4o" + assert call_args["messages"] == messages + assert call_args["temperature"] == 0.7 + assert call_args["max_tokens"] == 1000 + + # Verify response format + assert "choices" in result + assert len(result["choices"]) == 1 + assert result["choices"][0]["message"]["content"] == "Test response" + + def test_openai_tool_formatting(self): + """Test OpenAI tool formatting""" + client = OpenAIClient("gpt-4o", "test-key") + + tools = [ + { + "name": "add", + "description": "Add two numbers", + "inputSchema": { + "type": "object", + "properties": { + "a": {"type": "number"}, + "b": {"type": "number"} + }, + "required": ["a", "b"] + } + } + ] + + formatted = client._format_tools_for_provider(tools) + + assert len(formatted) == 1 + assert formatted[0]["type"] == "function" + assert formatted[0]["function"]["name"] == "add" + assert formatted[0]["function"]["description"] == "Add two numbers" + assert "parameters" in formatted[0]["function"] + + +class TestAIClientFactory: + """Test AI client factory""" + + def test_create_openai_client(self): + """Test creating OpenAI client via factory""" + client = AIClientFactory.create_openai_client( + model_name="gpt-4o", + api_key="test-key" + ) + + assert isinstance(client, OpenAIClient) + assert client.model_name == "gpt-4o" + + def test_create_client_by_provider(self): + """Test creating client by provider name""" + client = AIClientFactory.create_client( + provider="openai", + model_name="gpt-4o", + api_key="test-key" + ) + + assert isinstance(client, OpenAIClient) + assert client.model_name == "gpt-4o" + + def test_invalid_provider(self): + """Test creating client with invalid provider""" + with pytest.raises(ValueError, match="Unsupported AI provider"): + AIClientFactory.create_client("invalid", "model", "key") + + @patch('mcp_template.llm_client.client_factory.CONFIG_AVAILABLE', True) + @patch('mcp_template.llm_client.client_factory.Config') + def test_missing_api_key(self, mock_config): + """Test creating client without API key""" + # Mock config to return None for API key + mock_config.OPENAI_API_KEY = None + + with pytest.raises(ValueError, match="API key not provided and could not be loaded from config for provider: openai"): + AIClientFactory.create_client("openai", "gpt-4o") + + def test_available_providers(self): + """Test getting available providers""" + providers = AIClientFactory.get_available_providers() + assert "openai" in providers + assert "claude" in providers + assert "grok" in providers + + def test_validate_provider(self): + """Test provider validation""" + assert AIClientFactory.validate_provider("openai") is True + assert AIClientFactory.validate_provider("claude") is True + assert AIClientFactory.validate_provider("invalid") is False diff --git a/tests/unit/test_core_types.py b/tests/unit/test_core_types.py new file mode 100644 index 0000000..581df47 --- /dev/null +++ b/tests/unit/test_core_types.py @@ -0,0 +1,213 @@ +""" +Unit tests for core MCP types +""" +import pytest +from mcp_template.core.types import ( + TransportType, + MCPTool, + MCPResource, + MCPPrompt, + MCPServerConfig +) + + +class TestTransportType: + """Test TransportType enum""" + + def test_transport_type_values(self): + """Test transport type enum values""" + assert TransportType.SSE.value == "sse" + assert TransportType.STDIO.value == "stdio" + + def test_transport_type_from_string(self): + """Test creating transport type from string""" + assert TransportType("sse") == TransportType.SSE + assert TransportType("stdio") == TransportType.STDIO + assert TransportType("SSE") == TransportType.SSE + + +class TestMCPTool: + """Test MCPTool dataclass""" + + def test_tool_creation(self): + """Test creating a valid tool""" + async def sample_handler(a: int, b: int) -> int: + return a + b + + tool = MCPTool( + name="add", + description="Add two numbers", + input_schema={ + "type": "object", + "properties": { + "a": {"type": "number"}, + "b": {"type": "number"} + }, + "required": ["a", "b"] + }, + handler=sample_handler + ) + + assert tool.name == "add" + assert tool.description == "Add two numbers" + assert "a" in tool.input_schema["properties"] + + def test_tool_validation(self): + """Test tool validation""" + async def handler(): + pass + + # Valid tool + tool = MCPTool( + name="test", + description="Test tool", + input_schema={}, + handler=handler + ) + assert tool.name == "test" + + # Invalid tool - empty name + with pytest.raises(ValueError): + MCPTool( + name="", + description="Test tool", + input_schema={}, + handler=handler + ) + + # Invalid tool - empty description + with pytest.raises(ValueError): + MCPTool( + name="test", + description="", + input_schema={}, + handler=handler + ) + + +class TestMCPResource: + """Test MCPResource dataclass""" + + def test_resource_creation(self): + """Test creating a valid resource""" + resource = MCPResource( + uri="file:///test.txt", + name="Test File", + description="A test file", + mime_type="text/plain", + content="Hello, world!" + ) + + assert resource.uri == "file:///test.txt" + assert resource.name == "Test File" + assert resource.content == "Hello, world!" + + def test_resource_validation(self): + """Test resource validation""" + # Valid resource + resource = MCPResource( + uri="file:///test.txt", + name="Test", + description="Test resource", + mime_type="text/plain", + content=b"test" + ) + assert resource.uri == "file:///test.txt" + + # Invalid resource - empty URI + with pytest.raises(ValueError): + MCPResource( + uri="", + name="Test", + description="Test resource", + mime_type="text/plain", + content="test" + ) + + # Invalid resource - empty name + with pytest.raises(ValueError): + MCPResource( + uri="file:///test.txt", + name="", + description="Test resource", + mime_type="text/plain", + content="test" + ) + + +class TestMCPPrompt: + """Test MCPPrompt dataclass""" + + def test_prompt_creation(self): + """Test creating a valid prompt""" + prompt = MCPPrompt( + name="greeting", + description="A greeting prompt", + template="Hello, {name}! Welcome to {place}.", + arguments={ + "name": {"type": "string", "description": "Person's name"}, + "place": {"type": "string", "description": "Place name"} + } + ) + + assert prompt.name == "greeting" + assert "name" in prompt.template + assert prompt.arguments is not None + + def test_prompt_validation(self): + """Test prompt validation""" + # Valid prompt + prompt = MCPPrompt( + name="test", + description="Test prompt", + template="Hello, world!" + ) + assert prompt.name == "test" + + # Invalid prompt - empty name + with pytest.raises(ValueError): + MCPPrompt( + name="", + description="Test prompt", + template="Hello, world!" + ) + + # Invalid prompt - empty template + with pytest.raises(ValueError): + MCPPrompt( + name="test", + description="Test prompt", + template="" + ) + + +class TestMCPServerConfig: + """Test MCPServerConfig dataclass""" + + def test_server_config_creation(self): + """Test creating a valid server config""" + config = MCPServerConfig( + name="Test Server", + version="1.0.0", + transport=TransportType.SSE, + host="localhost", + port=8080, + stateless_http=True + ) + + assert config.name == "Test Server" + assert config.transport == TransportType.SSE + assert config.port == 8080 + assert config.tools == [] + assert config.resources == [] + assert config.prompts == [] + + def test_server_config_defaults(self): + """Test server config default values""" + config = MCPServerConfig(name="Test Server") + + assert config.version == "1.0.0" + assert config.transport == TransportType.STDIO + assert config.host == "0.0.0.0" + assert config.port == 8050 + assert config.stateless_http is True diff --git a/tests/unit/test_server.py b/tests/unit/test_server.py new file mode 100644 index 0000000..a1a1c37 --- /dev/null +++ b/tests/unit/test_server.py @@ -0,0 +1,495 @@ +""" +Unit tests for MCP server components +""" +import pytest +import asyncio +import tempfile +import os +from unittest.mock import Mock, AsyncMock, patch, MagicMock +from pathlib import Path +from mcp_template.core.types import MCPTool, MCPResource, MCPPrompt, MCPServerConfig, TransportType +# FastMCPServer not available, using ModularMCPServer for testing +from mcp_template.server.server_factory import MCPServerFactory +from mcp_template.server.modular_server import ModularMCPServer +from mcp_template.server.tools.tool_registry import ServerToolRegistry +from mcp_template.server.prompts.prompt_registry import ServerPromptRegistry +from mcp_template.server.resources.resource_registry import ServerResourceRegistry +from mcp_template.server.tools.base_tool import BaseServerTool, ContextAwareTool +from mcp_template.server.prompts.base_prompt import BaseServerPrompt +from mcp_template.server.resources.base_resource import BaseServerResource + + +class TestModularMCPServerBasic: + """Test basic ModularMCP server functionality (replacing FastMCPServer tests)""" + + @pytest.fixture + def sample_tool(self): + """Create a sample tool for testing""" + async def add_handler(a: int, b: int) -> int: + return a + b + + return MCPTool( + name="add", + description="Add two numbers", + input_schema={ + "type": "object", + "properties": { + "a": {"type": "number"}, + "b": {"type": "number"} + }, + "required": ["a", "b"] + }, + handler=add_handler + ) + + @pytest.fixture + def server_config(self): + """Create a sample server config""" + return MCPServerConfig( + name="Test Server", + transport=TransportType.STDIO, + host="localhost", + port=8050 + ) + + @pytest.mark.asyncio + async def test_server_initialization(self, server_config, sample_tool): + """Test server initialization""" + # Use factory to create server + server = MCPServerFactory.create_server( + name="Test Server", + transport="stdio" + ) + + # Server should not be initialized yet + assert not server._initialized + + # Initialize server + await server.initialize() + + # Server should be initialized + assert server._initialized + + @pytest.mark.asyncio + async def test_server_info(self, server_config): + """Test getting server information""" + server = MCPServerFactory.create_server( + name="Test Server", + transport="stdio" + ) + + # Test server info before initialization + info = server.get_server_info() + assert info["name"] == "Test Server" + assert info["host"] == "0.0.0.0" + assert info["port"] == 8050 + + def test_server_run_method(self, server_config): + """Test server run method""" + server = MCPServerFactory.create_server( + name="Test Server", + transport="stdio" + ) + + # Mock the run method to avoid actually starting the server + with patch.object(server.mcp, 'run') as mock_run: + server.run("stdio") + mock_run.assert_called_once_with(transport="stdio") + + + +class TestMCPServerFactory: + """Test MCP server factory""" + + def test_create_server_basic(self): + """Test creating a basic server""" + server = MCPServerFactory.create_server( + name="Test Server", + transport="stdio" + ) + + assert isinstance(server, ModularMCPServer) + assert server.name == "Test Server" + assert server.host == "0.0.0.0" + assert server.port == 8050 + + def test_create_calculator_server(self): + """Test creating a calculator server""" + server = MCPServerFactory.create_basic_calculator_server( + name="Calculator Server", + transport="sse", + port=8080 + ) + + assert isinstance(server, ModularMCPServer) + assert server.name == "Calculator Server" + assert server.host == "0.0.0.0" + assert server.port == 8080 + + # For ModularMCPServer, we can't easily test tool registration without mocking + # The tools would be registered through the registry system + assert server.tool_registry is not None + + def test_create_knowledge_base_server(self): + """Test creating a knowledge base server""" + kb_data = { + "policy": "Company vacation policy", + "faq": "Frequently asked questions" + } + + server = MCPServerFactory.create_knowledge_base_server( + name="KB Server", + kb_data=kb_data + ) + + assert isinstance(server, ModularMCPServer) + assert server.name == "KB Server" + + # For ModularMCPServer, we can't easily test tool registration without mocking + # The tools would be registered through the registry system + assert server.tool_registry is not None + + +class TestModularMCPServer: + """Test ModularMCP server implementation""" + + @pytest.fixture + def modular_server(self): + """Create a modular server for testing""" + return ModularMCPServer( + name="Test Modular Server", + host="localhost", + port=8051 + ) + + @pytest.mark.asyncio + async def test_modular_server_initialization(self, modular_server): + """Test modular server initialization""" + assert modular_server.name == "Test Modular Server" + assert modular_server.host == "localhost" + assert modular_server.port == 8051 + assert not modular_server._initialized + assert modular_server.tool_registry is not None + assert modular_server.prompt_registry is not None + assert modular_server.resource_registry is not None + + @pytest.mark.asyncio + async def test_modular_server_initialize(self, modular_server): + """Test modular server initialization process""" + # Mock the registry methods to avoid actual file discovery + with patch.object(modular_server.tool_registry, 'register_tools_with_server') as mock_tools, \ + patch.object(modular_server.tool_registry, 'get_all_tools', return_value=[]), \ + patch.object(modular_server.prompt_registry, 'register_prompts_with_server') as mock_prompts, \ + patch.object(modular_server.prompt_registry, 'get_all_prompts', return_value=[]), \ + patch.object(modular_server.resource_registry, 'register_resources_with_server') as mock_resources, \ + patch.object(modular_server.resource_registry, 'get_all_resources', return_value=[]): + + await modular_server.initialize() + + assert modular_server._initialized + mock_tools.assert_called_once_with(modular_server.mcp) + mock_prompts.assert_called_once_with(modular_server.mcp) + mock_resources.assert_called_once_with(modular_server.mcp) + + @pytest.mark.asyncio + async def test_modular_server_double_initialize(self, modular_server): + """Test that double initialization doesn't cause issues""" + with patch.object(modular_server.tool_registry, 'register_tools_with_server') as mock_tools, \ + patch.object(modular_server.tool_registry, 'get_all_tools', return_value=[]), \ + patch.object(modular_server.prompt_registry, 'register_prompts_with_server') as mock_prompts, \ + patch.object(modular_server.prompt_registry, 'get_all_prompts', return_value=[]), \ + patch.object(modular_server.resource_registry, 'register_resources_with_server') as mock_resources, \ + patch.object(modular_server.resource_registry, 'get_all_resources', return_value=[]): + + # Initialize twice + await modular_server.initialize() + await modular_server.initialize() + + # Should only be called once due to _initialized check + mock_tools.assert_called_once() + mock_prompts.assert_called_once() + mock_resources.assert_called_once() + + def test_modular_server_get_server_info(self, modular_server): + """Test getting server information""" + # Mock the registry methods + with patch.object(modular_server.tool_registry, 'get_all_tools', return_value=[Mock(name="tool1")]), \ + patch.object(modular_server.tool_registry, 'get_tool_names', return_value=["tool1"]), \ + patch.object(modular_server.prompt_registry, 'get_all_prompts', return_value=[Mock(name="prompt1")]), \ + patch.object(modular_server.prompt_registry, 'get_prompt_names', return_value=["prompt1"]), \ + patch.object(modular_server.resource_registry, 'get_all_resources', return_value=[Mock(uri="resource1")]), \ + patch.object(modular_server.resource_registry, 'get_resource_uris', return_value=["resource1"]): + + info = modular_server.get_server_info() + + assert info["name"] == "Test Modular Server" + assert info["host"] == "localhost" + assert info["port"] == 8051 + assert info["tools"]["count"] == 1 + assert info["tools"]["names"] == ["tool1"] + assert info["prompts"]["count"] == 1 + assert info["prompts"]["names"] == ["prompt1"] + assert info["resources"]["count"] == 1 + assert info["resources"]["uris"] == ["resource1"] + + def test_modular_server_run_without_initialize(self, modular_server): + """Test that run method initializes server if not already initialized""" + with patch.object(modular_server, 'initialize') as mock_init, \ + patch.object(modular_server.mcp, 'run') as mock_run: + + modular_server.run("stdio") + + mock_init.assert_called_once() + mock_run.assert_called_once_with(transport="stdio") + + def test_modular_server_run_already_initialized(self, modular_server): + """Test that run method doesn't reinitialize if already initialized""" + modular_server._initialized = True + + with patch.object(modular_server, 'initialize') as mock_init, \ + patch.object(modular_server.mcp, 'run') as mock_run: + + modular_server.run("sse") + + mock_init.assert_not_called() + mock_run.assert_called_once_with(transport="sse") + + def test_modular_server_custom_directories(self): + """Test modular server with custom directories""" + server = ModularMCPServer( + name="Custom Server", + tools_directory="/custom/tools", + prompts_directory="/custom/prompts", + resources_directory="/custom/resources" + ) + + assert server.tool_registry.directory == "/custom/tools" + assert server.prompt_registry.directory == "/custom/prompts" + assert server.resource_registry.directory == "/custom/resources" + + +class TestServerRegistries: + """Test server registry classes""" + + @pytest.fixture + def mock_tool(self): + """Create a mock tool for testing""" + tool = Mock() + tool.name = "test_tool" + tool.description = "A test tool" + tool.input_schema = {"type": "object"} + return tool + + @pytest.fixture + def mock_prompt(self): + """Create a mock prompt for testing""" + prompt = Mock() + prompt.name = "test_prompt" + prompt.description = "A test prompt" + prompt.arguments = {"name": {"type": "string"}} + return prompt + + @pytest.fixture + def mock_resource(self): + """Create a mock resource for testing""" + resource = Mock() + resource.uri = "test://resource" + resource.name = "Test Resource" + resource.description = "A test resource" + return resource + + def test_tool_registry_initialization(self): + """Test tool registry initialization""" + registry = ServerToolRegistry("/test/tools") + assert registry.directory == "/test/tools" + assert registry._tools == {} + + def test_tool_registry_register_tool(self, mock_tool): + """Test tool registration""" + registry = ServerToolRegistry() + registry.register_tool(mock_tool) + + assert "test_tool" in registry._tools + assert registry._tools["test_tool"] == mock_tool + + def test_tool_registry_get_all_tools(self, mock_tool): + """Test getting all tools""" + registry = ServerToolRegistry() + registry.register_tool(mock_tool) + + tools = registry.get_all_tools() + assert len(tools) == 1 + assert tools[0] == mock_tool + + def test_tool_registry_get_tool_names(self, mock_tool): + """Test getting tool names""" + registry = ServerToolRegistry() + registry.register_tool(mock_tool) + + names = registry.get_tool_names() + assert names == ["test_tool"] + + def test_prompt_registry_initialization(self): + """Test prompt registry initialization""" + registry = ServerPromptRegistry("/test/prompts") + assert registry.directory == "/test/prompts" + assert registry._prompts == {} + + def test_prompt_registry_register_prompt(self, mock_prompt): + """Test prompt registration""" + registry = ServerPromptRegistry() + registry.register_prompt(mock_prompt) + + assert "test_prompt" in registry._prompts + assert registry._prompts["test_prompt"] == mock_prompt + + def test_prompt_registry_get_all_prompts(self, mock_prompt): + """Test getting all prompts""" + registry = ServerPromptRegistry() + registry.register_prompt(mock_prompt) + + prompts = registry.get_all_prompts() + assert len(prompts) == 1 + assert prompts[0] == mock_prompt + + def test_prompt_registry_get_prompt_names(self, mock_prompt): + """Test getting prompt names""" + registry = ServerPromptRegistry() + registry.register_prompt(mock_prompt) + + names = registry.get_prompt_names() + assert names == ["test_prompt"] + + def test_resource_registry_initialization(self): + """Test resource registry initialization""" + registry = ServerResourceRegistry("/test/resources") + assert registry.directory == "/test/resources" + assert registry._resources == {} + + def test_resource_registry_register_resource(self, mock_resource): + """Test resource registration""" + registry = ServerResourceRegistry() + registry.register_resource(mock_resource) + + assert "test://resource" in registry._resources + assert registry._resources["test://resource"] == mock_resource + + def test_resource_registry_get_all_resources(self, mock_resource): + """Test getting all resources""" + registry = ServerResourceRegistry() + registry.register_resource(mock_resource) + + resources = registry.get_all_resources() + assert len(resources) == 1 + assert resources[0] == mock_resource + + def test_resource_registry_get_resource_uris(self, mock_resource): + """Test getting resource URIs""" + registry = ServerResourceRegistry() + registry.register_resource(mock_resource) + + uris = registry.get_resource_uris() + assert uris == ["test://resource"] + + +class TestBaseServerTools: + """Test base server tool classes""" + + def test_base_server_tool_initialization(self): + """Test BaseServerTool initialization""" + class TestTool(BaseServerTool): + async def execute(self, **kwargs): + return "test_result" + + tool = TestTool( + name="test_tool", + description="A test tool", + input_schema={"type": "object"} + ) + + assert tool.name == "test_tool" + assert tool.description == "A test tool" + assert tool.input_schema == {"type": "object"} + + def test_base_server_tool_get_tool_definition(self): + """Test getting tool definition""" + class TestTool(BaseServerTool): + async def execute(self, **kwargs): + return "test_result" + + tool = TestTool( + name="test_tool", + description="A test tool", + input_schema={"type": "object"} + ) + + definition = tool.get_tool_definition() + expected = { + "name": "test_tool", + "description": "A test tool", + "input_schema": {"type": "object"} + } + + assert definition == expected + + def test_base_server_tool_create_fastmcp_tool(self): + """Test creating FastMCP tool wrapper""" + class TestTool(BaseServerTool): + async def execute(self, **kwargs): + return "test_result" + + tool = TestTool( + name="test_tool", + description="A test tool", + input_schema={"type": "object"} + ) + + # Mock MCP server + mock_mcp = Mock() + mock_mcp.tool.return_value = lambda func: func + + wrapper = tool.create_fastmcp_tool(mock_mcp) + + assert wrapper.__name__ == "test_tool" + assert wrapper.__doc__ == "A test tool" + + def test_context_aware_tool_initialization(self): + """Test ContextAwareTool initialization""" + class TestContextTool(ContextAwareTool): + async def execute_with_context(self, ctx, **kwargs): + return "context_result" + + tool = TestContextTool( + name="context_tool", + description="A context-aware tool", + input_schema={"type": "object"} + ) + + assert tool.name == "context_tool" + assert tool.description == "A context-aware tool" + assert tool.input_schema == {"type": "object"} + + @pytest.mark.asyncio + async def test_context_aware_tool_execute_with_context(self): + """Test ContextAwareTool execution with context""" + from unittest.mock import Mock + + class TestContextTool(ContextAwareTool): + async def execute_with_context(self, ctx, **kwargs): + return f"Context: {self.context}, Args: {kwargs}" + + tool = TestContextTool( + name="context_tool", + description="A context-aware tool", + input_schema={"type": "object"} + ) + + # Set context + tool.context = {"user_id": "123"} + + # Mock the context + mock_ctx = Mock() + + result = await tool.execute_with_context(mock_ctx, test_arg="value") + assert "Context: {'user_id': '123'}" in result + assert "Args: {'test_arg': 'value'}" in result diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..cb65db1 --- /dev/null +++ b/uv.lock @@ -0,0 +1,594 @@ +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 = "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.14.0" +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/95/fd/d6e941a52446198b73e5e4a953441f667f1469aeb06fb382d9f6729d6168/mcp-1.14.0.tar.gz", hash = "sha256:2e7d98b195e08b2abc1dc6191f6f3dc0059604ac13ee6a40f88676274787fac4", size = 454855, upload-time = "2025-09-11T17:40:48.667Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/7b/84b0dd4c2c5a499d2c5d63fb7a1224c25fc4c8b6c24623fa7a566471480d/mcp-1.14.0-py3-none-any.whl", hash = "sha256:b2d27feba27b4c53d41b58aa7f4d090ae0cb740cbc4e339af10f8cbe54c4e19d", size = 163805, upload-time = "2025-09-11T17:40:46.891Z" }, +] + +[package.optional-dependencies] +cli = [ + { name = "python-dotenv" }, + { name = "typer" }, +] + +[[package]] +name = "mcp-template" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "mcp", extra = ["cli"] }, + { name = "nest-asyncio" }, + { name = "openai" }, + { name = "psutil" }, +] + +[package.metadata] +requires-dist = [ + { name = "mcp", extras = ["cli"], specifier = ">=1.14.0" }, + { name = "nest-asyncio", specifier = ">=1.6.0" }, + { name = "openai", specifier = ">=1.107.1" }, + { name = "psutil" }, +] + +[[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 = "psutil" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2a/80/336820c1ad9286a4ded7e845b2eccfcb27851ab8ac6abece774a6ff4d3de/psutil-7.0.0.tar.gz", hash = "sha256:7be9c3eba38beccb6495ea33afd982a44074b78f28c434a1f51cc07fd315c456", size = 497003, upload-time = "2025-02-13T21:54:07.946Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/e6/2d26234410f8b8abdbf891c9da62bee396583f713fb9f3325a4760875d22/psutil-7.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:101d71dc322e3cffd7cea0650b09b3d08b8e7c4109dd6809fe452dfd00e58b25", size = 238051, upload-time = "2025-02-13T21:54:12.36Z" }, + { url = "https://files.pythonhosted.org/packages/04/8b/30f930733afe425e3cbfc0e1468a30a18942350c1a8816acfade80c005c4/psutil-7.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:39db632f6bb862eeccf56660871433e111b6ea58f2caea825571951d4b6aa3da", size = 239535, upload-time = "2025-02-13T21:54:16.07Z" }, + { url = "https://files.pythonhosted.org/packages/2a/ed/d362e84620dd22876b55389248e522338ed1bf134a5edd3b8231d7207f6d/psutil-7.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fcee592b4c6f146991ca55919ea3d1f8926497a713ed7faaf8225e174581e91", size = 275004, upload-time = "2025-02-13T21:54:18.662Z" }, + { url = "https://files.pythonhosted.org/packages/bf/b9/b0eb3f3cbcb734d930fdf839431606844a825b23eaf9a6ab371edac8162c/psutil-7.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b1388a4f6875d7e2aff5c4ca1cc16c545ed41dd8bb596cefea80111db353a34", size = 277986, upload-time = "2025-02-13T21:54:21.811Z" }, + { url = "https://files.pythonhosted.org/packages/eb/a2/709e0fe2f093556c17fbafda93ac032257242cabcc7ff3369e2cb76a97aa/psutil-7.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5f098451abc2828f7dc6b58d44b532b22f2088f4999a937557b603ce72b1993", size = 279544, upload-time = "2025-02-13T21:54:24.68Z" }, + { url = "https://files.pythonhosted.org/packages/50/e6/eecf58810b9d12e6427369784efe814a1eec0f492084ce8eb8f4d89d6d61/psutil-7.0.0-cp37-abi3-win32.whl", hash = "sha256:ba3fcef7523064a6c9da440fc4d6bd07da93ac726b5733c29027d7dc95b39d99", size = 241053, upload-time = "2025-02-13T21:54:34.31Z" }, + { url = "https://files.pythonhosted.org/packages/50/1b/6921afe68c74868b4c9fa424dad3be35b095e16687989ebbb50ce4fceb7c/psutil-7.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:4cf3d4eb1aa9b348dec30105c55cd9b7d4629285735a102beb4441e38db90553", size = 244885, upload-time = "2025-02-13T21:54:37.486Z" }, +] + +[[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 = "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" }, +]