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