odds-api-mcp-server 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +131 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +404 -0
- package/package.json +42 -0
- package/src/index.ts +442 -0
- package/tsconfig.json +16 -0
package/README.md
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# Odds-API.io MCP Server
|
|
2
|
+
|
|
3
|
+
Model Context Protocol (MCP) server for [Odds-API.io](https://odds-api.io) - providing AI tools like Claude, Cursor, and VS Code with direct access to sports betting odds data.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **12 API tools** for fetching sports, bookmakers, events, odds, value bets, and arbitrage opportunities
|
|
8
|
+
- **Documentation resources** for AI context
|
|
9
|
+
- **Real-time data** from 265 bookmakers across 34 sports
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
### Claude Code CLI
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
claude mcp add odds-api --env ODDS_API_KEY="your-api-key" -- npx -y odds-api-mcp-server
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### Manual Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install -g odds-api-mcp-server
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### From Source
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
git clone https://github.com/oddsapi-io/mcp-server
|
|
29
|
+
cd mcp-server
|
|
30
|
+
npm install
|
|
31
|
+
npm run build
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Configuration
|
|
35
|
+
|
|
36
|
+
### Environment Variables
|
|
37
|
+
|
|
38
|
+
- `ODDS_API_KEY` (required): Your Odds-API.io API key
|
|
39
|
+
|
|
40
|
+
Get your API key at [odds-api.io](https://odds-api.io).
|
|
41
|
+
|
|
42
|
+
### Claude Desktop
|
|
43
|
+
|
|
44
|
+
Add to your Claude Desktop config (`~/.config/claude/claude_desktop_config.json` on macOS/Linux or `%APPDATA%\Claude\claude_desktop_config.json` on Windows):
|
|
45
|
+
|
|
46
|
+
```json
|
|
47
|
+
{
|
|
48
|
+
"mcpServers": {
|
|
49
|
+
"odds-api": {
|
|
50
|
+
"command": "npx",
|
|
51
|
+
"args": ["-y", "odds-api-mcp-server"],
|
|
52
|
+
"env": {
|
|
53
|
+
"ODDS_API_KEY": "your-api-key"
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Cursor
|
|
61
|
+
|
|
62
|
+
Add to your Cursor MCP settings:
|
|
63
|
+
|
|
64
|
+
```json
|
|
65
|
+
{
|
|
66
|
+
"odds-api": {
|
|
67
|
+
"command": "npx",
|
|
68
|
+
"args": ["-y", "odds-api-mcp-server"],
|
|
69
|
+
"env": {
|
|
70
|
+
"ODDS_API_KEY": "your-api-key"
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Available Tools
|
|
77
|
+
|
|
78
|
+
| Tool | Description |
|
|
79
|
+
|------|-------------|
|
|
80
|
+
| `get_sports` | List all available sports |
|
|
81
|
+
| `get_bookmakers` | List all available bookmakers |
|
|
82
|
+
| `get_leagues` | Get leagues for a sport |
|
|
83
|
+
| `get_events` | Get events with filtering options |
|
|
84
|
+
| `get_live_events` | Get currently live events |
|
|
85
|
+
| `search_events` | Search events by text |
|
|
86
|
+
| `get_odds` | Get odds for an event |
|
|
87
|
+
| `get_multi_odds` | Get odds for multiple events (batch) |
|
|
88
|
+
| `get_value_bets` | Get value betting opportunities |
|
|
89
|
+
| `get_arbitrage_bets` | Get arbitrage opportunities |
|
|
90
|
+
| `get_participants` | Get teams/participants |
|
|
91
|
+
| `get_documentation` | Get API documentation |
|
|
92
|
+
|
|
93
|
+
## Available Resources
|
|
94
|
+
|
|
95
|
+
| Resource | Description |
|
|
96
|
+
|----------|-------------|
|
|
97
|
+
| `odds-api://documentation` | Full API documentation |
|
|
98
|
+
| `odds-api://openapi` | OpenAPI specification |
|
|
99
|
+
|
|
100
|
+
## Example Usage
|
|
101
|
+
|
|
102
|
+
Once configured, you can ask your AI assistant:
|
|
103
|
+
|
|
104
|
+
- "Get me the list of available sports"
|
|
105
|
+
- "Show me upcoming Premier League matches"
|
|
106
|
+
- "Find value bets for Bet365 with expected value above 3%"
|
|
107
|
+
- "Search for events involving Liverpool"
|
|
108
|
+
- "Get odds for event 123456 from Pinnacle and Bet365"
|
|
109
|
+
|
|
110
|
+
## Development
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
# Install dependencies
|
|
114
|
+
npm install
|
|
115
|
+
|
|
116
|
+
# Build
|
|
117
|
+
npm run build
|
|
118
|
+
|
|
119
|
+
# Run in development mode
|
|
120
|
+
npm run dev
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## License
|
|
124
|
+
|
|
125
|
+
MIT
|
|
126
|
+
|
|
127
|
+
## Links
|
|
128
|
+
|
|
129
|
+
- [Odds-API.io](https://odds-api.io) - Main website
|
|
130
|
+
- [Documentation](https://docs.odds-api.io) - API documentation
|
|
131
|
+
- [Support](mailto:hello@odds-api.io) - Email support
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
|
|
5
|
+
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
6
|
+
const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
|
|
7
|
+
const API_BASE_URL = "https://api2.odds-api.io/v3";
|
|
8
|
+
const DOCS_BASE_URL = "https://docs.odds-api.io";
|
|
9
|
+
// Get API key from environment
|
|
10
|
+
const API_KEY = process.env.ODDS_API_KEY || "";
|
|
11
|
+
if (!API_KEY) {
|
|
12
|
+
console.error("Error: ODDS_API_KEY environment variable is required");
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
// Helper function to make API requests
|
|
16
|
+
async function apiRequest(endpoint, params = {}) {
|
|
17
|
+
const url = new URL(`${API_BASE_URL}${endpoint}`);
|
|
18
|
+
url.searchParams.set("apiKey", API_KEY);
|
|
19
|
+
for (const [key, value] of Object.entries(params)) {
|
|
20
|
+
if (value) {
|
|
21
|
+
url.searchParams.set(key, value);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const response = await fetch(url.toString());
|
|
25
|
+
if (!response.ok) {
|
|
26
|
+
const error = await response.text();
|
|
27
|
+
throw new Error(`API request failed: ${response.status} - ${error}`);
|
|
28
|
+
}
|
|
29
|
+
return response.json();
|
|
30
|
+
}
|
|
31
|
+
// Create the MCP server
|
|
32
|
+
const server = new index_js_1.Server({
|
|
33
|
+
name: "odds-api-mcp",
|
|
34
|
+
version: "1.0.0",
|
|
35
|
+
}, {
|
|
36
|
+
capabilities: {
|
|
37
|
+
tools: {},
|
|
38
|
+
resources: {},
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
// Define tools
|
|
42
|
+
server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
|
|
43
|
+
return {
|
|
44
|
+
tools: [
|
|
45
|
+
{
|
|
46
|
+
name: "get_sports",
|
|
47
|
+
description: "Get list of all available sports. Returns sport name and slug for each sport.",
|
|
48
|
+
inputSchema: {
|
|
49
|
+
type: "object",
|
|
50
|
+
properties: {},
|
|
51
|
+
required: [],
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: "get_bookmakers",
|
|
56
|
+
description: "Get list of all available bookmakers. Returns bookmaker name and active status.",
|
|
57
|
+
inputSchema: {
|
|
58
|
+
type: "object",
|
|
59
|
+
properties: {},
|
|
60
|
+
required: [],
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: "get_leagues",
|
|
65
|
+
description: "Get leagues for a specific sport. Returns league name, slug, and event count.",
|
|
66
|
+
inputSchema: {
|
|
67
|
+
type: "object",
|
|
68
|
+
properties: {
|
|
69
|
+
sport: {
|
|
70
|
+
type: "string",
|
|
71
|
+
description: "Sport slug (e.g., 'football', 'basketball', 'tennis')",
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
required: ["sport"],
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
name: "get_events",
|
|
79
|
+
description: "Get events for a sport, optionally filtered by league, status, or date range.",
|
|
80
|
+
inputSchema: {
|
|
81
|
+
type: "object",
|
|
82
|
+
properties: {
|
|
83
|
+
sport: {
|
|
84
|
+
type: "string",
|
|
85
|
+
description: "Sport slug (e.g., 'football', 'basketball')",
|
|
86
|
+
},
|
|
87
|
+
league: {
|
|
88
|
+
type: "string",
|
|
89
|
+
description: "Optional league slug (e.g., 'england-premier-league')",
|
|
90
|
+
},
|
|
91
|
+
status: {
|
|
92
|
+
type: "string",
|
|
93
|
+
description: "Optional comma-separated statuses (pending, live, settled)",
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
required: ["sport"],
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
name: "get_live_events",
|
|
101
|
+
description: "Get all currently live events, optionally filtered by sport.",
|
|
102
|
+
inputSchema: {
|
|
103
|
+
type: "object",
|
|
104
|
+
properties: {
|
|
105
|
+
sport: {
|
|
106
|
+
type: "string",
|
|
107
|
+
description: "Optional sport slug to filter live events",
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
required: [],
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
name: "search_events",
|
|
115
|
+
description: "Search events by team name or other text. Returns up to 10 matching events.",
|
|
116
|
+
inputSchema: {
|
|
117
|
+
type: "object",
|
|
118
|
+
properties: {
|
|
119
|
+
query: {
|
|
120
|
+
type: "string",
|
|
121
|
+
description: "Search term (minimum 3 characters)",
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
required: ["query"],
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
name: "get_odds",
|
|
129
|
+
description: "Get betting odds for a specific event from selected bookmakers.",
|
|
130
|
+
inputSchema: {
|
|
131
|
+
type: "object",
|
|
132
|
+
properties: {
|
|
133
|
+
eventId: {
|
|
134
|
+
type: "string",
|
|
135
|
+
description: "Event ID",
|
|
136
|
+
},
|
|
137
|
+
bookmakers: {
|
|
138
|
+
type: "string",
|
|
139
|
+
description: "Comma-separated list of bookmaker names (e.g., 'Bet365,Pinnacle,Unibet')",
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
required: ["eventId", "bookmakers"],
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
name: "get_multi_odds",
|
|
147
|
+
description: "Get odds for multiple events in a single request (up to 10 events). Counts as 1 API call.",
|
|
148
|
+
inputSchema: {
|
|
149
|
+
type: "object",
|
|
150
|
+
properties: {
|
|
151
|
+
eventIds: {
|
|
152
|
+
type: "string",
|
|
153
|
+
description: "Comma-separated list of event IDs (max 10)",
|
|
154
|
+
},
|
|
155
|
+
bookmakers: {
|
|
156
|
+
type: "string",
|
|
157
|
+
description: "Comma-separated list of bookmaker names",
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
required: ["eventIds", "bookmakers"],
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
name: "get_value_bets",
|
|
165
|
+
description: "Get value betting opportunities for a specific bookmaker. Returns bets where expected value is positive.",
|
|
166
|
+
inputSchema: {
|
|
167
|
+
type: "object",
|
|
168
|
+
properties: {
|
|
169
|
+
bookmaker: {
|
|
170
|
+
type: "string",
|
|
171
|
+
description: "Bookmaker name (e.g., 'Bet365')",
|
|
172
|
+
},
|
|
173
|
+
includeEventDetails: {
|
|
174
|
+
type: "boolean",
|
|
175
|
+
description: "Include full event details in response",
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
required: ["bookmaker"],
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
name: "get_arbitrage_bets",
|
|
183
|
+
description: "Get arbitrage betting opportunities across specified bookmakers.",
|
|
184
|
+
inputSchema: {
|
|
185
|
+
type: "object",
|
|
186
|
+
properties: {
|
|
187
|
+
bookmakers: {
|
|
188
|
+
type: "string",
|
|
189
|
+
description: "Comma-separated list of bookmaker names",
|
|
190
|
+
},
|
|
191
|
+
limit: {
|
|
192
|
+
type: "number",
|
|
193
|
+
description: "Maximum number of results (default 50, max 500)",
|
|
194
|
+
},
|
|
195
|
+
includeEventDetails: {
|
|
196
|
+
type: "boolean",
|
|
197
|
+
description: "Include full event details in response",
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
required: ["bookmakers"],
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
name: "get_participants",
|
|
205
|
+
description: "Get teams/participants for a sport, optionally filtered by search term.",
|
|
206
|
+
inputSchema: {
|
|
207
|
+
type: "object",
|
|
208
|
+
properties: {
|
|
209
|
+
sport: {
|
|
210
|
+
type: "string",
|
|
211
|
+
description: "Sport slug (e.g., 'football')",
|
|
212
|
+
},
|
|
213
|
+
search: {
|
|
214
|
+
type: "string",
|
|
215
|
+
description: "Optional search term to filter by name",
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
required: ["sport"],
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
name: "get_documentation",
|
|
223
|
+
description: "Get Odds-API.io documentation. Returns the full documentation text.",
|
|
224
|
+
inputSchema: {
|
|
225
|
+
type: "object",
|
|
226
|
+
properties: {},
|
|
227
|
+
required: [],
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
],
|
|
231
|
+
};
|
|
232
|
+
});
|
|
233
|
+
// Handle tool calls
|
|
234
|
+
server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
235
|
+
const { name, arguments: args } = request.params;
|
|
236
|
+
try {
|
|
237
|
+
switch (name) {
|
|
238
|
+
case "get_sports": {
|
|
239
|
+
const data = await apiRequest("/sports");
|
|
240
|
+
return {
|
|
241
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
case "get_bookmakers": {
|
|
245
|
+
const data = await apiRequest("/bookmakers");
|
|
246
|
+
return {
|
|
247
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
case "get_leagues": {
|
|
251
|
+
const params = args;
|
|
252
|
+
const data = await apiRequest("/leagues", { sport: params.sport });
|
|
253
|
+
return {
|
|
254
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
case "get_events": {
|
|
258
|
+
const params = args;
|
|
259
|
+
const data = await apiRequest("/events", {
|
|
260
|
+
sport: params.sport,
|
|
261
|
+
league: params.league || "",
|
|
262
|
+
status: params.status || "",
|
|
263
|
+
});
|
|
264
|
+
return {
|
|
265
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
case "get_live_events": {
|
|
269
|
+
const params = args;
|
|
270
|
+
const data = await apiRequest("/events/live", {
|
|
271
|
+
sport: params.sport || "",
|
|
272
|
+
});
|
|
273
|
+
return {
|
|
274
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
case "search_events": {
|
|
278
|
+
const params = args;
|
|
279
|
+
const data = await apiRequest("/events/search", { query: params.query });
|
|
280
|
+
return {
|
|
281
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
case "get_odds": {
|
|
285
|
+
const params = args;
|
|
286
|
+
const data = await apiRequest("/odds", {
|
|
287
|
+
eventId: params.eventId,
|
|
288
|
+
bookmakers: params.bookmakers,
|
|
289
|
+
});
|
|
290
|
+
return {
|
|
291
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
case "get_multi_odds": {
|
|
295
|
+
const params = args;
|
|
296
|
+
const data = await apiRequest("/odds/multi", {
|
|
297
|
+
eventIds: params.eventIds,
|
|
298
|
+
bookmakers: params.bookmakers,
|
|
299
|
+
});
|
|
300
|
+
return {
|
|
301
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
case "get_value_bets": {
|
|
305
|
+
const params = args;
|
|
306
|
+
const data = await apiRequest("/value-bets", {
|
|
307
|
+
bookmaker: params.bookmaker,
|
|
308
|
+
includeEventDetails: params.includeEventDetails ? "true" : "false",
|
|
309
|
+
});
|
|
310
|
+
return {
|
|
311
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
case "get_arbitrage_bets": {
|
|
315
|
+
const params = args;
|
|
316
|
+
const data = await apiRequest("/arbitrage-bets", {
|
|
317
|
+
bookmakers: params.bookmakers,
|
|
318
|
+
limit: params.limit?.toString() || "",
|
|
319
|
+
includeEventDetails: params.includeEventDetails ? "true" : "false",
|
|
320
|
+
});
|
|
321
|
+
return {
|
|
322
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
case "get_participants": {
|
|
326
|
+
const params = args;
|
|
327
|
+
const data = await apiRequest("/participants", {
|
|
328
|
+
sport: params.sport,
|
|
329
|
+
search: params.search || "",
|
|
330
|
+
});
|
|
331
|
+
return {
|
|
332
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
case "get_documentation": {
|
|
336
|
+
const response = await fetch(`${DOCS_BASE_URL}/llms-full.txt`);
|
|
337
|
+
const text = await response.text();
|
|
338
|
+
return {
|
|
339
|
+
content: [{ type: "text", text }],
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
default:
|
|
343
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
catch (error) {
|
|
347
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
348
|
+
return {
|
|
349
|
+
content: [{ type: "text", text: `Error: ${message}` }],
|
|
350
|
+
isError: true,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
// Define resources
|
|
355
|
+
server.setRequestHandler(types_js_1.ListResourcesRequestSchema, async () => {
|
|
356
|
+
return {
|
|
357
|
+
resources: [
|
|
358
|
+
{
|
|
359
|
+
uri: "odds-api://documentation",
|
|
360
|
+
name: "Odds-API.io Documentation",
|
|
361
|
+
description: "Complete API documentation for Odds-API.io",
|
|
362
|
+
mimeType: "text/plain",
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
uri: "odds-api://openapi",
|
|
366
|
+
name: "OpenAPI Specification",
|
|
367
|
+
description: "OpenAPI/Swagger specification for Odds-API.io",
|
|
368
|
+
mimeType: "application/json",
|
|
369
|
+
},
|
|
370
|
+
],
|
|
371
|
+
};
|
|
372
|
+
});
|
|
373
|
+
// Handle resource reads
|
|
374
|
+
server.setRequestHandler(types_js_1.ReadResourceRequestSchema, async (request) => {
|
|
375
|
+
const { uri } = request.params;
|
|
376
|
+
switch (uri) {
|
|
377
|
+
case "odds-api://documentation": {
|
|
378
|
+
const response = await fetch(`${DOCS_BASE_URL}/llms-full.txt`);
|
|
379
|
+
const text = await response.text();
|
|
380
|
+
return {
|
|
381
|
+
contents: [{ uri, mimeType: "text/plain", text }],
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
case "odds-api://openapi": {
|
|
385
|
+
const response = await fetch(`${DOCS_BASE_URL}/api-reference/openapi.json`);
|
|
386
|
+
const json = await response.json();
|
|
387
|
+
return {
|
|
388
|
+
contents: [{ uri, mimeType: "application/json", text: JSON.stringify(json, null, 2) }],
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
default:
|
|
392
|
+
throw new Error(`Unknown resource: ${uri}`);
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
// Start the server
|
|
396
|
+
async function main() {
|
|
397
|
+
const transport = new stdio_js_1.StdioServerTransport();
|
|
398
|
+
await server.connect(transport);
|
|
399
|
+
console.error("Odds-API.io MCP Server running on stdio");
|
|
400
|
+
}
|
|
401
|
+
main().catch((error) => {
|
|
402
|
+
console.error("Fatal error:", error);
|
|
403
|
+
process.exit(1);
|
|
404
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "odds-api-mcp-server",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server for Odds-API.io - Access sports betting odds data from AI tools like Claude, Cursor, and VS Code",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"odds-api-mcp": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc",
|
|
11
|
+
"start": "node dist/index.js",
|
|
12
|
+
"dev": "ts-node src/index.ts"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"mcp",
|
|
16
|
+
"model-context-protocol",
|
|
17
|
+
"odds-api",
|
|
18
|
+
"sports-betting",
|
|
19
|
+
"odds",
|
|
20
|
+
"ai",
|
|
21
|
+
"claude",
|
|
22
|
+
"cursor"
|
|
23
|
+
],
|
|
24
|
+
"author": "Odds-API.io",
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "https://github.com/oddsapi-io/mcp-server"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@modelcontextprotocol/sdk": "^0.5.0",
|
|
32
|
+
"zod": "^3.22.4"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/node": "^20.10.0",
|
|
36
|
+
"ts-node": "^10.9.2",
|
|
37
|
+
"typescript": "^5.3.2"
|
|
38
|
+
},
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=18.0.0"
|
|
41
|
+
}
|
|
42
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
4
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
+
import {
|
|
6
|
+
CallToolRequestSchema,
|
|
7
|
+
ListToolsRequestSchema,
|
|
8
|
+
ListResourcesRequestSchema,
|
|
9
|
+
ReadResourceRequestSchema,
|
|
10
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
11
|
+
import { z } from "zod";
|
|
12
|
+
|
|
13
|
+
const API_BASE_URL = "https://api2.odds-api.io/v3";
|
|
14
|
+
const DOCS_BASE_URL = "https://docs.odds-api.io";
|
|
15
|
+
|
|
16
|
+
// Get API key from environment
|
|
17
|
+
const API_KEY = process.env.ODDS_API_KEY || "";
|
|
18
|
+
|
|
19
|
+
if (!API_KEY) {
|
|
20
|
+
console.error("Error: ODDS_API_KEY environment variable is required");
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Helper function to make API requests
|
|
25
|
+
async function apiRequest(endpoint: string, params: Record<string, string> = {}) {
|
|
26
|
+
const url = new URL(`${API_BASE_URL}${endpoint}`);
|
|
27
|
+
url.searchParams.set("apiKey", API_KEY);
|
|
28
|
+
|
|
29
|
+
for (const [key, value] of Object.entries(params)) {
|
|
30
|
+
if (value) {
|
|
31
|
+
url.searchParams.set(key, value);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const response = await fetch(url.toString());
|
|
36
|
+
|
|
37
|
+
if (!response.ok) {
|
|
38
|
+
const error = await response.text();
|
|
39
|
+
throw new Error(`API request failed: ${response.status} - ${error}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return response.json();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Create the MCP server
|
|
46
|
+
const server = new Server(
|
|
47
|
+
{
|
|
48
|
+
name: "odds-api-mcp",
|
|
49
|
+
version: "1.0.0",
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
capabilities: {
|
|
53
|
+
tools: {},
|
|
54
|
+
resources: {},
|
|
55
|
+
},
|
|
56
|
+
}
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
// Define tools
|
|
60
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
61
|
+
return {
|
|
62
|
+
tools: [
|
|
63
|
+
{
|
|
64
|
+
name: "get_sports",
|
|
65
|
+
description: "Get list of all available sports. Returns sport name and slug for each sport.",
|
|
66
|
+
inputSchema: {
|
|
67
|
+
type: "object",
|
|
68
|
+
properties: {},
|
|
69
|
+
required: [],
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: "get_bookmakers",
|
|
74
|
+
description: "Get list of all available bookmakers. Returns bookmaker name and active status.",
|
|
75
|
+
inputSchema: {
|
|
76
|
+
type: "object",
|
|
77
|
+
properties: {},
|
|
78
|
+
required: [],
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: "get_leagues",
|
|
83
|
+
description: "Get leagues for a specific sport. Returns league name, slug, and event count.",
|
|
84
|
+
inputSchema: {
|
|
85
|
+
type: "object",
|
|
86
|
+
properties: {
|
|
87
|
+
sport: {
|
|
88
|
+
type: "string",
|
|
89
|
+
description: "Sport slug (e.g., 'football', 'basketball', 'tennis')",
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
required: ["sport"],
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
name: "get_events",
|
|
97
|
+
description: "Get events for a sport, optionally filtered by league, status, or date range.",
|
|
98
|
+
inputSchema: {
|
|
99
|
+
type: "object",
|
|
100
|
+
properties: {
|
|
101
|
+
sport: {
|
|
102
|
+
type: "string",
|
|
103
|
+
description: "Sport slug (e.g., 'football', 'basketball')",
|
|
104
|
+
},
|
|
105
|
+
league: {
|
|
106
|
+
type: "string",
|
|
107
|
+
description: "Optional league slug (e.g., 'england-premier-league')",
|
|
108
|
+
},
|
|
109
|
+
status: {
|
|
110
|
+
type: "string",
|
|
111
|
+
description: "Optional comma-separated statuses (pending, live, settled)",
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
required: ["sport"],
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
name: "get_live_events",
|
|
119
|
+
description: "Get all currently live events, optionally filtered by sport.",
|
|
120
|
+
inputSchema: {
|
|
121
|
+
type: "object",
|
|
122
|
+
properties: {
|
|
123
|
+
sport: {
|
|
124
|
+
type: "string",
|
|
125
|
+
description: "Optional sport slug to filter live events",
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
required: [],
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
name: "search_events",
|
|
133
|
+
description: "Search events by team name or other text. Returns up to 10 matching events.",
|
|
134
|
+
inputSchema: {
|
|
135
|
+
type: "object",
|
|
136
|
+
properties: {
|
|
137
|
+
query: {
|
|
138
|
+
type: "string",
|
|
139
|
+
description: "Search term (minimum 3 characters)",
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
required: ["query"],
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
name: "get_odds",
|
|
147
|
+
description: "Get betting odds for a specific event from selected bookmakers.",
|
|
148
|
+
inputSchema: {
|
|
149
|
+
type: "object",
|
|
150
|
+
properties: {
|
|
151
|
+
eventId: {
|
|
152
|
+
type: "string",
|
|
153
|
+
description: "Event ID",
|
|
154
|
+
},
|
|
155
|
+
bookmakers: {
|
|
156
|
+
type: "string",
|
|
157
|
+
description: "Comma-separated list of bookmaker names (e.g., 'Bet365,Pinnacle,Unibet')",
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
required: ["eventId", "bookmakers"],
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
name: "get_multi_odds",
|
|
165
|
+
description: "Get odds for multiple events in a single request (up to 10 events). Counts as 1 API call.",
|
|
166
|
+
inputSchema: {
|
|
167
|
+
type: "object",
|
|
168
|
+
properties: {
|
|
169
|
+
eventIds: {
|
|
170
|
+
type: "string",
|
|
171
|
+
description: "Comma-separated list of event IDs (max 10)",
|
|
172
|
+
},
|
|
173
|
+
bookmakers: {
|
|
174
|
+
type: "string",
|
|
175
|
+
description: "Comma-separated list of bookmaker names",
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
required: ["eventIds", "bookmakers"],
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
name: "get_value_bets",
|
|
183
|
+
description: "Get value betting opportunities for a specific bookmaker. Returns bets where expected value is positive.",
|
|
184
|
+
inputSchema: {
|
|
185
|
+
type: "object",
|
|
186
|
+
properties: {
|
|
187
|
+
bookmaker: {
|
|
188
|
+
type: "string",
|
|
189
|
+
description: "Bookmaker name (e.g., 'Bet365')",
|
|
190
|
+
},
|
|
191
|
+
includeEventDetails: {
|
|
192
|
+
type: "boolean",
|
|
193
|
+
description: "Include full event details in response",
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
required: ["bookmaker"],
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
name: "get_arbitrage_bets",
|
|
201
|
+
description: "Get arbitrage betting opportunities across specified bookmakers.",
|
|
202
|
+
inputSchema: {
|
|
203
|
+
type: "object",
|
|
204
|
+
properties: {
|
|
205
|
+
bookmakers: {
|
|
206
|
+
type: "string",
|
|
207
|
+
description: "Comma-separated list of bookmaker names",
|
|
208
|
+
},
|
|
209
|
+
limit: {
|
|
210
|
+
type: "number",
|
|
211
|
+
description: "Maximum number of results (default 50, max 500)",
|
|
212
|
+
},
|
|
213
|
+
includeEventDetails: {
|
|
214
|
+
type: "boolean",
|
|
215
|
+
description: "Include full event details in response",
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
required: ["bookmakers"],
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
name: "get_participants",
|
|
223
|
+
description: "Get teams/participants for a sport, optionally filtered by search term.",
|
|
224
|
+
inputSchema: {
|
|
225
|
+
type: "object",
|
|
226
|
+
properties: {
|
|
227
|
+
sport: {
|
|
228
|
+
type: "string",
|
|
229
|
+
description: "Sport slug (e.g., 'football')",
|
|
230
|
+
},
|
|
231
|
+
search: {
|
|
232
|
+
type: "string",
|
|
233
|
+
description: "Optional search term to filter by name",
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
required: ["sport"],
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
name: "get_documentation",
|
|
241
|
+
description: "Get Odds-API.io documentation. Returns the full documentation text.",
|
|
242
|
+
inputSchema: {
|
|
243
|
+
type: "object",
|
|
244
|
+
properties: {},
|
|
245
|
+
required: [],
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
],
|
|
249
|
+
};
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
// Handle tool calls
|
|
253
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
254
|
+
const { name, arguments: args } = request.params;
|
|
255
|
+
|
|
256
|
+
try {
|
|
257
|
+
switch (name) {
|
|
258
|
+
case "get_sports": {
|
|
259
|
+
const data = await apiRequest("/sports");
|
|
260
|
+
return {
|
|
261
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
case "get_bookmakers": {
|
|
266
|
+
const data = await apiRequest("/bookmakers");
|
|
267
|
+
return {
|
|
268
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
case "get_leagues": {
|
|
273
|
+
const params = args as { sport: string };
|
|
274
|
+
const data = await apiRequest("/leagues", { sport: params.sport });
|
|
275
|
+
return {
|
|
276
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
case "get_events": {
|
|
281
|
+
const params = args as { sport: string; league?: string; status?: string };
|
|
282
|
+
const data = await apiRequest("/events", {
|
|
283
|
+
sport: params.sport,
|
|
284
|
+
league: params.league || "",
|
|
285
|
+
status: params.status || "",
|
|
286
|
+
});
|
|
287
|
+
return {
|
|
288
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
case "get_live_events": {
|
|
293
|
+
const params = args as { sport?: string };
|
|
294
|
+
const data = await apiRequest("/events/live", {
|
|
295
|
+
sport: params.sport || "",
|
|
296
|
+
});
|
|
297
|
+
return {
|
|
298
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
case "search_events": {
|
|
303
|
+
const params = args as { query: string };
|
|
304
|
+
const data = await apiRequest("/events/search", { query: params.query });
|
|
305
|
+
return {
|
|
306
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
case "get_odds": {
|
|
311
|
+
const params = args as { eventId: string; bookmakers: string };
|
|
312
|
+
const data = await apiRequest("/odds", {
|
|
313
|
+
eventId: params.eventId,
|
|
314
|
+
bookmakers: params.bookmakers,
|
|
315
|
+
});
|
|
316
|
+
return {
|
|
317
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
case "get_multi_odds": {
|
|
322
|
+
const params = args as { eventIds: string; bookmakers: string };
|
|
323
|
+
const data = await apiRequest("/odds/multi", {
|
|
324
|
+
eventIds: params.eventIds,
|
|
325
|
+
bookmakers: params.bookmakers,
|
|
326
|
+
});
|
|
327
|
+
return {
|
|
328
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
case "get_value_bets": {
|
|
333
|
+
const params = args as { bookmaker: string; includeEventDetails?: boolean };
|
|
334
|
+
const data = await apiRequest("/value-bets", {
|
|
335
|
+
bookmaker: params.bookmaker,
|
|
336
|
+
includeEventDetails: params.includeEventDetails ? "true" : "false",
|
|
337
|
+
});
|
|
338
|
+
return {
|
|
339
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
case "get_arbitrage_bets": {
|
|
344
|
+
const params = args as { bookmakers: string; limit?: number; includeEventDetails?: boolean };
|
|
345
|
+
const data = await apiRequest("/arbitrage-bets", {
|
|
346
|
+
bookmakers: params.bookmakers,
|
|
347
|
+
limit: params.limit?.toString() || "",
|
|
348
|
+
includeEventDetails: params.includeEventDetails ? "true" : "false",
|
|
349
|
+
});
|
|
350
|
+
return {
|
|
351
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
case "get_participants": {
|
|
356
|
+
const params = args as { sport: string; search?: string };
|
|
357
|
+
const data = await apiRequest("/participants", {
|
|
358
|
+
sport: params.sport,
|
|
359
|
+
search: params.search || "",
|
|
360
|
+
});
|
|
361
|
+
return {
|
|
362
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
case "get_documentation": {
|
|
367
|
+
const response = await fetch(`${DOCS_BASE_URL}/llms-full.txt`);
|
|
368
|
+
const text = await response.text();
|
|
369
|
+
return {
|
|
370
|
+
content: [{ type: "text", text }],
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
default:
|
|
375
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
376
|
+
}
|
|
377
|
+
} catch (error) {
|
|
378
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
379
|
+
return {
|
|
380
|
+
content: [{ type: "text", text: `Error: ${message}` }],
|
|
381
|
+
isError: true,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
// Define resources
|
|
387
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
388
|
+
return {
|
|
389
|
+
resources: [
|
|
390
|
+
{
|
|
391
|
+
uri: "odds-api://documentation",
|
|
392
|
+
name: "Odds-API.io Documentation",
|
|
393
|
+
description: "Complete API documentation for Odds-API.io",
|
|
394
|
+
mimeType: "text/plain",
|
|
395
|
+
},
|
|
396
|
+
{
|
|
397
|
+
uri: "odds-api://openapi",
|
|
398
|
+
name: "OpenAPI Specification",
|
|
399
|
+
description: "OpenAPI/Swagger specification for Odds-API.io",
|
|
400
|
+
mimeType: "application/json",
|
|
401
|
+
},
|
|
402
|
+
],
|
|
403
|
+
};
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
// Handle resource reads
|
|
407
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
408
|
+
const { uri } = request.params;
|
|
409
|
+
|
|
410
|
+
switch (uri) {
|
|
411
|
+
case "odds-api://documentation": {
|
|
412
|
+
const response = await fetch(`${DOCS_BASE_URL}/llms-full.txt`);
|
|
413
|
+
const text = await response.text();
|
|
414
|
+
return {
|
|
415
|
+
contents: [{ uri, mimeType: "text/plain", text }],
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
case "odds-api://openapi": {
|
|
420
|
+
const response = await fetch(`${DOCS_BASE_URL}/api-reference/openapi.json`);
|
|
421
|
+
const json = await response.json();
|
|
422
|
+
return {
|
|
423
|
+
contents: [{ uri, mimeType: "application/json", text: JSON.stringify(json, null, 2) }],
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
default:
|
|
428
|
+
throw new Error(`Unknown resource: ${uri}`);
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
// Start the server
|
|
433
|
+
async function main() {
|
|
434
|
+
const transport = new StdioServerTransport();
|
|
435
|
+
await server.connect(transport);
|
|
436
|
+
console.error("Odds-API.io MCP Server running on stdio");
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
main().catch((error) => {
|
|
440
|
+
console.error("Fatal error:", error);
|
|
441
|
+
process.exit(1);
|
|
442
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"declaration": true
|
|
13
|
+
},
|
|
14
|
+
"include": ["src/**/*"],
|
|
15
|
+
"exclude": ["node_modules", "dist"]
|
|
16
|
+
}
|