This is page 1 of 3. Use http://codebase.md/hrgarber/wagyu_mcp_hackathon?lines=true&page={x} to view the full context. # Directory Structure ``` ├── .github │ ├── hooks │ │ ├── modules │ │ │ ├── api-key-check.sh │ │ │ └── env-check.sh │ │ └── pre-commit │ ├── scripts │ │ └── auto-setup.sh │ └── workflows │ └── auto-setup-hooks.yml ├── .gitignore ├── assets │ └── images │ └── wagyu_ninja.png ├── docs │ ├── README.md │ └── reagan_planning │ ├── meeting with reagan │ │ └── transcript.md │ └── transcript_insights.md ├── LICENSE ├── old │ ├── docs │ │ ├── 2025-01-10_odds_api_v4.md │ │ ├── 2025-01-15_mcp_infra.md │ │ ├── 2025-01-20_api_key_security.md │ │ ├── 2025-02-10_pip_install_fix_plan.md │ │ ├── 2025-02-15_python_odds_api_fix_postmortem.md │ │ └── 2025-02-20_task_context.md │ ├── espn_nonbetting_api │ │ └── espn_api_endpoints.md │ ├── README.md │ └── reagan_planning │ └── mcp_testing_approach.md ├── README.md └── wagyu_sports ├── __init__.py ├── build │ ├── pyproject.toml │ ├── requirements.txt │ └── setup.py ├── config │ ├── .env.example │ └── pytest.ini ├── conftest.py ├── docs │ └── LICENSE ├── examples │ ├── advanced_example.py │ ├── example.py │ ├── fetch_nba_odds.py │ ├── verify_import.py │ └── verify_install.py ├── Makefile ├── mcp_server │ ├── __init__.py │ ├── capture_live_responses.py │ ├── mocks_live │ │ ├── nba_games_live.json │ │ ├── quota_info_live.json │ │ ├── README.md │ │ └── sports_list_live.json │ ├── odds_client_server.py │ ├── odds_client.py │ ├── README.md │ └── test_server.py ├── odds_client.py ├── README.md ├── tests │ ├── README.md │ ├── test_odds_api.py │ ├── test_odds_mcp_server.py │ └── test_simple_mcp.py └── utils.py ``` # Files -------------------------------------------------------------------------------- /wagyu_sports/config/.env.example: -------------------------------------------------------------------------------- ``` 1 | # The Odds API Key 2 | # Get your API key from https://the-odds-api.com/ 3 | ODDS_API_KEY=your_api_key_here 4 | ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` 1 | # Node.js 2 | node_modules/ 3 | .env 4 | 5 | # Python 6 | __pycache__/ 7 | *.py[cod] 8 | *$py.class 9 | .pytest_cache/ 10 | .coverage 11 | htmlcov/ 12 | .tox/ 13 | .venv/ 14 | venv/ 15 | env/ 16 | *.egg-info/ 17 | dist/ 18 | build/ 19 | 20 | # macOS 21 | .DS_Store 22 | .AppleDouble 23 | .LSOverride 24 | Icon 25 | ._* 26 | .Spotlight-V100 27 | .Trashes 28 | .fseventsd 29 | .DocumentRevisions-V100 30 | .TemporaryItems 31 | .VolumeIcon.icns 32 | .com.apple.timemachine.donotpresent 33 | 34 | # IDE 35 | .idea/ 36 | .vscode/ 37 | *.swp 38 | *.swo 39 | ``` -------------------------------------------------------------------------------- /old/README.md: -------------------------------------------------------------------------------- ```markdown 1 | # Documentation Archive 2 | 3 | This directory contains historical documentation to track the project's development journey. 4 | 5 | ## Purpose 6 | 7 | The purpose of this archive is to chronologically document the development process through markdown and text files. This helps preserve the history of decisions, plans, and implementations without modifying the original documents. 8 | 9 | ## Structure 10 | 11 | ``` 12 | old/ 13 | ├── README.md (this file) 14 | └── docs/ (all historical documents) 15 | ``` 16 | 17 | ## Adding Documents 18 | 19 | 1. When a document becomes historical, copy it to the `docs/` directory 20 | 2. Add date prefix: `YYYY-MM-DD_filename.md` 21 | 3. Don't modify documents after archiving 22 | 23 | ## Guidelines 24 | 25 | - Only add markdown (.md) and text (.txt) files 26 | - Always include the date prefix in the format `YYYY-MM-DD_` 27 | - Original files should remain in their original locations 28 | - This system is intentionally minimal and can evolve as needed 29 | 30 | All MD and TXT files are welcome. No complex organization required. 31 | ``` -------------------------------------------------------------------------------- /wagyu_sports/mcp_server/mocks_live/README.md: -------------------------------------------------------------------------------- ```markdown 1 | # Live API Response Captures 2 | 3 | This directory contains captures of live API responses from the Odds API. These captures can be used for creating updated mock data for testing. 4 | 5 | ## Using Live Mode 6 | 7 | The MCP server has been modified to support overriding the test mode setting on a per-call basis. To use the live API (which costs money per call), set the `use_test_mode` parameter to `false` in your tool calls. 8 | 9 | ### Example Usage 10 | 11 | ```python 12 | # Get sports list using live API 13 | <use_mcp_tool> 14 | <server_name>wagyu-sports</server_name> 15 | <tool_name>get_sports</tool_name> 16 | <arguments> 17 | { 18 | "all_sports": true, 19 | "use_test_mode": false 20 | } 21 | </arguments> 22 | </use_mcp_tool> 23 | 24 | # Get NBA odds using live API 25 | <use_mcp_tool> 26 | <server_name>wagyu-sports</server_name> 27 | <tool_name>get_odds</tool_name> 28 | <arguments> 29 | { 30 | "sport": "basketball_nba", 31 | "regions": "us", 32 | "markets": "h2h,spreads", 33 | "use_test_mode": false 34 | } 35 | </arguments> 36 | </use_mcp_tool> 37 | 38 | # Get quota information using live API 39 | <use_mcp_tool> 40 | <server_name>wagyu-sports</server_name> 41 | <tool_name>get_quota_info</tool_name> 42 | <arguments> 43 | { 44 | "use_test_mode": false 45 | } 46 | </arguments> 47 | </use_mcp_tool> 48 | ``` 49 | 50 | ## Important Notes 51 | 52 | 1. **Cost Awareness**: Each live API call costs money. Use sparingly and only when necessary. 53 | 2. **Server Restart Required**: After modifying the server code, the MCP server needs to be restarted for changes to take effect. 54 | 3. **API Key**: The live mode requires a valid API key, which is already configured in the MCP settings. 55 | 56 | ## Captured Responses 57 | 58 | The following live API responses have been captured: 59 | 60 | - `sports_list_live.json`: List of available sports 61 | - `nba_games_live.json`: NBA game odds data 62 | - `quota_info_live.json`: API quota information 63 | ``` -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- ```markdown 1 | # API Documentation Collection 2 | 3 | This directory contains detailed documentation for various APIs used in our projects. Each markdown file provides comprehensive information about an API's capabilities, endpoints, and integration details. These documents serve as a reference for both human developers and AI assistants when building applications. 4 | 5 | ## Documentation Notice 6 | 7 | **Important**: Historical documentation has been moved to the `/old/docs/` directory with date prefixes for better chronological tracking. If you're looking for previous documentation, please check there. 8 | 9 | ## Documentation Structure 10 | 11 | Each API documentation file follows a consistent structure: 12 | 13 | 1. **Overview** - Basic information about the API 14 | 2. **Authentication** - How to authenticate with the API 15 | 3. **Available Endpoints** - Detailed endpoint documentation 16 | 4. **Integration Notes** - Best practices and implementation details 17 | 5. **Use Cases** - Common application scenarios 18 | 6. **Error Handling** - How to handle API errors 19 | 7. **Rate Limiting** - Usage limits and quotas 20 | 21 | ## Purpose 22 | 23 | These documentation files are designed to be: 24 | 25 | 1. **AI-Readable** - Structured in a way that AI can easily parse and understand the capabilities 26 | 2. **Comprehensive** - Including all relevant details about the API 27 | 3. **Practical** - Focusing on real-world usage and integration 28 | 4. **Maintainable** - Easy to update as APIs evolve 29 | 30 | ## Adding New API Documentation 31 | 32 | When adding documentation for a new API: 33 | 34 | 1. Create a new markdown file named appropriately (e.g., `api_name_v1.md`) 35 | 2. Follow the consistent documentation structure 36 | 3. Include all relevant details about authentication, endpoints, and usage 37 | 4. Update this README to include the new API documentation 38 | 39 | ## Archiving Documentation 40 | 41 | When documentation becomes historical: 42 | 43 | 1. Move it to the `/old/docs/` directory 44 | 2. Add a date prefix in the format `YYYY-MM-DD_filename.md` 45 | 3. See `/old/README.md` for more details on the archiving system 46 | ``` -------------------------------------------------------------------------------- /wagyu_sports/README.md: -------------------------------------------------------------------------------- ```markdown 1 | # Wagyu Sports 2 | 3 | A Python client for sports betting data with MCP server integration. 4 | 5 | ```mermaid 6 | graph LR 7 | User([Your Code]) --> Client[Wagyu Sports Client] --> API[The Odds API] 8 | MCP[MCP Server] --> Client 9 | Client --> MCP 10 | API --> Client --> User 11 | 12 | style User fill:#f8f8f8,stroke:#666,stroke-width:1px,color:#000 13 | style Client fill:#4285F4,stroke:#2965C9,stroke-width:2px,color:#fff 14 | style API fill:#F5F5F5,stroke:#999,stroke-width:1px,color:#000 15 | style MCP fill:#34A853,stroke:#1E8E3E,stroke-width:2px,color:#fff 16 | ``` 17 | 18 | ## Directory Structure 19 | 20 | The project has been reorganized for better maintainability: 21 | - `build/` - Build-related files (pyproject.toml, requirements.txt, setup.py) 22 | - `config/` - Configuration files (.env.example, pytest.ini) 23 | - `docs/` - Documentation (LICENSE, README.md) 24 | - `examples/` - Example scripts 25 | - `mcp_server/` - Model Context Protocol (MCP) server implementation 26 | - `tests/` - Test files 27 | 28 | ## Installation 29 | 30 | ```bash 31 | # Development installation 32 | uvx install -e . 33 | 34 | # User installation 35 | uv install wagyu_sports 36 | 37 | # Set up API key 38 | cp config/.env.example config/.env 39 | # Edit .env and add your API key from https://the-odds-api.com/ 40 | ``` 41 | 42 | ## Quick Start 43 | 44 | ```python 45 | from wagyu_sports import OddsClient 46 | import os 47 | from dotenv import load_dotenv 48 | 49 | # Load API key 50 | load_dotenv(dotenv_path="config/.env") 51 | api_key = os.getenv("ODDS_API_KEY") 52 | 53 | # Create client and get sports 54 | client = OddsClient(api_key) 55 | sports = client.get_sports() 56 | print(f"Available sports: {len(sports['data'])}") 57 | ``` 58 | 59 | ## Features 60 | 61 | - Access to sports betting data endpoints 62 | - Track API usage through response headers 63 | - Support for all API parameters and options 64 | 65 | ## Examples 66 | 67 | See the `examples/` directory for usage patterns: 68 | - `examples/example.py`: Basic usage 69 | - `examples/advanced_example.py`: Advanced features 70 | - `examples/verify_install.py`: Verify installation 71 | - `examples/fetch_nba_odds.py`: Fetch NBA odds example 72 | - `examples/verify_import.py`: Simple import verification 73 | 74 | ## Testing 75 | 76 | The testing suite has been cleaned up and improved for better organization and reliability. Run the tests using pytest: 77 | 78 | ```bash 79 | # Install test dependencies 80 | uvx install pytest pytest-asyncio 81 | 82 | # Run all tests 83 | uvx run pytest --rootdir=. -c config/pytest.ini 84 | 85 | # Run specific test file 86 | uvx run pytest tests/test_simple_mcp.py 87 | ``` 88 | 89 | Or use the Makefile: 90 | 91 | ```bash 92 | make test 93 | ``` 94 | 95 | The test suite includes: 96 | - **API Client Tests**: Tests for the core Odds API client functionality 97 | - **MCP Server Tests**: Tests for the MCP server implementation 98 | - **Client-based tests**: Test the full MCP protocol implementation 99 | - **Direct tests**: Simpler tests that directly test server methods 100 | 101 | See the `tests/README.md` file for more details on the testing approach. 102 | 103 | ## For MCP Server Information 104 | 105 | See the main README.md file for details on running and configuring the MCP server. 106 | ``` -------------------------------------------------------------------------------- /wagyu_sports/mcp_server/README.md: -------------------------------------------------------------------------------- ```markdown 1 | # Wagyu Sports MCP Server 2 | 3 | This directory contains a Model Context Protocol (MCP) server implementation for the Wagyu Sports API. The MCP server wraps the existing `OddsClient` and exposes its functionality through the standardized MCP interface. 4 | 5 | ## Features 6 | 7 | - Exposes sports betting data through MCP tools 8 | - Supports test mode with mock data for development and testing 9 | - Compatible with any MCP client (Claude Desktop, Cline, etc.) 10 | 11 | ## Available Tools 12 | 13 | The server exposes the following tools: 14 | 15 | - `get_sports`: Get a list of available sports 16 | - `get_odds`: Get odds for a specific sport 17 | - `get_quota_info`: Get API quota information 18 | 19 | ## Integration with MCP Clients 20 | 21 | ### Integration with Cline 22 | 23 | To use this MCP server with Cline: 24 | 25 | 1. Add the server to Cline's MCP settings file located at: 26 | - macOS: `~/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json` 27 | - Windows: `%APPDATA%\Code\User\globalStorage\saoudrizwan.claude-dev\settings\cline_mcp_settings.json` 28 | 29 | 2. Add the following configuration: 30 | ```json 31 | { 32 | "mcpServers": { 33 | "wagyu-sports": { 34 | "command": "python", 35 | "args": ["/path/to/wagyu_mcp_hackathon/wagyu_sports/mcp_server/test_server.py"], 36 | "env": { 37 | "ODDS_API_KEY": "your_api_key" 38 | }, 39 | "disabled": false, 40 | "autoApprove": [] 41 | } 42 | } 43 | } 44 | ``` 45 | 46 | 3. Replace `/path/to/wagyu_mcp_hackathon` with the actual path to your project 47 | 4. Replace `your_api_key` with your actual API key from [The Odds API](https://the-odds-api.com/) 48 | 5. Restart Cline 49 | 50 | ### Integration with Claude Desktop 51 | 52 | To use this MCP server with Claude Desktop: 53 | 54 | 1. Add the server to Claude Desktop's configuration file located at: 55 | - macOS: `~/Library/Application Support/Claude/claude_desktop_config.json` 56 | - Windows: `%APPDATA%\Claude\claude_desktop_config.json` 57 | 58 | 2. Add the following configuration: 59 | ```json 60 | { 61 | "mcpServers": { 62 | "wagyu-sports": { 63 | "command": "python", 64 | "args": ["/path/to/wagyu_mcp_hackathon/wagyu_sports/mcp_server/test_server.py"], 65 | "env": { 66 | "ODDS_API_KEY": "your_api_key" 67 | }, 68 | "disabled": false, 69 | "autoApprove": [] 70 | } 71 | } 72 | } 73 | ``` 74 | 75 | 3. Replace `/path/to/wagyu_mcp_hackathon` with the actual path to your project 76 | 4. Replace `your_api_key` with your actual API key from [The Odds API](https://the-odds-api.com/) 77 | 5. Restart Claude Desktop 78 | 79 | ## Test Mode 80 | 81 | The server can be run in test mode by adding the `--test-mode` flag to the command: 82 | 83 | ```json 84 | "args": ["/path/to/wagyu_mcp_hackathon/wagyu_sports/mcp_server/test_server.py", "--test-mode"] 85 | ``` 86 | 87 | In test mode, the server uses mock data from the `mocks_live/` directory instead of making real API calls, which: 88 | - Doesn't require an API key 89 | - Doesn't consume your API quota 90 | - Works offline 91 | ``` -------------------------------------------------------------------------------- /wagyu_sports/tests/README.md: -------------------------------------------------------------------------------- ```markdown 1 | # Wagyu Sports MCP Tests 2 | 3 | This directory contains tests for the Wagyu Sports MCP server implementation. 4 | 5 | ## Test Files 6 | 7 | - `test_odds_api.py` - Tests for the core Odds API client 8 | - `test_odds_mcp_server.py` - Tests for the MCP server implementation 9 | - `test_simple_mcp.py` - Simple direct tests for the MCP server functionality 10 | 11 | ## How to Run the Tests 12 | 13 | The project uses pytest for running tests. The configuration is in `wagyu_sports/config/pytest.ini`. 14 | 15 | ### Using pytest directly 16 | 17 | ```bash 18 | # Run all tests from the wagyu_sports directory 19 | cd wagyu_sports 20 | pytest 21 | 22 | # Run specific test file 23 | pytest tests/test_odds_mcp_server.py 24 | 25 | # Run with verbose output (already default in pytest.ini) 26 | pytest tests/test_odds_mcp_server.py 27 | 28 | # Run a specific test function 29 | pytest tests/test_odds_mcp_server.py::test_get_sports 30 | ``` 31 | 32 | ### Environment Setup 33 | 34 | The tests use the `test_mode=True` flag to run with mock data instead of making real API calls. This is handled automatically in the test code. 35 | 36 | ### Pytest Configuration 37 | 38 | The project's pytest.ini configuration: 39 | ```ini 40 | [pytest] 41 | testpaths = tests 42 | python_files = test_*.py 43 | python_functions = test_* 44 | addopts = -v 45 | ``` 46 | 47 | This configuration automatically: 48 | - Looks for tests in the `tests` directory 49 | - Runs files that start with `test_` 50 | - Runs functions that start with `test_` 51 | - Uses verbose output by default 52 | 53 | ## How to Add New Tests 54 | 55 | ### Test Approaches 56 | 57 | There are two main approaches to testing the MCP server: 58 | 59 | 1. **Client-based testing** (in `test_odds_mcp_server.py`): 60 | - Uses the MCP client session to test the server through the MCP protocol 61 | - Tests the full protocol implementation 62 | - Good for integration testing 63 | 64 | 2. **Direct testing** (in `test_simple_mcp.py`): 65 | - Tests the server methods directly 66 | - Faster and simpler for testing core functionality 67 | - Good for unit testing 68 | 69 | ### Client-Based Test Structure 70 | 71 | Client-based tests for the MCP server should follow this pattern: 72 | 73 | ```python 74 | @pytest.mark.anyio 75 | async def test_your_feature(): 76 | """Test description""" 77 | # Initialize server in test mode 78 | server = OddsMcpServer(test_mode=True) 79 | 80 | # Create client session 81 | async with client_session(server.server) as client: 82 | # Call the tool 83 | result = await client.call_tool("tool_name", {"param": "value"}) 84 | 85 | # Assert results 86 | assert len(result.content) > 0 87 | content = result.content[0] 88 | assert isinstance(content, TextContent) 89 | 90 | # Add specific assertions for your test case 91 | assert "expected_value" in content.text 92 | ``` 93 | 94 | ### Direct Test Structure 95 | 96 | Direct tests access the server methods directly: 97 | 98 | ```python 99 | @pytest.mark.asyncio 100 | async def test_direct_method(): 101 | """Test description""" 102 | # Initialize server in test mode 103 | server = OddsMcpServer(test_mode=True) 104 | 105 | # Test method directly 106 | mock_data = await server._get_mock_data("data_file.json") 107 | 108 | # Parse and verify the response 109 | data = json.loads(mock_data) 110 | assert "expected_key" in data 111 | # Add more assertions... 112 | ``` 113 | 114 | ### Testing New Tools 115 | 116 | When adding a new tool to the MCP server: 117 | 118 | 1. Add the tool implementation to `odds_client_server.py` 119 | 2. Create a mock data file in `wagyu_sports/mcp_server/mocks_live/` if needed 120 | 3. Add a test function in `test_odds_mcp_server.py` following the pattern above 121 | 4. Ensure your test verifies both the structure and content of the response 122 | 123 | ### Testing with Different Parameters 124 | 125 | To test a tool with different parameters: 126 | 127 | ```python 128 | @pytest.mark.anyio 129 | async def test_tool_with_params(): 130 | server = OddsMcpServer(test_mode=True) 131 | 132 | async with client_session(server.server) as client: 133 | result = await client.call_tool( 134 | "tool_name", 135 | { 136 | "param1": "value1", 137 | "param2": "value2" 138 | } 139 | ) 140 | # Assertions... 141 | ``` 142 | 143 | ### Testing Resources 144 | 145 | To test MCP resources: 146 | 147 | ```python 148 | @pytest.mark.anyio 149 | async def test_resource(): 150 | server = OddsMcpServer(test_mode=True) 151 | 152 | async with client_session(server.server) as client: 153 | # List resources 154 | resources = await client.list_resources() 155 | 156 | # Read a resource 157 | result = await client.read_resource("resource://uri") 158 | 159 | # Assertions... 160 | ``` 161 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | # Wagyu Sports MCP Server 2 | 3 | <p align="center"> 4 | <img src="assets/images/wagyu_ninja.png" width="250" alt="Wagyu Sports Logo"> 5 | </p> 6 | 7 | A Model Context Protocol (MCP) server for sports betting data, providing access to The Odds API through Claude and other MCP-compatible AI assistants. 8 | 9 | ```mermaid 10 | graph LR 11 | Claude([Claude]) --> MCP[Wagyu Sports MCP] 12 | MCP --> API[The Odds API] 13 | API --> MCP --> Claude 14 | 15 | style Claude fill:#f8f8f8,stroke:#666,stroke-width:1px,color:#000 16 | style MCP fill:#34A853,stroke:#1E8E3E,stroke-width:2px,color:#fff 17 | style API fill:#F5F5F5,stroke:#999,stroke-width:1px,color:#000 18 | ``` 19 | 20 | ## Quick Setup 21 | 22 | 1. **Clone the repository**: 23 | ```bash 24 | # Clone the repository 25 | git clone https://github.com/your-username/wagyu_mcp_hackathon.git 26 | cd wagyu_mcp_hackathon 27 | ``` 28 | 29 | 2. **Install the package**: 30 | ```bash 31 | # Using pip (recommended) 32 | pip install -e . 33 | 34 | # Or using uv (alternative) 35 | uv install -e . 36 | ``` 37 | 38 | 3. **Add to your MCP configuration**: 39 | 40 | For Cline, add to `~/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json`: 41 | 42 | ```json 43 | { 44 | "mcpServers": { 45 | "wagyu-sports": { 46 | "command": "python", 47 | "args": ["/absolute/path/to/wagyu_mcp_hackathon/wagyu_sports/mcp_server/test_server.py", "--test-mode"], 48 | "env": { 49 | "ODDS_API_KEY": "your_api_key_here" 50 | }, 51 | "disabled": false, 52 | "autoApprove": [] 53 | } 54 | } 55 | } 56 | ``` 57 | 58 | For Claude Desktop, add to `~/Library/Application Support/Claude/claude_desktop_config.json`: 59 | 60 | ```json 61 | { 62 | "mcpServers": { 63 | "wagyu-sports": { 64 | "command": "python", 65 | "args": ["/absolute/path/to/wagyu_mcp_hackathon/wagyu_sports/mcp_server/test_server.py", "--test-mode"], 66 | "env": { 67 | "ODDS_API_KEY": "your_api_key_here" 68 | }, 69 | "disabled": false, 70 | "autoApprove": [] 71 | } 72 | } 73 | } 74 | ``` 75 | 76 | > **IMPORTANT**: Replace `/absolute/path/to/wagyu_mcp_hackathon` with the actual full path to your repository. For example: `/Users/john/Documents/hackathon/wagyu_mcp_hackathon`. 77 | 78 | 4. **Get an API key** from [The Odds API](https://the-odds-api.com/) and replace `your_api_key_here` in the configuration. 79 | 80 | 5. **Restart your MCP client** (Cline or Claude Desktop). 81 | 82 | ## Available Tools 83 | 84 | The MCP server provides the following tools: 85 | 86 | - `get_sports`: Get a list of available sports 87 | - `get_odds`: Get odds for a specific sport 88 | - `get_quota_info`: Get API quota information 89 | 90 | ## Test Mode vs. Real Mode 91 | 92 | ### Test Mode (Recommended for Getting Started) 93 | 94 | Test mode uses mock data instead of making real API calls. This is useful for: 95 | - Development and testing without API rate limits 96 | - Demos and presentations 97 | - Learning how to use the MCP server 98 | 99 | To use test mode: 100 | 1. Set `--test-mode` in your MCP configuration (as shown in the Quick Setup) 101 | 2. No API key is required 102 | 3. The server will return consistent mock data for all requests 103 | 104 | Example configuration for test mode: 105 | ```json 106 | "args": ["/absolute/path/to/wagyu_mcp_hackathon/wagyu_sports/mcp_server/test_server.py", "--test-mode"] 107 | ``` 108 | 109 | ### Real Mode 110 | 111 | Real mode makes actual API calls to The Odds API. This is necessary for: 112 | - Getting real-time sports betting data 113 | - Production applications 114 | - Accurate odds information 115 | 116 | To use real mode: 117 | 1. Remove the `--test-mode` flag from your MCP configuration 118 | 2. Provide a valid API key from The Odds API 119 | 3. Be aware of API rate limits (typically 500 requests per month for free tier) 120 | 121 | Example configuration for real mode: 122 | ```json 123 | "args": ["/absolute/path/to/wagyu_mcp_hackathon/wagyu_sports/mcp_server/test_server.py"], 124 | "env": { 125 | "ODDS_API_KEY": "your_actual_api_key_here" 126 | } 127 | ``` 128 | 129 | You can also run the server directly with: 130 | ```bash 131 | python /path/to/wagyu_mcp_hackathon/wagyu_sports/mcp_server/test_server.py --api-key=your_api_key_here 132 | ``` 133 | 134 | ## Development 135 | 136 | For development and testing: 137 | 138 | ```bash 139 | # Clone the repository 140 | git clone https://github.com/your-username/wagyu_mcp_hackathon.git 141 | cd wagyu_mcp_hackathon 142 | 143 | # Install in development mode 144 | pip install -e . 145 | 146 | # Run tests 147 | python -m pytest wagyu_sports/tests 148 | 149 | # Run the server directly (test mode) 150 | python wagyu_sports/mcp_server/test_server.py --test-mode 151 | 152 | # Run the server directly (real mode) 153 | python wagyu_sports/mcp_server/test_server.py --api-key=your_api_key_here 154 | ``` 155 | 156 | > **Note**: This repository includes a post-commit Git hook that automatically cleans up Python cache files (`__pycache__`, `.pyc`, `.pyo`, `.pyd`) and `.pytest_cache` directories after each commit. 157 | 158 | ## Project Structure 159 | 160 | - `wagyu_sports/mcp_server/` - MCP server implementation 161 | - `wagyu_sports/tests/` - Test files 162 | - `wagyu_sports/examples/` - Example scripts 163 | 164 | ## For More Information 165 | 166 | See the `wagyu_sports/README.md` file for details on using the Python client directly. 167 | ``` -------------------------------------------------------------------------------- /wagyu_sports/config/pytest.ini: -------------------------------------------------------------------------------- ``` 1 | [pytest] 2 | testpaths = tests 3 | python_files = test_*.py 4 | python_functions = test_* 5 | addopts = -v 6 | ``` -------------------------------------------------------------------------------- /wagyu_sports/mcp_server/mocks_live/quota_info_live.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "_metadata": { 3 | "captured_at": "2025-03-03T01:50:08-08:00", 4 | "description": "Live data captured from the Odds API", 5 | "tool": "get_quota_info", 6 | "parameters": {} 7 | }, 8 | "remaining_requests": "488", 9 | "used_requests": "12" 10 | } 11 | ``` -------------------------------------------------------------------------------- /wagyu_sports/conftest.py: -------------------------------------------------------------------------------- ```python 1 | """ 2 | Configuration file for pytest. 3 | 4 | This file sets up the Python path for tests to ensure imports work correctly. 5 | """ 6 | import os 7 | import sys 8 | 9 | # Add the project root to the Python path 10 | sys.path.insert(0, os.path.abspath(os.path.dirname(__file__))) 11 | ``` -------------------------------------------------------------------------------- /wagyu_sports/mcp_server/__init__.py: -------------------------------------------------------------------------------- ```python 1 | """ 2 | Wagyu Sports MCP Server 3 | 4 | This module provides an MCP server implementation for the Wagyu Sports API. 5 | """ 6 | 7 | try: 8 | # When imported as a package 9 | from .odds_client_server import OddsMcpServer 10 | except ImportError: 11 | # When run directly 12 | from odds_client_server import OddsMcpServer 13 | 14 | __all__ = ["OddsMcpServer"] 15 | ``` -------------------------------------------------------------------------------- /wagyu_sports/__init__.py: -------------------------------------------------------------------------------- ```python 1 | """ 2 | Wagyu Sports Package 3 | 4 | This package provides a client for sports betting data, allowing users to fetch 5 | sports betting data including live odds, scores, and event information. 6 | """ 7 | 8 | from wagyu_sports.odds_client import OddsClient 9 | from wagyu_sports.utils import get_next_test_number, save_response, test_wagyu_sports 10 | 11 | __all__ = ['OddsClient', 'get_next_test_number', 'save_response', 'test_wagyu_sports'] 12 | ``` -------------------------------------------------------------------------------- /wagyu_sports/examples/verify_import.py: -------------------------------------------------------------------------------- ```python 1 | #!/usr/bin/env python3 2 | """ 3 | Simple verification script to check imports from the Wagyu Sports client. 4 | 5 | This script attempts to import the OddsClient class and create an instance, 6 | which verifies that the package is installed correctly and can be imported. 7 | """ 8 | 9 | try: 10 | print("Attempting to import OddsClient...") 11 | from wagyu_sports import OddsClient 12 | print("Successfully imported OddsClient!") 13 | 14 | # Try creating an instance 15 | client = OddsClient("test_key") 16 | print("Successfully created OddsClient instance!") 17 | 18 | except ImportError as e: 19 | print(f"Import Error: {e}") 20 | 21 | except Exception as e: 22 | print(f"Other Error: {e}") 23 | ``` -------------------------------------------------------------------------------- /.github/hooks/modules/env-check.sh: -------------------------------------------------------------------------------- ```bash 1 | #!/bin/bash 2 | # Module to check for .env files in commits 3 | # Prevents accidentally committing environment files with sensitive data 4 | 5 | # Check for .env files in the staged changes 6 | if git diff --cached --name-only | grep -q "\.env$"; then 7 | echo "ERROR: Attempting to commit .env file." 8 | echo "These files typically contain sensitive information like API keys." 9 | echo "Add them to .gitignore instead and use .env.example as a template." 10 | echo "" 11 | echo "Offending files:" 12 | git diff --cached --name-only | grep "\.env$" 13 | exit 1 14 | fi 15 | 16 | # Also check for files that might be .env files with different extensions 17 | if git diff --cached --name-only | grep -q "env\.\|\.env\."; then 18 | echo "WARNING: Possible environment file detected." 19 | echo "Please verify these files don't contain sensitive information:" 20 | git diff --cached --name-only | grep "env\.\|\.env\." 21 | echo "" 22 | echo "Continue with commit? (y/n)" 23 | read -r response 24 | if [[ "$response" != "y" ]]; then 25 | exit 1 26 | fi 27 | fi 28 | 29 | exit 0 30 | ``` -------------------------------------------------------------------------------- /.github/hooks/modules/api-key-check.sh: -------------------------------------------------------------------------------- ```bash 1 | #!/bin/bash 2 | # Module to check for API keys in commits 3 | # Scans for common API key patterns to prevent accidental exposure 4 | 5 | # Define patterns to check for 6 | # Add more patterns as needed for different types of API keys 7 | PATTERNS=( 8 | # The Odds API key pattern (alphanumeric, typically 32 chars) 9 | "ODDS_API_KEY=[a-zA-Z0-9]{32}" 10 | 11 | # Generic API key patterns 12 | "api[_-]key[=\"':= ][a-zA-Z0-9]" 13 | "apikey[=\"':= ][a-zA-Z0-9]" 14 | "key[=\"':= ][a-zA-Z0-9]{32}" 15 | "secret[=\"':= ][a-zA-Z0-9]" 16 | "password[=\"':= ][a-zA-Z0-9]" 17 | "token[=\"':= ][a-zA-Z0-9]" 18 | ) 19 | 20 | # Check staged files for API key patterns 21 | for pattern in "${PATTERNS[@]}"; do 22 | # Use git grep to search staged changes 23 | matches=$(git diff --cached -U0 | grep -i -E "$pattern" || true) 24 | 25 | if [ -n "$matches" ]; then 26 | echo "ERROR: Potential API key or sensitive data found in commit." 27 | echo "Pattern matched: $pattern" 28 | echo "" 29 | echo "Please remove the sensitive data and try again." 30 | echo "Consider using environment variables or a secure vault." 31 | exit 1 32 | fi 33 | done 34 | 35 | exit 0 36 | ``` -------------------------------------------------------------------------------- /.github/workflows/auto-setup-hooks.yml: -------------------------------------------------------------------------------- ```yaml 1 | name: Auto Setup Git Hooks 2 | 3 | on: 4 | push: 5 | branches: [ main, master ] 6 | pull_request: 7 | branches: [ main, master ] 8 | workflow_dispatch: # Allows manual triggering 9 | 10 | jobs: 11 | setup-hooks: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout repository 15 | uses: actions/checkout@v3 16 | 17 | - name: Configure Git Hooks 18 | run: | 19 | chmod +x .github/scripts/auto-setup.sh 20 | .github/scripts/auto-setup.sh 21 | 22 | - name: Verify Hooks Configuration 23 | run: | 24 | echo "Verifying hooks configuration..." 25 | if [ "$(git config --get core.hooksPath)" = ".github/hooks" ]; then 26 | echo "✅ Hooks configured successfully" 27 | else 28 | echo "❌ Hooks configuration failed" 29 | exit 1 30 | fi 31 | 32 | - name: Check for .env Files 33 | run: | 34 | echo "Checking for .env files..." 35 | if git ls-files | grep -q "\.env$"; then 36 | echo "⚠️ Warning: .env files found in repository" 37 | git ls-files | grep "\.env$" 38 | else 39 | echo "✅ No .env files found in repository" 40 | fi 41 | ``` -------------------------------------------------------------------------------- /.github/scripts/auto-setup.sh: -------------------------------------------------------------------------------- ```bash 1 | #!/bin/bash 2 | # Auto-setup script for git hooks in local development 3 | # This script is automatically triggered by GitHub Actions in CI/CD 4 | # but can also be run manually for local development 5 | 6 | # Exit on error 7 | set -e 8 | 9 | # Get the root directory of the git repository 10 | REPO_ROOT=$(git rev-parse --show-toplevel) 11 | cd "$REPO_ROOT" 12 | 13 | echo "🔒 Setting up automatic git hooks..." 14 | 15 | # Configure git to use hooks from .github/hooks 16 | git config --local core.hooksPath .github/hooks 17 | echo "✓ Configured git to use hooks from .github/hooks" 18 | 19 | # Make sure all hook scripts are executable 20 | chmod +x .github/hooks/pre-commit 21 | echo "✓ Made pre-commit hook executable" 22 | 23 | # Make all module scripts executable 24 | MODULE_COUNT=0 25 | for module in .github/hooks/modules/*.sh; do 26 | if [ -f "$module" ]; then 27 | chmod +x "$module" 28 | echo "✓ Made $(basename "$module") executable" 29 | MODULE_COUNT=$((MODULE_COUNT+1)) 30 | fi 31 | done 32 | 33 | echo "" 34 | echo "✅ Git hooks configured automatically!" 35 | echo "The following hooks are now active:" 36 | echo "- pre-commit: Prevents committing sensitive information like API keys" 37 | echo "" 38 | echo "Modules configured ($MODULE_COUNT total):" 39 | echo "- env-check.sh: Prevents committing .env files" 40 | echo "- api-key-check.sh: Scans for API key patterns in code" 41 | echo "" 42 | echo "Your repository is now protected against accidental API key commits!" 43 | echo "This configuration will persist across all branches." 44 | ``` -------------------------------------------------------------------------------- /old/docs/2025-02-15_python_odds_api_fix_postmortem.md: -------------------------------------------------------------------------------- ```markdown 1 | # Python Odds API Fix Post-Mortem 2 | 3 | ## Issues & Fixes 4 | 5 | ### Issues Identified 6 | 1. **Deprecation Warning**: Legacy editable install warning during pip installation 7 | 2. **Import Error**: Package couldn't be imported after installation 8 | 3. **Package Structure**: Flat structure not following Python packaging conventions 9 | 10 | ### Implemented Fixes 11 | 1. Added `pyproject.toml` to fix deprecation warning 12 | 2. Restructured package to use proper nested directory: 13 | ``` 14 | wagyu_sports/ 15 | ├── pyproject.toml 16 | ├── setup.py 17 | └── wagyu_sports/ <-- New subdirectory 18 | ├── __init__.py 19 | ├── odds_client.py 20 | └── utils.py 21 | ``` 22 | 3. Updated root `__init__.py` to import from subdirectory 23 | 24 | ## Divergence from Original Plan 25 | 26 | The implementation followed the original plan with two key differences: 27 | 28 | 1. **Minimal pyproject.toml**: Used minimal configuration rather than comprehensive metadata 29 | 2. **Kept Original Files**: Didn't move test files to a separate tests directory to minimize changes 30 | 31 | These decisions were made to implement the most critical fixes with minimal changes to the codebase. 32 | 33 | ## Installation Instructions 34 | 35 | ```bash 36 | # Clone the repository 37 | git clone <repository-url> 38 | cd wagyu_mcp_hackathon/api_test/wagyu_sports 39 | 40 | # Install the package 41 | pip install -e . 42 | 43 | # Set up API key 44 | cp .env.example .env 45 | # Edit .env and add your API key 46 | 47 | # Verify installation 48 | python run_test.py 49 | ``` 50 | 51 | ## Usage Example 52 | 53 | ```python 54 | from wagyu_sports import OddsClient 55 | from dotenv import load_dotenv 56 | import os 57 | 58 | # Load API key 59 | load_dotenv() 60 | api_key = os.getenv("ODDS_API_KEY") 61 | 62 | # Create client and fetch sports 63 | client = OddsClient(api_key) 64 | sports = client.get_sports() 65 | print(f"Found {len(sports['data'])} sports") 66 | ``` -------------------------------------------------------------------------------- /wagyu_sports/mcp_server/test_server.py: -------------------------------------------------------------------------------- ```python 1 | #!/usr/bin/env python3 2 | """ 3 | Test script for the Wagyu Sports MCP Server. 4 | 5 | This script initializes and runs the MCP server in test mode, 6 | which uses mock data instead of making real API calls. 7 | """ 8 | import asyncio 9 | import os 10 | import sys 11 | import argparse 12 | from pathlib import Path 13 | 14 | # Import directly from the current directory 15 | from odds_client_server import OddsMcpServer 16 | 17 | async def main(): 18 | """Run the MCP server.""" 19 | # Parse command line arguments 20 | parser = argparse.ArgumentParser(description="Wagyu Sports MCP Server") 21 | parser.add_argument("--test-mode", action="store_true", help="Use mock data instead of real API calls") 22 | args = parser.parse_args() 23 | 24 | # Determine if we should use test mode 25 | test_mode = args.test_mode 26 | 27 | if test_mode: 28 | print("Starting Wagyu Sports MCP Server in test mode...") 29 | print("This will use mock data instead of making real API calls.") 30 | else: 31 | print("Starting Wagyu Sports MCP Server in live mode...") 32 | print("This will make real API calls that cost money.") 33 | print() 34 | 35 | # Initialize server 36 | server = OddsMcpServer(test_mode=test_mode) 37 | 38 | # Run the server 39 | await server.run() 40 | 41 | if __name__ == "__main__": 42 | print("=" * 80) 43 | print("Wagyu Sports MCP Server Test") 44 | print("=" * 80) 45 | print() 46 | print("To test this server with mcp_inspector:") 47 | print("1. In another terminal, run:") 48 | print(" npx @modelcontextprotocol/inspector python wagyu_sports/mcp_server/test_server.py") 49 | print() 50 | print("2. The inspector will connect to this server and allow you to:") 51 | print(" - View and test available tools") 52 | print(" - See the results of tool calls") 53 | print(" - Monitor server logs") 54 | print() 55 | print("3. Try calling these tools:") 56 | print(" - get_sports") 57 | print(" - get_odds (with sport='basketball_nba')") 58 | print(" - get_quota_info") 59 | print() 60 | print("=" * 80) 61 | print() 62 | 63 | try: 64 | asyncio.run(main()) 65 | except KeyboardInterrupt: 66 | print("\nServer stopped by user.") 67 | ``` -------------------------------------------------------------------------------- /wagyu_sports/examples/fetch_nba_odds.py: -------------------------------------------------------------------------------- ```python 1 | #!/usr/bin/env python3 2 | """ 3 | Example script for fetching NBA odds using the Wagyu Sports client. 4 | 5 | This script demonstrates how to use the Wagyu Sports client to fetch NBA odds 6 | and save the response to a file. 7 | """ 8 | import os 9 | import json 10 | from dotenv import load_dotenv 11 | import requests 12 | 13 | 14 | def fetch_nba_odds(): 15 | """ 16 | Fetch NBA odds from the sports data API. 17 | 18 | This function: 19 | 1. Loads the API key from environment variables 20 | 2. Makes a direct request to the sports data API for NBA odds 21 | 3. Saves the response to a file 22 | 23 | Returns: 24 | str: Path to the saved file 25 | """ 26 | # Load environment variables 27 | load_dotenv(dotenv_path="config/.env") 28 | 29 | # Get API key 30 | api_key = os.getenv("ODDS_API_KEY") 31 | if not api_key: 32 | print("Error: ODDS_API_KEY not found in environment variables") 33 | print("Please copy config/.env.example to config/.env and add your API key: ODDS_API_KEY=your_api_key_here") 34 | return 35 | 36 | try: 37 | # Get NBA odds 38 | response = requests.get( 39 | 'https://api.the-odds-api.com/v4/sports/basketball_nba/odds', 40 | params={ 41 | 'apiKey': api_key, 42 | 'regions': 'us', 43 | 'markets': 'h2h,spreads', 44 | 'oddsFormat': 'american' 45 | } 46 | ) 47 | 48 | # Check for successful response 49 | response.raise_for_status() 50 | 51 | # Prepare output 52 | output = { 53 | 'odds': response.json(), 54 | 'remainingRequests': response.headers.get('x-requests-remaining') 55 | } 56 | 57 | # Write response to file 58 | with open('nba_odds.json', 'w') as f: 59 | json.dump(output, f, indent=2) 60 | 61 | print('NBA odds written to nba_odds.json') 62 | print(f'Remaining requests: {output["remainingRequests"]}') 63 | 64 | return 'nba_odds.json' 65 | 66 | except requests.exceptions.RequestException as e: 67 | print(f'Error: {e.response.json() if hasattr(e, "response") else str(e)}') 68 | return None 69 | 70 | 71 | if __name__ == "__main__": 72 | # Run the example if this file is executed directly 73 | fetch_nba_odds() 74 | ``` -------------------------------------------------------------------------------- /old/docs/2025-02-20_task_context.md: -------------------------------------------------------------------------------- ```markdown 1 | # Python Odds API Fix Task Context 2 | 3 | ## Current State 4 | 5 | - Branch: `fix/python_api` 6 | - Working Directory: `/Users/brandonbutterwick/Documents/hackathon/wagyu_mcp_hackathon` 7 | - Project Location: `api_test/wagyu_sports/` 8 | 9 | ## Background Context 10 | 11 | 1. The Python Odds API package is currently experiencing installation and import issues 12 | 2. A friend's PC was able to run the package successfully, indicating environment-specific problems 13 | 3. The package uses a conda environment named 'sports' for development 14 | 4. Current dependencies are managed through requirements.txt: 15 | - requests>=2.25.0 16 | - python-dotenv>=0.15.0 17 | 18 | ## Known Issues 19 | 20 | 1. Package fails to import when running `run_test.py` 21 | 2. Legacy editable install warning appears during pip installation 22 | 3. Import paths are not resolving correctly 23 | 4. Package structure needs modernization 24 | 25 | ## Environment Setup 26 | 27 | 1. Conda environment 'sports' is being used 28 | 2. Dependencies have been installed in the conda environment 29 | 3. The .env file contains the ODDS_API_KEY for testing 30 | 31 | ## Recent Changes 32 | 33 | 1. Created detailed implementation plan in `docs/pip_install_fix_plan.md` 34 | 2. No structural changes have been made yet 35 | 3. Original package structure and files remain intact 36 | 37 | ## Next Action Items 38 | 39 | 1. Create new directory structure as outlined in the plan 40 | 2. Implement modern Python packaging with pyproject.toml 41 | 3. Update package configuration in setup.py 42 | 4. Reorganize test files into dedicated test directory 43 | 44 | ## Important Files 45 | 46 | - `run_test.py`: Main test script showing import issues 47 | - `odds_client.py`: Core client implementation 48 | - `setup.py`: Package configuration (needs modernization) 49 | - `.env`: Contains API key for testing 50 | - `requirements.txt`: Current dependency specifications 51 | 52 | ## Testing Notes 53 | 54 | 1. Test script requires ODDS_API_KEY environment variable 55 | 2. Current test failure is at import level, not API functionality 56 | 3. Package should be tested in both regular and editable install modes 57 | 58 | ## Additional Context 59 | 60 | 1. Package is intended to be pip-installable for end users 61 | 2. Need to maintain compatibility with existing API usage patterns 62 | 3. Modern Python packaging practices should be followed 63 | 4. Documentation needs to be updated after fixes 64 | 65 | This context document should help Roo pick up the task and continue with the implementation phase of the pip installation fixes. ``` -------------------------------------------------------------------------------- /wagyu_sports/tests/test_simple_mcp.py: -------------------------------------------------------------------------------- ```python 1 | """Simple tests for Wagyu Sports MCP server following the Python MCP SDK example pattern""" 2 | 3 | import json 4 | import os 5 | import sys 6 | import pytest 7 | from unittest.mock import MagicMock, patch 8 | 9 | # Import directly from the module 10 | from wagyu_sports.mcp_server.odds_client_server import OddsMcpServer 11 | 12 | @pytest.mark.asyncio 13 | async def test_simple_get_sports(): 14 | """Test the basic functionality of the get_sports tool""" 15 | # Create an instance of the server in test mode 16 | server = OddsMcpServer(test_mode=True) 17 | 18 | # Test the _get_mock_data method directly 19 | mock_data = await server._get_mock_data("sports_list.json") 20 | 21 | # Parse and verify the response 22 | data = json.loads(mock_data) 23 | assert "data" in data 24 | assert isinstance(data["data"], list) 25 | assert any(sport["key"] == "basketball_nba" for sport in data["data"]) 26 | 27 | 28 | @pytest.mark.asyncio 29 | async def test_simple_get_odds(): 30 | """Test the basic functionality of the get_odds tool""" 31 | # Create an instance of the server in test mode 32 | server = OddsMcpServer(test_mode=True) 33 | 34 | # Test the _get_mock_data method directly for NBA games 35 | mock_data = await server._get_mock_data("nba_games.json") 36 | 37 | # Parse and verify the response 38 | data = json.loads(mock_data) 39 | assert "data" in data 40 | assert isinstance(data["data"], list) 41 | assert len(data["data"]) > 0 42 | 43 | # Check the first game 44 | first_game = data["data"][0] 45 | assert "sport_key" in first_game 46 | assert first_game["sport_key"] == "basketball_nba" 47 | assert "home_team" in first_game 48 | assert "away_team" in first_game 49 | 50 | 51 | @pytest.mark.asyncio 52 | async def test_simple_get_quota_info(): 53 | """Test the basic functionality of the get_quota_info tool""" 54 | # Create an instance of the server in test mode 55 | server = OddsMcpServer(test_mode=True) 56 | 57 | # In test mode, get_quota_info returns a fixed response 58 | # We can construct this manually to test the structure 59 | expected_data = { 60 | "remaining_requests": "100", 61 | "used_requests": "50" 62 | } 63 | 64 | # Create a response similar to what the tool would return 65 | quota_response = json.dumps(expected_data, indent=2) 66 | quota_data = json.loads(quota_response) 67 | 68 | # Verify the expected structure 69 | assert "remaining_requests" in quota_data 70 | assert "used_requests" in quota_data 71 | 72 | 73 | if __name__ == "__main__": 74 | pytest.main(["-xvs", __file__]) 75 | ``` -------------------------------------------------------------------------------- /old/docs/2025-01-20_api_key_security.md: -------------------------------------------------------------------------------- ```markdown 1 | # API Key Security 2 | 3 | ## Current Status 4 | 5 | After investigating the repository, we've confirmed that: 6 | 7 | 1. The `.env` file containing your API key is **not currently tracked by git** 8 | 2. The API key `22c87ba01f0fecd6a3acbf114ebcb940` does **not appear in the git history** 9 | 3. The `.gitignore` file is correctly configured to ignore `.env` files 10 | 11 | This means your API key is currently secure and has not been exposed in the git history. 12 | 13 | ## Implemented Security Measures 14 | 15 | We've created a new branch `feature/git_action_.env` with git hooks to prevent accidentally committing API keys or `.env` files in the future: 16 | 17 | 1. **Pre-commit hook**: Automatically runs checks before each commit 18 | 2. **Environment file check**: Prevents committing `.env` files 19 | 3. **API key pattern detection**: Scans for API keys in staged changes 20 | 21 | ## Fully Automated Git Hooks 22 | 23 | The git hooks are now **fully automated** using GitHub Actions: 24 | 25 | 1. **No manual setup required** - hooks are configured automatically by CI/CD 26 | 2. **Works across all branches** - protection is consistent everywhere 27 | 3. **Automatic for all developers** - no need to run setup scripts 28 | 29 | ### For Local Development 30 | 31 | If you're working locally without GitHub Actions, you can run: 32 | 33 | ```bash 34 | .github/scripts/auto-setup.sh 35 | ``` 36 | 37 | This will configure Git to use the hooks from `.github/hooks` instead of the default `.git/hooks` directory, making the hooks a default part of the repository. 38 | 39 | ### CI/CD Integration 40 | 41 | The GitHub Actions workflow in `.github/workflows/auto-setup-hooks.yml` automatically: 42 | 1. Sets up the hooks for all developers 43 | 2. Verifies the hooks are configured correctly 44 | 3. Checks for any .env files that might have been committed 45 | 46 | ## Best Practices for API Key Security 47 | 48 | 1. **Never commit `.env` files**: Store sensitive information in environment variables 49 | 2. **Use `.env.example` files**: Provide templates without real values 50 | 3. **Rotate API keys regularly**: Change keys periodically, especially after suspected exposure 51 | 4. **Use different keys**: Use separate keys for development, testing, and production 52 | 5. **Set up access controls**: Limit who has access to production API keys 53 | 54 | ## What to Do If You Accidentally Commit an API Key 55 | 56 | If you accidentally commit an API key: 57 | 58 | 1. **Rotate the key immediately**: Generate a new key from the service provider 59 | 2. **Remove from git history**: Use tools like `git-filter-repo` to remove sensitive data 60 | 3. **Force push**: Update the remote repository with the cleaned history 61 | 4. **Notify team members**: Ensure everyone pulls the updated history 62 | 63 | ## Additional Resources 64 | 65 | - [Git documentation on .gitignore](https://git-scm.com/docs/gitignore) 66 | - [GitHub's guide on removing sensitive data](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/removing-sensitive-data-from-a-repository) 67 | ``` -------------------------------------------------------------------------------- /wagyu_sports/examples/verify_install.py: -------------------------------------------------------------------------------- ```python 1 | #!/usr/bin/env python3 2 | """ 3 | Verification script to check the installation and functionality of the Wagyu Sports client. 4 | 5 | This script: 6 | 1. Checks if the required dependencies are installed 7 | 2. Verifies that the API key is set in the environment 8 | 3. Runs a simple test to fetch sports data 9 | """ 10 | import os 11 | import sys 12 | import importlib.util 13 | 14 | 15 | def check_dependency(package_name): 16 | """Check if a Python package is installed.""" 17 | spec = importlib.util.find_spec(package_name) 18 | if spec is None: 19 | print(f"Error: {package_name} is not installed.") 20 | print(f"Please install it using: pip install {package_name}") 21 | return False 22 | return True 23 | 24 | 25 | def main(): 26 | """Main function to run tests.""" 27 | # Check dependencies 28 | dependencies = ["requests", "dotenv"] 29 | all_installed = True 30 | 31 | for dep in dependencies: 32 | if not check_dependency(dep): 33 | all_installed = False 34 | 35 | if not all_installed: 36 | print("\nPlease install all required dependencies and try again.") 37 | print("You can install them using: pip install -r build/requirements.txt") 38 | return 39 | 40 | # Now that we know dependencies are installed, import them 41 | from dotenv import load_dotenv 42 | 43 | # Check for API key 44 | load_dotenv(dotenv_path="config/.env") 45 | api_key = os.getenv("ODDS_API_KEY") 46 | 47 | if not api_key: 48 | print("Error: ODDS_API_KEY not found in environment variables.") 49 | print("Please copy config/.env.example to .env and add your API key: ODDS_API_KEY=your_api_key_here") 50 | return 51 | 52 | # Try to import the client 53 | try: 54 | from wagyu_sports import OddsClient 55 | 56 | # Create client 57 | client = OddsClient(api_key) 58 | 59 | # Test getting sports 60 | print("Testing API connection by fetching available sports...") 61 | response = client.get_sports() 62 | 63 | # Check response 64 | if "data" in response and isinstance(response["data"], list): 65 | sport_count = len(response["data"]) 66 | print(f"Success! Fetched {sport_count} available sports.") 67 | print(f"Remaining API requests: {response['headers']['x-requests-remaining']}") 68 | 69 | # Show a few sports as example 70 | if sport_count > 0: 71 | print("\nSample sports:") 72 | for sport in response["data"][:3]: # Show first 3 sports 73 | print(f"- {sport.get('title', 'Unknown')}: {sport.get('key', 'Unknown')}") 74 | 75 | print("\nThe Wagyu Sports client is working correctly!") 76 | else: 77 | print("Error: Unexpected response format from the API.") 78 | print("Response:", response) 79 | 80 | except ImportError: 81 | print("Error: Could not import the Wagyu Sports client.") 82 | print("Make sure the package is installed correctly.") 83 | 84 | except Exception as e: 85 | print(f"Error during API test: {e}") 86 | 87 | 88 | if __name__ == "__main__": 89 | main() 90 | ``` -------------------------------------------------------------------------------- /old/reagan_planning/mcp_testing_approach.md: -------------------------------------------------------------------------------- ```markdown 1 | # MCP Testing Approach: Mock Response System 2 | 3 | ## Overview 4 | 5 | This document outlines a testing approach for sports betting MCPs that uses mock data instead of live API calls. This method allows for consistent, repeatable testing without API rate limits or dependencies on current sporting events. 6 | 7 | ## Core Concept 8 | 9 | Create a layered MCP architecture with a base "Odds MCP" that other specialized MCPs can use. This base MCP can operate in either live mode (real API calls) or test mode (predetermined mock responses). 10 | 11 | ## Implementation Steps 12 | 13 | ### 1. Create Mock Data Files 14 | 15 | ``` 16 | /mocks/ 17 | sports_list.json # Available sports 18 | nba_games.json # List of NBA games 19 | game_odds_caesars.json # Single game odds from default book 20 | game_odds_all_books.json # Comparison across books 21 | futures_odds.json # Championship futures odds 22 | ``` 23 | 24 | ### 2. Build a Test-Ready Base MCP 25 | 26 | ```python 27 | # Base MCP with test/live mode toggle 28 | class OddsMCP: 29 | def __init__(self, api_key=None, test_mode=False): 30 | self.api_key = api_key 31 | self.test_mode = test_mode 32 | 33 | def get_data(self, endpoint, params=None): 34 | if self.test_mode: 35 | # Return appropriate mock based on endpoint and params 36 | return self._get_mock_response(endpoint, params) 37 | else: 38 | # Make actual API call 39 | return self._call_api(endpoint, params) 40 | ``` 41 | 42 | ### 3. Create Specialized MCPs Using the Base 43 | 44 | ```python 45 | # Example of a specialized MCP 46 | class GameBrowserMCP: 47 | def __init__(self, odds_mcp): 48 | self.odds_mcp = odds_mcp 49 | 50 | def get_available_sports(self): 51 | # Uses the base MCP, which could be in test mode 52 | return self.odds_mcp.get_data("sports") 53 | 54 | def get_games_for_sport(self, sport): 55 | return self.odds_mcp.get_data("games", {"sport": sport}) 56 | ``` 57 | 58 | ## Benefits 59 | 60 | 1. **Controlled Testing Environment**: Predictable data for each test scenario 61 | 2. **Fast Execution**: No network delays or API rate limits 62 | 3. **Comprehensive Coverage**: Test edge cases and rare situations 63 | 4. **Isolated Components**: Test each MCP layer independently 64 | 5. **Repeatable Results**: Tests produce consistent output 65 | 66 | ## Mock Data Example 67 | 68 | ```json 69 | // Example mock for a game odds response 70 | { 71 | "game": { 72 | "home_team": "Lakers", 73 | "away_team": "Grizzlies", 74 | "commence_time": "2025-03-02T19:30:00Z" 75 | }, 76 | "odds": { 77 | "caesars": { 78 | "spread": {"home": -4.5, "away": 4.5, "odds": -110}, 79 | "moneyline": {"home": -190, "away": 160}, 80 | "totals": {"over": 224.5, "under": 224.5, "odds": -110} 81 | } 82 | } 83 | } 84 | ``` 85 | 86 | ## Test Flow 87 | 88 | 1. Initialize test versions of MCPs with `test_mode=True` 89 | 2. Run through user scenarios from the test_scenarios.md document 90 | 3. Compare MCP outputs to expected responses 91 | 4. Toggle to live mode for final verification with real data 92 | 93 | This approach provides a stable foundation for development while keeping the option to test against live data when needed. 94 | ``` -------------------------------------------------------------------------------- /docs/reagan_planning/transcript_insights.md: -------------------------------------------------------------------------------- ```markdown 1 | # Sports Betting AI - Transcript Insights 2 | 3 | This document summarizes key insights from the conversation with Reagan about sports betting user needs and preferences. 4 | 5 | ## 1. User Stories / Scenarios 6 | 7 | ### Scenario 1: The Value Shopper 8 | **User Need:** "I want to place a bet on Loyola Marymount at the highest possible odds" 9 | **Action:** Compares odds across multiple sportsbooks for a specific selection 10 | **Output Example:** "DraftKings offers Loyola Marymount at +925, while FanDuel has them at +900" 11 | 12 | ### Scenario 2: The Casual Game Night Better 13 | **User Need:** "I'm watching the Lakers game with friends and want to place a quick bet" 14 | **Action:** Looks up basic odds for a specific game 15 | **Output Example:** "Lakers vs. Grizzlies: Lakers -4.5, O/U 224.5, Lakers ML -190" 16 | 17 | ### Scenario 3: The Trend Spotter 18 | **User Need:** "I want to see if there's unusual line movement on any games tonight" 19 | **Action:** Looks for significant recent changes in betting lines 20 | **Output Example:** "The White Sox line has moved from +300 to +375 in the last hour despite 60% of bets being placed on them" 21 | 22 | ## 2. Data Points to Track 23 | 24 | | Data Category | Priority | Examples | 25 | |---------------|----------|----------| 26 | | Basic Odds | High | Spread, Money Line, Over/Under | 27 | | Book Comparison | High | Same bet across multiple sportsbooks | 28 | | Line Movement | Medium | Changes from opening line to current | 29 | | Bet Distribution | Medium | % of bets vs % of money on each side | 30 | | Futures | Medium | Long-term bets with higher variation across books | 31 | 32 | ## 3. User Experience Flow 33 | 34 | 1. **Initial Query** 35 | - User asks: "Show me the line for Lakers-Grizzlies" 36 | 37 | 2. **Primary Response** 38 | - Default sportsbook odds (Caesar's suggested) 39 | - Standard format: Spread, O/U, Money Line 40 | 41 | 3. **Secondary Options** 42 | - "Would you like to see odds from other sportsbooks?" 43 | - "Would you like to see betting trends for this game?" 44 | 45 | 4. **Advanced Data (On Request)** 46 | - Line movement information 47 | - Bet distribution (money vs. bets) 48 | 49 | ## 4. Implementation Priorities 50 | 51 | 1. Basic odds retrieval for major US sports (single sportsbook) 52 | 2. Multiple sportsbook comparison for same game/bet 53 | 3. Format optimization for AI chat interface 54 | 4. Line movement indicators (if time permits) 55 | 5. Bet distribution data (if available in your API) 56 | 57 | ## 5. Key Insights from Reagan 58 | 59 | - **Futures bets** have the most variation between sportsbooks and offer the best opportunity for value shopping 60 | - **Line movement** is a key indicator that bettors look for to identify potential value 61 | - **Bet distribution** (% of money vs. % of bets) can signal "sharp" money and is valuable information 62 | - **Layered information** is important - start with basic odds, then offer more detailed information 63 | - **User preferences** for which sportsbooks to display is important 64 | - **Anomalies** in odds or betting patterns can either attract or repel bettors depending on their strategy 65 | - **Default display** should be simple (spread, money line, over/under) with options to see more 66 | ``` -------------------------------------------------------------------------------- /old/docs/2025-02-10_pip_install_fix_plan.md: -------------------------------------------------------------------------------- ```markdown 1 | # Plan for Making Python Odds API Properly Pip-Installable 2 | 3 | After analyzing the current state of the Python Odds API package, I've identified several issues that prevent it from being properly installed and imported. Here's a comprehensive plan to fix these issues: 4 | 5 | ## 1. Diagnose Current Issues 6 | 7 | Currently, the package can be installed with `pip install -e .` but fails with an import error when running `run_test.py`. The main issues appear to be: 8 | 9 | - Package structure is not following modern Python packaging best practices 10 | - The installation process is working but the import path is not resolving correctly 11 | - The setup.py file needs modernization 12 | - There's a deprecation warning about legacy editable installs 13 | 14 | ## 2. Package Restructuring 15 | 16 | ### 2.1. Create a Modern Package Structure 17 | 18 | ``` 19 | api_test/wagyu_sports/ 20 | ├── pyproject.toml # New file for modern Python packaging 21 | ├── setup.py # Updated version 22 | ├── README.md # Existing file 23 | ├── requirements.txt # Existing file 24 | ├── wagyu_sports/ # Package directory (new or renamed) 25 | │ ├── __init__.py # Updated to expose the correct modules 26 | │ ├── odds_client.py # Main module 27 | │ └── utils.py # Utilities module 28 | └── tests/ # Separate test directory (new) 29 | ├── __init__.py 30 | ├── run_test.py # Moved from root 31 | └── test_odds_client.py # Existing test 32 | ``` 33 | 34 | ## 3. Update Package Files 35 | 36 | ### 3.1. Create a Modern pyproject.toml 37 | 38 | Create a new `pyproject.toml` file to use modern Python packaging tools (PEP 517/518) with: 39 | - Build system specification 40 | - Project metadata 41 | - Dependencies 42 | - Development dependencies 43 | 44 | ### 3.2. Update setup.py 45 | 46 | Modernize the existing setup.py: 47 | - Ensure package discovery works correctly 48 | - Update metadata 49 | - Fix URL and author information 50 | - Make sure dependencies match requirements.txt 51 | 52 | ### 3.3. Update __init__.py 53 | 54 | Ensure the package's `__init__.py` correctly exposes the intended classes and functions. 55 | 56 | ## 4. Fix Import Paths 57 | 58 | ### 4.1. Update Import Statements 59 | 60 | Ensure all import statements in the package are consistent with the new structure. 61 | 62 | ### 4.2. Update Test Scripts 63 | 64 | Modify test scripts to use the correct import paths. 65 | 66 | ## 5. Testing Plan 67 | 68 | ### 5.1. Test Installation 69 | 70 | Test the package installation in different scenarios: 71 | - Regular install: `pip install .` 72 | - Editable install: `pip install -e .` 73 | - Install from source distribution: `pip install dist/*.tar.gz` 74 | 75 | ### 5.2. Test Imports 76 | 77 | Verify imports work correctly: 78 | - In different Python environments 79 | - From different directories 80 | - With different import statements 81 | 82 | ### 5.3. Functional Testing 83 | 84 | Run the test scripts to ensure the client functions correctly: 85 | - Basic API functionality 86 | - Error handling 87 | - Edge cases 88 | 89 | ## 6. Documentation Updates 90 | 91 | ### 6.1. Update README 92 | 93 | Update installation and usage instructions in the README.md file. 94 | 95 | ### 6.2. Add Examples 96 | 97 | Provide clear examples of how to install and use the package. 98 | 99 | ## 7. Cleanup 100 | 101 | Remove any temporary files or old structures that are no longer needed. 102 | 103 | ## Implementation Timeline 104 | 105 | 1. Package restructuring (30 minutes) 106 | 2. Update configuration files (20 minutes) 107 | 3. Fix import paths (15 minutes) 108 | 4. Testing (20 minutes) 109 | 5. Documentation updates (15 minutes) 110 | 111 | ## Next Steps 112 | 113 | 1. Create the new directory structure 114 | 2. Create pyproject.toml 115 | 3. Update setup.py with modern configuration 116 | 4. Move and update test files 117 | 5. Test the installation process ``` -------------------------------------------------------------------------------- /wagyu_sports/examples/example.py: -------------------------------------------------------------------------------- ```python 1 | #!/usr/bin/env python3 2 | """ 3 | Example script demonstrating how to use the Wagyu Sports client. 4 | """ 5 | import os 6 | from dotenv import load_dotenv 7 | from wagyu_sports import OddsClient 8 | 9 | 10 | def main(): 11 | """ 12 | Main function demonstrating the use of the Wagyu Sports client. 13 | """ 14 | # Load environment variables 15 | load_dotenv(dotenv_path="config/.env") 16 | 17 | # Get API key 18 | api_key = os.getenv("ODDS_API_KEY") 19 | if not api_key: 20 | print("Error: ODDS_API_KEY not found in environment variables") 21 | print("Please copy config/.env.example to config/.env and add your API key: ODDS_API_KEY=your_api_key_here") 22 | return 23 | 24 | # Create client 25 | client = OddsClient(api_key) 26 | 27 | # Get available sports 28 | try: 29 | print("Fetching available sports...") 30 | sports_response = client.get_sports() 31 | 32 | print(f"\nAvailable sports: {len(sports_response['data'])}") 33 | print("Sample sports:") 34 | for sport in sports_response['data'][:5]: # Show first 5 sports 35 | print(f"- {sport.get('title', 'Unknown')}: {sport.get('key', 'Unknown')}") 36 | 37 | print(f"\nRemaining requests: {sports_response['headers']['x-requests-remaining']}") 38 | 39 | # Get a sport key for the next request 40 | sport_key = None 41 | for sport in sports_response['data']: 42 | if sport.get('title') == 'NBA': 43 | sport_key = sport.get('key') 44 | break 45 | 46 | if not sport_key: 47 | # If NBA is not found, use the first available sport 48 | sport_key = sports_response['data'][0].get('key') if sports_response['data'] else None 49 | 50 | if not sport_key: 51 | print("\nNo sports available to fetch odds") 52 | return 53 | 54 | # Get odds for the selected sport 55 | print(f"\nFetching odds for {sport_key}...") 56 | odds_options = { 57 | "regions": "us", 58 | "markets": "h2h", 59 | "oddsFormat": "decimal" 60 | } 61 | odds_response = client.get_odds(sport_key, odds_options) 62 | 63 | games_count = len(odds_response['data']) 64 | print(f"\nFetched odds for {games_count} games") 65 | 66 | if games_count > 0: 67 | print("\nSample games:") 68 | for game in odds_response['data'][:3]: # Show first 3 games 69 | home_team = game.get('home_team', 'Unknown') 70 | away_team = game.get('away_team', 'Unknown') 71 | commence_time = game.get('commence_time', 'Unknown') 72 | print(f"- {away_team} @ {home_team} (Start: {commence_time})") 73 | 74 | # Print sample odds from first bookmaker if available 75 | if game.get('bookmakers') and len(game.get('bookmakers')) > 0: 76 | bookmaker = game.get('bookmakers')[0] 77 | print(f" Odds from {bookmaker.get('title', 'Unknown')}:") 78 | 79 | for market in bookmaker.get('markets', []): 80 | if market.get('key') == 'h2h': 81 | print(" Moneyline:") 82 | for outcome in market.get('outcomes', []): 83 | print(f" {outcome.get('name', 'Unknown')}: {outcome.get('price', 'Unknown')}") 84 | 85 | print(f"\nRemaining requests: {odds_response['headers']['x-requests-remaining']}") 86 | 87 | except Exception as e: 88 | print(f"Error: {e}") 89 | 90 | 91 | if __name__ == "__main__": 92 | main() 93 | ``` -------------------------------------------------------------------------------- /wagyu_sports/mcp_server/capture_live_responses.py: -------------------------------------------------------------------------------- ```python 1 | #!/usr/bin/env python3 2 | """ 3 | Script to capture live API responses from the Odds API. 4 | 5 | This script makes live API calls and saves the responses to JSON files 6 | in the mocks_live directory. These captures can be used for creating 7 | updated mock data for testing. 8 | 9 | IMPORTANT: Each live API call costs money. Use sparingly. 10 | """ 11 | import os 12 | import sys 13 | import json 14 | import asyncio 15 | from pathlib import Path 16 | from datetime import datetime 17 | 18 | # Add parent directory to path to import from wagyu_sports 19 | sys.path.insert(0, str(Path(__file__).parent.parent.parent)) 20 | 21 | from wagyu_sports.odds_client import OddsClient 22 | 23 | async def capture_response(client, method_name, filename, **kwargs): 24 | """ 25 | Capture a response from the API and save it to a file. 26 | 27 | Args: 28 | client: The OddsClient instance 29 | method_name: The name of the method to call 30 | filename: The name of the file to save the response to 31 | **kwargs: Arguments to pass to the method 32 | """ 33 | print(f"Calling {method_name} with {kwargs}...") 34 | 35 | # Call the method 36 | method = getattr(client, method_name) 37 | result = method(**kwargs) 38 | 39 | # Add metadata 40 | response = { 41 | "_metadata": { 42 | "captured_at": datetime.now().isoformat(), 43 | "method": method_name, 44 | "parameters": kwargs 45 | }, 46 | "data": result 47 | } 48 | 49 | # Save to file 50 | output_path = Path(__file__).parent / "mocks_live" / filename 51 | with open(output_path, "w") as f: 52 | json.dump(response, f, indent=2) 53 | 54 | print(f"Response saved to {output_path}") 55 | return result 56 | 57 | async def main(): 58 | """Run the capture script.""" 59 | # Get API key from environment 60 | api_key = os.environ.get("ODDS_API_KEY") 61 | if not api_key: 62 | print("Error: ODDS_API_KEY environment variable is not set") 63 | sys.exit(1) 64 | 65 | print("=" * 80) 66 | print("Wagyu Sports Live API Response Capture") 67 | print("=" * 80) 68 | print() 69 | print("WARNING: This script makes live API calls that cost money.") 70 | print("Only run this script when necessary.") 71 | print() 72 | 73 | # Initialize client 74 | client = OddsClient(api_key) 75 | 76 | # Capture sports list 77 | await capture_response( 78 | client, 79 | "get_sports", 80 | "sports_list_live.json", 81 | all_sports=True 82 | ) 83 | 84 | # Capture NBA odds 85 | await capture_response( 86 | client, 87 | "get_odds", 88 | "nba_games_live.json", 89 | sport="basketball_nba", 90 | options={ 91 | "regions": "us", 92 | "markets": "h2h,spreads" 93 | } 94 | ) 95 | 96 | # Capture soccer odds 97 | await capture_response( 98 | client, 99 | "get_odds", 100 | "soccer_epl_live.json", 101 | sport="soccer_epl", 102 | options={ 103 | "regions": "us,uk", 104 | "markets": "h2h,spreads,totals" 105 | } 106 | ) 107 | 108 | # Print quota information 109 | print("\nQuota Information:") 110 | print(f"Remaining requests: {client.remaining_requests}") 111 | print(f"Used requests: {client.used_requests}") 112 | 113 | # Save quota information 114 | quota_info = { 115 | "_metadata": { 116 | "captured_at": datetime.now().isoformat() 117 | }, 118 | "remaining_requests": client.remaining_requests, 119 | "used_requests": client.used_requests 120 | } 121 | 122 | quota_path = Path(__file__).parent / "mocks_live" / "quota_info_live.json" 123 | with open(quota_path, "w") as f: 124 | json.dump(quota_info, f, indent=2) 125 | 126 | print(f"Quota information saved to {quota_path}") 127 | 128 | print("\nCapture complete!") 129 | 130 | if __name__ == "__main__": 131 | try: 132 | asyncio.run(main()) 133 | except KeyboardInterrupt: 134 | print("\nCapture stopped by user.") 135 | except Exception as e: 136 | print(f"\nError: {e}") 137 | ``` -------------------------------------------------------------------------------- /wagyu_sports/mcp_server/odds_client.py: -------------------------------------------------------------------------------- ```python 1 | #!/usr/bin/env python3 2 | """ 3 | Wagyu Sports Client Module 4 | 5 | This module provides a client for interacting with sports betting data APIs. 6 | """ 7 | import requests 8 | from typing import Dict, List, Optional, Any, Union 9 | 10 | 11 | class OddsClient: 12 | """ 13 | Client for sports betting data. 14 | 15 | This class provides methods for fetching sports betting data including 16 | available sports and odds for specific sports. 17 | """ 18 | 19 | BASE_URL = "https://api.the-odds-api.com/v4" 20 | 21 | def __init__(self, api_key: str): 22 | """ 23 | Initialize the Wagyu Sports client. 24 | 25 | Args: 26 | api_key (str): API key for authentication with The Odds API 27 | """ 28 | self.api_key = api_key 29 | self.remaining_requests = None 30 | self.used_requests = None 31 | 32 | def get_sports(self, all_sports: bool = False) -> Dict[str, Any]: 33 | """ 34 | Get a list of available sports. 35 | 36 | Args: 37 | all_sports (bool, optional): Include out-of-season sports. Defaults to False. 38 | 39 | Returns: 40 | Dict[str, Any]: Response containing available sports data 41 | 42 | Raises: 43 | requests.exceptions.RequestException: If the request fails 44 | """ 45 | params = {"apiKey": self.api_key} 46 | if all_sports: 47 | params["all"] = "true" 48 | 49 | return self.make_request("/sports", params) 50 | 51 | def get_odds(self, sport: str, options: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: 52 | """ 53 | Get odds for a specific sport. 54 | 55 | Args: 56 | sport (str): Sport key (e.g., 'basketball_nba') 57 | options (Dict[str, Any], optional): Additional options for the request. Defaults to None. 58 | Possible options include: 59 | - regions: Comma-separated list of regions (e.g., 'us,uk') 60 | - markets: Comma-separated list of markets (e.g., 'h2h,spreads') 61 | - oddsFormat: Format for odds ('decimal' or 'american') 62 | - dateFormat: Format for dates ('unix' or 'iso') 63 | 64 | Returns: 65 | Dict[str, Any]: Response containing odds data 66 | 67 | Raises: 68 | requests.exceptions.RequestException: If the request fails 69 | """ 70 | endpoint = f"/sports/{sport}/odds" 71 | params = {"apiKey": self.api_key} 72 | 73 | if options: 74 | params.update(options) 75 | 76 | return self.make_request(endpoint, params) 77 | 78 | def make_request(self, endpoint: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: 79 | """ 80 | Make a request to the sports data API. 81 | 82 | Args: 83 | endpoint (str): API endpoint (e.g., '/sports') 84 | params (Dict[str, Any], optional): Query parameters. Defaults to None. 85 | 86 | Returns: 87 | Dict[str, Any]: Response data 88 | 89 | Raises: 90 | requests.exceptions.RequestException: If the request fails 91 | """ 92 | url = f"{self.BASE_URL}{endpoint}" 93 | response = requests.get(url, params=params) 94 | 95 | # Store quota information from headers 96 | if 'x-requests-remaining' in response.headers: 97 | self.remaining_requests = response.headers['x-requests-remaining'] 98 | if 'x-requests-used' in response.headers: 99 | self.used_requests = response.headers['x-requests-used'] 100 | 101 | # Raise exception for error status codes 102 | response.raise_for_status() 103 | 104 | # Return JSON response 105 | return { 106 | "data": response.json(), 107 | "headers": { 108 | "x-requests-remaining": self.remaining_requests, 109 | "x-requests-used": self.used_requests 110 | } 111 | } 112 | ``` -------------------------------------------------------------------------------- /wagyu_sports/odds_client.py: -------------------------------------------------------------------------------- ```python 1 | #!/usr/bin/env python3 2 | """ 3 | Wagyu Sports Client Module 4 | 5 | This module provides a client for interacting with sports betting data APIs. 6 | """ 7 | import requests 8 | from typing import Dict, List, Optional, Any, Union 9 | 10 | 11 | class OddsClient: 12 | """ 13 | Client for sports betting data. 14 | 15 | This class provides methods for fetching sports betting data including 16 | available sports and odds for specific sports. 17 | """ 18 | 19 | BASE_URL = "https://api.the-odds-api.com/v4" 20 | 21 | def __init__(self, api_key: str): 22 | """ 23 | Initialize the Wagyu Sports client. 24 | 25 | Args: 26 | api_key (str): API key for authentication with The Odds API 27 | """ 28 | self.api_key = api_key 29 | self.remaining_requests = None 30 | self.used_requests = None 31 | 32 | def get_sports(self, all_sports: bool = False) -> Dict[str, Any]: 33 | """ 34 | Get a list of available sports. 35 | 36 | Args: 37 | all_sports (bool, optional): Include out-of-season sports. Defaults to False. 38 | 39 | Returns: 40 | Dict[str, Any]: Response containing available sports data 41 | 42 | Raises: 43 | requests.exceptions.RequestException: If the request fails 44 | """ 45 | params = {"apiKey": self.api_key} 46 | if all_sports: 47 | params["all"] = "true" 48 | 49 | return self.make_request("/sports", params) 50 | 51 | def get_odds(self, sport: str, options: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: 52 | """ 53 | Get odds for a specific sport. 54 | 55 | Args: 56 | sport (str): Sport key (e.g., 'basketball_nba') 57 | options (Dict[str, Any], optional): Additional options for the request. Defaults to None. 58 | Possible options include: 59 | - regions: Comma-separated list of regions (e.g., 'us,uk') 60 | - markets: Comma-separated list of markets (e.g., 'h2h,spreads') 61 | - oddsFormat: Format for odds ('decimal' or 'american') 62 | - dateFormat: Format for dates ('unix' or 'iso') 63 | 64 | Returns: 65 | Dict[str, Any]: Response containing odds data 66 | 67 | Raises: 68 | requests.exceptions.RequestException: If the request fails 69 | """ 70 | endpoint = f"/sports/{sport}/odds" 71 | params = {"apiKey": self.api_key} 72 | 73 | if options: 74 | params.update(options) 75 | 76 | return self.make_request(endpoint, params) 77 | 78 | def make_request(self, endpoint: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: 79 | """ 80 | Make a request to the sports data API. 81 | 82 | Args: 83 | endpoint (str): API endpoint (e.g., '/sports') 84 | params (Dict[str, Any], optional): Query parameters. Defaults to None. 85 | 86 | Returns: 87 | Dict[str, Any]: Response data 88 | 89 | Raises: 90 | requests.exceptions.RequestException: If the request fails 91 | """ 92 | url = f"{self.BASE_URL}{endpoint}" 93 | response = requests.get(url, params=params) 94 | 95 | # Store quota information from headers 96 | if 'x-requests-remaining' in response.headers: 97 | self.remaining_requests = response.headers['x-requests-remaining'] 98 | if 'x-requests-used' in response.headers: 99 | self.used_requests = response.headers['x-requests-used'] 100 | 101 | # Raise exception for error status codes 102 | response.raise_for_status() 103 | 104 | # Return JSON response 105 | return { 106 | "data": response.json(), 107 | "headers": { 108 | "x-requests-remaining": self.remaining_requests, 109 | "x-requests-used": self.used_requests 110 | } 111 | } 112 | ``` -------------------------------------------------------------------------------- /old/espn_nonbetting_api/espn_api_endpoints.md: -------------------------------------------------------------------------------- ```markdown 1 | # ESPN's hidden API endpoints 2 | 3 | ## Football 4 | 5 | ### College Football 6 | 7 | **Latest News**: http://site.api.espn.com/apis/site/v2/sports/football/college-football/news 8 | 9 | **Latest Scores**: http://site.api.espn.com/apis/site/v2/sports/football/college-football/scoreboard 10 | 11 | - query params: 12 | 13 | - calendar: 'blacklist' 14 | - dates: any date in YYYYMMDD 15 | 16 | **Game Information**: http://site.api.espn.com/apis/site/v2/sports/football/college-football/summary?event=:gameId 17 | 18 | - params: 19 | 20 | - gameId: identifier of some game (EX: 400934572 for 2017 Army vs Navy) 21 | 22 | **Team Information**: http://site.api.espn.com/apis/site/v2/sports/football/college-football/teams/:team 23 | 24 | - params: 25 | 26 | - team: some team abbreviation (EX: 'all' for Allegheny, 'gt' for Georgia Tech, 'wisconsin' for Wisconsin) 27 | 28 | **Rankings**: http://site.api.espn.com/apis/site/v2/sports/football/college-football/rankings 29 | 30 | ### NFL 31 | 32 | **Scores**: http://site.api.espn.com/apis/site/v2/sports/football/nfl/scoreboard 33 | 34 | **News**: http://site.api.espn.com/apis/site/v2/sports/football/nfl/news 35 | 36 | **All Teams**: http://site.api.espn.com/apis/site/v2/sports/football/nfl/teams 37 | 38 | **Specific Team**: http://site.api.espn.com/apis/site/v2/sports/football/nfl/teams/:team 39 | 40 | 41 | ## Baseball 42 | 43 | ### MLB 44 | 45 | **Scores**: http://site.api.espn.com/apis/site/v2/sports/baseball/mlb/scoreboard 46 | 47 | **News**: http://site.api.espn.com/apis/site/v2/sports/baseball/mlb/news 48 | 49 | **All Teams**: http://site.api.espn.com/apis/site/v2/sports/baseball/mlb/teams 50 | 51 | **Specific Team**: http://site.api.espn.com/apis/site/v2/sports/baseball/mlb/teams/:team 52 | 53 | ### College Baseball 54 | 55 | **Scores**: https://site.api.espn.com/apis/site/v2/sports/baseball/college-baseball/scoreboard 56 | 57 | ## Hockey 58 | 59 | **Scores**: http://site.api.espn.com/apis/site/v2/sports/hockey/nhl/scoreboard 60 | 61 | **News**: http://site.api.espn.com/apis/site/v2/sports/hockey/nhl/news 62 | 63 | **All Teams**: http://site.api.espn.com/apis/site/v2/sports/hockey/nhl/teams 64 | 65 | **Specific Team**: http://site.api.espn.com/apis/site/v2/sports/hockey/nhl/teams/:team 66 | 67 | 68 | ## Basketball 69 | 70 | ### NBA 71 | 72 | **Scores**: http://site.api.espn.com/apis/site/v2/sports/basketball/nba/scoreboard 73 | 74 | **News**: http://site.api.espn.com/apis/site/v2/sports/basketball/nba/news 75 | 76 | **All Teams**: http://site.api.espn.com/apis/site/v2/sports/basketball/nba/teams 77 | 78 | **Specific Team**: http://site.api.espn.com/apis/site/v2/sports/basketball/nba/teams/:team 79 | 80 | 81 | ### WNBA 82 | 83 | **Scores**: http://site.api.espn.com/apis/site/v2/sports/basketball/wnba/scoreboard 84 | 85 | **News**: http://site.api.espn.com/apis/site/v2/sports/basketball/wnba/news 86 | 87 | **All Teams**: http://site.api.espn.com/apis/site/v2/sports/basketball/wnba/teams 88 | 89 | **Specific Team**: http://site.api.espn.com/apis/site/v2/sports/basketball/wnba/teams/:team 90 | 91 | 92 | ### Women's College Basketball 93 | 94 | **Scores**: http://site.api.espn.com/apis/site/v2/sports/basketball/womens-college-basketball/scoreboard 95 | 96 | **News**: http://site.api.espn.com/apis/site/v2/sports/basketball/womens-college-basketball/news 97 | 98 | **All Teams**: http://site.api.espn.com/apis/site/v2/sports/basketball/womens-college-basketball/teams 99 | 100 | **Specific Team**: http://site.api.espn.com/apis/site/v2/sports/basketball/womens-college-basketball/teams/:team 101 | 102 | 103 | ### Men's College Basketball 104 | 105 | **Scores**: http://site.api.espn.com/apis/site/v2/sports/basketball/mens-college-basketball/scoreboard 106 | 107 | **News**: http://site.api.espn.com/apis/site/v2/sports/basketball/mens-college-basketball/news 108 | 109 | **All Teams**: http://site.api.espn.com/apis/site/v2/sports/basketball/mens-college-basketball/teams 110 | 111 | **Specific Team**: http://site.api.espn.com/apis/site/v2/sports/basketball/mens-college-basketball/teams/:team 112 | 113 | 114 | 115 | ## Soccer 116 | 117 | **Scores**: http://site.api.espn.com/apis/site/v2/sports/soccer/:league/scoreboard 118 | 119 | - params: 120 | 121 | - league: some league abbreviation (EX: 'eng.1' for EPL, 'usa.1' for MLS) 122 | 123 | **Latest News**: http://site.api.espn.com/apis/site/v2/sports/soccer/:league/news 124 | 125 | **List of Team Information**: http://site.api.espn.com/apis/site/v2/sports/soccer/:league/teams 126 | 127 | 128 | Will update with more information as I find more... 129 | ``` -------------------------------------------------------------------------------- /wagyu_sports/utils.py: -------------------------------------------------------------------------------- ```python 1 | #!/usr/bin/env python3 2 | """ 3 | Utility functions for working with the Wagyu Sports client. 4 | """ 5 | import os 6 | import json 7 | import glob 8 | from typing import Dict, Any, Optional, Tuple 9 | from pathlib import Path 10 | from dotenv import load_dotenv 11 | 12 | from .odds_client import OddsClient 13 | 14 | 15 | def get_next_test_number() -> int: 16 | """ 17 | Get the next sequential test number for the output directory. 18 | 19 | This function looks at existing test directories and returns the next number in sequence. 20 | 21 | Returns: 22 | int: Next test number 23 | """ 24 | # Define the base directory for test outputs 25 | base_dir = os.path.join(os.getcwd(), "test_outputs") 26 | 27 | # Ensure the directory exists 28 | os.makedirs(base_dir, exist_ok=True) 29 | 30 | # Find all test directories 31 | test_dirs = glob.glob(os.path.join(base_dir, "test*")) 32 | 33 | if not test_dirs: 34 | return 1 35 | 36 | # Extract numbers from directory names 37 | numbers = [] 38 | for dir_path in test_dirs: 39 | dir_name = os.path.basename(dir_path) 40 | try: 41 | # Extract number from "test{number}" 42 | num = int(dir_name.replace("test", "")) 43 | numbers.append(num) 44 | except ValueError: 45 | continue 46 | 47 | # Return next number in sequence 48 | return max(numbers) + 1 if numbers else 1 49 | 50 | 51 | def save_response(filename: str, data: Dict[str, Any], test_number: Optional[int] = None) -> str: 52 | """ 53 | Save API response to a JSON file. 54 | 55 | Args: 56 | filename (str): Name of the file to save 57 | data (Dict[str, Any]): Data to save 58 | test_number (int, optional): Test number for directory. If None, uses next available. 59 | 60 | Returns: 61 | str: Path to the saved file 62 | """ 63 | # Get test number if not provided 64 | if test_number is None: 65 | test_number = get_next_test_number() 66 | 67 | # Define directory path 68 | dir_path = os.path.join(os.getcwd(), "test_outputs", f"test{test_number}") 69 | 70 | # Ensure directory exists 71 | os.makedirs(dir_path, exist_ok=True) 72 | 73 | # Define file path 74 | file_path = os.path.join(dir_path, filename) 75 | 76 | # Save data to file 77 | with open(file_path, 'w') as f: 78 | json.dump(data, f, indent=2) 79 | 80 | return file_path 81 | 82 | 83 | def test_wagyu_sports() -> Tuple[str, str]: 84 | """ 85 | Example function that demonstrates full API workflow. 86 | 87 | This function: 88 | 1. Loads the API key from environment variables 89 | 2. Creates an OddsClient instance 90 | 3. Fetches available sports 91 | 4. Fetches NBA odds 92 | 5. Saves responses to files 93 | 94 | Returns: 95 | Tuple[str, str]: Paths to the saved files (sports, odds) 96 | """ 97 | # Load environment variables 98 | load_dotenv(dotenv_path="config/.env") 99 | 100 | # Get API key 101 | api_key = os.getenv("ODDS_API_KEY") 102 | if not api_key: 103 | raise ValueError("ODDS_API_KEY not found in environment variables") 104 | 105 | # Create client 106 | client = OddsClient(api_key) 107 | 108 | # Get test number 109 | test_number = get_next_test_number() 110 | 111 | # Get available sports 112 | try: 113 | sports_response = client.get_sports() 114 | sports_file = save_response("1_available_sports.json", sports_response, test_number) 115 | print(f"Available sports saved to {sports_file}") 116 | print(f"Remaining requests: {sports_response['headers']['x-requests-remaining']}") 117 | except Exception as e: 118 | print(f"Error fetching sports: {e}") 119 | return "", "" 120 | 121 | # Get NBA odds 122 | try: 123 | odds_options = { 124 | "regions": "us", 125 | "markets": "h2h,spreads", 126 | "oddsFormat": "american" 127 | } 128 | odds_response = client.get_odds("basketball_nba", odds_options) 129 | odds_file = save_response("2_nba_odds.json", odds_response, test_number) 130 | print(f"NBA odds saved to {odds_file}") 131 | print(f"Remaining requests: {odds_response['headers']['x-requests-remaining']}") 132 | except Exception as e: 133 | print(f"Error fetching NBA odds: {e}") 134 | return sports_file, "" 135 | 136 | return sports_file, odds_file 137 | 138 | 139 | if __name__ == "__main__": 140 | # Run the test if this file is executed directly 141 | test_wagyu_sports() 142 | ``` -------------------------------------------------------------------------------- /wagyu_sports/tests/test_odds_api.py: -------------------------------------------------------------------------------- ```python 1 | #!/usr/bin/env python3 2 | """ 3 | Tests for the Wagyu Sports client. 4 | 5 | This file contains all tests for the Wagyu Sports client, including: 6 | - Unit tests for the OddsClient class 7 | - Import verification 8 | - Installation verification 9 | """ 10 | import os 11 | import sys 12 | import pytest 13 | from unittest.mock import patch, MagicMock 14 | import importlib.util 15 | 16 | # Add the parent directory to the path so we can import the package 17 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) 18 | 19 | # Import the client 20 | from wagyu_sports import OddsClient 21 | from dotenv import load_dotenv 22 | 23 | 24 | def test_import(): 25 | """Test that the OddsClient can be imported.""" 26 | # If we got this far, the import worked 27 | assert OddsClient is not None 28 | 29 | # Try creating an instance 30 | client = OddsClient("test_key") 31 | assert client is not None 32 | assert client.api_key == "test_key" 33 | 34 | 35 | def test_dependencies(): 36 | """Test that required dependencies are installed.""" 37 | dependencies = ["requests", "dotenv"] 38 | 39 | for dep in dependencies: 40 | spec = importlib.util.find_spec(dep) 41 | assert spec is not None, f"{dep} is not installed" 42 | 43 | 44 | @pytest.fixture 45 | def client(): 46 | """Fixture to create an OddsClient instance.""" 47 | return OddsClient("test_api_key") 48 | 49 | 50 | @patch('requests.get') 51 | def test_get_sports(mock_get, client): 52 | """Test the get_sports method.""" 53 | # Mock response 54 | mock_response = MagicMock() 55 | mock_response.json.return_value = [ 56 | {"key": "sport1", "title": "Sport 1"}, 57 | {"key": "sport2", "title": "Sport 2"} 58 | ] 59 | mock_response.headers = { 60 | 'x-requests-remaining': '100', 61 | 'x-requests-used': '5' 62 | } 63 | mock_get.return_value = mock_response 64 | 65 | # Call the method 66 | result = client.get_sports() 67 | 68 | # Verify the request 69 | mock_get.assert_called_once_with( 70 | 'https://api.the-odds-api.com/v4/sports', 71 | params={'apiKey': 'test_api_key'} 72 | ) 73 | 74 | # Verify the result 75 | assert result['data'] == mock_response.json.return_value 76 | assert result['headers']['x-requests-remaining'] == '100' 77 | assert result['headers']['x-requests-used'] == '5' 78 | 79 | 80 | @patch('requests.get') 81 | def test_get_odds(mock_get, client): 82 | """Test the get_odds method.""" 83 | # Mock response 84 | mock_response = MagicMock() 85 | mock_response.json.return_value = [ 86 | { 87 | "id": "game1", 88 | "home_team": "Team A", 89 | "away_team": "Team B", 90 | "bookmakers": [] 91 | } 92 | ] 93 | mock_response.headers = { 94 | 'x-requests-remaining': '99', 95 | 'x-requests-used': '6' 96 | } 97 | mock_get.return_value = mock_response 98 | 99 | # Options for the request 100 | options = { 101 | "regions": "us", 102 | "markets": "h2h", 103 | "oddsFormat": "decimal" 104 | } 105 | 106 | # Call the method 107 | result = client.get_odds("basketball_nba", options) 108 | 109 | # Verify the request 110 | expected_params = {'apiKey': 'test_api_key'} 111 | expected_params.update(options) 112 | mock_get.assert_called_once_with( 113 | 'https://api.the-odds-api.com/v4/sports/basketball_nba/odds', 114 | params=expected_params 115 | ) 116 | 117 | # Verify the result 118 | assert result['data'] == mock_response.json.return_value 119 | assert result['headers']['x-requests-remaining'] == '99' 120 | assert result['headers']['x-requests-used'] == '6' 121 | 122 | 123 | @patch('requests.get') 124 | def test_make_request_error(mock_get, client): 125 | """Test error handling in make_request method.""" 126 | # Mock response with error 127 | mock_response = MagicMock() 128 | mock_response.raise_for_status.side_effect = Exception("API Error") 129 | mock_get.return_value = mock_response 130 | 131 | # Call the method and expect exception 132 | with pytest.raises(Exception): 133 | client.make_request("/test") 134 | 135 | # Verify the request was made 136 | mock_get.assert_called_once_with( 137 | 'https://api.the-odds-api.com/v4/test', 138 | params=None 139 | ) 140 | 141 | 142 | def test_api_key_env(): 143 | """Test that the API key can be loaded from environment variables.""" 144 | # Load environment variables from .env file 145 | load_dotenv(dotenv_path="config/.env") 146 | 147 | # Get API key 148 | api_key = os.getenv("ODDS_API_KEY") 149 | if not api_key: 150 | pytest.skip("ODDS_API_KEY not set in environment") 151 | 152 | # If we have an API key, create a client and verify it works 153 | # Note: This test only verifies the API key is loaded correctly, 154 | # it does NOT make any actual API calls 155 | client = OddsClient(api_key) 156 | assert client.api_key == api_key 157 | ``` -------------------------------------------------------------------------------- /wagyu_sports/tests/test_odds_mcp_server.py: -------------------------------------------------------------------------------- ```python 1 | """Tests for Wagyu Sports MCP server""" 2 | 3 | import os 4 | import sys 5 | import pytest 6 | import json 7 | 8 | # Add the parent directory to the path so we can import the package 9 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) 10 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../..'))) 11 | 12 | from mcp.shared.memory import ( 13 | create_connected_server_and_client_session as client_session, 14 | ) 15 | from mcp.types import TextContent, TextResourceContents 16 | 17 | from wagyu_sports.mcp_server.odds_client_server import OddsMcpServer 18 | 19 | 20 | @pytest.mark.anyio 21 | async def test_get_sports(): 22 | """Test the get_sports tool""" 23 | server = OddsMcpServer(test_mode=True) 24 | 25 | async with client_session(server.server) as client: 26 | result = await client.call_tool("get_sports", {}) 27 | assert len(result.content) == 1 28 | content = result.content[0] 29 | assert isinstance(content, TextContent) 30 | 31 | # Parse the JSON response 32 | response_data = json.loads(content.text) 33 | 34 | # Check that the response contains expected data 35 | assert "data" in response_data 36 | assert isinstance(response_data["data"], list) 37 | 38 | # Check for a specific sport (basketball_nba should be in the mock data) 39 | basketball_nba = next((s for s in response_data["data"] if s["key"] == "basketball_nba"), None) 40 | assert basketball_nba is not None 41 | assert basketball_nba["title"] == "NBA" 42 | 43 | 44 | @pytest.mark.anyio 45 | async def test_get_odds(): 46 | """Test the get_odds tool""" 47 | server = OddsMcpServer(test_mode=True) 48 | 49 | async with client_session(server.server) as client: 50 | result = await client.call_tool("get_odds", {"sport": "basketball_nba"}) 51 | assert len(result.content) == 1 52 | content = result.content[0] 53 | assert isinstance(content, TextContent) 54 | 55 | # Parse the JSON response 56 | response_data = json.loads(content.text) 57 | 58 | # Check that the response contains expected data 59 | assert "data" in response_data 60 | assert isinstance(response_data["data"], list) 61 | 62 | # Check that at least one game is returned 63 | assert len(response_data["data"]) > 0 64 | 65 | # Check that the first game has the expected fields 66 | first_game = response_data["data"][0] 67 | assert "sport_key" in first_game 68 | assert first_game["sport_key"] == "basketball_nba" 69 | assert "home_team" in first_game 70 | assert "away_team" in first_game 71 | assert "bookmakers" in first_game 72 | 73 | 74 | @pytest.mark.anyio 75 | async def test_get_odds_with_options(): 76 | """Test the get_odds tool with options""" 77 | server = OddsMcpServer(test_mode=True) 78 | 79 | async with client_session(server.server) as client: 80 | result = await client.call_tool( 81 | "get_odds", 82 | { 83 | "sport": "soccer_epl", 84 | "regions": "us", 85 | "markets": "h2h,spreads", 86 | "odds_format": "american", 87 | "date_format": "iso" 88 | } 89 | ) 90 | assert len(result.content) == 1 91 | content = result.content[0] 92 | assert isinstance(content, TextContent) 93 | 94 | # Parse the JSON response 95 | response_data = json.loads(content.text) 96 | 97 | # Check that the response contains expected data 98 | assert "data" in response_data 99 | assert isinstance(response_data["data"], list) 100 | 101 | # Check that at least one game is returned 102 | assert len(response_data["data"]) > 0 103 | 104 | # Check that the first game has the expected fields 105 | first_game = response_data["data"][0] 106 | assert "sport_key" in first_game 107 | assert first_game["sport_key"] == "soccer_epl" 108 | 109 | 110 | @pytest.mark.anyio 111 | async def test_get_quota_info(): 112 | """Test the get_quota_info tool""" 113 | server = OddsMcpServer(test_mode=True) 114 | 115 | async with client_session(server.server) as client: 116 | result = await client.call_tool("get_quota_info", {}) 117 | assert len(result.content) == 1 118 | content = result.content[0] 119 | assert isinstance(content, TextContent) 120 | 121 | # Parse the JSON response 122 | response_data = json.loads(content.text) 123 | 124 | # Check that the response contains expected fields 125 | assert "remaining_requests" in response_data 126 | assert "used_requests" in response_data 127 | 128 | 129 | if __name__ == "__main__": 130 | pytest.main(["-xvs", __file__]) 131 | ``` -------------------------------------------------------------------------------- /wagyu_sports/examples/advanced_example.py: -------------------------------------------------------------------------------- ```python 1 | #!/usr/bin/env python3 2 | """ 3 | Advanced example script demonstrating more complex usage of the Wagyu Sports client. 4 | 5 | This example shows: 6 | 1. Error handling 7 | 2. Handling API quota limits 8 | 3. Fetching multiple sports and odds 9 | 4. Filtering and processing data 10 | """ 11 | import os 12 | import time 13 | from datetime import datetime, timedelta 14 | from dotenv import load_dotenv 15 | import requests 16 | 17 | from wagyu_sports import OddsClient 18 | 19 | 20 | def format_datetime(dt_str): 21 | """Format ISO datetime string to a more readable format.""" 22 | try: 23 | dt = datetime.fromisoformat(dt_str.replace('Z', '+00:00')) 24 | return dt.strftime('%Y-%m-%d %H:%M:%S') 25 | except (ValueError, AttributeError): 26 | return dt_str 27 | 28 | 29 | def get_upcoming_games(client, sport_key, hours=24): 30 | """ 31 | Get upcoming games for a sport within the next X hours. 32 | 33 | Args: 34 | client (OddsClient): The Wagyu Sports client 35 | sport_key (str): Sport key (e.g., 'basketball_nba') 36 | hours (int): Number of hours to look ahead 37 | 38 | Returns: 39 | list: List of upcoming games 40 | """ 41 | try: 42 | # Get odds with options 43 | options = { 44 | "regions": "us", 45 | "markets": "h2h", 46 | "oddsFormat": "decimal", 47 | "dateFormat": "iso" 48 | } 49 | 50 | response = client.get_odds(sport_key, options) 51 | 52 | # Calculate cutoff time 53 | now = datetime.now() 54 | cutoff = now + timedelta(hours=hours) 55 | 56 | # Filter games by commence time 57 | upcoming_games = [] 58 | for game in response['data']: 59 | if 'commence_time' in game: 60 | try: 61 | game_time = datetime.fromisoformat( 62 | game['commence_time'].replace('Z', '+00:00') 63 | ) 64 | if now <= game_time <= cutoff: 65 | upcoming_games.append(game) 66 | except (ValueError, TypeError): 67 | # Skip games with invalid datetime 68 | continue 69 | 70 | return upcoming_games, response['headers']['x-requests-remaining'] 71 | 72 | except requests.exceptions.RequestException as e: 73 | print(f"Error fetching odds for {sport_key}: {e}") 74 | return [], None 75 | 76 | 77 | def find_best_odds(games): 78 | """ 79 | Find the best odds for each team across all bookmakers. 80 | 81 | Args: 82 | games (list): List of games with odds 83 | 84 | Returns: 85 | dict: Dictionary mapping team names to their best odds 86 | """ 87 | best_odds = {} 88 | 89 | for game in games: 90 | home_team = game.get('home_team') 91 | away_team = game.get('away_team') 92 | 93 | if not home_team or not away_team: 94 | continue 95 | 96 | # Initialize best odds for teams if not already present 97 | if home_team not in best_odds: 98 | best_odds[home_team] = 0 99 | if away_team not in best_odds: 100 | best_odds[away_team] = 0 101 | 102 | # Check all bookmakers 103 | for bookmaker in game.get('bookmakers', []): 104 | for market in bookmaker.get('markets', []): 105 | if market.get('key') == 'h2h': 106 | for outcome in market.get('outcomes', []): 107 | team = outcome.get('name') 108 | price = outcome.get('price') 109 | 110 | if team and price and team in best_odds: 111 | best_odds[team] = max(best_odds[team], price) 112 | 113 | return best_odds 114 | 115 | 116 | def main(): 117 | """Main function demonstrating advanced usage.""" 118 | # Load environment variables 119 | load_dotenv(dotenv_path="config/.env") 120 | 121 | # Get API key 122 | api_key = os.getenv("ODDS_API_KEY") 123 | if not api_key: 124 | print("Error: ODDS_API_KEY not found in environment variables") 125 | print("Please copy config/.env.example to config/.env and add your API key: ODDS_API_KEY=your_api_key_here") 126 | return 127 | 128 | # Create client 129 | client = OddsClient(api_key) 130 | 131 | try: 132 | # Get available sports 133 | print("Fetching available sports...") 134 | sports_response = client.get_sports() 135 | 136 | # Check if we have sports data 137 | if not sports_response['data']: 138 | print("No sports data available") 139 | return 140 | 141 | print(f"Found {len(sports_response['data'])} sports") 142 | print(f"Remaining requests: {sports_response['headers']['x-requests-remaining']}") 143 | 144 | # Get active sports (in-season) 145 | active_sports = [] 146 | for sport in sports_response['data']: 147 | if sport.get('active'): 148 | active_sports.append(sport) 149 | 150 | print(f"Found {len(active_sports)} active sports") 151 | 152 | # Process top 3 active sports 153 | for i, sport in enumerate(active_sports[:3]): 154 | sport_key = sport.get('key') 155 | sport_title = sport.get('title') 156 | 157 | print(f"\nProcessing {sport_title} ({sport_key})...") 158 | 159 | # Get upcoming games 160 | upcoming_games, remaining = get_upcoming_games(client, sport_key, hours=48) 161 | 162 | if not upcoming_games: 163 | print(f"No upcoming games found for {sport_title}") 164 | continue 165 | 166 | print(f"Found {len(upcoming_games)} upcoming games in the next 48 hours") 167 | 168 | # Find best odds 169 | best_odds = find_best_odds(upcoming_games) 170 | 171 | # Display results 172 | print(f"\nUpcoming {sport_title} games:") 173 | for game in upcoming_games[:5]: # Show up to 5 games 174 | home = game.get('home_team', 'Unknown') 175 | away = game.get('away_team', 'Unknown') 176 | time_str = format_datetime(game.get('commence_time', '')) 177 | print(f"- {away} @ {home} (Start: {time_str})") 178 | 179 | # Show bookmakers 180 | if game.get('bookmakers'): 181 | print(f" Available at {len(game['bookmakers'])} bookmakers") 182 | 183 | print(f"\nBest odds for {sport_title} teams:") 184 | sorted_odds = sorted(best_odds.items(), key=lambda x: x[1], reverse=True) 185 | for team, odds in sorted_odds[:5]: # Show top 5 teams by odds 186 | print(f"- {team}: {odds:.2f}") 187 | 188 | # Check remaining requests 189 | if remaining and int(remaining) < 10: 190 | print(f"\nWarning: Only {remaining} API requests remaining") 191 | print("Waiting 5 seconds before next request to avoid rate limiting...") 192 | time.sleep(5) 193 | 194 | print(f"Remaining requests: {remaining}") 195 | 196 | except Exception as e: 197 | print(f"Error: {e}") 198 | 199 | 200 | if __name__ == "__main__": 201 | main() 202 | ``` -------------------------------------------------------------------------------- /wagyu_sports/mcp_server/odds_client_server.py: -------------------------------------------------------------------------------- ```python 1 | #!/usr/bin/env python3 2 | """ 3 | Wagyu Sports MCP Server Implementation 4 | 5 | This module provides an MCP server that exposes the Wagyu Sports API 6 | functionality through the Model Context Protocol. 7 | """ 8 | import os 9 | import sys 10 | import json 11 | import asyncio 12 | from typing import Dict, Any, Optional, List, Union 13 | from pathlib import Path 14 | 15 | from mcp.server.fastmcp import FastMCP 16 | from mcp.server.stdio import stdio_server 17 | import mcp.types as types 18 | 19 | try: 20 | # When imported as a package 21 | from .odds_client import OddsClient 22 | except ImportError: 23 | # When run directly 24 | from odds_client import OddsClient 25 | 26 | class OddsMcpServer: 27 | """MCP server for Wagyu Sports odds API.""" 28 | 29 | def __init__(self, api_key: Optional[str] = None, test_mode: bool = False): 30 | """ 31 | Initialize the MCP server. 32 | 33 | Args: 34 | api_key (str, optional): API key for the Odds API. If not provided, 35 | will try to get from environment variable. 36 | test_mode (bool): Whether to use mock data instead of real API calls. 37 | """ 38 | # Get API key from environment if not provided 39 | self.api_key = api_key or os.environ.get("ODDS_API_KEY") 40 | if not self.api_key and not test_mode: 41 | raise ValueError("API key is required when not in test mode") 42 | 43 | self.test_mode = test_mode 44 | self.mock_data_dir = Path(__file__).parent / "mocks_live" 45 | 46 | # Initialize client 47 | self.client = OddsClient(self.api_key) if not test_mode else None 48 | 49 | # Initialize server with FastMCP 50 | self.server = FastMCP("wagyu-sports-mcp") 51 | 52 | # Register tools 53 | self.register_tools() 54 | 55 | def register_tools(self): 56 | """Register MCP tools.""" 57 | 58 | @self.server.tool() 59 | async def get_sports(all_sports: bool = False, use_test_mode: Optional[bool] = None) -> str: 60 | """ 61 | Get a list of available sports. 62 | 63 | Args: 64 | all_sports: Include out-of-season sports if True 65 | use_test_mode: Override server test_mode setting (True for mock data, False for real API) 66 | 67 | Returns: 68 | JSON string with sports data 69 | """ 70 | # Determine if we should use test mode 71 | test_mode = use_test_mode if use_test_mode is not None else self.test_mode 72 | 73 | if test_mode: 74 | return await self._get_mock_data("sports_list_live.json") 75 | 76 | result = self.client.get_sports(all_sports=all_sports) 77 | return json.dumps(result, indent=2) 78 | 79 | @self.server.tool() 80 | async def get_odds(sport: str, regions: Optional[str] = None, 81 | markets: Optional[str] = None, 82 | odds_format: Optional[str] = None, 83 | date_format: Optional[str] = None, 84 | use_test_mode: Optional[bool] = None) -> str: 85 | """ 86 | Get odds for a specific sport. 87 | 88 | Args: 89 | sport: Sport key (e.g., 'basketball_nba') 90 | regions: Comma-separated list of regions (e.g., 'us,uk') 91 | markets: Comma-separated list of markets (e.g., 'h2h,spreads') 92 | odds_format: Format for odds ('decimal' or 'american') 93 | date_format: Format for dates ('unix' or 'iso') 94 | use_test_mode: Override server test_mode setting (True for mock data, False for real API) 95 | 96 | Returns: 97 | JSON string with odds data 98 | """ 99 | # Determine if we should use test mode 100 | test_mode = use_test_mode if use_test_mode is not None else self.test_mode 101 | 102 | if test_mode: 103 | if sport == "basketball_nba": 104 | return await self._get_mock_data("nba_games_live.json") 105 | # Fall back to nba_games_live.json since we don't have a live version of game_odds_all_books.json 106 | return await self._get_mock_data("nba_games_live.json") 107 | 108 | options = {} 109 | if regions: 110 | options["regions"] = regions 111 | if markets: 112 | options["markets"] = markets 113 | if odds_format: 114 | options["oddsFormat"] = odds_format 115 | if date_format: 116 | options["dateFormat"] = date_format 117 | 118 | result = self.client.get_odds(sport, options=options) 119 | return json.dumps(result, indent=2) 120 | 121 | @self.server.tool() 122 | async def get_quota_info(use_test_mode: Optional[bool] = None) -> str: 123 | """ 124 | Get API quota information. 125 | 126 | Args: 127 | use_test_mode: Override server test_mode setting (True for mock data, False for real API) 128 | 129 | Returns: 130 | JSON string with quota information 131 | """ 132 | # Determine if we should use test mode 133 | test_mode = use_test_mode if use_test_mode is not None else self.test_mode 134 | 135 | if test_mode: 136 | return await self._get_mock_data("quota_info_live.json") 137 | 138 | return json.dumps({ 139 | "remaining_requests": self.client.remaining_requests, 140 | "used_requests": self.client.used_requests 141 | }, indent=2) 142 | 143 | async def _get_mock_data(self, filename: str) -> str: 144 | """ 145 | Get mock data from a JSON file. 146 | 147 | Args: 148 | filename: Name of the mock data file 149 | 150 | Returns: 151 | JSON string with mock data 152 | """ 153 | try: 154 | mock_file = self.mock_data_dir / filename 155 | if not mock_file.exists(): 156 | return json.dumps({"error": f"Mock file {filename} not found"}) 157 | 158 | with open(mock_file, "r") as f: 159 | data = json.load(f) 160 | 161 | return json.dumps(data, indent=2) 162 | except Exception as e: 163 | return json.dumps({"error": f"Error loading mock data: {str(e)}"}) 164 | 165 | async def run(self): 166 | """Run the MCP server.""" 167 | # FastMCP has a different API for running the server 168 | # We need to use the run_stdio_async method directly 169 | await self.server.run_stdio_async() 170 | 171 | def main(): 172 | """Run the MCP server as a standalone process.""" 173 | # Parse arguments 174 | import argparse 175 | parser = argparse.ArgumentParser(description="Wagyu Sports MCP Server") 176 | parser.add_argument("--api-key", help="API key for the Odds API") 177 | parser.add_argument("--test-mode", action="store_true", help="Use mock data instead of real API calls") 178 | args = parser.parse_args() 179 | 180 | # Create and run server 181 | server = OddsMcpServer(api_key=args.api_key, test_mode=args.test_mode) 182 | asyncio.run(server.run()) 183 | 184 | if __name__ == "__main__": 185 | main() 186 | ``` -------------------------------------------------------------------------------- /old/docs/2025-01-10_odds_api_v4.md: -------------------------------------------------------------------------------- ```markdown 1 | # The Odds API v4 Documentation 2 | 3 | ## Overview 4 | The Odds API provides comprehensive access to sports betting data, including live odds, scores, and event information from multiple bookmakers worldwide. This document serves as a detailed reference for the API's capabilities and integration points. 5 | 6 | ## Host 7 | - Primary: `https://api.the-odds-api.com` 8 | - IPv6: `https://ipv6-api.the-odds-api.com` 9 | 10 | ## Authentication 11 | The Odds API uses API keys for authentication. All requests require an API key passed as a query parameter `apiKey`. 12 | 13 | ## Available Endpoints 14 | 15 | ### 1. Sports List (`GET /v4/sports`) 16 | **Cost:** Free (doesn't count against quota) 17 | 18 | **Capabilities:** 19 | - List all in-season sports 20 | - Option to include out-of-season sports 21 | - Provides sport keys used in other endpoints 22 | 23 | **Parameters:** 24 | - `apiKey`: Required - API authentication key 25 | - `all`: Optional - Include out-of-season sports if true 26 | 27 | ### 2. Odds Data (`GET /v4/sports/{sport}/odds`) 28 | **Cost:** 1 credit per region per market 29 | 30 | **Capabilities:** 31 | - Fetch odds for upcoming and live games 32 | - Multiple betting markets support 33 | - Regional bookmaker coverage 34 | - Customizable odds format 35 | - Optional bet limits information 36 | - Bookmaker deep linking 37 | 38 | **Markets Available:** 39 | - `h2h`: Head to head/moneyline 40 | - `spreads`: Points handicaps 41 | - `totals`: Over/under 42 | - `outrights`: Futures 43 | - `h2h_lay`: Lay odds (betting exchanges) 44 | - `outrights_lay`: Outright lay odds 45 | 46 | **Regions Available:** 47 | - `us`: United States 48 | - `us2`: United States (additional bookmakers) 49 | - `uk`: United Kingdom 50 | - `au`: Australia 51 | - `eu`: Europe 52 | 53 | **Parameters:** 54 | - `sport`: Required - Sport key from sports list 55 | - `regions`: Required - Comma-separated list of regions 56 | - `markets`: Optional - Comma-separated list of markets (default: h2h) 57 | - `dateFormat`: Optional - 'unix' or 'iso' (default: iso) 58 | - `oddsFormat`: Optional - 'decimal' or 'american' (default: decimal) 59 | - `eventIds`: Optional - Filter specific events 60 | - `bookmakers`: Optional - Filter specific bookmakers 61 | - `commenceTimeFrom`: Optional - Filter by start time (ISO 8601) 62 | - `commenceTimeTo`: Optional - Filter by end time (ISO 8601) 63 | - `includeLinks`: Optional - Include bookmaker links 64 | - `includeSids`: Optional - Include source IDs 65 | - `includeBetLimits`: Optional - Include betting limits 66 | 67 | ### 3. Scores (`GET /v4/sports/{sport}/scores`) 68 | **Cost:** 69 | - 1 credit for live and upcoming games 70 | - 2 credits when including historical data 71 | 72 | **Capabilities:** 73 | - Live scores (30-second updates) 74 | - Historical scores (up to 3 days) 75 | - Upcoming game schedules 76 | - Detailed scoring information 77 | - Match status tracking 78 | 79 | **Parameters:** 80 | - `sport`: Required - Sport key 81 | - `daysFrom`: Optional - Historical data (1-3 days) 82 | - `dateFormat`: Optional - 'unix' or 'iso' 83 | - `eventIds`: Optional - Filter specific events 84 | 85 | ### 4. Events (`GET /v4/sports/{sport}/events`) 86 | **Cost:** Free (doesn't count against quota) 87 | 88 | **Capabilities:** 89 | - List in-play and pre-match events 90 | - Basic game information 91 | - Team details 92 | - Event scheduling 93 | - Date range filtering 94 | 95 | **Parameters:** 96 | - `sport`: Required - Sport key 97 | - `dateFormat`: Optional - 'unix' or 'iso' 98 | - `eventIds`: Optional - Filter specific events 99 | - `commenceTimeFrom`: Optional - Start time filter (ISO 8601) 100 | - `commenceTimeTo`: Optional - End time filter (ISO 8601) 101 | 102 | ### 5. Event-Specific Odds (`GET /v4/sports/{sport}/events/{eventId}/odds`) 103 | **Cost:** Varies based on markets and regions 104 | 105 | **Capabilities:** 106 | - Detailed odds for single events 107 | - All available betting markets 108 | - Market-specific descriptions 109 | - Granular market updates 110 | - Same parameter options as main odds endpoint 111 | 112 | ### 6. Participants (`GET /v4/sports/{sport}/participants`) 113 | **Cost:** 1 credit 114 | 115 | **Capabilities:** 116 | - List all participants (teams or individuals) for a sport 117 | - Does not include players on teams 118 | - Includes both active and inactive participants 119 | 120 | **Parameters:** 121 | - `sport`: Required - Sport key 122 | - `apiKey`: Required - API authentication key 123 | 124 | ### 7. Historical Odds (`GET /v4/historical/sports/{sport}/odds`) 125 | **Cost:** 10 credits per region per market 126 | 127 | **Capabilities:** 128 | - Historical odds data from June 6th, 2020 129 | - 10-minute intervals until September 2022 130 | - 5-minute intervals after September 2022 131 | - Available only on paid plans 132 | 133 | **Parameters:** 134 | - All parameters from the odds endpoint, plus: 135 | - `date`: Required - Timestamp for historical data (ISO 8601) 136 | 137 | ### 8. Historical Events (`GET /v4/historical/sports/{sport}/events`) 138 | **Cost:** 1 credit (free if no events found) 139 | 140 | **Capabilities:** 141 | - List historical events at a specified timestamp 142 | - Includes event details without odds 143 | - Useful for finding historical event IDs 144 | 145 | **Parameters:** 146 | - Same as events endpoint, plus: 147 | - `date`: Required - Timestamp for historical data (ISO 8601) 148 | 149 | ### 9. Historical Event Odds (`GET /v4/historical/sports/{sport}/events/{eventId}/odds`) 150 | **Cost:** 10 credits per region per market 151 | 152 | **Capabilities:** 153 | - Historical odds for a single event 154 | - Support for all betting markets 155 | - Additional markets available after May 3rd, 2023 156 | - Available only on paid plans 157 | 158 | **Parameters:** 159 | - Same as event-specific odds endpoint, plus: 160 | - `date`: Required - Timestamp for historical data (ISO 8601) 161 | 162 | ## Integration Notes 163 | 164 | ### Quota Management 165 | - Track usage through response headers: 166 | - `x-requests-remaining`: Credits remaining until quota reset 167 | - `x-requests-used`: Credits used since last quota reset 168 | - `x-requests-last`: Usage cost of last API call 169 | - Costs vary by endpoint and parameters 170 | - Some endpoints are free 171 | - Multiple markets/regions multiply quota cost 172 | 173 | ### Best Practices 174 | 1. Use free endpoints for basic data 175 | 2. Batch requests when possible 176 | 3. Cache responses when appropriate 177 | 4. Monitor quota usage headers 178 | 5. Use event-specific endpoint for detailed market data 179 | 6. Filter by eventIds to reduce data volume 180 | 181 | ### Data Updates 182 | - Live scores update ~every 30 seconds 183 | - Odds updates vary by market and bookmaker 184 | - Events may become temporarily unavailable between rounds 185 | - Completed events are removed from odds endpoints 186 | - Historical scores available up to 3 days 187 | 188 | ### Special Features 189 | 1. **Betting Exchange Support** 190 | - Automatic lay odds inclusion 191 | - Bet limits information 192 | - Exchange-specific markets 193 | 194 | 2. **Deep Linking** 195 | - Bookmaker event links 196 | - Market-specific links 197 | - Betslip integration 198 | - Source IDs for custom linking 199 | 200 | 3. **Market Coverage** 201 | - Full coverage for major markets 202 | - Expanding coverage for additional markets 203 | - Sport-specific market availability 204 | - Regional variations in coverage 205 | 206 | ## Rate Limiting 207 | - Requests are rate limited to protect systems 208 | - Status code 429 indicates rate limit reached 209 | - Space out requests over several seconds when rate limited 210 | - Quota reset period defined by subscription 211 | - Usage tracked per endpoint 212 | - Some endpoints exempt from quota 213 | - Multiple markets/regions affect usage 214 | - Remaining quota in response headers 215 | 216 | ## Error Handling 217 | - API returns standard HTTP status codes 218 | - Error messages include descriptive text 219 | - Quota exceeded returns specific error 220 | - Invalid parameters clearly identified 221 | - Rate limiting information included ``` -------------------------------------------------------------------------------- /wagyu_sports/mcp_server/mocks_live/sports_list_live.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "_metadata": { 3 | "captured_at": "2025-03-03T01:43:40-08:00", 4 | "description": "Live data captured from the Odds API", 5 | "tool": "get_sports", 6 | "parameters": { 7 | "all_sports": true 8 | } 9 | }, 10 | "data": [ 11 | { 12 | "key": "americanfootball_cfl", 13 | "group": "American Football", 14 | "title": "CFL", 15 | "description": "Canadian Football League", 16 | "active": false, 17 | "has_outrights": false 18 | }, 19 | { 20 | "key": "americanfootball_ncaaf", 21 | "group": "American Football", 22 | "title": "NCAAF", 23 | "description": "US College Football", 24 | "active": true, 25 | "has_outrights": false 26 | }, 27 | { 28 | "key": "americanfootball_ncaaf_championship_winner", 29 | "group": "American Football", 30 | "title": "NCAAF Championship Winner", 31 | "description": "US College Football Championship Winner", 32 | "active": true, 33 | "has_outrights": true 34 | }, 35 | { 36 | "key": "americanfootball_nfl", 37 | "group": "American Football", 38 | "title": "NFL", 39 | "description": "US Football", 40 | "active": false, 41 | "has_outrights": false 42 | }, 43 | { 44 | "key": "americanfootball_nfl_preseason", 45 | "group": "American Football", 46 | "title": "NFL Preseason", 47 | "description": "US Football", 48 | "active": false, 49 | "has_outrights": false 50 | }, 51 | { 52 | "key": "americanfootball_nfl_super_bowl_winner", 53 | "group": "American Football", 54 | "title": "NFL Super Bowl Winner", 55 | "description": "Super Bowl Winner 2025/2026", 56 | "active": true, 57 | "has_outrights": true 58 | }, 59 | { 60 | "key": "americanfootball_ufl", 61 | "group": "American Football", 62 | "title": "UFL", 63 | "description": "United Football League", 64 | "active": false, 65 | "has_outrights": false 66 | }, 67 | { 68 | "key": "aussierules_afl", 69 | "group": "Aussie Rules", 70 | "title": "AFL", 71 | "description": "Aussie Football", 72 | "active": true, 73 | "has_outrights": false 74 | }, 75 | { 76 | "key": "baseball_kbo", 77 | "group": "Baseball", 78 | "title": "KBO", 79 | "description": "KBO League", 80 | "active": false, 81 | "has_outrights": false 82 | }, 83 | { 84 | "key": "baseball_milb", 85 | "group": "Baseball", 86 | "title": "MiLB", 87 | "description": "Minor League Baseball", 88 | "active": false, 89 | "has_outrights": false 90 | }, 91 | { 92 | "key": "baseball_mlb", 93 | "group": "Baseball", 94 | "title": "MLB", 95 | "description": "Major League Baseball", 96 | "active": true, 97 | "has_outrights": false 98 | }, 99 | { 100 | "key": "baseball_mlb_preseason", 101 | "group": "Baseball", 102 | "title": "MLB Preseason", 103 | "description": "Major League Baseball", 104 | "active": true, 105 | "has_outrights": false 106 | }, 107 | { 108 | "key": "baseball_mlb_world_series_winner", 109 | "group": "Baseball", 110 | "title": "MLB World Series Winner", 111 | "description": "World Series Winner 2025", 112 | "active": true, 113 | "has_outrights": true 114 | }, 115 | { 116 | "key": "baseball_ncaa", 117 | "group": "Baseball", 118 | "title": "NCAA Baseball", 119 | "description": "US College Baseball", 120 | "active": false, 121 | "has_outrights": false 122 | }, 123 | { 124 | "key": "baseball_npb", 125 | "group": "Baseball", 126 | "title": "NPB", 127 | "description": "Nippon Professional Baseball", 128 | "active": false, 129 | "has_outrights": false 130 | }, 131 | { 132 | "key": "basketball_euroleague", 133 | "group": "Basketball", 134 | "title": "Basketball Euroleague", 135 | "description": "Basketball Euroleague", 136 | "active": true, 137 | "has_outrights": false 138 | }, 139 | { 140 | "key": "basketball_nba", 141 | "group": "Basketball", 142 | "title": "NBA", 143 | "description": "US Basketball", 144 | "active": true, 145 | "has_outrights": false 146 | }, 147 | { 148 | "key": "basketball_nba_championship_winner", 149 | "group": "Basketball", 150 | "title": "NBA Championship Winner", 151 | "description": "Championship Winner 2024/2025", 152 | "active": true, 153 | "has_outrights": true 154 | }, 155 | { 156 | "key": "basketball_nba_preseason", 157 | "group": "Basketball", 158 | "title": "NBA Preseason", 159 | "description": "US Basketball", 160 | "active": false, 161 | "has_outrights": false 162 | }, 163 | { 164 | "key": "basketball_nbl", 165 | "group": "Basketball", 166 | "title": "NBL", 167 | "description": "AU National Basketball League", 168 | "active": true, 169 | "has_outrights": false 170 | }, 171 | { 172 | "key": "basketball_ncaab", 173 | "group": "Basketball", 174 | "title": "NCAAB", 175 | "description": "US College Basketball", 176 | "active": true, 177 | "has_outrights": false 178 | }, 179 | { 180 | "key": "basketball_ncaab_championship_winner", 181 | "group": "Basketball", 182 | "title": "NCAAB Championship Winner", 183 | "description": "US College Basketball Championship Winner", 184 | "active": true, 185 | "has_outrights": true 186 | }, 187 | { 188 | "key": "basketball_wnba", 189 | "group": "Basketball", 190 | "title": "WNBA", 191 | "description": "US Basketball", 192 | "active": false, 193 | "has_outrights": false 194 | }, 195 | { 196 | "key": "basketball_wncaab", 197 | "group": "Basketball", 198 | "title": "WNCAAB", 199 | "description": "US Women's College Basketball", 200 | "active": false, 201 | "has_outrights": false 202 | }, 203 | { 204 | "key": "boxing_boxing", 205 | "group": "Boxing", 206 | "title": "Boxing", 207 | "description": "Boxing Bouts", 208 | "active": true, 209 | "has_outrights": false 210 | }, 211 | { 212 | "key": "cricket_asia_cup", 213 | "group": "Cricket", 214 | "title": "Asia Cup", 215 | "description": "Asia Cup", 216 | "active": false, 217 | "has_outrights": false 218 | }, 219 | { 220 | "key": "cricket_big_bash", 221 | "group": "Cricket", 222 | "title": "Big Bash", 223 | "description": "Big Bash League", 224 | "active": false, 225 | "has_outrights": false 226 | }, 227 | { 228 | "key": "cricket_caribbean_premier_league", 229 | "group": "Cricket", 230 | "title": "CPLT20", 231 | "description": "Caribbean Premier League", 232 | "active": false, 233 | "has_outrights": false 234 | }, 235 | { 236 | "key": "cricket_icc_trophy", 237 | "group": "Cricket", 238 | "title": "ICC Champions Trophy", 239 | "description": "ICC Champions Trophy", 240 | "active": true, 241 | "has_outrights": false 242 | }, 243 | { 244 | "key": "cricket_icc_world_cup", 245 | "group": "Cricket", 246 | "title": "ICC World Cup", 247 | "description": "ICC World Cup", 248 | "active": false, 249 | "has_outrights": false 250 | }, 251 | { 252 | "key": "cricket_international_t20", 253 | "group": "Cricket", 254 | "title": "International Twenty20", 255 | "description": "International Twenty20", 256 | "active": false, 257 | "has_outrights": false 258 | }, 259 | { 260 | "key": "cricket_ipl", 261 | "group": "Cricket", 262 | "title": "IPL", 263 | "description": "Indian Premier League", 264 | "active": false, 265 | "has_outrights": false 266 | }, 267 | { 268 | "key": "cricket_odi", 269 | "group": "Cricket", 270 | "title": "One Day Internationals", 271 | "description": "One Day Internationals", 272 | "active": false, 273 | "has_outrights": false 274 | }, 275 | { 276 | "key": "cricket_psl", 277 | "group": "Cricket", 278 | "title": "Pakistan Super League", 279 | "description": "Pakistan Super League", 280 | "active": false, 281 | "has_outrights": false 282 | }, 283 | { 284 | "key": "cricket_t20_blast", 285 | "group": "Cricket", 286 | "title": "T20 Blast", 287 | "description": "T20 Blast", 288 | "active": false, 289 | "has_outrights": false 290 | }, 291 | { 292 | "key": "cricket_test_match", 293 | "group": "Cricket", 294 | "title": "Test Matches", 295 | "description": "International Test Matches", 296 | "active": false, 297 | "has_outrights": false 298 | }, 299 | { 300 | "key": "cricket_the_hundred", 301 | "group": "Cricket", 302 | "title": "The Hundred", 303 | "description": "The Hundred", 304 | "active": false, 305 | "has_outrights": false 306 | }, 307 | { 308 | "key": "golf_masters_tournament_winner", 309 | "group": "Golf", 310 | "title": "Masters Tournament Winner", 311 | "description": "2025 Winner", 312 | "active": true, 313 | "has_outrights": true 314 | }, 315 | { 316 | "key": "golf_pga_championship_winner", 317 | "group": "Golf", 318 | "title": "PGA Championship Winner", 319 | "description": "2025 Winner", 320 | "active": true, 321 | "has_outrights": true 322 | }, 323 | { 324 | "key": "golf_the_open_championship_winner", 325 | "group": "Golf", 326 | "title": "The Open Winner", 327 | "description": "2025 Winner", 328 | "active": true, 329 | "has_outrights": true 330 | }, 331 | { 332 | "key": "golf_us_open_winner", 333 | "group": "Golf", 334 | "title": "US Open Winner", 335 | "description": "2025 Winner", 336 | "active": true, 337 | "has_outrights": true 338 | }, 339 | { 340 | "key": "icehockey_ahl", 341 | "group": "Ice Hockey", 342 | "title": "AHL", 343 | "description": "American Hockey League", 344 | "active": false, 345 | "has_outrights": false 346 | }, 347 | { 348 | "key": "icehockey_liiga", 349 | "group": "Ice Hockey", 350 | "title": "Liiga", 351 | "description": "Finnish SM League", 352 | "active": true, 353 | "has_outrights": false 354 | }, 355 | { 356 | "key": "icehockey_mestis", 357 | "group": "Ice Hockey", 358 | "title": "Mestis", 359 | "description": "Finnish Mestis League", 360 | "active": true, 361 | "has_outrights": false 362 | }, 363 | { 364 | "key": "icehockey_nhl", 365 | "group": "Ice Hockey", 366 | "title": "NHL", 367 | "description": "US Ice Hockey", 368 | "active": true, 369 | "has_outrights": false 370 | }, 371 | { 372 | "key": "icehockey_nhl_championship_winner", 373 | "group": "Ice Hockey", 374 | "title": "NHL Championship Winner", 375 | "description": "Stanley Cup Winner 2024/2025", 376 | "active": true, 377 | "has_outrights": true 378 | }, 379 | { 380 | "key": "icehockey_sweden_allsvenskan", 381 | "group": "Ice Hockey", 382 | "title": "HockeyAllsvenskan", 383 | "description": "Swedish Hockey Allsvenskan", 384 | "active": true, 385 | "has_outrights": false 386 | }, 387 | { 388 | "key": "icehockey_sweden_hockey_league", 389 | "group": "Ice Hockey", 390 | "title": "SHL", 391 | "description": "Swedish Hockey League", 392 | "active": true, 393 | "has_outrights": false 394 | }, 395 | { 396 | "key": "lacrosse_ncaa", 397 | "group": "Lacrosse", 398 | "title": "NCAA Lacrosse", 399 | "description": "College Lacrosse", 400 | "active": true, 401 | "has_outrights": false 402 | }, 403 | { 404 | "key": "lacrosse_pll", 405 | "group": "Lacrosse", 406 | "title": "PLL", 407 | "description": "Premier Lacrosse League", 408 | "active": false, 409 | "has_outrights": false 410 | }, 411 | { 412 | "key": "mma_mixed_martial_arts", 413 | "group": "Mixed Martial Arts", 414 | "title": "MMA", 415 | "description": "Mixed Martial Arts", 416 | "active": true, 417 | "has_outrights": false 418 | }, 419 | { 420 | "key": "politics_us_presidential_election_winner", 421 | "group": "Politics", 422 | "title": "US Presidential Elections Winner", 423 | "description": "2028 US Presidential Election Winner", 424 | "active": false, 425 | "has_outrights": true 426 | }, 427 | { 428 | "key": "rugbyleague_nrl", 429 | "group": "Rugby League", 430 | "title": "NRL", 431 | "description": "Aussie Rugby League", 432 | "active": true, 433 | "has_outrights": false 434 | }, 435 | { 436 | "key": "rugbyunion_six_nations", 437 | "group": "Rugby Union", 438 | "title": "Six Nations", 439 | "description": "Six Nations Championship", 440 | "active": true, 441 | "has_outrights": false 442 | }, 443 | { 444 | "key": "soccer_africa_cup_of_nations", 445 | "group": "Soccer", 446 | "title": "Africa Cup of Nations", 447 | "description": "Africa Cup of Nations", 448 | "active": false, 449 | "has_outrights": false 450 | }, 451 | { 452 | "key": "soccer_argentina_primera_division", 453 | "group": "Soccer", 454 | "title": "Primera Divisi\u00f3n - Argentina", 455 | "description": "Argentine Primera Divisi\u00f3n", 456 | "active": true, 457 | "has_outrights": false 458 | }, 459 | { 460 | "key": "soccer_australia_aleague", 461 | "group": "Soccer", 462 | "title": "A-League", 463 | "description": "Aussie Soccer", 464 | "active": true, 465 | "has_outrights": false 466 | }, 467 | { 468 | "key": "soccer_austria_bundesliga", 469 | "group": "Soccer", 470 | "title": "Austrian Football Bundesliga", 471 | "description": "Austrian Soccer", 472 | "active": true, 473 | "has_outrights": false 474 | }, 475 | { 476 | "key": "soccer_belgium_first_div", 477 | "group": "Soccer", 478 | "title": "Belgium First Div", 479 | "description": "Belgian First Division A", 480 | "active": true, 481 | "has_outrights": false 482 | }, 483 | { 484 | "key": "soccer_brazil_campeonato", 485 | "group": "Soccer", 486 | "title": "Brazil S\u00e9rie A", 487 | "description": "Brasileir\u00e3o S\u00e9rie A", 488 | "active": true, 489 | "has_outrights": false 490 | }, 491 | { 492 | "key": "soccer_brazil_serie_b", 493 | "group": "Soccer", 494 | "title": "Brazil S\u00e9rie B", 495 | "description": "Campeonato Brasileiro S\u00e9rie B", 496 | "active": false, 497 | "has_outrights": false 498 | }, 499 | { 500 | "key": "soccer_chile_campeonato", 501 | "group": "Soccer", 502 | "title": "Primera Divisi\u00f3n - Chile", 503 | "description": "Campeonato Chileno", 504 | "active": true, 505 | "has_outrights": false 506 | }, 507 | { 508 | "key": "soccer_china_superleague", 509 | "group": "Soccer", 510 | "title": "Super League - China", 511 | "description": "Chinese Soccer", 512 | "active": false, 513 | "has_outrights": false 514 | }, 515 | { 516 | "key": "soccer_conmebol_copa_america", 517 | "group": "Soccer", 518 | "title": "Copa Am\u00e9rica", 519 | "description": "CONMEBOL Copa Am\u00e9rica", 520 | "active": false, 521 | "has_outrights": false 522 | }, 523 | { 524 | "key": "soccer_conmebol_copa_libertadores", 525 | "group": "Soccer", 526 | "title": "Copa Libertadores", 527 | "description": "CONMEBOL Copa Libertadores", 528 | "active": true, 529 | "has_outrights": false 530 | }, 531 | { 532 | "key": "soccer_denmark_superliga", 533 | "group": "Soccer", 534 | "title": "Denmark Superliga", 535 | "description": "Danish Soccer", 536 | "active": true, 537 | "has_outrights": false 538 | }, 539 | { 540 | "key": "soccer_efl_champ", 541 | "group": "Soccer", 542 | "title": "Championship", 543 | "description": "EFL Championship", 544 | "active": true, 545 | "has_outrights": false 546 | }, 547 | { 548 | "key": "soccer_england_efl_cup", 549 | "group": "Soccer", 550 | "title": "EFL Cup", 551 | "description": "League Cup", 552 | "active": true, 553 | "has_outrights": false 554 | }, 555 | { 556 | "key": "soccer_england_league1", 557 | "group": "Soccer", 558 | "title": "League 1", 559 | "description": "EFL League 1", 560 | "active": true, 561 | "has_outrights": false 562 | }, 563 | { 564 | "key": "soccer_england_league2", 565 | "group": "Soccer", 566 | "title": "League 2", 567 | "description": "EFL League 2 ", 568 | "active": true, 569 | "has_outrights": false 570 | }, 571 | { 572 | "key": "soccer_epl", 573 | "group": "Soccer", 574 | "title": "EPL", 575 | "description": "English Premier League", 576 | "active": true, 577 | "has_outrights": false 578 | }, 579 | { 580 | "key": "soccer_fa_cup", 581 | "group": "Soccer", 582 | "title": "FA Cup", 583 | "description": "Football Association Challenge Cup", 584 | "active": true, 585 | "has_outrights": false 586 | }, 587 | { 588 | "key": "soccer_fifa_world_cup", 589 | "group": "Soccer", 590 | "title": "FIFA World Cup", 591 | "description": "FIFA World Cup 2022", 592 | "active": false, 593 | "has_outrights": false 594 | }, 595 | { 596 | "key": "soccer_fifa_world_cup_winner", 597 | "group": "Soccer", 598 | "title": "FIFA World Cup Winner", 599 | "description": "FIFA World Cup Winner 2026", 600 | "active": true, 601 | "has_outrights": true 602 | }, 603 | { 604 | "key": "soccer_fifa_world_cup_womens", 605 | "group": "Soccer", 606 | "title": "FIFA Women's World Cup", 607 | "description": "FIFA Women's World Cup", 608 | "active": false, 609 | "has_outrights": false 610 | }, 611 | { 612 | "key": "soccer_finland_veikkausliiga", 613 | "group": "Soccer", 614 | "title": "Veikkausliiga - Finland", 615 | "description": "Finnish Soccer", 616 | "active": false, 617 | "has_outrights": false 618 | }, 619 | { 620 | "key": "soccer_france_ligue_one", 621 | "group": "Soccer", 622 | "title": "Ligue 1 - France", 623 | "description": "French Soccer", 624 | "active": true, 625 | "has_outrights": false 626 | }, 627 | { 628 | "key": "soccer_france_ligue_two", 629 | "group": "Soccer", 630 | "title": "Ligue 2 - France", 631 | "description": "French Soccer", 632 | "active": true, 633 | "has_outrights": false 634 | }, 635 | { 636 | "key": "soccer_germany_bundesliga", 637 | "group": "Soccer", 638 | "title": "Bundesliga - Germany", 639 | "description": "German Soccer", 640 | "active": true, 641 | "has_outrights": false 642 | }, 643 | { 644 | "key": "soccer_germany_bundesliga2", 645 | "group": "Soccer", 646 | "title": "Bundesliga 2 - Germany", 647 | "description": "German Soccer", 648 | "active": true, 649 | "has_outrights": false 650 | }, 651 | { 652 | "key": "soccer_germany_liga3", 653 | "group": "Soccer", 654 | "title": "3. Liga - Germany", 655 | "description": "German Soccer", 656 | "active": true, 657 | "has_outrights": false 658 | }, 659 | { 660 | "key": "soccer_greece_super_league", 661 | "group": "Soccer", 662 | "title": "Super League - Greece", 663 | "description": "Greek Soccer", 664 | "active": true, 665 | "has_outrights": false 666 | }, 667 | { 668 | "key": "soccer_italy_serie_a", 669 | "group": "Soccer", 670 | "title": "Serie A - Italy", 671 | "description": "Italian Soccer", 672 | "active": true, 673 | "has_outrights": false 674 | }, 675 | { 676 | "key": "soccer_italy_serie_b", 677 | "group": "Soccer", 678 | "title": "Serie B - Italy", 679 | "description": "Italian Soccer", 680 | "active": true, 681 | "has_outrights": false 682 | }, 683 | { 684 | "key": "soccer_japan_j_league", 685 | "group": "Soccer", 686 | "title": "J League", 687 | "description": "Japan Soccer League", 688 | "active": true, 689 | "has_outrights": false 690 | }, 691 | { 692 | "key": "soccer_korea_kleague1", 693 | "group": "Soccer", 694 | "title": "K League 1", 695 | "description": "Korean Soccer", 696 | "active": true, 697 | "has_outrights": false 698 | }, 699 | { 700 | "key": "soccer_league_of_ireland", 701 | "group": "Soccer", 702 | "title": "League of Ireland", 703 | "description": "Airtricity League Premier Division", 704 | "active": true, 705 | "has_outrights": false 706 | }, 707 | { 708 | "key": "soccer_mexico_ligamx", 709 | "group": "Soccer", 710 | "title": "Liga MX", 711 | "description": "Mexican Soccer", 712 | "active": true, 713 | "has_outrights": false 714 | }, 715 | { 716 | "key": "soccer_netherlands_eredivisie", 717 | "group": "Soccer", 718 | "title": "Dutch Eredivisie", 719 | "description": "Dutch Soccer", 720 | "active": true, 721 | "has_outrights": false 722 | }, 723 | { 724 | "key": "soccer_norway_eliteserien", 725 | "group": "Soccer", 726 | "title": "Eliteserien - Norway", 727 | "description": "Norwegian Soccer", 728 | "active": true, 729 | "has_outrights": false 730 | }, 731 | { 732 | "key": "soccer_poland_ekstraklasa", 733 | "group": "Soccer", 734 | "title": "Ekstraklasa - Poland", 735 | "description": "Polish Soccer", 736 | "active": true, 737 | "has_outrights": false 738 | }, 739 | { 740 | "key": "soccer_portugal_primeira_liga", 741 | "group": "Soccer", 742 | "title": "Primeira Liga - Portugal", 743 | "description": "Portugese Soccer", 744 | "active": true, 745 | "has_outrights": false 746 | }, 747 | { 748 | "key": "soccer_spain_la_liga", 749 | "group": "Soccer", 750 | "title": "La Liga - Spain", 751 | "description": "Spanish Soccer", 752 | "active": true, 753 | "has_outrights": false 754 | }, 755 | { 756 | "key": "soccer_spain_segunda_division", 757 | "group": "Soccer", 758 | "title": "La Liga 2 - Spain", 759 | "description": "Spanish Soccer", 760 | "active": true, 761 | "has_outrights": false 762 | }, 763 | { 764 | "key": "soccer_spl", 765 | "group": "Soccer", 766 | "title": "Premiership - Scotland", 767 | "description": "Scottish Premiership", 768 | "active": true, 769 | "has_outrights": false 770 | }, 771 | { 772 | "key": "soccer_sweden_allsvenskan", 773 | "group": "Soccer", 774 | "title": "Allsvenskan - Sweden", 775 | "description": "Swedish Soccer", 776 | "active": true, 777 | "has_outrights": false 778 | }, 779 | { 780 | "key": "soccer_sweden_superettan", 781 | "group": "Soccer", 782 | "title": "Superettan - Sweden", 783 | "description": "Swedish Soccer", 784 | "active": false, 785 | "has_outrights": false 786 | }, 787 | { 788 | "key": "soccer_switzerland_superleague", 789 | "group": "Soccer", 790 | "title": "Swiss Superleague", 791 | "description": "Swiss Soccer", 792 | "active": true, 793 | "has_outrights": false 794 | }, 795 | { 796 | "key": "soccer_turkey_super_league", 797 | "group": "Soccer", 798 | "title": "Turkey Super League", 799 | "description": "Turkish Soccer", 800 | "active": true, 801 | "has_outrights": false 802 | }, 803 | { 804 | "key": "soccer_uefa_champs_league", 805 | "group": "Soccer", 806 | "title": "UEFA Champions League", 807 | "description": "European Champions League", 808 | "active": true, 809 | "has_outrights": false 810 | }, 811 | { 812 | "key": "soccer_uefa_champs_league_qualification", 813 | "group": "Soccer", 814 | "title": "UEFA Champions League Qualification", 815 | "description": "European Champions League Qualification", 816 | "active": false, 817 | "has_outrights": false 818 | }, 819 | { 820 | "key": "soccer_uefa_euro_qualification", 821 | "group": "Soccer", 822 | "title": "UEFA Euro Qualification", 823 | "description": "European Championship Qualification", 824 | "active": false, 825 | "has_outrights": false 826 | }, 827 | { 828 | "key": "soccer_uefa_europa_conference_league", 829 | "group": "Soccer", 830 | "title": "UEFA Europa Conference League", 831 | "description": "UEFA Europa Conference League", 832 | "active": true, 833 | "has_outrights": false 834 | }, 835 | { 836 | "key": "soccer_uefa_europa_league", 837 | "group": "Soccer", 838 | "title": "UEFA Europa League", 839 | "description": "European Europa League", 840 | "active": true, 841 | "has_outrights": false 842 | }, 843 | { 844 | "key": "soccer_uefa_european_championship", 845 | "group": "Soccer", 846 | "title": "UEFA Euro 2024", 847 | "description": "UEFA European Championship", 848 | "active": false, 849 | "has_outrights": false 850 | }, 851 | { 852 | "key": "soccer_uefa_nations_league", 853 | "group": "Soccer", 854 | "title": "UEFA Nations League", 855 | "description": "UEFA Nations League", 856 | "active": true, 857 | "has_outrights": false 858 | }, 859 | { 860 | "key": "soccer_usa_mls", 861 | "group": "Soccer", 862 | "title": "MLS", 863 | "description": "Major League Soccer", 864 | "active": true, 865 | "has_outrights": false 866 | }, 867 | { 868 | "key": "tennis_atp_aus_open_singles", 869 | "group": "Tennis", 870 | "title": "ATP Australian Open", 871 | "description": "Men's Singles", 872 | "active": false, 873 | "has_outrights": false 874 | }, 875 | { 876 | "key": "tennis_atp_canadian_open", 877 | "group": "Tennis", 878 | "title": "ATP Canadian Open", 879 | "description": "Men's Singles", 880 | "active": false, 881 | "has_outrights": false 882 | }, 883 | { 884 | "key": "tennis_atp_china_open", 885 | "group": "Tennis", 886 | "title": "ATP China Open", 887 | "description": "Men's Singles", 888 | "active": false, 889 | "has_outrights": false 890 | }, 891 | { 892 | "key": "tennis_atp_cincinnati_open", 893 | "group": "Tennis", 894 | "title": "ATP Cincinnati Open", 895 | "description": "Men's Singles", 896 | "active": false, 897 | "has_outrights": false 898 | }, 899 | { 900 | "key": "tennis_atp_dubai", 901 | "group": "Tennis", 902 | "title": "ATP Dubai", 903 | "description": "Men's Singles", 904 | "active": false, 905 | "has_outrights": false 906 | }, 907 | { 908 | "key": "tennis_atp_french_open", 909 | "group": "Tennis", 910 | "title": "ATP French Open", 911 | "description": "Men's Singles", 912 | "active": false, 913 | "has_outrights": false 914 | }, 915 | { 916 | "key": "tennis_atp_paris_masters", 917 | "group": "Tennis", 918 | "title": "ATP Paris Masters", 919 | "description": "Men's Singles", 920 | "active": false, 921 | "has_outrights": false 922 | }, 923 | { 924 | "key": "tennis_atp_qatar_open", 925 | "group": "Tennis", 926 | "title": "ATP Qatar Open", 927 | "description": "Men's Singles", 928 | "active": false, 929 | "has_outrights": false 930 | }, 931 | { 932 | "key": "tennis_atp_shanghai_masters", 933 | "group": "Tennis", 934 | "title": "ATP Shanghai Masters", 935 | "description": "Men's Singles", 936 | "active": false, 937 | "has_outrights": false 938 | }, 939 | { 940 | "key": "tennis_atp_us_open", 941 | "group": "Tennis", 942 | "title": "ATP US Open", 943 | "description": "Men's Singles", 944 | "active": false, 945 | "has_outrights": false 946 | }, 947 | { 948 | "key": "tennis_atp_wimbledon", 949 | "group": "Tennis", 950 | "title": "ATP Wimbledon", 951 | "description": "Men's Singles", 952 | "active": false, 953 | "has_outrights": false 954 | }, 955 | { 956 | "key": "tennis_wta_aus_open_singles", 957 | "group": "Tennis", 958 | "title": "WTA Australian Open", 959 | "description": "Women's Singles", 960 | "active": false, 961 | "has_outrights": false 962 | } 963 | ], 964 | "headers": { 965 | "x-requests-remaining": "492", 966 | "x-requests-used": "8" 967 | } 968 | } 969 | ```