MCP Toolbox

# MCP-Toolbox Development Guide for LLMs This guide is designed to help you (an LLM) effectively contribute to the mcp-toolbox project. It provides essential information about the project structure, development workflow, and best practices. ## Project Overview MCP-Toolbox is a Python package that provides tools for enhancing LLMs through the Model Context Protocol (MCP). The project implements various API integrations as MCP tools, allowing LLMs to interact with external services. ### Key Components - **mcp_toolbox/app.py**: Initializes the FastMCP server - **mcp_toolbox/cli.py**: Command-line interface for running the MCP server - **mcp_toolbox/config.py**: Configuration management using Pydantic - **mcp_toolbox/figma/**: Figma API integration tools - **tests/**: Test files for the project ## Environment Setup Always help the user set up a proper development environment using `uv`. This is the preferred package manager for this project. ### Setting Up the Environment ```bash # Install uv if not already installed curl -LsSf https://astral.sh/uv/install.sh | sh # For macOS/Linux # or powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" # For Windows # Clone the repository (if not already done) git clone https://github.com/username/mcp-toolbox.git cd mcp-toolbox # Create and activate a virtual environment uv venv source .venv/bin/activate # For macOS/Linux # or .venv\Scripts\activate # For Windows # Install the package in development mode uv pip install -e . # Install development dependencies uv pip install -e ".[dev]" ``` ## GitHub Workflow Always follow proper GitHub workflow when making changes: 1. **Create a branch with a descriptive name**: ```bash # Assume the user already has their own fork git checkout -b feature/add-spotify-integration ``` 2. **Make your changes**: Implement the requested features or fixes 3. **Run tests and checks**: ```bash make check # Run linting and formatting make test # Run tests ``` 4. **Commit your changes with descriptive messages**: ```bash git add . git commit -m "feat: add Spotify API integration" ``` 5. **Push your changes**: ```bash git push origin feature/add-spotify-integration ``` 6. **Create a pull request**: Guide the user to create a PR from their branch to the main repository ## Adding New Tools When adding new API integrations or tools, follow this pattern. Here's an example of adding Spotify API integration: ### 1. Update Config Class First, update the `config.py` file to include the new API key: ```python class Config(BaseSettings): figma_api_key: str | None = None spotify_client_id: str | None = None spotify_client_secret: str | None = None cache_dir: str = (HOME / "cache").expanduser().resolve().absolute().as_posix() ``` ### 2. Create Module Structure Create a new module for the integration: ```bash mkdir -p mcp_toolbox/spotify touch mcp_toolbox/spotify/__init__.py touch mcp_toolbox/spotify/tools.py ``` ### 3. Implement API Client and Tools In `mcp_toolbox/spotify/tools.py`: ```python import json from typing import Any, List, Dict, Optional import httpx from pydantic import BaseModel from mcp_toolbox.app import mcp from mcp_toolbox.config import Config class SpotifyApiClient: BASE_URL = "https://api.spotify.com/v1" def __init__(self): self.config = Config() self.access_token = None async def get_access_token(self) -> str: """Get or refresh the Spotify access token.""" if not self.config.spotify_client_id or not self.config.spotify_client_secret: raise ValueError( "Spotify credentials not provided. Set SPOTIFY_CLIENT_ID and SPOTIFY_CLIENT_SECRET environment variables." ) auth_url = "https://accounts.spotify.com/api/token" async with httpx.AsyncClient() as client: response = await client.post( auth_url, data={"grant_type": "client_credentials"}, auth=(self.config.spotify_client_id, self.config.spotify_client_secret), ) response.raise_for_status() data = response.json() self.access_token = data["access_token"] return self.access_token async def make_request(self, path: str, method: str = "GET", params: Dict[str, Any] = None) -> Dict[str, Any]: """Make a request to the Spotify API.""" token = await self.get_access_token() async with httpx.AsyncClient() as client: headers = {"Authorization": f"Bearer {token}"} url = f"{self.BASE_URL}{path}" try: if method == "GET": response = await client.get(url, headers=headers, params=params) else: raise ValueError(f"Unsupported HTTP method: {method}") response.raise_for_status() return response.json() except httpx.HTTPStatusError as e: spotify_error = e.response.json() if e.response.content else {"status": e.response.status_code, "error": str(e)} raise ValueError(f"Spotify API error: {spotify_error}") from e except httpx.RequestError as e: raise ValueError(f"Request error: {e!s}") from e # Initialize API client api_client = SpotifyApiClient() # Tool implementations @mcp.tool( description="Search for tracks on Spotify. Args: query (required, The search query), limit (optional, Maximum number of results to return)" ) async def spotify_search_tracks(query: str, limit: int = 10) -> Dict[str, Any]: """Search for tracks on Spotify. Args: query: The search query limit: Maximum number of results to return (default: 10) """ params = {"q": query, "type": "track", "limit": limit} return await api_client.make_request("/search", params=params) @mcp.tool( description="Get details about a specific track. Args: track_id (required, The Spotify ID of the track)" ) async def spotify_get_track(track_id: str) -> Dict[str, Any]: """Get details about a specific track. Args: track_id: The Spotify ID of the track """ return await api_client.make_request(f"/tracks/{track_id}") @mcp.tool( description="Get an artist's top tracks. Args: artist_id (required, The Spotify ID of the artist), market (optional, An ISO 3166-1 alpha-2 country code)" ) async def spotify_get_artist_top_tracks(artist_id: str, market: str = "US") -> Dict[str, Any]: """Get an artist's top tracks. Args: artist_id: The Spotify ID of the artist market: An ISO 3166-1 alpha-2 country code (default: US) """ params = {"market": market} return await api_client.make_request(f"/artists/{artist_id}/top-tracks", params=params) ``` ### 4. Create Tests Create test files for your new tools: ```bash mkdir -p tests/spotify touch tests/spotify/test_tools.py mkdir -p tests/mock/spotify ``` ### 5. Update README Always update the README.md when adding new environment variables or tools: ```markdown ## Environment Variables The following environment variables can be configured: - `FIGMA_API_KEY`: API key for Figma integration - `SPOTIFY_CLIENT_ID`: Client ID for Spotify API - `SPOTIFY_CLIENT_SECRET`: Client Secret for Spotify API ``` ## Error Handling Best Practices When implementing tools, follow these error handling best practices: 1. **Graceful Degradation**: If one API key is missing, other tools should still work ```python async def get_access_token(self) -> str: if not self.config.spotify_client_id or not self.config.spotify_client_secret: raise ValueError( "Spotify credentials not provided. Set SPOTIFY_CLIENT_ID and SPOTIFY_CLIENT_SECRET environment variables." ) # Rest of the method... ``` 2. **Descriptive Error Messages**: Provide clear error messages that help users understand what went wrong ```python except httpx.HTTPStatusError as e: spotify_error = e.response.json() if e.response.content else {"status": e.response.status_code, "error": str(e)} raise ValueError(f"Spotify API error: {spotify_error}") from e ``` 3. **Proper Exception Handling**: Catch specific exceptions and handle them appropriately 4. **Fallbacks**: Implement fallback mechanisms when possible ## Testing Always write tests for new functionality: ```python import json from pathlib import Path from unittest.mock import patch import pytest from mcp_toolbox.spotify.tools import ( SpotifyApiClient, spotify_search_tracks, spotify_get_track, spotify_get_artist_top_tracks, ) # Helper function to load mock data def load_mock_data(filename): mock_dir = Path(__file__).parent.parent / "mock" / "spotify" file_path = mock_dir / filename if not file_path.exists(): # Create empty mock data if it doesn't exist mock_data = {"mock": "data"} with open(file_path, "w") as f: json.dump(mock_data, f) with open(file_path) as f: return json.load(f) # Patch the SpotifyApiClient.make_request method @pytest.fixture def mock_make_request(): with patch.object(SpotifyApiClient, "make_request") as mock: def side_effect(path, method="GET", params=None): if path == "/search": return load_mock_data("search_tracks.json") elif path.startswith("/tracks/"): return load_mock_data("get_track.json") elif path.endswith("/top-tracks"): return load_mock_data("get_artist_top_tracks.json") return {"mock": "data"} mock.side_effect = side_effect yield mock # Test spotify_search_tracks function @pytest.mark.asyncio async def test_spotify_search_tracks(mock_make_request): result = await spotify_search_tracks("test query") mock_make_request.assert_called_once() assert mock_make_request.call_args[0][0] == "/search" # Test spotify_get_track function @pytest.mark.asyncio async def test_spotify_get_track(mock_make_request): result = await spotify_get_track("track_id") mock_make_request.assert_called_once() assert mock_make_request.call_args[0][0] == "/tracks/track_id" # Test spotify_get_artist_top_tracks function @pytest.mark.asyncio async def test_spotify_get_artist_top_tracks(mock_make_request): result = await spotify_get_artist_top_tracks("artist_id") mock_make_request.assert_called_once() assert mock_make_request.call_args[0][0] == "/artists/artist_id/top-tracks" ``` ## Documentation When adding new tools, make sure to: 1. Add clear docstrings to all functions and classes 2. Include detailed argument descriptions in the `@mcp.tool` decorator 3. Add type hints to all functions and methods 4. Update the README.md with new environment variables and tools ## Final Checklist Before submitting your changes: 1. ✅ Run `make check` to ensure code quality 2. ✅ Run `make test` to ensure all tests pass 3. ✅ Update documentation if needed 4. ✅ Add new environment variables to README.md 5. ✅ Follow proper Git workflow (branch, commit, push)