Skip to main content
Glama
aryankeluskar

Polymarket MCP Server

index.tsβ€’18.3 kB
import Anthropic from "@anthropic-ai/sdk"; import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; import { UnauthorizedError } from "@modelcontextprotocol/sdk/client/auth.js"; import express from "express"; import { WebSocketServer, WebSocket } from "ws"; import * as dotenv from "dotenv"; import cors from "cors"; import { SimpleOAuthProvider } from "./oauth-provider.js"; dotenv.config(); // ============================================================================ // CONFIGURATION // ============================================================================ const PORT = process.env.PORT || 5090; const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY; const POLYMARKET_MCP_URL = process.env.POLYMARKET_MCP_URL || "https://server.smithery.ai/@aryankeluskar/polymarket-mcp/mcp"; if (!ANTHROPIC_API_KEY) { console.error("❌ ANTHROPIC_API_KEY is required. Please set it in .env file"); process.exit(1); } // ============================================================================ // MCP CLIENT SETUP // ============================================================================ let mcpClient: Client | null = null; let mcpTransport: StreamableHTTPClientTransport | null = null; let oauthProvider: SimpleOAuthProvider | null = null; let availableTools: Anthropic.Messages.Tool[] = []; async function initializeMCPClient() { try { console.log("πŸ”Œ Connecting to Polymarket MCP server..."); console.log(` URL: ${POLYMARKET_MCP_URL}`); // Create OAuth provider oauthProvider = new SimpleOAuthProvider( `http://localhost:${PORT}/oauth/callback`, { client_name: "Polymarket MCP Demo", redirect_uris: [`http://localhost:${PORT}/oauth/callback`], grant_types: ["authorization_code", "refresh_token"], response_types: ["code"], token_endpoint_auth_method: "none", scope: "mcp:tools mcp:prompts mcp:resources", } ); // Try to load saved tokens await oauthProvider.loadSavedTokens(); // Use StreamableHTTP transport for Smithery servers mcpTransport = new StreamableHTTPClientTransport( new URL(POLYMARKET_MCP_URL), { authProvider: oauthProvider } ); mcpClient = new Client( { name: "polymarket-demo-client", version: "1.0.0", }, { capabilities: {}, } ); await mcpClient.connect(mcpTransport); console.log("βœ… Connected to Polymarket MCP server"); // List available tools const toolsResponse = await mcpClient.listTools(); console.log(`πŸ“‹ Found ${toolsResponse.tools.length} tools:`); toolsResponse.tools.forEach((tool, index) => { console.log(` ${index + 1}. ${tool.name} - ${tool.description}`); }); // Convert MCP tools to Anthropic tool format availableTools = toolsResponse.tools.map((tool) => ({ name: tool.name, description: tool.description || "", input_schema: tool.inputSchema as Anthropic.Messages.Tool.InputSchema, })); console.log("βœ… Tools converted to Anthropic format"); // List available prompts try { const promptsResponse = await mcpClient.listPrompts(); console.log(`πŸ“ Found ${promptsResponse.prompts.length} prompts:`); promptsResponse.prompts.forEach((prompt, index) => { console.log(` ${index + 1}. ${prompt.name} - ${prompt.description}`); }); } catch (error) { console.log("ℹ️ No prompts available"); } // List available resources try { const resourcesResponse = await mcpClient.listResources(); console.log(`πŸ“š Found ${resourcesResponse.resources.length} resources:`); resourcesResponse.resources.forEach((resource, index) => { console.log(` ${index + 1}. ${resource.uri} - ${resource.name}`); }); } catch (error) { console.log("ℹ️ No resources available"); } } catch (error) { if (error instanceof UnauthorizedError) { // OAuth authentication required console.log("\n⏳ Waiting for OAuth authentication to complete..."); console.log(" Once you authorize, the server will automatically reconnect.\n"); return; // Don't throw, let the callback handle reconnection } console.error("❌ Failed to connect to MCP server:", error); throw error; } } async function initializeTools(): Promise<void> { if (!mcpClient) { throw new Error("MCP client not initialized"); } // List available tools const toolsResponse = await mcpClient.listTools(); console.log(`πŸ“‹ Found ${toolsResponse.tools.length} tools:`); toolsResponse.tools.forEach((tool, index) => { console.log(` ${index + 1}. ${tool.name} - ${tool.description}`); }); // Convert MCP tools to Anthropic tool format availableTools = toolsResponse.tools.map((tool) => ({ name: tool.name, description: tool.description || "", input_schema: tool.inputSchema as Anthropic.Messages.Tool.InputSchema, })); console.log("βœ… Tools converted to Anthropic format"); // List prompts try { const promptsResponse = await mcpClient.listPrompts(); console.log(`πŸ“ Found ${promptsResponse.prompts.length} prompts:`); promptsResponse.prompts.forEach((prompt, index) => { console.log(` ${index + 1}. ${prompt.name} - ${prompt.description}`); }); } catch (error) { console.log("ℹ️ No prompts available"); } // List resources try { const resourcesResponse = await mcpClient.listResources(); console.log(`πŸ“š Found ${resourcesResponse.resources.length} resources:`); resourcesResponse.resources.forEach((resource, index) => { console.log(` ${index + 1}. ${resource.uri} - ${resource.name}`); }); } catch (error) { console.log("ℹ️ No resources available"); } } async function reconnectAfterAuth(authCode: string): Promise<void> { if (!mcpTransport || !oauthProvider) { throw new Error("OAuth provider not initialized"); } console.log("\nπŸ”„ Completing OAuth flow..."); await mcpTransport.finishAuth(authCode); oauthProvider.clearPendingAuth(); console.log("βœ… OAuth authentication successful!"); console.log("πŸ”Œ Fetching available tools...\n"); await initializeTools(); console.log("\nβœ… Backend fully connected and ready!"); console.log("πŸ’‘ You can now use the chat interface!\n"); } async function reconnectMCPSession(): Promise<void> { console.log("\nπŸ”„ Session expired, reconnecting to MCP server..."); // Close existing client if (mcpClient) { await mcpClient.close(); } // Create new transport and client if (!oauthProvider) { throw new Error("OAuth provider not initialized"); } mcpTransport = new StreamableHTTPClientTransport( new URL(POLYMARKET_MCP_URL), { authProvider: oauthProvider, } ); mcpClient = new Client( { name: "polymarket-mcp-demo", version: "1.0.0", }, { capabilities: {}, } ); await mcpClient.connect(mcpTransport); console.log("βœ… Reconnected to Polymarket MCP server"); await initializeTools(); console.log("βœ… Session restored!\n"); } // ============================================================================ // CLAUDE AI INTEGRATION // ============================================================================ const anthropic = new Anthropic({ apiKey: ANTHROPIC_API_KEY, }); interface Message { role: "user" | "assistant"; content: string; } async function processMessageWithClaude( userMessage: string, conversationHistory: Message[], ws: WebSocket ): Promise<void> { try { const messages: Anthropic.Messages.MessageParam[] = [ ...conversationHistory.map((msg) => ({ role: msg.role, content: msg.content, })), { role: "user", content: userMessage, }, ]; let response = await anthropic.messages.create({ model: "claude-haiku-4-5", max_tokens: 4096, system: `You are a helpful assistant with access to Polymarket prediction market data. You can help users: - Analyze prediction markets and their probabilities - Find trending markets and events - Compare markets within events - Discover markets by category - View recent trading activity - Provide insights on market sentiment **IMPORTANT SEARCH STRATEGIES:** - When searching for specific markets, ALWAYS use the \`query\` parameter in \`search_markets\` with relevant keywords - Try multiple search variations if the first search doesn't find results (e.g., "Bitcoin $100k", "BTC 100k", "Bitcoin exceed 100000") - By default, searches only return active markets. Set \`closed: true\` if you need to search closed markets - If a specific search fails, try broader searches or use \`list_tags\` to find relevant categories - Order results by \`volume24hr\` or \`volume\` to find the most active markets When users ask about prediction markets, use the available tools to fetch real-time data from Polymarket. Present the information in a clear, engaging way with proper formatting. Always explain what the probabilities mean and provide context for the markets you're analyzing.`, messages, tools: availableTools, }); // Handle tool use loop while (response.stop_reason === "tool_use") { // Find ALL tool_use blocks (Claude might call multiple tools at once) const toolUseBlocks = response.content.filter( (block) => block.type === "tool_use" ) as Anthropic.Messages.ToolUseBlock[]; if (toolUseBlocks.length === 0) break; // Add assistant message with tool uses messages.push({ role: "assistant", content: response.content, }); // Call all tools and collect results const toolResults = await Promise.all( toolUseBlocks.map(async (toolUse) => { console.log(`πŸ”§ Tool called: ${toolUse.name}`); console.log(` Input:`, JSON.stringify(toolUse.input, null, 2)); // Retry logic for session expiration let retries = 0; const maxRetries = 2; while (retries < maxRetries) { try { const result = await mcpClient!.callTool({ name: toolUse.name, arguments: toolUse.input as Record<string, unknown>, }); console.log(`βœ… Tool result received for ${toolUse.name} : ${JSON.stringify(result.content)}`); return { type: "tool_result" as const, tool_use_id: toolUse.id, content: JSON.stringify(result.content), }; } catch (error: any) { const errorMessage = error?.message || String(error); // Check if it's a session expiration error if (errorMessage.includes("Session not found or expired") && retries < maxRetries - 1) { console.log(`⚠️ Session expired, reconnecting... (attempt ${retries + 1}/${maxRetries - 1})`); await reconnectMCPSession(); retries++; continue; } // If not session error or out of retries, throw throw error; } } // This should never be reached, but TypeScript needs it throw new Error(`Failed to call tool ${toolUse.name} after ${maxRetries} attempts`); }) ); // Add user message with all tool results messages.push({ role: "user", content: toolResults, }); response = await anthropic.messages.create({ model: "claude-haiku-4-5", max_tokens: 4096, messages, tools: availableTools, }); } // Stream the final response const textContent = response.content.find( (block) => block.type === "text" ) as Anthropic.Messages.TextBlock | undefined; if (textContent) { const fullText = textContent.text; // Stream word by word for better UX const words = fullText.split(" "); for (let i = 0; i < words.length; i++) { const word = i === words.length - 1 ? words[i] : words[i] + " "; ws.send(word); // Small delay for streaming effect await new Promise((resolve) => setTimeout(resolve, 10)); } } ws.send("[END]"); } catch (error) { console.error("❌ Error processing message:", error); ws.send(`Error: ${error instanceof Error ? error.message : "Unknown error"}`); ws.send("[END]"); } } // ============================================================================ // EXPRESS & WEBSOCKET SERVER // ============================================================================ const app = express(); app.use(cors()); app.use(express.json()); // OAuth callback endpoint app.get("/oauth/callback", async (req, res) => { const code = req.query.code as string; const error = req.query.error as string; if (code) { try { await reconnectAfterAuth(code); res.send(` <html> <head> <title>OAuth Success</title> <style> body { font-family: system-ui; max-width: 600px; margin: 50px auto; padding: 20px; } .success { background: #d4edda; border: 1px solid #c3e6cb; color: #155724; padding: 20px; border-radius: 8px; } h1 { margin-top: 0; } </style> </head> <body> <div class="success"> <h1>βœ… Authentication Successful!</h1> <p>You can close this window and return to your terminal.</p> <p>The Polymarket MCP server is now connected and ready to use.</p> </div> </body> </html> `); } catch (err) { console.error("Failed to complete OAuth:", err); res.status(500).send(` <html> <head> <title>OAuth Error</title> <style> body { font-family: system-ui; max-width: 600px; margin: 50px auto; padding: 20px; } .error { background: #f8d7da; border: 1px solid #f5c6cb; color: #721c24; padding: 20px; border-radius: 8px; } h1 { margin-top: 0; } </style> </head> <body> <div class="error"> <h1>❌ Authentication Failed</h1> <p>Error: ${err instanceof Error ? err.message : 'Unknown error'}</p> <p>Please check the server logs and try again.</p> </div> </body> </html> `); } } else if (error) { res.status(400).send(` <html> <head> <title>OAuth Error</title> <style> body { font-family: system-ui; max-width: 600px; margin: 50px auto; padding: 20px; } .error { background: #f8d7da; border: 1px solid #f5c6cb; color: #721c24; padding: 20px; border-radius: 8px; } h1 { margin-top: 0; } </style> </head> <body> <div class="error"> <h1>❌ Authorization Failed</h1> <p>Error: ${error}</p> </div> </body> </html> `); } else { res.status(400).send("Invalid OAuth callback"); } }); // Health check endpoint app.get("/health", (req, res) => { res.json({ status: "ok", mcpConnected: mcpClient !== null, toolsAvailable: availableTools.length, }); }); // Get available tools endpoint app.get("/tools", (req, res) => { res.json({ tools: availableTools.map((tool) => ({ name: tool.name, description: tool.description, })), }); }); // Start Express server const server = app.listen(PORT, () => { console.log(`βœ… HTTP server running on port ${PORT}`); }); // WebSocket server for chat const wss = new WebSocketServer({ server }); // Store conversation history per client const conversationHistories = new Map<WebSocket, Message[]>(); wss.on("connection", (ws) => { console.log("πŸ‘€ New client connected"); conversationHistories.set(ws, []); ws.on("message", async (data) => { const userMessage = data.toString(); console.log(`πŸ“¨ Received: ${userMessage}`); const history = conversationHistories.get(ws) || []; // Add user message to history history.push({ role: "user", content: userMessage }); // Process with Claude await processMessageWithClaude(userMessage, history.slice(0, -1), ws); // Update history with assistant response // Note: We'd need to capture the full response to add it to history // For now, we'll keep the conversation context simple }); ws.on("close", () => { console.log("πŸ‘‹ Client disconnected"); conversationHistories.delete(ws); }); ws.on("error", (error) => { console.error("❌ WebSocket error:", error); }); }); // ============================================================================ // STARTUP // ============================================================================ async function startup() { console.log("πŸš€ Starting Polymarket MCP Demo Backend...\n"); try { await initializeMCPClient(); if (mcpClient && availableTools.length > 0) { console.log("\nβœ… Backend ready!"); console.log(`πŸ“‘ WebSocket server: ws://localhost:${PORT}`); console.log(`🌐 HTTP server: http://localhost:${PORT}`); console.log("\nπŸ’‘ Connect your frontend to ws://localhost:${PORT}\n"); } else { // OAuth authentication is in progress console.log("\n⏳ Server is running, waiting for OAuth authentication..."); console.log(`πŸ“‘ WebSocket server: ws://localhost:${PORT}`); console.log(`🌐 HTTP server: http://localhost:${PORT}`); console.log(`πŸ” OAuth callback: http://localhost:${PORT}/oauth/callback\n`); } } catch (error) { console.error("❌ Startup failed:", error); process.exit(1); } } startup(); // Graceful shutdown process.on("SIGINT", async () => { console.log("\nπŸ›‘ Shutting down gracefully..."); if (mcpClient) { await mcpClient.close(); } wss.close(() => { server.close(() => { console.log("βœ… Server closed"); process.exit(0); }); }); });

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/aryankeluskar/polymarket-mcp'

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