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/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
- // Get API key from environment
10
- const API_KEY = process.env.ODDS_API_KEY || "";
11
- if (!API_KEY) {
12
- console.error("Error: ODDS_API_KEY environment variable is required");
13
- process.exit(1);
14
- }
15
- // Helper function to make API requests
16
- async function apiRequest(endpoint, params = {}) {
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 error = await response.text();
27
- throw new Error(`API request failed: ${response.status} - ${error}`);
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
- // Create the MCP server
32
- const server = new index_js_1.Server({
33
- name: "odds-api-mcp",
34
- version: "1.0.0",
35
- }, {
36
- capabilities: {
37
- tools: {},
38
- resources: {},
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
- // Define tools
42
- server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
43
- return {
44
- tools: [
45
- {
46
- name: "get_sports",
47
- description: "Get list of all available sports. Returns sport name and slug for each sport.",
48
- inputSchema: {
49
- type: "object",
50
- properties: {},
51
- required: [],
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
- name: "get_bookmakers",
56
- description: "Get list of all available bookmakers. Returns bookmaker name and active status.",
57
- inputSchema: {
58
- type: "object",
59
- properties: {},
60
- required: [],
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
- name: "get_leagues",
65
- description: "Get leagues for a specific sport. Returns league name, slug, and event count.",
66
- inputSchema: {
67
- type: "object",
68
- properties: {
69
- sport: {
70
- type: "string",
71
- description: "Sport slug (e.g., 'football', 'basketball', 'tennis')",
72
- },
73
- },
74
- required: ["sport"],
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
- name: "get_events",
79
- description: "Get events for a sport, optionally filtered by league, status, or date range.",
80
- inputSchema: {
81
- type: "object",
82
- properties: {
83
- sport: {
84
- type: "string",
85
- description: "Sport slug (e.g., 'football', 'basketball')",
86
- },
87
- league: {
88
- type: "string",
89
- description: "Optional league slug (e.g., 'england-premier-league')",
90
- },
91
- status: {
92
- type: "string",
93
- description: "Optional comma-separated statuses (pending, live, settled)",
94
- },
95
- },
96
- required: ["sport"],
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
- name: "get_live_events",
101
- description: "Get all currently live events, optionally filtered by sport.",
102
- inputSchema: {
103
- type: "object",
104
- properties: {
105
- sport: {
106
- type: "string",
107
- description: "Optional sport slug to filter live events",
108
- },
109
- },
110
- required: [],
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
- name: "search_events",
115
- description: "Search events by team name or other text. Returns up to 10 matching events.",
116
- inputSchema: {
117
- type: "object",
118
- properties: {
119
- query: {
120
- type: "string",
121
- description: "Search term (minimum 3 characters)",
122
- },
123
- },
124
- required: ["query"],
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
- name: "get_odds",
129
- description: "Get betting odds for a specific event from selected bookmakers.",
130
- inputSchema: {
131
- type: "object",
132
- properties: {
133
- eventId: {
134
- type: "string",
135
- description: "Event ID",
136
- },
137
- bookmakers: {
138
- type: "string",
139
- description: "Comma-separated list of bookmaker names (e.g., 'Bet365,Pinnacle,Unibet')",
140
- },
141
- },
142
- required: ["eventId", "bookmakers"],
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
- name: "get_multi_odds",
147
- description: "Get odds for multiple events in a single request (up to 10 events). Counts as 1 API call.",
148
- inputSchema: {
149
- type: "object",
150
- properties: {
151
- eventIds: {
152
- type: "string",
153
- description: "Comma-separated list of event IDs (max 10)",
154
- },
155
- bookmakers: {
156
- type: "string",
157
- description: "Comma-separated list of bookmaker names",
158
- },
159
- },
160
- required: ["eventIds", "bookmakers"],
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
- name: "get_value_bets",
165
- description: "Get value betting opportunities for a specific bookmaker. Returns bets where expected value is positive.",
166
- inputSchema: {
167
- type: "object",
168
- properties: {
169
- bookmaker: {
170
- type: "string",
171
- description: "Bookmaker name (e.g., 'Bet365')",
172
- },
173
- includeEventDetails: {
174
- type: "boolean",
175
- description: "Include full event details in response",
176
- },
177
- },
178
- required: ["bookmaker"],
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
- name: "get_arbitrage_bets",
183
- description: "Get arbitrage betting opportunities across specified bookmakers.",
184
- inputSchema: {
185
- type: "object",
186
- properties: {
187
- bookmakers: {
188
- type: "string",
189
- description: "Comma-separated list of bookmaker names",
190
- },
191
- limit: {
192
- type: "number",
193
- description: "Maximum number of results (default 50, max 500)",
194
- },
195
- includeEventDetails: {
196
- type: "boolean",
197
- description: "Include full event details in response",
198
- },
199
- },
200
- required: ["bookmakers"],
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
- name: "get_participants",
205
- description: "Get teams/participants for a sport, optionally filtered by search term.",
206
- inputSchema: {
207
- type: "object",
208
- properties: {
209
- sport: {
210
- type: "string",
211
- description: "Sport slug (e.g., 'football')",
212
- },
213
- search: {
214
- type: "string",
215
- description: "Optional search term to filter by name",
216
- },
217
- },
218
- required: ["sport"],
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
- name: "get_documentation",
223
- description: "Get Odds-API.io documentation. Returns the full documentation text.",
224
- inputSchema: {
225
- type: "object",
226
- properties: {},
227
- required: [],
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
- // Handle tool calls
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
- switch (name) {
238
- case "get_sports": {
239
- const data = await apiRequest("/sports");
240
- return {
241
- content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
242
- };
243
- }
244
- case "get_bookmakers": {
245
- const data = await apiRequest("/bookmakers");
246
- return {
247
- content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
248
- };
249
- }
250
- case "get_leagues": {
251
- const params = args;
252
- const data = await apiRequest("/leagues", { sport: params.sport });
253
- return {
254
- content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
255
- };
256
- }
257
- case "get_events": {
258
- const params = args;
259
- const data = await apiRequest("/events", {
260
- sport: params.sport,
261
- league: params.league || "",
262
- status: params.status || "",
263
- });
264
- return {
265
- content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
266
- };
267
- }
268
- case "get_live_events": {
269
- const params = args;
270
- const data = await apiRequest("/events/live", {
271
- sport: params.sport || "",
272
- });
273
- return {
274
- content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
275
- };
276
- }
277
- case "search_events": {
278
- const params = args;
279
- const data = await apiRequest("/events/search", { query: params.query });
280
- return {
281
- content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
282
- };
283
- }
284
- case "get_odds": {
285
- const params = args;
286
- const data = await apiRequest("/odds", {
287
- eventId: params.eventId,
288
- bookmakers: params.bookmakers,
289
- });
290
- return {
291
- content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
292
- };
293
- }
294
- case "get_multi_odds": {
295
- const params = args;
296
- const data = await apiRequest("/odds/multi", {
297
- eventIds: params.eventIds,
298
- bookmakers: params.bookmakers,
299
- });
300
- return {
301
- content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
302
- };
303
- }
304
- case "get_value_bets": {
305
- const params = args;
306
- const data = await apiRequest("/value-bets", {
307
- bookmaker: params.bookmaker,
308
- includeEventDetails: params.includeEventDetails ? "true" : "false",
309
- });
310
- return {
311
- content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
312
- };
313
- }
314
- case "get_arbitrage_bets": {
315
- const params = args;
316
- const data = await apiRequest("/arbitrage-bets", {
317
- bookmakers: params.bookmakers,
318
- limit: params.limit?.toString() || "",
319
- includeEventDetails: params.includeEventDetails ? "true" : "false",
320
- });
321
- return {
322
- content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
323
- };
324
- }
325
- case "get_participants": {
326
- const params = args;
327
- const data = await apiRequest("/participants", {
328
- sport: params.sport,
329
- search: params.search || "",
330
- });
331
- return {
332
- content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
333
- };
334
- }
335
- case "get_documentation": {
336
- const response = await fetch(`${DOCS_BASE_URL}/llms-full.txt`);
337
- const text = await response.text();
338
- return {
339
- content: [{ type: "text", text }],
340
- };
341
- }
342
- default:
343
- throw new Error(`Unknown tool: ${name}`);
344
- }
510
+ return await tool.handler(args ?? {});
345
511
  }
346
512
  catch (error) {
347
- const message = error instanceof Error ? error.message : "Unknown error";
348
- return {
349
- content: [{ type: "text", text: `Error: ${message}` }],
350
- isError: true,
351
- };
513
+ const message = error instanceof Error ? error.message : String(error);
514
+ return errorResponse(message);
352
515
  }
353
516
  });
354
- // Define resources
355
- server.setRequestHandler(types_js_1.ListResourcesRequestSchema, async () => {
356
- return {
357
- resources: [
358
- {
359
- uri: "odds-api://documentation",
360
- name: "Odds-API.io Documentation",
361
- description: "Complete API documentation for Odds-API.io",
362
- mimeType: "text/plain",
363
- },
364
- {
365
- uri: "odds-api://openapi",
366
- name: "OpenAPI Specification",
367
- description: "OpenAPI/Swagger specification for Odds-API.io",
368
- mimeType: "application/json",
369
- },
370
- ],
371
- };
372
- });
373
- // Handle resource reads
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
- const text = await response.text();
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
- const json = await response.json();
547
+ if (!response.ok)
548
+ throw new Error(`Failed to fetch OpenAPI spec: ${response.status}`);
387
549
  return {
388
- contents: [{ uri, mimeType: "application/json", text: JSON.stringify(json, null, 2) }],
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
- // Start the server
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");