From f0221320b7a97972d02bc925910ad4e9d6323a7e Mon Sep 17 00:00:00 2001 From: Valentina Sgarbossa <65012675+vale9888@users.noreply.github.com> Date: Thu, 28 Aug 2025 21:02:19 +0200 Subject: [PATCH] feat: Add Polymarket connector with Gamma API integration - Add Polymarket connector with 7 tools for prediction markets - Implement Gamma API client with proper error handling - Add comprehensive test suite following project standards - Update TypeScript config to include DOM lib for fetch support - Add detailed integration plan documentation - Support both public data (no API key) and private data (with API key) - Tools include: get markets, search markets, trending markets, user profile, positions, trade history --- docs/polymarket-integration-plan.md | 385 ++++++++++++++++ .../src/connectors/polymarket.spec.ts | 406 +++++++++++++++++ .../src/connectors/polymarket.ts | 431 ++++++++++++++++++ packages/mcp-connectors/src/index.ts | 3 + packages/mcp-connectors/tsconfig.json | 2 +- 5 files changed, 1226 insertions(+), 1 deletion(-) create mode 100644 docs/polymarket-integration-plan.md create mode 100644 packages/mcp-connectors/src/connectors/polymarket.spec.ts create mode 100644 packages/mcp-connectors/src/connectors/polymarket.ts diff --git a/docs/polymarket-integration-plan.md b/docs/polymarket-integration-plan.md new file mode 100644 index 00000000..f98e7d75 --- /dev/null +++ b/docs/polymarket-integration-plan.md @@ -0,0 +1,385 @@ +# Polymarket Integration: Development & Testing Plan + +## Overview + +This document outlines the structured approach for building and testing a Polymarket integration for the MCP Connectors project. Polymarket is a prediction market platform where users can trade on real-world event outcomes. + +## 1. Research & Planning Phase + +### 1.1 Understanding Polymarket's Domain + +**Core Concepts:** +- **Prediction Markets**: Markets where users bet on event outcomes +- **Markets**: Trading venues for specific questions/events +- **Outcomes**: Possible results (Yes/No, multiple choice) +- **Positions**: User holdings in specific outcomes +- **Liquidity**: Available trading volume +- **Settlement**: How markets resolve and payouts occur + +**Key Use Cases:** +- Browse active prediction markets +- Get detailed market information and current probabilities +- Search for specific markets by topic +- View trending markets +- Check user portfolio and positions (with API key) +- View trading history (with API key) + +### 1.2 API Research Requirements + +**Before Implementation:** +1. **Study Polymarket's API documentation** (if available) +2. **Identify the actual API endpoints** and their structure +3. **Understand authentication methods** (API keys, OAuth, etc.) +4. **Map out data models** for markets, outcomes, positions, trades +5. **Test API endpoints** manually to understand response formats + +**Key Questions to Answer:** +- What's the base URL for Polymarket's API? +- What authentication method does Polymarket use? +- What are the actual endpoint paths and parameters? +- How are probabilities and prices represented? +- What's the rate limiting structure? +- Are there different permission levels (public vs private data)? + +## 2. Implementation Structure + +### 2.1 File Organization + +``` +packages/mcp-connectors/src/connectors/ +├── polymarket.ts # Main connector implementation +├── polymarket.spec.ts # Comprehensive test suite +└── __mocks__/ + └── context.ts # Mock context for testing +``` + +### 2.2 Code Structure (Following Established Patterns) + +**1. Type Definitions:** +```typescript +interface PolymarketMarket { + id: string; + question: string; + outcomes: PolymarketOutcome[]; + liquidity: number; + status: 'open' | 'closed' | 'settled'; + // ... other fields +} +``` + +**2. API Client Class:** +```typescript +class PolymarketClient { + constructor(apiKey?: string) { /* ... */ } + + async getMarkets(limit?: number, category?: string): Promise + async getMarket(marketId: string): Promise + async searchMarkets(query: string): Promise + // ... other methods +} +``` + +**3. Connector Configuration:** +```typescript +export const PolymarketConnectorConfig = mcpConnectorConfig({ + name: 'Polymarket', + key: 'polymarket', + // ... configuration + tools: (tool) => ({ + GET_MARKETS: tool({ /* ... */ }), + GET_MARKET: tool({ /* ... */ }), + // ... other tools + }), +}); +``` + +### 2.3 Tools to Implement + +**Public Data Tools (No API Key Required):** +1. `polymarket_get_markets` - List markets with optional filtering +2. `polymarket_get_market` - Get detailed market information +3. `polymarket_search_markets` - Search markets by keyword +4. `polymarket_get_trending_markets` - Get trending markets + +**Private Data Tools (API Key Required):** +5. `polymarket_get_user_profile` - User balance and portfolio summary +6. `polymarket_get_user_positions` - Current positions across markets +7. `polymarket_get_trade_history` - Recent trading activity + +## 3. Testing Strategy + +### 3.1 Test Structure (Following Project Standards) + +**File: `polymarket.spec.ts`** + +**Test Organization:** +```typescript +describe('#PolymarketConnector', () => { + describe('.GET_MARKETS', () => { + describe('when API call succeeds', () => { + describe('and markets are returned', () => { + it('returns formatted markets list', async () => { + // Test implementation + }); + }); + + describe('and no markets are returned', () => { + it('returns no markets message', async () => { + // Test implementation + }); + }); + }); + + describe('when API call fails', () => { + it('returns error message', async () => { + // Test implementation + }); + }); + }); + + // ... similar structure for all tools +}); +``` + +### 3.2 Test Coverage Requirements + +**For Each Tool:** +- ✅ **Happy Path**: Successful API calls with valid data +- ✅ **Empty Results**: API returns empty arrays/objects +- ✅ **Error Handling**: API errors, network failures +- ✅ **Input Validation**: Invalid parameters, missing required fields +- ✅ **Authentication**: API key required vs optional scenarios + +**Specific Test Cases:** +1. **GET_MARKETS**: Test with/without category filter, different limits +2. **GET_MARKET**: Test with valid/invalid market IDs +3. **SEARCH_MARKETS**: Test with various search queries +4. **User Tools**: Test with/without API key, various user states + +### 3.3 Mocking Strategy + +**Use MSW (Mock Service Worker) for API mocking:** +```typescript +import { http, HttpResponse } from 'msw'; +import { setupServer } from 'msw/node'; + +const server = setupServer( + http.get('https://api.polymarket.com/v1/markets', () => { + return HttpResponse.json({ + data: [/* mock market data */] + }); + }) +); +``` + +**Mock Context:** +```typescript +const mockContext = createMockConnectorContext({ + apiKey: 'test-api-key', +}); +``` + +## 4. Development Workflow + +### 4.1 Step-by-Step Implementation + +**Phase 1: Basic Structure** +1. ✅ Create `polymarket.ts` with type definitions +2. ✅ Implement `PolymarketClient` class with basic methods +3. ✅ Create connector configuration with placeholder tools +4. ✅ Add to main index exports + +**Phase 2: Core Functionality** +1. 🔄 Implement public data tools (GET_MARKETS, GET_MARKET, etc.) +2. 🔄 Add proper error handling and input validation +3. 🔄 Create formatting functions for LLM consumption +4. 🔄 Test with real API endpoints (if available) + +**Phase 3: User-Specific Features** +1. ⏳ Implement private data tools (user profile, positions, trades) +2. ⏳ Add authentication handling +3. ⏳ Test with API key scenarios + +**Phase 4: Testing & Polish** +1. ⏳ Write comprehensive test suite +2. ⏳ Add edge case handling +3. ⏳ Performance optimization +4. ⏳ Documentation updates + +### 4.2 Debugging Strategy + +**Local Development:** +1. **Use the testing agent** to test the connector locally +2. **Add console.log statements** for debugging API responses +3. **Test with real Polymarket API** (if available) +4. **Use browser dev tools** to inspect network requests + +**Error Handling:** +```typescript +try { + const result = await client.getMarkets(args.limit); + return formatMarketsList(result); +} catch (error) { + console.error('Polymarket API error:', error); + return `Failed to get markets: ${error instanceof Error ? error.message : String(error)}`; +} +``` + +## 5. Integration Points + +### 5.1 Adding to Main Exports + +**Update `packages/mcp-connectors/src/index.ts`:** +```typescript +import { PolymarketConnectorConfig } from './connectors/polymarket'; + +export const Connectors: readonly MCPConnectorConfig[] = [ + // ... existing connectors + PolymarketConnectorConfig, + // ... rest of connectors +]; + +export { + // ... existing exports + PolymarketConnectorConfig, + // ... rest of exports +}; +``` + +### 5.2 Configuration Requirements + +**Credentials Schema:** +```typescript +credentials: z.object({ + apiKey: z + .string() + .optional() + .describe('Polymarket API Key (optional for public data, required for user-specific data)'), +}), +``` + +**Setup Schema:** +```typescript +setup: z.object({}), +``` + +## 6. Quality Assurance + +### 6.1 Code Quality Checks + +**Before Submission:** +1. ✅ Run `npm run check` for linting +2. ✅ Run `npm run test` for all tests +3. ✅ Run `npm run build` to ensure compilation +4. ✅ Test with the testing agent locally + +### 6.2 Testing Checklist + +**Functional Testing:** +- [ ] All tools work with valid inputs +- [ ] Error handling works for invalid inputs +- [ ] API key requirements are enforced correctly +- [ ] Response formatting is consistent and readable +- [ ] Rate limiting is handled gracefully + +**Integration Testing:** +- [ ] Connector loads without errors +- [ ] All tools are accessible through MCP server +- [ ] Credentials are handled correctly +- [ ] Error messages are user-friendly + +## 7. Future Enhancements + +### 7.1 Potential Additional Features + +**Advanced Trading Features:** +- Place buy/sell orders (if API supports) +- Get order book depth +- Real-time price updates +- Market alerts and notifications + +**Analytics Features:** +- Market performance metrics +- User trading statistics +- Portfolio analytics +- Risk assessment tools + +**Social Features:** +- Market comments/discussions +- User reputation systems +- Market creation (if supported) + +### 7.2 Performance Optimizations + +**Caching Strategy:** +- Cache market data for short periods +- Implement request deduplication +- Use pagination for large datasets + +**Error Recovery:** +- Implement retry logic for failed requests +- Add circuit breaker patterns +- Graceful degradation for partial failures + +## 8. Documentation + +### 8.1 User Documentation + +**Example Prompts:** +- "Show me trending prediction markets" +- "Get details about the Bitcoin price prediction market" +- "Search for markets related to the 2024 election" +- "What are my current positions and P&L?" + +**API Reference:** +- Document all tool parameters +- Provide example responses +- Explain authentication requirements +- List common error scenarios + +### 8.2 Developer Documentation + +**Implementation Notes:** +- API endpoint documentation +- Data model explanations +- Error handling patterns +- Testing strategies + +## 9. Deployment & Monitoring + +### 9.1 Production Considerations + +**Environment Variables:** +- API rate limits +- Timeout configurations +- Logging levels + +**Monitoring:** +- API response times +- Error rates +- Usage patterns +- Rate limit usage + +### 9.2 Rollout Strategy + +**Phases:** +1. **Alpha**: Internal testing with mock data +2. **Beta**: Limited external testing with real API +3. **Production**: Full release with monitoring + +**Rollback Plan:** +- Feature flags for gradual rollout +- Monitoring alerts for issues +- Quick rollback procedures + +--- + +## Next Steps + +1. **Research Polymarket's actual API** and update the implementation accordingly +2. **Implement the basic structure** following the established patterns +3. **Write comprehensive tests** for all scenarios +4. **Test with real API endpoints** when available +5. **Iterate based on feedback** and real-world usage + +This plan provides a structured approach to building a robust Polymarket integration that follows the project's established patterns and quality standards. \ No newline at end of file diff --git a/packages/mcp-connectors/src/connectors/polymarket.spec.ts b/packages/mcp-connectors/src/connectors/polymarket.spec.ts new file mode 100644 index 00000000..7c79e1d3 --- /dev/null +++ b/packages/mcp-connectors/src/connectors/polymarket.spec.ts @@ -0,0 +1,406 @@ +import { describe, expect, it } from 'vitest'; +import { http, HttpResponse } from 'msw'; +import { setupServer } from 'msw/node'; +import type { MCPToolDefinition } from '@stackone/mcp-config-types'; +import { createMockConnectorContext } from '../__mocks__/context'; +import { PolymarketConnectorConfig } from './polymarket'; + +// Setup MSW server for API mocking +const server = setupServer( + http.get('https://gamma-api.polymarket.com/markets', () => { + return HttpResponse.json({ + data: [ + { + id: 'market-1', + question: 'Will Bitcoin reach $100k by end of 2024?', + outcomes: [ + { id: 'yes', name: 'Yes', probability: 0.65 }, + { id: 'no', name: 'No', probability: 0.35 }, + ], + liquidity: 50000, + status: 'open', + }, + ], + }); + }), + http.get('https://gamma-api.polymarket.com/markets/market-1', () => { + return HttpResponse.json({ + data: { + id: 'market-1', + question: 'Will Bitcoin reach $100k by end of 2024?', + outcomes: [ + { id: 'yes', name: 'Yes', probability: 0.65 }, + { id: 'no', name: 'No', probability: 0.35 }, + ], + liquidity: 50000, + status: 'open', + category: 'crypto', + }, + }); + }), + http.get('https://gamma-api.polymarket.com/markets/search', () => { + return HttpResponse.json({ + data: [ + { + id: 'market-1', + question: 'Bitcoin price prediction', + outcomes: [ + { id: 'yes', name: 'Yes', probability: 0.65 }, + { id: 'no', name: 'No', probability: 0.35 }, + ], + liquidity: 50000, + status: 'open', + }, + ], + }); + }), + http.get('https://gamma-api.polymarket.com/markets/trending', () => { + return HttpResponse.json({ + data: [ + { + id: 'trending-1', + question: 'Trending market question', + outcomes: [ + { id: 'yes', name: 'Yes', probability: 0.75 }, + { id: 'no', name: 'No', probability: 0.25 }, + ], + liquidity: 100000, + status: 'open', + }, + ], + }); + }), + http.get('https://gamma-api.polymarket.com/user/profile', () => { + return HttpResponse.json({ + data: { + id: 'user-1', + username: 'testuser', + balance: 1000, + totalPnL: 250, + positions: [], + }, + }); + }), + http.get('https://gamma-api.polymarket.com/user/positions', () => { + return HttpResponse.json({ + data: [ + { + marketId: 'market-1', + outcomeId: 'yes', + amount: 100, + averagePrice: 0.65, + unrealizedPnL: 50, + }, + ], + }); + }), + http.get('https://gamma-api.polymarket.com/user/trades', () => { + return HttpResponse.json({ + data: [ + { + id: 'trade-1', + marketId: 'market-1', + outcomeId: 'yes', + side: 'buy', + amount: 100, + price: 0.65, + timestamp: '2024-01-01T12:00:00Z', + status: 'filled', + }, + ], + }); + }) +); + +describe('#PolymarketConnector', () => { + beforeAll(() => server.listen()); + afterEach(() => server.resetHandlers()); + afterAll(() => server.close()); + + describe('.GET_MARKETS', () => { + describe('when API call succeeds', () => { + describe('and markets are returned', () => { + it('returns formatted markets list', async () => { + const tool = PolymarketConnectorConfig.tools.GET_MARKETS as MCPToolDefinition; + const mockContext = createMockConnectorContext({ + apiKey: 'test-api-key', + }); + + const actual = await tool.handler({ limit: 10 }, mockContext); + + expect(actual).toContain('Found 1 markets:'); + expect(actual).toContain('Will Bitcoin reach $100k by end of 2024?'); + expect(actual).toContain('Yes (65.0%)'); + }); + }); + + describe('and no markets are returned', () => { + it('returns no markets message', async () => { + server.use( + http.get('https://gamma-api.polymarket.com/markets', () => { + return HttpResponse.json({ data: [] }); + }) + ); + + const tool = PolymarketConnectorConfig.tools.GET_MARKETS as MCPToolDefinition; + const mockContext = createMockConnectorContext({ + apiKey: 'test-api-key', + }); + + const actual = await tool.handler({ limit: 10 }, mockContext); + + expect(actual).toBe('No markets found matching your criteria.'); + }); + }); + }); + + describe('when API call fails', () => { + it('returns error message', async () => { + server.use( + http.get('https://gamma-api.polymarket.com/markets', () => { + return new HttpResponse(null, { status: 500 }); + }) + ); + + const tool = PolymarketConnectorConfig.tools.GET_MARKETS as MCPToolDefinition; + const mockContext = createMockConnectorContext({ + apiKey: 'test-api-key', + }); + + const actual = await tool.handler({ limit: 10 }, mockContext); + + expect(actual).toContain('Failed to get markets:'); + expect(actual).toContain('500'); + }); + }); + }); + + describe('.GET_MARKET', () => { + describe('when API call succeeds', () => { + it('returns formatted market details', async () => { + const tool = PolymarketConnectorConfig.tools.GET_MARKET as MCPToolDefinition; + const mockContext = createMockConnectorContext({ + apiKey: 'test-api-key', + }); + + const actual = await tool.handler({ marketId: 'market-1' }, mockContext); + + expect(actual).toContain('Market: Will Bitcoin reach $100k by end of 2024?'); + expect(actual).toContain('ID: market-1'); + expect(actual).toContain('Status: open'); + expect(actual).toContain('Liquidity: $50,000'); + expect(actual).toContain('Category: crypto'); + expect(actual).toContain('• Yes: 65.0%'); + expect(actual).toContain('• No: 35.0%'); + }); + }); + + describe('when API call fails', () => { + it('returns error message', async () => { + server.use( + http.get('https://gamma-api.polymarket.com/markets/market-1', () => { + return new HttpResponse(null, { status: 404 }); + }) + ); + + const tool = PolymarketConnectorConfig.tools.GET_MARKET as MCPToolDefinition; + const mockContext = createMockConnectorContext({ + apiKey: 'test-api-key', + }); + + const actual = await tool.handler({ marketId: 'market-1' }, mockContext); + + expect(actual).toContain('Failed to get market:'); + expect(actual).toContain('404'); + }); + }); + }); + + describe('.SEARCH_MARKETS', () => { + describe('when API call succeeds', () => { + it('returns formatted search results', async () => { + const tool = PolymarketConnectorConfig.tools.SEARCH_MARKETS as MCPToolDefinition; + const mockContext = createMockConnectorContext({ + apiKey: 'test-api-key', + }); + + const actual = await tool.handler({ query: 'bitcoin', limit: 5 }, mockContext); + + expect(actual).toContain('Found 1 markets:'); + expect(actual).toContain('Bitcoin price prediction'); + }); + }); + }); + + describe('.GET_TRENDING_MARKETS', () => { + describe('when API call succeeds', () => { + it('returns formatted trending markets', async () => { + const tool = PolymarketConnectorConfig.tools.GET_TRENDING_MARKETS as MCPToolDefinition; + const mockContext = createMockConnectorContext({ + apiKey: 'test-api-key', + }); + + const actual = await tool.handler({ limit: 5 }, mockContext); + + expect(actual).toContain('Found 1 markets:'); + expect(actual).toContain('Trending market question'); + expect(actual).toContain('Yes (75.0%)'); + }); + }); + }); + + describe('.GET_USER_PROFILE', () => { + describe('when API key is provided', () => { + describe('and API call succeeds', () => { + it('returns formatted user profile', async () => { + const tool = PolymarketConnectorConfig.tools.GET_USER_PROFILE as MCPToolDefinition; + const mockContext = createMockConnectorContext({ + apiKey: 'test-api-key', + }); + + const actual = await tool.handler({}, mockContext); + + expect(actual).toContain('User Profile:'); + expect(actual).toContain('Username: testuser'); + expect(actual).toContain('Balance: $1,000'); + expect(actual).toContain('Total P&L: $250'); + expect(actual).toContain('Active Positions: 0'); + }); + }); + + describe('and API call fails', () => { + it('returns error message', async () => { + server.use( + http.get('https://gamma-api.polymarket.com/user/profile', () => { + return new HttpResponse(null, { status: 401 }); + }) + ); + + const tool = PolymarketConnectorConfig.tools.GET_USER_PROFILE as MCPToolDefinition; + const mockContext = createMockConnectorContext({ + apiKey: 'test-api-key', + }); + + const actual = await tool.handler({}, mockContext); + + expect(actual).toContain('Failed to get user profile:'); + expect(actual).toContain('401'); + }); + }); + }); + + describe('when API key is not provided', () => { + it('returns API key required message', async () => { + const tool = PolymarketConnectorConfig.tools.GET_USER_PROFILE as MCPToolDefinition; + const mockContext = createMockConnectorContext({}); + + const actual = await tool.handler({}, mockContext); + + expect(actual).toBe('API key required to access user profile. Please provide your Polymarket API key in the credentials.'); + }); + }); + }); + + describe('.GET_USER_POSITIONS', () => { + describe('when API key is provided', () => { + describe('and user has positions', () => { + it('returns formatted positions list', async () => { + const tool = PolymarketConnectorConfig.tools.GET_USER_POSITIONS as MCPToolDefinition; + const mockContext = createMockConnectorContext({ + apiKey: 'test-api-key', + }); + + const actual = await tool.handler({}, mockContext); + + expect(actual).toContain('Active Positions (1):'); + expect(actual).toContain('Market ID: market-1'); + expect(actual).toContain('Outcome ID: yes'); + expect(actual).toContain('Amount: 100'); + expect(actual).toContain('Average Price: $0.65'); + expect(actual).toContain('Unrealized P&L: $50'); + }); + }); + + describe('and user has no positions', () => { + it('returns no positions message', async () => { + server.use( + http.get('https://gamma-api.polymarket.com/user/positions', () => { + return HttpResponse.json({ data: [] }); + }) + ); + + const tool = PolymarketConnectorConfig.tools.GET_USER_POSITIONS as MCPToolDefinition; + const mockContext = createMockConnectorContext({ + apiKey: 'test-api-key', + }); + + const actual = await tool.handler({}, mockContext); + + expect(actual).toBe('No active positions found.'); + }); + }); + }); + + describe('when API key is not provided', () => { + it('returns API key required message', async () => { + const tool = PolymarketConnectorConfig.tools.GET_USER_POSITIONS as MCPToolDefinition; + const mockContext = createMockConnectorContext({}); + + const actual = await tool.handler({}, mockContext); + + expect(actual).toBe('API key required to access user positions. Please provide your Polymarket API key in the credentials.'); + }); + }); + }); + + describe('.GET_TRADE_HISTORY', () => { + describe('when API key is provided', () => { + describe('and user has trade history', () => { + it('returns formatted trade history', async () => { + const tool = PolymarketConnectorConfig.tools.GET_TRADE_HISTORY as MCPToolDefinition; + const mockContext = createMockConnectorContext({ + apiKey: 'test-api-key', + }); + + const actual = await tool.handler({ limit: 10 }, mockContext); + + expect(actual).toContain('Recent Trades (1):'); + expect(actual).toContain('BUY 100 @ $0.65'); + expect(actual).toContain('Market: market-1'); + expect(actual).toContain('Outcome: yes'); + expect(actual).toContain('Status: filled'); + }); + }); + + describe('and user has no trade history', () => { + it('returns no trade history message', async () => { + server.use( + http.get('https://gamma-api.polymarket.com/user/trades', () => { + return HttpResponse.json({ data: [] }); + }) + ); + + const tool = PolymarketConnectorConfig.tools.GET_TRADE_HISTORY as MCPToolDefinition; + const mockContext = createMockConnectorContext({ + apiKey: 'test-api-key', + }); + + const actual = await tool.handler({ limit: 10 }, mockContext); + + expect(actual).toBe('No trade history found.'); + }); + }); + }); + + describe('when API key is not provided', () => { + it('returns API key required message', async () => { + const tool = PolymarketConnectorConfig.tools.GET_TRADE_HISTORY as MCPToolDefinition; + const mockContext = createMockConnectorContext({}); + + const actual = await tool.handler({ limit: 10 }, mockContext); + + expect(actual).toBe('API key required to access trade history. Please provide your Polymarket API key in the credentials.'); + }); + }); + }); +}); \ No newline at end of file diff --git a/packages/mcp-connectors/src/connectors/polymarket.ts b/packages/mcp-connectors/src/connectors/polymarket.ts new file mode 100644 index 00000000..cf93a067 --- /dev/null +++ b/packages/mcp-connectors/src/connectors/polymarket.ts @@ -0,0 +1,431 @@ +import { mcpConnectorConfig } from '@stackone/mcp-config-types'; +import { z } from 'zod'; + +// Polymarket Gamma API Types +interface PolymarketMarket { + id: string; + question: string; + description?: string; + outcomes: PolymarketOutcome[]; + endDate?: string; + liquidity: number; + volume24h?: number; + status: 'open' | 'closed' | 'settled'; + category?: string; + tags?: string[]; + created_at: string; + updated_at: string; + // Gamma API specific fields + slug?: string; + closeDate?: string; + totalVolume?: number; + totalLiquidity?: number; +} + +interface PolymarketOutcome { + id: string; + name: string; + probability: number; // Current probability (0-1) + lastPrice?: number; + volume24h?: number; + liquidity?: number; + // Gamma API specific fields + slug?: string; + totalVolume?: number; + totalLiquidity?: number; +} + +interface PolymarketPosition { + marketId: string; + outcomeId: string; + amount: number; + averagePrice: number; + unrealizedPnL?: number; + realizedPnL?: number; +} + +interface PolymarketTrade { + id: string; + marketId: string; + outcomeId: string; + side: 'buy' | 'sell'; + amount: number; + price: number; + timestamp: string; + status: 'pending' | 'filled' | 'cancelled'; +} + +interface PolymarketUser { + id: string; + username?: string; + balance: number; + totalPnL?: number; + positions: PolymarketPosition[]; +} + +// Polymarket API Client +class PolymarketClient { + private headers: { Authorization?: string; 'Content-Type': string }; + private baseUrl: string; + + constructor(apiKey?: string) { + this.headers = { + 'Content-Type': 'application/json', + }; + + if (apiKey) { + this.headers.Authorization = `Bearer ${apiKey}`; + } + + this.baseUrl = 'https://gamma-api.polymarket.com'; + } + + async getMarkets(limit = 20, category?: string): Promise { + let url = `${this.baseUrl}/markets?limit=${limit}`; + if (category) { + url += `&category=${encodeURIComponent(category)}`; + } + + const response = await fetch(url, { + headers: this.headers, + }); + + if (!response.ok) { + throw new Error(`Polymarket Gamma API error: ${response.status} ${response.statusText}`); + } + + const result = await response.json(); + return result.data || result; + } + + async getMarket(marketId: string): Promise { + const response = await fetch(`${this.baseUrl}/markets/${marketId}`, { + headers: this.headers, + }); + + if (!response.ok) { + throw new Error(`Polymarket Gamma API error: ${response.status} ${response.statusText}`); + } + + const result = await response.json(); + return result.data || result; + } + + async searchMarkets(query: string, limit = 10): Promise { + const response = await fetch(`${this.baseUrl}/markets/search?q=${encodeURIComponent(query)}&limit=${limit}`, { + headers: this.headers, + }); + + if (!response.ok) { + throw new Error(`Polymarket Gamma API error: ${response.status} ${response.statusText}`); + } + + const result = await response.json(); + return result.data || result; + } + + async getTrendingMarkets(limit = 10): Promise { + const response = await fetch(`${this.baseUrl}/markets/trending?limit=${limit}`, { + headers: this.headers, + }); + + if (!response.ok) { + throw new Error(`Polymarket Gamma API error: ${response.status} ${response.statusText}`); + } + + const result = await response.json(); + return result.data || result; + } + + async getUserProfile(): Promise { + if (!this.headers.Authorization) { + throw new Error('API key required for user profile access'); + } + + const response = await fetch(`${this.baseUrl}/user/profile`, { + headers: this.headers, + }); + + if (!response.ok) { + throw new Error(`Polymarket Gamma API error: ${response.status} ${response.statusText}`); + } + + const result = await response.json(); + return result.data || result; + } + + async getUserPositions(): Promise { + if (!this.headers.Authorization) { + throw new Error('API key required for user positions access'); + } + + const response = await fetch(`${this.baseUrl}/user/positions`, { + headers: this.headers, + }); + + if (!response.ok) { + throw new Error(`Polymarket Gamma API error: ${response.status} ${response.statusText}`); + } + + const result = await response.json(); + return result.data || result; + } + + async getTradeHistory(limit = 20): Promise { + if (!this.headers.Authorization) { + throw new Error('API key required for trade history access'); + } + + const response = await fetch(`${this.baseUrl}/user/trades?limit=${limit}`, { + headers: this.headers, + }); + + if (!response.ok) { + throw new Error(`Polymarket Gamma API error: ${response.status} ${response.statusText}`); + } + + const result = await response.json(); + return result.data || result; + } +} + +// Helper function to format market data for LLM consumption +const formatMarketForLLM = (market: PolymarketMarket): string => { + const outcomes = market.outcomes + .map(outcome => ` • ${outcome.name}: ${(outcome.probability * 100).toFixed(1)}%`) + .join('\n'); + + return `Market: ${market.question} +ID: ${market.id} +Status: ${market.status} +Liquidity: $${market.liquidity.toLocaleString()} +${market.volume24h ? `24h Volume: $${market.volume24h.toLocaleString()}` : ''} +${market.endDate ? `End Date: ${new Date(market.endDate).toLocaleDateString()}` : ''} +${market.category ? `Category: ${market.category}` : ''} + +Outcomes: +${outcomes}`; +}; + +const formatMarketsList = (markets: PolymarketMarket[]): string => { + if (markets.length === 0) { + return 'No markets found matching your criteria.'; + } + + const formatted = markets.map((market, index) => { + const topOutcome = market.outcomes.reduce((prev, current) => + current.probability > prev.probability ? current : prev + ); + + return `${index + 1}. ${market.question} + ID: ${market.id} + Top Outcome: ${topOutcome.name} (${(topOutcome.probability * 100).toFixed(1)}%) + Liquidity: $${market.liquidity.toLocaleString()} + Status: ${market.status}`; + }).join('\n\n'); + + return `Found ${markets.length} markets:\n\n${formatted}`; +}; + +export const PolymarketConnectorConfig = mcpConnectorConfig({ + name: 'Polymarket', + key: 'polymarket', + logo: 'https://stackone-logos.com/api/polymarket/filled/svg', + version: '1.0.0', + credentials: z.object({ + apiKey: z + .string() + .optional() + .describe( + 'Polymarket Gamma API Key (optional for public data, required for user-specific data) :: https://gamma-api.polymarket.com' + ), + }), + setup: z.object({}), + examplePrompt: + 'Show me trending prediction markets and get detailed information about the market with the highest liquidity.', + tools: (tool) => ({ + GET_MARKETS: tool({ + name: 'polymarket_get_markets', + description: 'Get a list of prediction markets from Polymarket', + schema: z.object({ + limit: z + .number() + .min(1) + .max(100) + .default(20) + .describe('Maximum number of markets to return (1-100)'), + category: z + .string() + .optional() + .describe('Filter markets by category (e.g., "politics", "sports", "crypto")'), + }), + handler: async (args, context) => { + try { + const { apiKey } = await context.getCredentials(); + const client = new PolymarketClient(apiKey); + const markets = await client.getMarkets(args.limit, args.category); + return formatMarketsList(markets); + } catch (error) { + return `Failed to get markets: ${error instanceof Error ? error.message : String(error)}`; + } + }, + }), + GET_MARKET: tool({ + name: 'polymarket_get_market', + description: 'Get detailed information about a specific prediction market', + schema: z.object({ + marketId: z.string().describe('The ID of the market to retrieve'), + }), + handler: async (args, context) => { + try { + const { apiKey } = await context.getCredentials(); + const client = new PolymarketClient(apiKey); + const market = await client.getMarket(args.marketId); + return formatMarketForLLM(market); + } catch (error) { + return `Failed to get market: ${error instanceof Error ? error.message : String(error)}`; + } + }, + }), + SEARCH_MARKETS: tool({ + name: 'polymarket_search_markets', + description: 'Search for prediction markets by keyword or topic', + schema: z.object({ + query: z.string().describe('Search query for markets'), + limit: z + .number() + .min(1) + .max(50) + .default(10) + .describe('Maximum number of results to return (1-50)'), + }), + handler: async (args, context) => { + try { + const { apiKey } = await context.getCredentials(); + const client = new PolymarketClient(apiKey); + const markets = await client.searchMarkets(args.query, args.limit); + return formatMarketsList(markets); + } catch (error) { + return `Failed to search markets: ${error instanceof Error ? error.message : String(error)}`; + } + }, + }), + GET_TRENDING_MARKETS: tool({ + name: 'polymarket_get_trending_markets', + description: 'Get currently trending prediction markets', + schema: z.object({ + limit: z + .number() + .min(1) + .max(50) + .default(10) + .describe('Maximum number of trending markets to return (1-50)'), + }), + handler: async (args, context) => { + try { + const { apiKey } = await context.getCredentials(); + const client = new PolymarketClient(apiKey); + const markets = await client.getTrendingMarkets(args.limit); + return formatMarketsList(markets); + } catch (error) { + return `Failed to get trending markets: ${error instanceof Error ? error.message : String(error)}`; + } + }, + }), + GET_USER_PROFILE: tool({ + name: 'polymarket_get_user_profile', + description: 'Get user profile information including balance and portfolio summary', + schema: z.object({}), + handler: async (args, context) => { + try { + const { apiKey } = await context.getCredentials(); + if (!apiKey) { + return 'API key required to access user profile. Please provide your Polymarket API key in the credentials.'; + } + + const client = new PolymarketClient(apiKey); + const user = await client.getUserProfile(); + + return `User Profile: +Username: ${user.username || 'N/A'} +Balance: $${user.balance.toLocaleString()} +${user.totalPnL ? `Total P&L: $${user.totalPnL.toLocaleString()}` : ''} +Active Positions: ${user.positions.length}`; + } catch (error) { + return `Failed to get user profile: ${error instanceof Error ? error.message : String(error)}`; + } + }, + }), + GET_USER_POSITIONS: tool({ + name: 'polymarket_get_user_positions', + description: 'Get user\'s current positions across all markets', + schema: z.object({}), + handler: async (args, context) => { + try { + const { apiKey } = await context.getCredentials(); + if (!apiKey) { + return 'API key required to access user positions. Please provide your Polymarket API key in the credentials.'; + } + + const client = new PolymarketClient(apiKey); + const positions = await client.getUserPositions(); + + if (positions.length === 0) { + return 'No active positions found.'; + } + + const formatted = positions.map((pos, index) => + `${index + 1}. Market ID: ${pos.marketId} + Outcome ID: ${pos.outcomeId} + Amount: ${pos.amount} + Average Price: $${pos.averagePrice} + ${pos.unrealizedPnL ? `Unrealized P&L: $${pos.unrealizedPnL.toLocaleString()}` : ''}` + ).join('\n\n'); + + return `Active Positions (${positions.length}):\n\n${formatted}`; + } catch (error) { + return `Failed to get user positions: ${error instanceof Error ? error.message : String(error)}`; + } + }, + }), + GET_TRADE_HISTORY: tool({ + name: 'polymarket_get_trade_history', + description: 'Get user\'s recent trading history', + schema: z.object({ + limit: z + .number() + .min(1) + .max(100) + .default(20) + .describe('Maximum number of trades to return (1-100)'), + }), + handler: async (args, context) => { + try { + const { apiKey } = await context.getCredentials(); + if (!apiKey) { + return 'API key required to access trade history. Please provide your Polymarket API key in the credentials.'; + } + + const client = new PolymarketClient(apiKey); + const trades = await client.getTradeHistory(args.limit); + + if (trades.length === 0) { + return 'No trade history found.'; + } + + const formatted = trades.map((trade, index) => + `${index + 1}. ${trade.side.toUpperCase()} ${trade.amount} @ $${trade.price} + Market: ${trade.marketId} + Outcome: ${trade.outcomeId} + Status: ${trade.status} + Date: ${new Date(trade.timestamp).toLocaleString()}` + ).join('\n\n'); + + return `Recent Trades (${trades.length}):\n\n${formatted}`; + } catch (error) { + return `Failed to get trade history: ${error instanceof Error ? error.message : String(error)}`; + } + }, + }), + }), +}); \ No newline at end of file diff --git a/packages/mcp-connectors/src/index.ts b/packages/mcp-connectors/src/index.ts index bd784f61..83e83520 100644 --- a/packages/mcp-connectors/src/index.ts +++ b/packages/mcp-connectors/src/index.ts @@ -26,6 +26,7 @@ import { NotionConnectorConfig } from './connectors/notion'; import { OnePasswordConnectorConfig } from './connectors/onepassword'; import { ParallelConnectorConfig } from './connectors/parallel'; import { PerplexityConnectorConfig } from './connectors/perplexity'; +import { PolymarketConnectorConfig } from './connectors/polymarket'; import { ProducthuntConnectorConfig } from './connectors/producthunt'; import { LogfireConnectorConfig } from './connectors/pydantic-logfire'; import { PylonConnectorConfig } from './connectors/pylon'; @@ -71,6 +72,7 @@ export const Connectors: readonly MCPConnectorConfig[] = [ OnePasswordConnectorConfig, ParallelConnectorConfig, PerplexityConnectorConfig, + PolymarketConnectorConfig, ProducthuntConnectorConfig, PylonConnectorConfig, ReplicateConnectorConfig, @@ -114,6 +116,7 @@ export { OnePasswordConnectorConfig, ParallelConnectorConfig, PerplexityConnectorConfig, + PolymarketConnectorConfig, ProducthuntConnectorConfig, PylonConnectorConfig, ReplicateConnectorConfig, diff --git a/packages/mcp-connectors/tsconfig.json b/packages/mcp-connectors/tsconfig.json index d695f3bc..a6b279d8 100644 --- a/packages/mcp-connectors/tsconfig.json +++ b/packages/mcp-connectors/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "lib": ["ESNext"], + "lib": ["ESNext", "DOM"], "target": "ESNext", "module": "ESNext", "moduleDetection": "force",