# Directory Structure ``` ├── .gitignore ├── Dockerfile ├── LICENSE ├── pyproject.toml ├── README.md ├── smithery.yaml └── src └── polymarket_mcp ├── __init__.py └── server.py ``` # Files -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` 1 | # Python 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | *.so 6 | .Python 7 | build/ 8 | develop-eggs/ 9 | dist/ 10 | downloads/ 11 | eggs/ 12 | .eggs/ 13 | lib/ 14 | lib64/ 15 | parts/ 16 | sdist/ 17 | var/ 18 | wheels/ 19 | *.egg-info/ 20 | .installed.cfg 21 | *.egg 22 | 23 | # Virtual Environment 24 | venv/ 25 | ENV/ 26 | env/ 27 | .env 28 | 29 | # IDE 30 | .idea/ 31 | .vscode/ 32 | *.swp 33 | *.swo 34 | 35 | # Other 36 | .DS_Store 37 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | 2 | # PolyMarket MCP Server 3 | [](https://smithery.ai/server/polymarket_mcp) 4 | 5 | A Model Context Protocol (MCP) server that provides access to prediction market data through the PolyMarket API. This server implements a standardized interface for retrieving market information, prices, and historical data from prediction markets. 6 | 7 | <a href="https://glama.ai/mcp/servers/c255m147fd"> 8 | <img width="380" height="200" src="https://glama.ai/mcp/servers/c255m147fd/badge" alt="PolyMarket Server MCP server" /> 9 | </a> 10 | 11 | [](https://mseep.ai/app/berlinbra-polymarket-mcp) 12 | 13 | ## Features 14 | 15 | - Real-time prediction market data with current prices and probabilities 16 | - Detailed market information including categories, resolution dates, and descriptions 17 | - Historical price and volume data with customizable timeframes (1d, 7d, 30d, all) 18 | - Built-in error handling and rate limit management 19 | - Clean data formatting for easy consumption 20 | 21 | ## Installation 22 | 23 | #### Installing via Smithery 24 | 25 | To install PolyMarket Predictions for Claude Desktop automatically via [Smithery](https://smithery.ai/server/polymarket_mcp): 26 | 27 | ```bash 28 | npx -y @smithery/cli install polymarket_mcp --client claude 29 | ``` 30 | 31 | #### Claude Desktop 32 | - On MacOS: `~/Library/Application\ Support/Claude/claude_desktop_config.json` 33 | - On Windows: `%APPDATA%/Claude/claude_desktop_config.json` 34 | 35 | <summary>Development/Unpublished Servers Configuration</summary> 36 | 37 | ```json 38 | "mcpServers": { 39 | "polymarket-mcp": { 40 | "command": "uv", 41 | "args": [ 42 | "--directory", 43 | "/Users/{INSERT_USER}/YOUR/PATH/TO/polymarket-mcp", 44 | "run", 45 | "polymarket-mcp" //or src/polymarket_mcp/server.py 46 | ], 47 | "env": { 48 | "KEY": "<insert poly market api key>", 49 | "FUNDER": "<insert polymarket wallet address>" 50 | } 51 | } 52 | } 53 | ``` 54 | 55 | ### Running Locally 56 | 1. Clone the repository and install dependencies: 57 | 58 | #### Install Libraries 59 | ``` 60 | uv pip install -e . 61 | ``` 62 | 63 | ### Running 64 | After connecting Claude client with the MCP tool via json file and installing the packages, Claude should see the server's mcp tools: 65 | 66 | You can run the sever yourself via: 67 | In polymarket-mcp repo: 68 | ``` 69 | uv run src/polymarket_mcp/server.py 70 | ``` 71 | 72 | *if you want to run the server inspector along with the server: 73 | ``` 74 | npx @modelcontextprotocol/inspector uv --directory C:\\Users\\{INSERT_USER}\\YOUR\\PATH\\TO\\polymarket-mcp run src/polymarket_mcp/server.py 75 | ``` 76 | 77 | 2. Create a `.env` file with your PolyMarket API key: 78 | ``` 79 | Key=your_api_key_here 80 | Funder=poly market wallet address 81 | ``` 82 | 83 | After connecting Claude client with the MCP tool via json file, run the server: 84 | In alpha-vantage-mcp repo: `uv run src/polymarket_mcp/server.py` 85 | 86 | 87 | ## Available Tools 88 | 89 | The server implements four tools: 90 | - `get-market-info`: Get detailed information about a specific prediction market 91 | - `list-markets`: List available prediction markets with filtering options 92 | - `get-market-prices`: Get current prices and trading information 93 | - `get-market-history`: Get historical price and volume data 94 | 95 | ### get-market-info 96 | 97 | **Input Schema:** 98 | ```json 99 | { 100 | "market_id": { 101 | "type": "string", 102 | "description": "Market ID or slug" 103 | } 104 | } 105 | ``` 106 | 107 | **Example Response:** 108 | ``` 109 | Title: Example Market 110 | Category: Politics 111 | Status: Open 112 | Resolution Date: 2024-12-31 113 | Volume: $1,234,567.89 114 | Liquidity: $98,765.43 115 | Description: This is an example prediction market... 116 | --- 117 | ``` 118 | 119 | ### list-markets 120 | 121 | **Input Schema:** 122 | ```json 123 | { 124 | "status": { 125 | "type": "string", 126 | "description": "Filter by market status", 127 | "enum": ["open", "closed", "resolved"] 128 | }, 129 | "limit": { 130 | "type": "integer", 131 | "description": "Number of markets to return", 132 | "default": 10, 133 | "minimum": 1, 134 | "maximum": 100 135 | }, 136 | "offset": { 137 | "type": "integer", 138 | "description": "Number of markets to skip (for pagination)", 139 | "default": 0, 140 | "minimum": 0 141 | } 142 | } 143 | ``` 144 | 145 | **Example Response:** 146 | ``` 147 | Available Markets: 148 | 149 | ID: market-123 150 | Title: US Presidential Election 2024 151 | Status: Open 152 | Volume: $1,234,567.89 153 | --- 154 | 155 | ID: market-124 156 | Title: Oscar Best Picture 2024 157 | Status: Open 158 | Volume: $234,567.89 159 | --- 160 | ``` 161 | 162 | ### get-market-prices 163 | 164 | **Input Schema:** 165 | ```json 166 | { 167 | "market_id": { 168 | "type": "string", 169 | "description": "Market ID or slug" 170 | } 171 | } 172 | ``` 173 | 174 | **Example Response:** 175 | ``` 176 | Current Market Prices for US Presidential Election 2024 177 | 178 | Outcome: Democratic 179 | Price: $0.6500 180 | Probability: 65.0% 181 | --- 182 | 183 | Outcome: Republican 184 | Price: $0.3500 185 | Probability: 35.0% 186 | --- 187 | ``` 188 | 189 | ### get-market-history 190 | 191 | **Input Schema:** 192 | ```json 193 | { 194 | "market_id": { 195 | "type": "string", 196 | "description": "Market ID or slug" 197 | }, 198 | "timeframe": { 199 | "type": "string", 200 | "description": "Time period for historical data", 201 | "enum": ["1d", "7d", "30d", "all"], 202 | "default": "7d" 203 | } 204 | } 205 | ``` 206 | 207 | **Example Response:** 208 | ``` 209 | Historical Data for US Presidential Election 2024 210 | Time Period: 7d 211 | 212 | Time: 2024-01-20T12:00:00Z 213 | Price: $0.6500 214 | Volume: $123,456.78 215 | --- 216 | 217 | Time: 2024-01-19T12:00:00Z 218 | Price: $0.6300 219 | Volume: $98,765.43 220 | --- 221 | ``` 222 | 223 | ## Error Handling 224 | 225 | The server includes comprehensive error handling for various scenarios: 226 | 227 | - Rate limiting (429 errors) 228 | - Invalid API keys (403 errors) 229 | - Invalid market IDs (404 errors) 230 | - Network connectivity issues 231 | - API timeout conditions (30-second timeout) 232 | - Malformed responses 233 | 234 | Error messages are returned in a clear, human-readable format. 235 | 236 | ## Prerequisites 237 | 238 | - Python 3.9 or higher 239 | - httpx>=0.24.0 240 | - mcp-core 241 | - python-dotenv>=1.0.0 242 | 243 | ## Contributing 244 | 245 | Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change. ``` -------------------------------------------------------------------------------- /src/polymarket_mcp/__init__.py: -------------------------------------------------------------------------------- ```python 1 | """ 2 | PolyMarket MCP Server 3 | An MCP server implementation for interacting with the PolyMarket API. 4 | """ 5 | 6 | __version__ = "0.1.0" ``` -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- ```toml 1 | [build-system] 2 | requires = ["hatchling"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "polymarket_mcp" 7 | version = "0.1.0" 8 | description = "MCP Server for PolyMarket API" 9 | requires-python = ">=3.10" 10 | dependencies = [ 11 | "mcp>=0.1.0", 12 | "httpx>=0.24.0", 13 | "python-dotenv>=1.0.0", 14 | "py-clob-client" 15 | ] 16 | 17 | [project.scripts] 18 | polymarket-mcp = "polymarket_mcp:main" ``` -------------------------------------------------------------------------------- /smithery.yaml: -------------------------------------------------------------------------------- ```yaml 1 | # Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml 2 | 3 | startCommand: 4 | type: stdio 5 | configSchema: 6 | # JSON Schema defining the configuration options for the MCP. 7 | type: object 8 | required: 9 | - polymarketApiKey 10 | properties: 11 | polymarketApiKey: 12 | type: string 13 | description: The API key for accessing the PolyMarket API. 14 | commandFunction: 15 | # A function that produces the CLI command to start the MCP on stdio. 16 | |- 17 | (config) => ({ command: 'python', args: ['src/polymarket_mcp/server.py'], env: { POLYMARKET_API_KEY: config.polymarketApiKey } }) ``` -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- ```dockerfile 1 | # Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile 2 | # Use the official Python image from the Docker Hub 3 | FROM python:3.10-slim 4 | 5 | # Set the working directory 6 | WORKDIR /app 7 | 8 | # Copy the requirements and source code into the container 9 | COPY pyproject.toml /app/ 10 | COPY src /app/src 11 | 12 | # Install dependencies 13 | RUN pip install --no-cache-dir --upgrade pip \ 14 | && pip install --no-cache-dir hatchling 15 | 16 | # Install the project in the container 17 | RUN pip install --no-cache-dir /app 18 | 19 | # Define environment variables 20 | ENV POLYMARKET_API_KEY="<insert api key>" 21 | 22 | # Run the server 23 | CMD ["python", "src/polymarket_mcp/server.py"] ``` -------------------------------------------------------------------------------- /src/polymarket_mcp/server.py: -------------------------------------------------------------------------------- ```python 1 | from typing import Any 2 | import asyncio 3 | import httpx 4 | import json 5 | from mcp.server.models import InitializationOptions 6 | import mcp.types as types 7 | from mcp.server import NotificationOptions, Server 8 | import mcp.server.stdio 9 | import os 10 | from dotenv import load_dotenv 11 | from py_clob_client.client import ClobClient 12 | from py_clob_client.clob_types import OrderArgs 13 | from py_clob_client.constants import POLYGON 14 | 15 | # Load environment variables 16 | load_dotenv() 17 | 18 | server = Server("polymarket_predictions") 19 | 20 | # Initialize CLOB client 21 | def get_clob_client() -> ClobClient: 22 | host = os.getenv("CLOB_HOST", "https://clob.polymarket.com") 23 | key = os.getenv("KEY") # Private key exported from polymarket UI 24 | funder = os.getenv("FUNDER") # Funder address from polymarket UI 25 | chain_id = POLYGON 26 | 27 | client = ClobClient( 28 | host, 29 | key=key, 30 | chain_id=POLYGON, 31 | funder=funder, 32 | signature_type=1, 33 | ) 34 | client.set_api_creds(client.create_or_derive_api_creds()) 35 | return client 36 | 37 | @server.list_tools() 38 | async def handle_list_tools() -> list[types.Tool]: 39 | """ 40 | List available tools for interacting with the PolyMarket API. 41 | Each tool specifies its arguments using JSON Schema validation. 42 | """ 43 | return [ 44 | types.Tool( 45 | name="get-market-info", 46 | description="Get detailed information about a specific prediction market", 47 | inputSchema={ 48 | "type": "object", 49 | "properties": { 50 | "market_id": { 51 | "type": "string", 52 | "description": "Market ID or slug", 53 | }, 54 | }, 55 | "required": ["market_id"], 56 | }, 57 | ), 58 | types.Tool( 59 | name="list-markets", 60 | description="Get a list of prediction markets with optional filters", 61 | inputSchema={ 62 | "type": "object", 63 | "properties": { 64 | "status": { 65 | "type": "string", 66 | "description": "Filter by market status (e.g., open, closed, resolved)", 67 | "enum": ["active", "resolved"], 68 | }, 69 | "limit": { 70 | "type": "integer", 71 | "description": "Number of markets to return (default: 10)", 72 | "default": 10, 73 | "minimum": 1, 74 | "maximum": 100 75 | }, 76 | "offset": { 77 | "type": "integer", 78 | "description": "Number of markets to skip (for pagination)", 79 | "default": 0, 80 | "minimum": 0 81 | } 82 | }, 83 | }, 84 | ), 85 | types.Tool( 86 | name="get-market-prices", 87 | description="Get current prices and trading information for a market", 88 | inputSchema={ 89 | "type": "object", 90 | "properties": { 91 | "market_id": { 92 | "type": "string", 93 | "description": "Market ID or slug", 94 | }, 95 | }, 96 | "required": ["market_id"], 97 | }, 98 | ), 99 | types.Tool( 100 | name="get-market-history", 101 | description="Get historical price and volume data for a market", 102 | inputSchema={ 103 | "type": "object", 104 | "properties": { 105 | "market_id": { 106 | "type": "string", 107 | "description": "Market ID or slug", 108 | }, 109 | "timeframe": { 110 | "type": "string", 111 | "description": "Time period for historical data", 112 | "enum": ["1d", "7d", "30d", "all"], 113 | "default": "7d" 114 | } 115 | }, 116 | "required": ["market_id"], 117 | }, 118 | ) 119 | ] 120 | 121 | def format_market_info(market_data: dict) -> str: 122 | """Format market information into a concise string.""" 123 | try: 124 | if not market_data or not isinstance(market_data, dict): 125 | return "No market information available" 126 | 127 | condition_id = market_data.get('condition_id', 'N/A') 128 | title = market_data.get('title', 'N/A') 129 | status = market_data.get('status', 'N/A') 130 | resolution_date = market_data.get('resolution_date', 'N/A') 131 | 132 | return ( 133 | f"Condition ID: {condition_id}\n" 134 | f"Title: {title}\n" 135 | f"Status: {status}\n" 136 | f"Resolution Date: {resolution_date}\n" 137 | "---" 138 | ) 139 | except Exception as e: 140 | return f"Error formatting market data: {str(e)}" 141 | 142 | def format_market_list(markets_data: list) -> str: 143 | """Format list of markets into a concise string.""" 144 | try: 145 | if not markets_data: 146 | return "No markets available" 147 | 148 | formatted_markets = ["Available Markets:\n"] 149 | 150 | for market in markets_data: 151 | try: 152 | volume = float(market.get('volume', 0)) 153 | volume_str = f"${volume:,.2f}" 154 | except (ValueError, TypeError): 155 | volume_str = f"${market.get('volume', 0)}" 156 | 157 | formatted_markets.append( 158 | f"Condition ID: {market.get('condition_id', 'N/A')}\n" 159 | f"Description: {market.get('description', 'N/A')}\n" 160 | f"Category: {market.get('category', 'N/A')}\n" 161 | f"Tokens: {market.get('question', 'N/A')}\n" 162 | f"Question: {market.get('active', 'N/A')}\n" 163 | f"Rewards: {market.get('rewards', 'N/A')}\n" 164 | f"Active: {market.get('active', 'N/A')}\n" 165 | f"Closed: {market.get('closed', 'N/A')}\n" 166 | f"Slug: {market.get('market_slug', 'N/A')}\n" 167 | f"Min Incentive size: {market.get('min_incentive_size', 'N/A')}\n" 168 | f"Max Incentive size: {market.get('max_incentive_spread', 'N/A')}\n" 169 | f"End date: {market.get('end_date_iso', 'N/A')}\n" 170 | f"Start time: {market.get('game_start_time', 'N/A')}\n" 171 | f"Min order size: {market.get('minimum_order_size', 'N/A')}\n" 172 | f"Max tick size: {market.get('minimum_tick_size', 'N/A')}\n" 173 | f"Volume: {volume_str}\n" 174 | "---\n" 175 | ) 176 | 177 | return "\n".join(formatted_markets) 178 | except Exception as e: 179 | return f"Error formatting markets list: {str(e)}" 180 | 181 | def format_market_prices(market_data: dict) -> str: 182 | """Format market prices into a concise string.""" 183 | try: 184 | if not market_data or not isinstance(market_data, dict): 185 | return market_data 186 | 187 | formatted_prices = [ 188 | f"Current Market Prices for {market_data.get('title', 'Unknown Market')}\n" 189 | ] 190 | 191 | # Extract price information from market data 192 | # Note: Adjust this based on actual CLOB client response structure 193 | current_price = market_data.get('current_price', 'N/A') 194 | formatted_prices.append( 195 | f"Current Price: {current_price}\n" 196 | "---\n" 197 | ) 198 | 199 | return "\n".join(formatted_prices) 200 | except Exception as e: 201 | return f"Error formatting price data: {str(e)}" 202 | 203 | def format_market_history(history_data: dict) -> str: 204 | """Format market history data into a concise string.""" 205 | try: 206 | if not history_data or not isinstance(history_data, dict): 207 | return "No historical data available" 208 | 209 | formatted_history = [ 210 | f"Historical Data for {history_data.get('title', 'Unknown Market')}\n" 211 | ] 212 | 213 | # Format historical data points 214 | # Note: Adjust this based on actual CLOB client response structure 215 | for point in history_data.get('history', [])[-5:]: 216 | formatted_history.append( 217 | f"Time: {point.get('timestamp', 'N/A')}\n" 218 | f"Price: {point.get('price', 'N/A')}\n" 219 | "---\n" 220 | ) 221 | 222 | return "\n".join(formatted_history) 223 | except Exception as e: 224 | return f"Error formatting historical data: {str(e)}" 225 | 226 | @server.call_tool() 227 | async def handle_call_tool( 228 | name: str, arguments: dict | None 229 | ) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]: 230 | """ 231 | Handle tool execution requests. 232 | Tools can fetch prediction market data and notify clients of changes. 233 | """ 234 | if not arguments: 235 | return [types.TextContent(type="text", text="Missing arguments for the request")] 236 | 237 | client = get_clob_client() 238 | 239 | try: 240 | if name == "get-market-info": 241 | market_id = arguments.get("market_id") 242 | if not market_id: 243 | return [types.TextContent(type="text", text="Missing market_id parameter")] 244 | 245 | market_data = client.get_market(market_id) 246 | formatted_info = format_market_info(market_data) 247 | return [types.TextContent(type="text", text=formatted_info)] 248 | 249 | elif name == "list-markets": 250 | status = arguments.get("status") 251 | 252 | # Get markets using CLOB client 253 | markets_data = client.get_markets() 254 | 255 | # Handle string response (if the response is a JSON string) 256 | if isinstance(markets_data, str): 257 | try: 258 | markets_data = json.loads(markets_data) 259 | except json.JSONDecodeError: 260 | return [types.TextContent(type="text", text="Error: Invalid response format from API")] 261 | 262 | # Ensure we have a list of markets 263 | if not isinstance(markets_data, list): 264 | if isinstance(markets_data, dict) and 'data' in markets_data: 265 | markets_data = markets_data['data'] 266 | else: 267 | return [types.TextContent(type="text", text="Error: Unexpected response format from API")] 268 | 269 | # Filter by status if specified 270 | if status: 271 | markets_data = [ 272 | market for market in markets_data 273 | if isinstance(market, dict) and market.get('status', '').lower() == status.lower() 274 | ] 275 | 276 | # Apply pagination 277 | offset = arguments.get("offset", 0) 278 | limit = arguments.get("limit", 10) 279 | markets_data = markets_data[offset:offset + limit] 280 | 281 | formatted_list = format_market_list(markets_data) 282 | return [types.TextContent(type="text", text=formatted_list)] 283 | 284 | elif name == "get-market-prices": 285 | market_id = arguments.get("market_id") 286 | if not market_id: 287 | return [types.TextContent(type="text", text="Missing market_id parameter")] 288 | 289 | market_data = client.get_market(market_id) 290 | formatted_prices = format_market_prices(market_data) 291 | return [types.TextContent(type="text", text=formatted_prices)] 292 | 293 | elif name == "get-market-history": 294 | market_id = arguments.get("market_id") 295 | timeframe = arguments.get("timeframe", "7d") 296 | 297 | if not market_id: 298 | return [types.TextContent(type="text", text="Missing market_id parameter")] 299 | 300 | # Note: Adjust this based on actual CLOB client capabilities 301 | market_data = client.get_market(market_id) 302 | formatted_history = format_market_history(market_data) 303 | return [types.TextContent(type="text", text=formatted_history)] 304 | 305 | else: 306 | return [types.TextContent(type="text", text=f"Unknown tool: {name}")] 307 | 308 | except Exception as e: 309 | return [types.TextContent(type="text", text=f"Error executing tool: {str(e)}")] 310 | 311 | async def main(): 312 | """Main entry point for the MCP server.""" 313 | async with mcp.server.stdio.stdio_server() as (read_stream, write_stream): 314 | await server.run( 315 | read_stream, 316 | write_stream, 317 | InitializationOptions( 318 | server_name="polymarket_predictions", 319 | server_version="0.1.0", 320 | capabilities=server.get_capabilities( 321 | notification_options=NotificationOptions(), 322 | experimental_capabilities={}, 323 | ), 324 | ), 325 | ) 326 | 327 | if __name__ == "__main__": 328 | asyncio.run(main()) ```