odds-api-mcp-server 1.0.0 → 1.1.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 +2 -2
- package/dist/index.d.ts +40 -1
- package/dist/index.js +501 -329
- package/package.json +12 -6
- package/src/index.test.ts +556 -0
- package/src/index.ts +601 -335
- package/tsconfig.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,399 +1,571 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.toolMap = exports.tools = void 0;
|
|
5
|
+
exports.apiRequest = apiRequest;
|
|
6
|
+
exports.jsonResponse = jsonResponse;
|
|
7
|
+
exports.textResponse = textResponse;
|
|
8
|
+
exports.errorResponse = errorResponse;
|
|
4
9
|
const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
|
|
5
10
|
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
6
11
|
const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
|
|
12
|
+
// ── Constants ────────────────────────────────────────────────────────────────
|
|
7
13
|
const API_BASE_URL = "https://api2.odds-api.io/v3";
|
|
8
14
|
const DOCS_BASE_URL = "https://docs.odds-api.io";
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
}
|
|
15
|
-
// Helper function to make API requests
|
|
16
|
-
async function apiRequest(endpoint, params = {}) {
|
|
15
|
+
const API_KEY = process.env.ODDS_API_KEY ?? "";
|
|
16
|
+
// ── API Client ───────────────────────────────────────────────────────────────
|
|
17
|
+
async function apiRequest(endpoint, params = {}, method = "GET") {
|
|
18
|
+
if (!API_KEY) {
|
|
19
|
+
throw new Error("ODDS_API_KEY environment variable is not set");
|
|
20
|
+
}
|
|
17
21
|
const url = new URL(`${API_BASE_URL}${endpoint}`);
|
|
18
22
|
url.searchParams.set("apiKey", API_KEY);
|
|
19
23
|
for (const [key, value] of Object.entries(params)) {
|
|
20
|
-
if (value) {
|
|
21
|
-
url.searchParams.set(key, value);
|
|
24
|
+
if (value !== undefined) {
|
|
25
|
+
url.searchParams.set(key, String(value));
|
|
22
26
|
}
|
|
23
27
|
}
|
|
24
|
-
const response = await fetch(url.toString());
|
|
28
|
+
const response = await fetch(url.toString(), { method });
|
|
25
29
|
if (!response.ok) {
|
|
26
|
-
const
|
|
27
|
-
throw new Error(`API
|
|
30
|
+
const body = await response.text();
|
|
31
|
+
throw new Error(`API error ${response.status}: ${body}`);
|
|
28
32
|
}
|
|
29
33
|
return response.json();
|
|
30
34
|
}
|
|
31
|
-
//
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
35
|
+
// ── Response Helpers ─────────────────────────────────────────────────────────
|
|
36
|
+
function jsonResponse(data) {
|
|
37
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
38
|
+
}
|
|
39
|
+
function textResponse(text) {
|
|
40
|
+
return { content: [{ type: "text", text }] };
|
|
41
|
+
}
|
|
42
|
+
function errorResponse(message) {
|
|
43
|
+
return { content: [{ type: "text", text: `Error: ${message}` }], isError: true };
|
|
44
|
+
}
|
|
45
|
+
// ── Tool Definitions ─────────────────────────────────────────────────────────
|
|
46
|
+
const tools = [
|
|
47
|
+
// ── Sports ──────────────────────────────────────────────────────
|
|
48
|
+
{
|
|
49
|
+
name: "get_sports",
|
|
50
|
+
description: "List all available sports with their name and slug identifier.",
|
|
51
|
+
inputSchema: { type: "object", properties: {}, required: [] },
|
|
52
|
+
async handler() {
|
|
53
|
+
return jsonResponse(await apiRequest("/sports"));
|
|
54
|
+
},
|
|
39
55
|
},
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
56
|
+
// ── Bookmakers ──────────────────────────────────────────────────
|
|
57
|
+
{
|
|
58
|
+
name: "get_bookmakers",
|
|
59
|
+
description: "List all supported bookmakers with their name and active status.",
|
|
60
|
+
inputSchema: { type: "object", properties: {}, required: [] },
|
|
61
|
+
async handler() {
|
|
62
|
+
return jsonResponse(await apiRequest("/bookmakers"));
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: "get_selected_bookmakers",
|
|
67
|
+
description: "Get the authenticated user's currently selected bookmakers.",
|
|
68
|
+
inputSchema: { type: "object", properties: {}, required: [] },
|
|
69
|
+
async handler() {
|
|
70
|
+
return jsonResponse(await apiRequest("/bookmakers/selected"));
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
name: "select_bookmakers",
|
|
75
|
+
description: "Add bookmakers to the authenticated user's selection.",
|
|
76
|
+
inputSchema: {
|
|
77
|
+
type: "object",
|
|
78
|
+
properties: {
|
|
79
|
+
bookmakers: {
|
|
80
|
+
type: "string",
|
|
81
|
+
description: "Comma-separated bookmaker names (e.g., 'Bet365,SingBet')",
|
|
52
82
|
},
|
|
53
83
|
},
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
84
|
+
required: ["bookmakers"],
|
|
85
|
+
},
|
|
86
|
+
async handler(args) {
|
|
87
|
+
const { bookmakers } = args;
|
|
88
|
+
return jsonResponse(await apiRequest("/bookmakers/selected/select", { bookmakers }, "PUT"));
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
name: "clear_selected_bookmakers",
|
|
93
|
+
description: "Clear all selected bookmakers for the authenticated user. Limited to once every 12 hours.",
|
|
94
|
+
inputSchema: { type: "object", properties: {}, required: [] },
|
|
95
|
+
async handler() {
|
|
96
|
+
return jsonResponse(await apiRequest("/bookmakers/selected/clear", {}, "PUT"));
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
// ── Leagues ─────────────────────────────────────────────────────
|
|
100
|
+
{
|
|
101
|
+
name: "get_leagues",
|
|
102
|
+
description: "Get leagues for a sport. Returns league name, slug, and active event count.",
|
|
103
|
+
inputSchema: {
|
|
104
|
+
type: "object",
|
|
105
|
+
properties: {
|
|
106
|
+
sport: {
|
|
107
|
+
type: "string",
|
|
108
|
+
description: "Sport slug (e.g., 'football', 'basketball', 'tennis')",
|
|
109
|
+
},
|
|
110
|
+
all: {
|
|
111
|
+
type: "boolean",
|
|
112
|
+
description: "If true, include leagues without active events",
|
|
61
113
|
},
|
|
62
114
|
},
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
115
|
+
required: ["sport"],
|
|
116
|
+
},
|
|
117
|
+
async handler(args) {
|
|
118
|
+
const { sport, all } = args;
|
|
119
|
+
return jsonResponse(await apiRequest("/leagues", { sport, all: all || undefined }));
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
// ── Events ──────────────────────────────────────────────────────
|
|
123
|
+
{
|
|
124
|
+
name: "get_events",
|
|
125
|
+
description: "Get events for a sport with filtering by league, participant, status, date range, bookmaker availability, and pagination support.",
|
|
126
|
+
inputSchema: {
|
|
127
|
+
type: "object",
|
|
128
|
+
properties: {
|
|
129
|
+
sport: {
|
|
130
|
+
type: "string",
|
|
131
|
+
description: "Sport slug (e.g., 'football', 'basketball')",
|
|
132
|
+
},
|
|
133
|
+
league: {
|
|
134
|
+
type: "string",
|
|
135
|
+
description: "League slug (e.g., 'england-premier-league')",
|
|
136
|
+
},
|
|
137
|
+
participantId: {
|
|
138
|
+
type: "number",
|
|
139
|
+
description: "Filter by participant/team ID (matches home or away)",
|
|
140
|
+
},
|
|
141
|
+
status: {
|
|
142
|
+
type: "string",
|
|
143
|
+
description: "Comma-separated event statuses: pending, live, settled",
|
|
144
|
+
},
|
|
145
|
+
from: {
|
|
146
|
+
type: "string",
|
|
147
|
+
description: "Start date/time in RFC3339 format (e.g., '2025-10-28T10:00:00Z')",
|
|
148
|
+
},
|
|
149
|
+
to: {
|
|
150
|
+
type: "string",
|
|
151
|
+
description: "End date/time in RFC3339 format (e.g., '2025-10-28T23:59:59Z')",
|
|
152
|
+
},
|
|
153
|
+
bookmaker: {
|
|
154
|
+
type: "string",
|
|
155
|
+
description: "Only return events with odds from this bookmaker (e.g., 'Bet365')",
|
|
156
|
+
},
|
|
157
|
+
limit: {
|
|
158
|
+
type: "number",
|
|
159
|
+
description: "Maximum number of events to return",
|
|
160
|
+
},
|
|
161
|
+
skip: {
|
|
162
|
+
type: "number",
|
|
163
|
+
description: "Number of events to skip for pagination",
|
|
75
164
|
},
|
|
76
165
|
},
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
166
|
+
required: ["sport"],
|
|
167
|
+
},
|
|
168
|
+
async handler(args) {
|
|
169
|
+
const { sport, league, participantId, status, from, to, bookmaker, limit, skip } = args;
|
|
170
|
+
return jsonResponse(await apiRequest("/events", {
|
|
171
|
+
sport,
|
|
172
|
+
league,
|
|
173
|
+
participantId,
|
|
174
|
+
status,
|
|
175
|
+
from,
|
|
176
|
+
to,
|
|
177
|
+
bookmaker,
|
|
178
|
+
limit,
|
|
179
|
+
skip,
|
|
180
|
+
}));
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
name: "get_event",
|
|
185
|
+
description: "Get a single event by its unique ID.",
|
|
186
|
+
inputSchema: {
|
|
187
|
+
type: "object",
|
|
188
|
+
properties: {
|
|
189
|
+
id: { type: "number", description: "Event ID" },
|
|
190
|
+
},
|
|
191
|
+
required: ["id"],
|
|
192
|
+
},
|
|
193
|
+
async handler(args) {
|
|
194
|
+
const { id } = args;
|
|
195
|
+
return jsonResponse(await apiRequest(`/events/${id}`));
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
name: "get_live_events",
|
|
200
|
+
description: "Get all currently live events across all sports, optionally filtered by sport.",
|
|
201
|
+
inputSchema: {
|
|
202
|
+
type: "object",
|
|
203
|
+
properties: {
|
|
204
|
+
sport: {
|
|
205
|
+
type: "string",
|
|
206
|
+
description: "Sport slug to filter live events (e.g., 'football')",
|
|
97
207
|
},
|
|
98
208
|
},
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
209
|
+
required: [],
|
|
210
|
+
},
|
|
211
|
+
async handler(args) {
|
|
212
|
+
const { sport } = args;
|
|
213
|
+
return jsonResponse(await apiRequest("/events/live", { sport }));
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
name: "search_events",
|
|
218
|
+
description: "Search events by team name or text query. Returns up to 10 matching results.",
|
|
219
|
+
inputSchema: {
|
|
220
|
+
type: "object",
|
|
221
|
+
properties: {
|
|
222
|
+
query: {
|
|
223
|
+
type: "string",
|
|
224
|
+
description: "Search term (minimum 3 characters)",
|
|
111
225
|
},
|
|
112
226
|
},
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
227
|
+
required: ["query"],
|
|
228
|
+
},
|
|
229
|
+
async handler(args) {
|
|
230
|
+
const { query } = args;
|
|
231
|
+
return jsonResponse(await apiRequest("/events/search", { query }));
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
// ── Odds ────────────────────────────────────────────────────────
|
|
235
|
+
{
|
|
236
|
+
name: "get_odds",
|
|
237
|
+
description: "Get betting odds for a specific event from selected bookmakers.",
|
|
238
|
+
inputSchema: {
|
|
239
|
+
type: "object",
|
|
240
|
+
properties: {
|
|
241
|
+
eventId: { type: "string", description: "Event ID" },
|
|
242
|
+
bookmakers: {
|
|
243
|
+
type: "string",
|
|
244
|
+
description: "Comma-separated bookmaker names, max 30 (e.g., 'Bet365,Pinnacle,Unibet')",
|
|
125
245
|
},
|
|
126
246
|
},
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
247
|
+
required: ["eventId", "bookmakers"],
|
|
248
|
+
},
|
|
249
|
+
async handler(args) {
|
|
250
|
+
const { eventId, bookmakers } = args;
|
|
251
|
+
return jsonResponse(await apiRequest("/odds", { eventId, bookmakers }));
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
name: "get_multi_odds",
|
|
256
|
+
description: "Get odds for multiple events in a single request (up to 10 events). Counts as only 1 API call.",
|
|
257
|
+
inputSchema: {
|
|
258
|
+
type: "object",
|
|
259
|
+
properties: {
|
|
260
|
+
eventIds: {
|
|
261
|
+
type: "string",
|
|
262
|
+
description: "Comma-separated event IDs (max 10)",
|
|
263
|
+
},
|
|
264
|
+
bookmakers: {
|
|
265
|
+
type: "string",
|
|
266
|
+
description: "Comma-separated bookmaker names (max 30)",
|
|
143
267
|
},
|
|
144
268
|
},
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
269
|
+
required: ["eventIds", "bookmakers"],
|
|
270
|
+
},
|
|
271
|
+
async handler(args) {
|
|
272
|
+
const { eventIds, bookmakers } = args;
|
|
273
|
+
return jsonResponse(await apiRequest("/odds/multi", { eventIds, bookmakers }));
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
name: "get_odds_movements",
|
|
278
|
+
description: "Get historical odds line movements for an event from a bookmaker. Returns opening, latest, and all intermediate price changes for a specific market.",
|
|
279
|
+
inputSchema: {
|
|
280
|
+
type: "object",
|
|
281
|
+
properties: {
|
|
282
|
+
eventId: { type: "string", description: "Event ID" },
|
|
283
|
+
bookmaker: {
|
|
284
|
+
type: "string",
|
|
285
|
+
description: "Bookmaker name (e.g., 'Bet365')",
|
|
286
|
+
},
|
|
287
|
+
market: {
|
|
288
|
+
type: "string",
|
|
289
|
+
description: "Market type (e.g., 'ML', 'Spread', 'Totals')",
|
|
290
|
+
},
|
|
291
|
+
marketLine: {
|
|
292
|
+
type: "string",
|
|
293
|
+
description: "Handicap/total line (e.g., '0.5', '2.5'). Not required for ML markets.",
|
|
161
294
|
},
|
|
162
295
|
},
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
296
|
+
required: ["eventId", "bookmaker", "market"],
|
|
297
|
+
},
|
|
298
|
+
async handler(args) {
|
|
299
|
+
const { eventId, bookmaker, market, marketLine } = args;
|
|
300
|
+
return jsonResponse(await apiRequest("/odds/movements", { eventId, bookmaker, market, marketLine }));
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
name: "get_updated_odds",
|
|
305
|
+
description: "Get odds updated since a Unix timestamp for a bookmaker and sport. The timestamp must be at most 1 minute old. Useful for efficient polling.",
|
|
306
|
+
inputSchema: {
|
|
307
|
+
type: "object",
|
|
308
|
+
properties: {
|
|
309
|
+
since: {
|
|
310
|
+
type: "number",
|
|
311
|
+
description: "Unix timestamp (must be within the last 60 seconds)",
|
|
312
|
+
},
|
|
313
|
+
bookmaker: {
|
|
314
|
+
type: "string",
|
|
315
|
+
description: "Bookmaker name (e.g., 'Bet365')",
|
|
316
|
+
},
|
|
317
|
+
sport: {
|
|
318
|
+
type: "string",
|
|
319
|
+
description: "Sport slug (e.g., 'football')",
|
|
179
320
|
},
|
|
180
321
|
},
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
322
|
+
required: ["since", "bookmaker", "sport"],
|
|
323
|
+
},
|
|
324
|
+
async handler(args) {
|
|
325
|
+
const { since, bookmaker, sport } = args;
|
|
326
|
+
return jsonResponse(await apiRequest("/odds/updated", { since, bookmaker, sport }));
|
|
327
|
+
},
|
|
328
|
+
},
|
|
329
|
+
// ── Historical ──────────────────────────────────────────────────
|
|
330
|
+
{
|
|
331
|
+
name: "get_historical_events",
|
|
332
|
+
description: "Get finished events for a sport and league within a date range (max 31-day span).",
|
|
333
|
+
inputSchema: {
|
|
334
|
+
type: "object",
|
|
335
|
+
properties: {
|
|
336
|
+
sport: {
|
|
337
|
+
type: "string",
|
|
338
|
+
description: "Sport slug (e.g., 'football')",
|
|
339
|
+
},
|
|
340
|
+
league: {
|
|
341
|
+
type: "string",
|
|
342
|
+
description: "League slug (e.g., 'england-premier-league')",
|
|
343
|
+
},
|
|
344
|
+
from: {
|
|
345
|
+
type: "string",
|
|
346
|
+
description: "Start date in RFC3339 format (e.g., '2026-01-01T00:00:00Z')",
|
|
347
|
+
},
|
|
348
|
+
to: {
|
|
349
|
+
type: "string",
|
|
350
|
+
description: "End date in RFC3339 format (e.g., '2026-01-31T23:59:59Z')",
|
|
201
351
|
},
|
|
202
352
|
},
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
353
|
+
required: ["sport", "league", "from", "to"],
|
|
354
|
+
},
|
|
355
|
+
async handler(args) {
|
|
356
|
+
const { sport, league, from, to } = args;
|
|
357
|
+
return jsonResponse(await apiRequest("/historical/events", { sport, league, from, to }));
|
|
358
|
+
},
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
name: "get_historical_odds",
|
|
362
|
+
description: "Get closing odds and final scores for a finished event from selected bookmakers.",
|
|
363
|
+
inputSchema: {
|
|
364
|
+
type: "object",
|
|
365
|
+
properties: {
|
|
366
|
+
eventId: {
|
|
367
|
+
type: "string",
|
|
368
|
+
description: "Event ID (from get_historical_events)",
|
|
369
|
+
},
|
|
370
|
+
bookmakers: {
|
|
371
|
+
type: "string",
|
|
372
|
+
description: "Comma-separated bookmaker names (max 30)",
|
|
219
373
|
},
|
|
220
374
|
},
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
375
|
+
required: ["eventId", "bookmakers"],
|
|
376
|
+
},
|
|
377
|
+
async handler(args) {
|
|
378
|
+
const { eventId, bookmakers } = args;
|
|
379
|
+
return jsonResponse(await apiRequest("/historical/odds", { eventId, bookmakers }));
|
|
380
|
+
},
|
|
381
|
+
},
|
|
382
|
+
// ── Value Bets ──────────────────────────────────────────────────
|
|
383
|
+
{
|
|
384
|
+
name: "get_value_bets",
|
|
385
|
+
description: "Get positive expected value betting opportunities for a bookmaker. Updated every 5 seconds. EV formula: (Probability x Odds) - 1.",
|
|
386
|
+
inputSchema: {
|
|
387
|
+
type: "object",
|
|
388
|
+
properties: {
|
|
389
|
+
bookmaker: {
|
|
390
|
+
type: "string",
|
|
391
|
+
description: "Bookmaker name (e.g., 'Bet365')",
|
|
392
|
+
},
|
|
393
|
+
includeEventDetails: {
|
|
394
|
+
type: "boolean",
|
|
395
|
+
description: "Include full event details (sport, league, teams, date) in response",
|
|
228
396
|
},
|
|
229
397
|
},
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
398
|
+
required: ["bookmaker"],
|
|
399
|
+
},
|
|
400
|
+
async handler(args) {
|
|
401
|
+
const { bookmaker, includeEventDetails } = args;
|
|
402
|
+
return jsonResponse(await apiRequest("/value-bets", {
|
|
403
|
+
bookmaker,
|
|
404
|
+
includeEventDetails: includeEventDetails || undefined,
|
|
405
|
+
}));
|
|
406
|
+
},
|
|
407
|
+
},
|
|
408
|
+
// ── Arbitrage Bets ──────────────────────────────────────────────
|
|
409
|
+
{
|
|
410
|
+
name: "get_arbitrage_bets",
|
|
411
|
+
description: "Get arbitrage opportunities across bookmakers. Returns profit margin, optimal stake distribution, and direct bet links for each leg.",
|
|
412
|
+
inputSchema: {
|
|
413
|
+
type: "object",
|
|
414
|
+
properties: {
|
|
415
|
+
bookmakers: {
|
|
416
|
+
type: "string",
|
|
417
|
+
description: "Comma-separated bookmaker names (e.g., 'Bet365,SingBet')",
|
|
418
|
+
},
|
|
419
|
+
limit: {
|
|
420
|
+
type: "number",
|
|
421
|
+
description: "Maximum results (default 50, max 500)",
|
|
422
|
+
},
|
|
423
|
+
includeEventDetails: {
|
|
424
|
+
type: "boolean",
|
|
425
|
+
description: "Include full event details in response",
|
|
426
|
+
},
|
|
427
|
+
},
|
|
428
|
+
required: ["bookmakers"],
|
|
429
|
+
},
|
|
430
|
+
async handler(args) {
|
|
431
|
+
const { bookmakers, limit, includeEventDetails } = args;
|
|
432
|
+
return jsonResponse(await apiRequest("/arbitrage-bets", {
|
|
433
|
+
bookmakers,
|
|
434
|
+
limit,
|
|
435
|
+
includeEventDetails: includeEventDetails || undefined,
|
|
436
|
+
}));
|
|
437
|
+
},
|
|
438
|
+
},
|
|
439
|
+
// ── Participants ────────────────────────────────────────────────
|
|
440
|
+
{
|
|
441
|
+
name: "get_participants",
|
|
442
|
+
description: "Get teams/participants for a sport, optionally filtered by name search.",
|
|
443
|
+
inputSchema: {
|
|
444
|
+
type: "object",
|
|
445
|
+
properties: {
|
|
446
|
+
sport: {
|
|
447
|
+
type: "string",
|
|
448
|
+
description: "Sport slug (e.g., 'football')",
|
|
449
|
+
},
|
|
450
|
+
search: {
|
|
451
|
+
type: "string",
|
|
452
|
+
description: "Search term to filter by team/participant name",
|
|
453
|
+
},
|
|
454
|
+
},
|
|
455
|
+
required: ["sport"],
|
|
456
|
+
},
|
|
457
|
+
async handler(args) {
|
|
458
|
+
const { sport, search } = args;
|
|
459
|
+
return jsonResponse(await apiRequest("/participants", { sport, search }));
|
|
460
|
+
},
|
|
461
|
+
},
|
|
462
|
+
{
|
|
463
|
+
name: "get_participant",
|
|
464
|
+
description: "Get a single participant/team by their unique ID.",
|
|
465
|
+
inputSchema: {
|
|
466
|
+
type: "object",
|
|
467
|
+
properties: {
|
|
468
|
+
id: { type: "number", description: "Participant ID" },
|
|
469
|
+
},
|
|
470
|
+
required: ["id"],
|
|
471
|
+
},
|
|
472
|
+
async handler(args) {
|
|
473
|
+
const { id } = args;
|
|
474
|
+
return jsonResponse(await apiRequest(`/participants/${id}`));
|
|
475
|
+
},
|
|
476
|
+
},
|
|
477
|
+
// ── Documentation ───────────────────────────────────────────────
|
|
478
|
+
{
|
|
479
|
+
name: "get_documentation",
|
|
480
|
+
description: "Fetch the complete Odds-API.io API documentation.",
|
|
481
|
+
inputSchema: { type: "object", properties: {}, required: [] },
|
|
482
|
+
async handler() {
|
|
483
|
+
const response = await fetch(`${DOCS_BASE_URL}/llms-full.txt`);
|
|
484
|
+
if (!response.ok) {
|
|
485
|
+
throw new Error(`Failed to fetch documentation: ${response.status}`);
|
|
486
|
+
}
|
|
487
|
+
return textResponse(await response.text());
|
|
488
|
+
},
|
|
489
|
+
},
|
|
490
|
+
];
|
|
491
|
+
exports.tools = tools;
|
|
492
|
+
// ── Server Setup ─────────────────────────────────────────────────────────────
|
|
493
|
+
const toolMap = new Map(tools.map((tool) => [tool.name, tool]));
|
|
494
|
+
exports.toolMap = toolMap;
|
|
495
|
+
const server = new index_js_1.Server({ name: "odds-api-mcp", version: "1.1.0" }, { capabilities: { tools: {}, resources: {} } });
|
|
496
|
+
server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
|
|
497
|
+
tools: tools.map(({ name, description, inputSchema }) => ({
|
|
498
|
+
name,
|
|
499
|
+
description,
|
|
500
|
+
inputSchema,
|
|
501
|
+
})),
|
|
502
|
+
}));
|
|
234
503
|
server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
235
504
|
const { name, arguments: args } = request.params;
|
|
505
|
+
const tool = toolMap.get(name);
|
|
506
|
+
if (!tool) {
|
|
507
|
+
return errorResponse(`Unknown tool: ${name}`);
|
|
508
|
+
}
|
|
236
509
|
try {
|
|
237
|
-
|
|
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
|
-
}
|
|
510
|
+
return await tool.handler(args ?? {});
|
|
345
511
|
}
|
|
346
512
|
catch (error) {
|
|
347
|
-
const message = error instanceof Error ? error.message :
|
|
348
|
-
return
|
|
349
|
-
content: [{ type: "text", text: `Error: ${message}` }],
|
|
350
|
-
isError: true,
|
|
351
|
-
};
|
|
513
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
514
|
+
return errorResponse(message);
|
|
352
515
|
}
|
|
353
516
|
});
|
|
354
|
-
//
|
|
355
|
-
server.setRequestHandler(types_js_1.ListResourcesRequestSchema, async () => {
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
};
|
|
372
|
-
});
|
|
373
|
-
// Handle resource reads
|
|
517
|
+
// ── Resources ────────────────────────────────────────────────────────────────
|
|
518
|
+
server.setRequestHandler(types_js_1.ListResourcesRequestSchema, async () => ({
|
|
519
|
+
resources: [
|
|
520
|
+
{
|
|
521
|
+
uri: "odds-api://documentation",
|
|
522
|
+
name: "Odds-API.io Documentation",
|
|
523
|
+
description: "Complete API documentation for Odds-API.io",
|
|
524
|
+
mimeType: "text/plain",
|
|
525
|
+
},
|
|
526
|
+
{
|
|
527
|
+
uri: "odds-api://openapi",
|
|
528
|
+
name: "OpenAPI Specification",
|
|
529
|
+
description: "OpenAPI/Swagger specification for Odds-API.io",
|
|
530
|
+
mimeType: "application/json",
|
|
531
|
+
},
|
|
532
|
+
],
|
|
533
|
+
}));
|
|
374
534
|
server.setRequestHandler(types_js_1.ReadResourceRequestSchema, async (request) => {
|
|
375
535
|
const { uri } = request.params;
|
|
376
536
|
switch (uri) {
|
|
377
537
|
case "odds-api://documentation": {
|
|
378
538
|
const response = await fetch(`${DOCS_BASE_URL}/llms-full.txt`);
|
|
379
|
-
|
|
539
|
+
if (!response.ok)
|
|
540
|
+
throw new Error(`Failed to fetch docs: ${response.status}`);
|
|
380
541
|
return {
|
|
381
|
-
contents: [{ uri, mimeType: "text/plain", text }],
|
|
542
|
+
contents: [{ uri, mimeType: "text/plain", text: await response.text() }],
|
|
382
543
|
};
|
|
383
544
|
}
|
|
384
545
|
case "odds-api://openapi": {
|
|
385
546
|
const response = await fetch(`${DOCS_BASE_URL}/api-reference/openapi.json`);
|
|
386
|
-
|
|
547
|
+
if (!response.ok)
|
|
548
|
+
throw new Error(`Failed to fetch OpenAPI spec: ${response.status}`);
|
|
387
549
|
return {
|
|
388
|
-
contents: [
|
|
550
|
+
contents: [
|
|
551
|
+
{
|
|
552
|
+
uri,
|
|
553
|
+
mimeType: "application/json",
|
|
554
|
+
text: JSON.stringify(await response.json(), null, 2),
|
|
555
|
+
},
|
|
556
|
+
],
|
|
389
557
|
};
|
|
390
558
|
}
|
|
391
559
|
default:
|
|
392
560
|
throw new Error(`Unknown resource: ${uri}`);
|
|
393
561
|
}
|
|
394
562
|
});
|
|
395
|
-
//
|
|
563
|
+
// ── Bootstrap ────────────────────────────────────────────────────────────────
|
|
396
564
|
async function main() {
|
|
565
|
+
if (!API_KEY) {
|
|
566
|
+
console.error("Error: ODDS_API_KEY environment variable is required");
|
|
567
|
+
process.exit(1);
|
|
568
|
+
}
|
|
397
569
|
const transport = new stdio_js_1.StdioServerTransport();
|
|
398
570
|
await server.connect(transport);
|
|
399
571
|
console.error("Odds-API.io MCP Server running on stdio");
|