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/src/index.ts CHANGED
@@ -8,429 +8,691 @@ import {
8
8
  ListResourcesRequestSchema,
9
9
  ReadResourceRequestSchema,
10
10
  } from "@modelcontextprotocol/sdk/types.js";
11
- import { z } from "zod";
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
- // Get API key from environment
17
- const API_KEY = process.env.ODDS_API_KEY || "";
18
+ // ── Types ────────────────────────────────────────────────────────────────────
18
19
 
19
- if (!API_KEY) {
20
- console.error("Error: ODDS_API_KEY environment variable is required");
21
- process.exit(1);
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
- // Helper function to make API requests
25
- async function apiRequest(endpoint: string, params: Record<string, string> = {}) {
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 error = await response.text();
39
- throw new Error(`API request failed: ${response.status} - ${error}`);
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
- // Create the MCP server
46
- const server = new Server(
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: "odds-api-mcp",
49
- version: "1.0.0",
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
- capabilities: {
53
- tools: {},
54
- resources: {},
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
- // 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: [],
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
- 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: [],
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
- 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"],
156
+ all: {
157
+ type: "boolean",
158
+ description: "If true, include leagues without active events",
93
159
  },
94
160
  },
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"],
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
- 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: [],
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
- 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"],
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
- 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"],
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
- 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"],
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
- 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"],
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
- 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"],
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
- 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"],
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
- name: "get_documentation",
241
- description: "Get Odds-API.io documentation. Returns the full documentation text.",
242
- inputSchema: {
243
- type: "object",
244
- properties: {},
245
- required: [],
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
- // Handle tool calls
253
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
254
- const { name, arguments: args } = request.params;
477
+ // ── Value Bets ──────────────────────────────────────────────────
255
478
 
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
- }
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
- case "get_bookmakers": {
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
- 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
- }
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
- 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
- }
551
+ // ── Participants ────────────────────────────────────────────────
291
552
 
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
- }
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
- 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
- }
591
+ // ── Documentation ───────────────────────────────────────────────
309
592
 
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
- };
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
- 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
- }
607
+ // ── Server Setup ─────────────────────────────────────────────────────────────
331
608
 
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
- }
609
+ const toolMap = new Map(tools.map((tool) => [tool.name, tool]));
342
610
 
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
- }
611
+ const server = new Server(
612
+ { name: "odds-api-mcp", version: "1.1.1" },
613
+ { capabilities: { tools: {}, resources: {} } },
614
+ );
354
615
 
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
- }
616
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
617
+ tools: tools.map(({ name, description, inputSchema }) => ({
618
+ name,
619
+ description,
620
+ inputSchema,
621
+ })),
622
+ }));
365
623
 
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
- }
624
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
625
+ const { name, arguments: args } = request.params;
626
+ const tool = toolMap.get(name);
373
627
 
374
- default:
375
- throw new Error(`Unknown tool: ${name}`);
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 : "Unknown error";
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
- // 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
- });
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
- const text = await response.text();
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
- const json = await response.json();
672
+ if (!response.ok) throw new Error(`Failed to fetch OpenAPI spec: ${response.status}`);
422
673
  return {
423
- contents: [{ uri, mimeType: "application/json", text: JSON.stringify(json, null, 2) }],
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
- // Start the server
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 };