Skip to main content
Glama
main.rs26.3 kB
mod config; mod error; mod models; mod polymarket_client; use anyhow::Result; use config::Config; use models::*; use polymarket_client::PolymarketClient; use serde_json::{json, Value}; use std::collections::HashMap; use std::sync::Arc; use tokio::sync::RwLock; use tracing_subscriber::{self, EnvFilter, FmtSubscriber}; #[derive(Debug)] pub struct PolymarketMcpServer { client: Arc<PolymarketClient>, resource_cache: Arc<RwLock<HashMap<String, ResourceCache>>>, config: Arc<Config>, } impl PolymarketMcpServer { pub fn new() -> Result<Self> { let config = Arc::new(Config::load()?); let client = Arc::new(PolymarketClient::new_with_config(&config)?); Ok(Self { client, resource_cache: Arc::new(RwLock::new(HashMap::new())), config, }) } pub fn with_config(config: Config) -> Result<Self> { let config = Arc::new(config); let client = Arc::new(PolymarketClient::new_with_config(&config)?); Ok(Self { client, resource_cache: Arc::new(RwLock::new(HashMap::new())), config, }) } pub async fn get_active_markets(&self, limit: Option<u32>) -> Result<Value> { let markets = self.client.get_active_markets(limit).await?; Ok(json!({ "markets": markets, "count": markets.len() })) } pub async fn get_market_details(&self, market_id: String) -> Result<Value> { let market = self.client.get_market_by_id(&market_id).await?; Ok(json!(market)) } pub async fn search_markets(&self, keyword: String, limit: Option<u32>) -> Result<Value> { let markets = self.client.search_markets(&keyword, limit).await?; Ok(json!({ "markets": markets, "count": markets.len(), "keyword": keyword })) } pub async fn get_market_prices(&self, market_id: String) -> Result<Value> { let prices = self.client.get_market_prices(&market_id).await?; Ok(json!({ "market_id": market_id, "prices": prices })) } pub async fn get_trending_markets(&self, limit: Option<u32>) -> Result<Value> { let markets = self.client.get_trending_markets(limit).await?; Ok(json!({ "markets": markets, "count": markets.len() })) } // MCP Resources Support pub async fn list_resources(&self) -> Result<Value> { let resources = vec![ McpResource { uri: "markets:active".to_string(), name: "Active Markets".to_string(), description: "List of currently active prediction markets".to_string(), mime_type: "application/json".to_string(), }, McpResource { uri: "markets:trending".to_string(), name: "Trending Markets".to_string(), description: "Markets with highest trading volume".to_string(), mime_type: "application/json".to_string(), }, ]; Ok(json!({ "resources": resources })) } pub async fn read_resource(&self, uri: &str) -> Result<Value> { { let cache = self.resource_cache.read().await; if let Some(cached) = cache.get(uri) { if !cached.is_expired() { return Ok(json!({ "contents": [{ "uri": uri, "mimeType": "application/json", "text": cached.data }] })); } } } let content = match uri { "markets:active" => { let markets = self.client.get_active_markets(Some(20)).await?; serde_json::to_string_pretty(&json!({ "markets": markets, "count": markets.len(), "last_updated": chrono::Utc::now().to_rfc3339() }))? } "markets:trending" => { let markets = self.client.get_trending_markets(Some(10)).await?; serde_json::to_string_pretty(&json!({ "markets": markets, "count": markets.len(), "last_updated": chrono::Utc::now().to_rfc3339() }))? } _ if uri.starts_with("market:") => { let market_id = uri.strip_prefix("market:").unwrap(); let market = self.client.get_market_by_id(market_id).await?; serde_json::to_string_pretty(&market)? } _ => { return Err(anyhow::anyhow!("Unknown resource URI: {}", uri)); } }; if self.config.cache.enabled { let mut cache = self.resource_cache.write().await; let ttl = self.config.resource_cache_ttl().as_secs(); cache.insert(uri.to_string(), ResourceCache::new(content.clone(), ttl)); } Ok(json!({ "contents": [{ "uri": uri, "mimeType": "application/json", "text": content }] })) } // MCP Prompts Support pub async fn list_prompts(&self) -> Result<Value> { let prompts = vec![ McpPrompt { name: "analyze_market".to_string(), description: "Analyze a prediction market and provide insights on trends, liquidity, and potential opportunities".to_string(), arguments: vec![ McpPromptArgument { name: "market_id".to_string(), description: "The ID of the market to analyze".to_string(), required: true, } ], }, McpPrompt { name: "find_arbitrage".to_string(), description: "Look for arbitrage opportunities across multiple markets with similar outcomes".to_string(), arguments: vec![ McpPromptArgument { name: "keyword".to_string(), description: "Keyword to search for related markets".to_string(), required: true, }, McpPromptArgument { name: "limit".to_string(), description: "Maximum number of markets to analyze (default: 10)".to_string(), required: false, } ], }, McpPrompt { name: "market_summary".to_string(), description: "Provide a comprehensive summary of the top prediction markets".to_string(), arguments: vec![ McpPromptArgument { name: "category".to_string(), description: "Filter by category (optional)".to_string(), required: false, }, McpPromptArgument { name: "limit".to_string(), description: "Number of markets to include (default: 5)".to_string(), required: false, } ], }, ]; Ok(json!({ "prompts": prompts })) } pub async fn get_prompt(&self, name: &str, arguments: Option<Value>) -> Result<Value> { let args = arguments.unwrap_or_default(); let messages = match name { "analyze_market" => { let market_id = args .get("market_id") .and_then(|v| v.as_str()) .ok_or_else(|| anyhow::anyhow!("market_id argument is required"))?; let market = self.client.get_market_by_id(market_id).await?; let prices = self.client.get_market_prices(market_id).await?; vec![ McpPromptMessage { role: "user".to_string(), content: McpPromptContent::Text(format!( "Analyze this prediction market:\n\nMarket: {}\nQuestion: {}\nLiquidity: ${:.0}\nVolume: ${:.0}\nActive: {}\n\nCurrent Prices:\n{}\n\nProvide analysis on:\n1. Market sentiment and trends\n2. Liquidity assessment\n3. Price efficiency\n4. Potential trading opportunities\n5. Risk factors", market.id, market.question, market.liquidity, market.volume, market.active, serde_json::to_string_pretty(&prices)? )) } ] } "find_arbitrage" => { let keyword = args .get("keyword") .and_then(|v| v.as_str()) .ok_or_else(|| anyhow::anyhow!("keyword argument is required"))?; let limit = args .get("limit") .and_then(|v| v.as_u64()) .map(|l| l as u32) .unwrap_or(10); let markets = self.client.search_markets(keyword, Some(limit)).await?; vec![ McpPromptMessage { role: "user".to_string(), content: McpPromptContent::Text(format!( "Find arbitrage opportunities among these related markets:\n\nKeyword: {}\nMarkets found: {}\n\n{}\n\nAnalyze:\n1. Similar questions with different prices\n2. Cross-market arbitrage opportunities\n3. Risk-adjusted returns\n4. Execution feasibility\n5. Recommended actions", keyword, markets.len(), serde_json::to_string_pretty(&markets)? )) } ] } "market_summary" => { let limit = args .get("limit") .and_then(|v| v.as_u64()) .map(|l| l as u32) .unwrap_or(5); let trending = self.client.get_trending_markets(Some(limit)).await?; let active = self.client.get_active_markets(Some(limit)).await?; vec![ McpPromptMessage { role: "user".to_string(), content: McpPromptContent::Text(format!( "Provide a comprehensive market summary:\n\nTop Trending Markets (by volume):\n{}\n\nTop Active Markets:\n{}\n\nSummarize:\n1. Overall market sentiment\n2. Popular categories and themes\n3. Liquidity distribution\n4. Notable price movements\n5. Trading recommendations", serde_json::to_string_pretty(&trending)?, serde_json::to_string_pretty(&active)? )) } ] } _ => { return Err(anyhow::anyhow!("Unknown prompt: {}", name)); } }; Ok(json!({ "messages": messages })) } } use clap::{Arg, Command}; use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader as AsyncBufReader}; use tokio::signal; #[tokio::main] async fn main() -> Result<()> { // Parse command line arguments let matches = Command::new("polymarket-mcp") .version(env!("CARGO_PKG_VERSION")) .about("Polymarket Model Context Protocol Server") .arg( Arg::new("config") .short('c') .long("config") .value_name("FILE") .help("Configuration file path"), ) .arg( Arg::new("log-level") .short('l') .long("log-level") .value_name("LEVEL") .help("Log level (trace, debug, info, warn, error)") .default_value("info"), ) .arg( Arg::new("port") .short('p') .long("port") .value_name("PORT") .help("Port to listen on (for TCP mode)") .value_parser(clap::value_parser!(u16)), ) .get_matches(); // Load environment variables from .env file if it exists dotenv::dotenv().ok(); // Load configuration with optional config file override let mut config = Config::load()?; if let Some(config_path) = matches.get_one::<String>("config") { config = Config::load_from_file(config_path)?; } // Override log level if specified if let Some(log_level) = matches.get_one::<String>("log-level") { config.logging.level = log_level.clone(); } // Initialize tracing subscriber to write to stderr only let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(&config.logging.level)); // Write logs to stderr to avoid interfering with MCP JSON protocol on stdout FmtSubscriber::builder() .with_env_filter(env_filter) .with_writer(std::io::stderr) .compact() .init(); // Create the MCP server handler with configuration let server = Arc::new(PolymarketMcpServer::with_config(config)?); // Set up graceful shutdown handling let shutdown_signal = async { signal::ctrl_c() .await .expect("Failed to install CTRL+C signal handler"); }; // Set up MCP server using stdin/stdout let stdin = tokio::io::stdin(); let stdout = tokio::io::stdout(); let mut reader = AsyncBufReader::new(stdin); let mut writer = stdout; let mut line = String::new(); // Main server loop with graceful shutdown tokio::select! { _ = shutdown_signal => {} _ = async { loop { line.clear(); match reader.read_line(&mut line).await { Ok(0) => break, // EOF Ok(_) => { if let Ok(request) = serde_json::from_str::<serde_json::Value>(&line) { if let Some(response) = handle_mcp_request(&server, request).await { if let Ok(response_json) = serde_json::to_string(&response) { if writer.write_all(response_json.as_bytes()).await.is_err() || writer.write_all(b"\n").await.is_err() || writer.flush().await.is_err() { break; } } else { tracing::error!("Failed to serialize JSON response"); break; } } } else { tracing::warn!("Failed to parse JSON request: {}", line.trim()); } } Err(_) => break, } } } => {} } Ok(()) } async fn handle_mcp_request( server: &Arc<PolymarketMcpServer>, request: serde_json::Value, ) -> Option<serde_json::Value> { let method = request.get("method")?.as_str()?; let id = request.get("id").cloned(); let params = request .get("params") .cloned() .unwrap_or(serde_json::Value::Null); // Handle notifications (no response expected) if method.starts_with("notifications/") { return None; } let result = match method { "initialize" => { json!({ "protocolVersion": "2024-11-05", "capabilities": { "tools": {}, "resources": {}, "prompts": {} }, "serverInfo": { "name": "polymarket-mcp", "version": env!("CARGO_PKG_VERSION") } }) } "tools/list" => { json!({ "tools": [ { "name": "get_active_markets", "description": "Get list of active prediction markets", "inputSchema": { "type": "object", "properties": { "limit": { "type": "number", "description": "Maximum number of markets to return" } } } }, { "name": "get_market_details", "description": "Get detailed information about a specific market", "inputSchema": { "type": "object", "properties": { "market_id": { "type": "string", "description": "The ID of the market" } }, "required": ["market_id"] } }, { "name": "search_markets", "description": "Search markets by keyword", "inputSchema": { "type": "object", "properties": { "keyword": { "type": "string", "description": "Keyword to search for" }, "limit": { "type": "number", "description": "Maximum number of results" } }, "required": ["keyword"] } }, { "name": "get_market_prices", "description": "Get current prices for a market", "inputSchema": { "type": "object", "properties": { "market_id": { "type": "string", "description": "The ID of the market" } }, "required": ["market_id"] } }, { "name": "get_trending_markets", "description": "Get trending markets with high volume", "inputSchema": { "type": "object", "properties": { "limit": { "type": "number", "description": "Maximum number of markets to return" } } } } ] }) } "tools/call" => { let name = params.get("name")?.as_str()?; let arguments = params .get("arguments") .cloned() .unwrap_or(serde_json::Value::Object(Default::default())); match name { "get_active_markets" => { let limit = arguments .get("limit") .and_then(|v| v.as_u64()) .map(|l| l as u32); match server.get_active_markets(limit).await { Ok(result) => json!({ "content": [{ "type": "text", "text": serde_json::to_string_pretty(&result).unwrap() }] }), Err(e) => json!({ "content": [{ "type": "text", "text": format!("Error: {}", e) }], "isError": true }), } } "get_market_details" => { let market_id = arguments.get("market_id")?.as_str()?.to_string(); match server.get_market_details(market_id).await { Ok(result) => json!({ "content": [{ "type": "text", "text": serde_json::to_string_pretty(&result).unwrap() }] }), Err(e) => json!({ "content": [{ "type": "text", "text": format!("Error: {}", e) }], "isError": true }), } } "search_markets" => { let keyword = arguments.get("keyword")?.as_str()?.to_string(); let limit = arguments .get("limit") .and_then(|v| v.as_u64()) .map(|l| l as u32); match server.search_markets(keyword, limit).await { Ok(result) => json!({ "content": [{ "type": "text", "text": serde_json::to_string_pretty(&result).unwrap() }] }), Err(e) => json!({ "content": [{ "type": "text", "text": format!("Error: {}", e) }], "isError": true }), } } "get_market_prices" => { let market_id = arguments.get("market_id")?.as_str()?.to_string(); match server.get_market_prices(market_id).await { Ok(result) => json!({ "content": [{ "type": "text", "text": serde_json::to_string_pretty(&result).unwrap() }] }), Err(e) => json!({ "content": [{ "type": "text", "text": format!("Error: {}", e) }], "isError": true }), } } "get_trending_markets" => { let limit = arguments .get("limit") .and_then(|v| v.as_u64()) .map(|l| l as u32); match server.get_trending_markets(limit).await { Ok(result) => json!({ "content": [{ "type": "text", "text": serde_json::to_string_pretty(&result).unwrap() }] }), Err(e) => json!({ "content": [{ "type": "text", "text": format!("Error: {}", e) }], "isError": true }), } } _ => json!({ "content": [{ "type": "text", "text": format!("Unknown tool: {}", name) }], "isError": true }), } } "resources/list" => match server.list_resources().await { Ok(result) => result, Err(e) => json!({ "resources": [], "error": format!("Error listing resources: {}", e) }), }, "resources/read" => { let uri = params.get("uri")?.as_str()?; match server.read_resource(uri).await { Ok(result) => result, Err(e) => json!({ "contents": [], "error": format!("Error reading resource: {}", e) }), } } "prompts/list" => match server.list_prompts().await { Ok(result) => result, Err(e) => json!({ "prompts": [], "error": format!("Error listing prompts: {}", e) }), }, "prompts/get" => { let name = params.get("name")?.as_str()?; let arguments = params.get("arguments").cloned(); match server.get_prompt(name, arguments).await { Ok(result) => result, Err(e) => json!({ "messages": [], "error": format!("Error getting prompt: {}", e) }), } } _ => { json!({ "error": { "code": -32601, "message": "Method not found" } }) } }; Some(json!({ "jsonrpc": "2.0", "id": id, "result": result })) }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/ozgureyilmaz/polymarket-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server