@davincibets/mcp-server 1.0.1 → 1.3.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/dist/index.js CHANGED
@@ -1,15 +1,20 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Da Vinci Bets MCP Server
3
+ * Da Vinci Bets MCP Server v1.3.1
4
4
  *
5
5
  * Gives any AI assistant (Claude, Cursor, Windsurf, etc.) native access
6
6
  * to Da Vinci Bets predictions via the user's personal API key.
7
7
  *
8
8
  * Tools:
9
- * get_predictions — Fetch AI predictions by sport, market, confidence
9
+ * get_predictions — Fetch AI predictions by sport, market, team, confidence
10
10
  * get_top_picks — Get highest-conviction picks across all sports
11
+ * get_game — Find a specific game by team name (intelligent lookup)
12
+ * lookup_teams — Search/list teams by name, abbreviation, city, or sport
11
13
  * get_pick_card — Render a pick as a shareable PNG (partner tier)
12
- * get_performance — Win rates and track record stats
14
+ * get_batch_cards — Render multiple pick cards in batch
15
+ * get_performance — Active predictions snapshot
16
+ * get_track_record — Verified win rates, ROI, streaks from settled picks
17
+ * get_schedule — Today's games with prediction coverage
13
18
  *
14
19
  * Auth: DVB_API_KEY env var (dvb_live_xxx from Settings → API tab)
15
20
  * Docs: https://www.davincibets.io/features
@@ -18,13 +23,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
18
23
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
19
24
  import { z } from "zod";
20
25
  // ─── Configuration ──────────────────────────────────────────────────────────
21
- // FINDING 4 FIX: Validate DVB_API_URL to prevent key exfiltration via SSRF
22
- const ALLOWED_API_HOSTS = new Set([
23
- "www.davincibets.io",
24
- "davincibets.io",
25
- "localhost",
26
- "127.0.0.1",
27
- ]);
26
+ const ALLOWED_API_HOSTS = new Set(["www.davincibets.io", "davincibets.io"]);
28
27
  const _rawUrl = process.env.DVB_API_URL || "https://www.davincibets.io";
29
28
  try {
30
29
  const parsed = new URL(_rawUrl);
@@ -52,6 +51,1062 @@ if (!_rawKey.startsWith("dvb_live_")) {
52
51
  process.exit(1);
53
52
  }
54
53
  const API_KEY = _rawKey;
54
+ const TEAMS = [
55
+ // ── NBA (30) ────────────────────────────────────────────────────────────
56
+ {
57
+ abbr: "ATL",
58
+ nickname: "Hawks",
59
+ city: "Atlanta",
60
+ fullName: "Atlanta Hawks",
61
+ sport: "NBA",
62
+ aliases: [],
63
+ },
64
+ {
65
+ abbr: "BOS",
66
+ nickname: "Celtics",
67
+ city: "Boston",
68
+ fullName: "Boston Celtics",
69
+ sport: "NBA",
70
+ aliases: [],
71
+ },
72
+ {
73
+ abbr: "BKN",
74
+ nickname: "Nets",
75
+ city: "Brooklyn",
76
+ fullName: "Brooklyn Nets",
77
+ sport: "NBA",
78
+ aliases: ["BRK"],
79
+ },
80
+ {
81
+ abbr: "CHA",
82
+ nickname: "Hornets",
83
+ city: "Charlotte",
84
+ fullName: "Charlotte Hornets",
85
+ sport: "NBA",
86
+ aliases: ["CHH"],
87
+ },
88
+ {
89
+ abbr: "CHI",
90
+ nickname: "Bulls",
91
+ city: "Chicago",
92
+ fullName: "Chicago Bulls",
93
+ sport: "NBA",
94
+ aliases: [],
95
+ },
96
+ {
97
+ abbr: "CLE",
98
+ nickname: "Cavaliers",
99
+ city: "Cleveland",
100
+ fullName: "Cleveland Cavaliers",
101
+ sport: "NBA",
102
+ aliases: ["Cavs"],
103
+ },
104
+ {
105
+ abbr: "DAL",
106
+ nickname: "Mavericks",
107
+ city: "Dallas",
108
+ fullName: "Dallas Mavericks",
109
+ sport: "NBA",
110
+ aliases: ["Mavs"],
111
+ },
112
+ {
113
+ abbr: "DEN",
114
+ nickname: "Nuggets",
115
+ city: "Denver",
116
+ fullName: "Denver Nuggets",
117
+ sport: "NBA",
118
+ aliases: [],
119
+ },
120
+ {
121
+ abbr: "DET",
122
+ nickname: "Pistons",
123
+ city: "Detroit",
124
+ fullName: "Detroit Pistons",
125
+ sport: "NBA",
126
+ aliases: [],
127
+ },
128
+ {
129
+ abbr: "GSW",
130
+ nickname: "Warriors",
131
+ city: "Golden State",
132
+ fullName: "Golden State Warriors",
133
+ sport: "NBA",
134
+ aliases: ["GS", "SF Warriors"],
135
+ },
136
+ {
137
+ abbr: "HOU",
138
+ nickname: "Rockets",
139
+ city: "Houston",
140
+ fullName: "Houston Rockets",
141
+ sport: "NBA",
142
+ aliases: [],
143
+ },
144
+ {
145
+ abbr: "IND",
146
+ nickname: "Pacers",
147
+ city: "Indiana",
148
+ fullName: "Indiana Pacers",
149
+ sport: "NBA",
150
+ aliases: ["Indianapolis"],
151
+ },
152
+ {
153
+ abbr: "LAC",
154
+ nickname: "Clippers",
155
+ city: "Los Angeles",
156
+ fullName: "Los Angeles Clippers",
157
+ sport: "NBA",
158
+ aliases: ["LA Clippers"],
159
+ },
160
+ {
161
+ abbr: "LAL",
162
+ nickname: "Lakers",
163
+ city: "Los Angeles",
164
+ fullName: "Los Angeles Lakers",
165
+ sport: "NBA",
166
+ aliases: ["LA Lakers"],
167
+ },
168
+ {
169
+ abbr: "MEM",
170
+ nickname: "Grizzlies",
171
+ city: "Memphis",
172
+ fullName: "Memphis Grizzlies",
173
+ sport: "NBA",
174
+ aliases: ["Grizz"],
175
+ },
176
+ {
177
+ abbr: "MIA",
178
+ nickname: "Heat",
179
+ city: "Miami",
180
+ fullName: "Miami Heat",
181
+ sport: "NBA",
182
+ aliases: [],
183
+ },
184
+ {
185
+ abbr: "MIL",
186
+ nickname: "Bucks",
187
+ city: "Milwaukee",
188
+ fullName: "Milwaukee Bucks",
189
+ sport: "NBA",
190
+ aliases: [],
191
+ },
192
+ {
193
+ abbr: "MIN",
194
+ nickname: "Timberwolves",
195
+ city: "Minnesota",
196
+ fullName: "Minnesota Timberwolves",
197
+ sport: "NBA",
198
+ aliases: ["Wolves", "TWolves", "Minneapolis"],
199
+ },
200
+ {
201
+ abbr: "NOP",
202
+ nickname: "Pelicans",
203
+ city: "New Orleans",
204
+ fullName: "New Orleans Pelicans",
205
+ sport: "NBA",
206
+ aliases: ["NO", "NOLA", "Pels"],
207
+ },
208
+ {
209
+ abbr: "NYK",
210
+ nickname: "Knicks",
211
+ city: "New York",
212
+ fullName: "New York Knicks",
213
+ sport: "NBA",
214
+ aliases: ["NY Knicks"],
215
+ },
216
+ {
217
+ abbr: "OKC",
218
+ nickname: "Thunder",
219
+ city: "Oklahoma City",
220
+ fullName: "Oklahoma City Thunder",
221
+ sport: "NBA",
222
+ aliases: [],
223
+ },
224
+ {
225
+ abbr: "ORL",
226
+ nickname: "Magic",
227
+ city: "Orlando",
228
+ fullName: "Orlando Magic",
229
+ sport: "NBA",
230
+ aliases: [],
231
+ },
232
+ {
233
+ abbr: "PHI",
234
+ nickname: "76ers",
235
+ city: "Philadelphia",
236
+ fullName: "Philadelphia 76ers",
237
+ sport: "NBA",
238
+ aliases: ["Sixers", "Philly"],
239
+ },
240
+ {
241
+ abbr: "PHX",
242
+ nickname: "Suns",
243
+ city: "Phoenix",
244
+ fullName: "Phoenix Suns",
245
+ sport: "NBA",
246
+ aliases: ["PHO"],
247
+ },
248
+ {
249
+ abbr: "POR",
250
+ nickname: "Trail Blazers",
251
+ city: "Portland",
252
+ fullName: "Portland Trail Blazers",
253
+ sport: "NBA",
254
+ aliases: ["Blazers"],
255
+ },
256
+ {
257
+ abbr: "SAC",
258
+ nickname: "Kings",
259
+ city: "Sacramento",
260
+ fullName: "Sacramento Kings",
261
+ sport: "NBA",
262
+ aliases: [],
263
+ },
264
+ {
265
+ abbr: "SAS",
266
+ nickname: "Spurs",
267
+ city: "San Antonio",
268
+ fullName: "San Antonio Spurs",
269
+ sport: "NBA",
270
+ aliases: ["SA"],
271
+ },
272
+ {
273
+ abbr: "TOR",
274
+ nickname: "Raptors",
275
+ city: "Toronto",
276
+ fullName: "Toronto Raptors",
277
+ sport: "NBA",
278
+ aliases: ["Raps"],
279
+ },
280
+ {
281
+ abbr: "UTA",
282
+ nickname: "Jazz",
283
+ city: "Utah",
284
+ fullName: "Utah Jazz",
285
+ sport: "NBA",
286
+ aliases: ["SLC", "Salt Lake"],
287
+ },
288
+ {
289
+ abbr: "WAS",
290
+ nickname: "Wizards",
291
+ city: "Washington",
292
+ fullName: "Washington Wizards",
293
+ sport: "NBA",
294
+ aliases: ["DC", "Wiz"],
295
+ },
296
+ // ── NFL (32) ────────────────────────────────────────────────────────────
297
+ {
298
+ abbr: "ARI",
299
+ nickname: "Cardinals",
300
+ city: "Arizona",
301
+ fullName: "Arizona Cardinals",
302
+ sport: "NFL",
303
+ aliases: ["Phoenix"],
304
+ },
305
+ {
306
+ abbr: "ATL",
307
+ nickname: "Falcons",
308
+ city: "Atlanta",
309
+ fullName: "Atlanta Falcons",
310
+ sport: "NFL",
311
+ aliases: [],
312
+ },
313
+ {
314
+ abbr: "BAL",
315
+ nickname: "Ravens",
316
+ city: "Baltimore",
317
+ fullName: "Baltimore Ravens",
318
+ sport: "NFL",
319
+ aliases: [],
320
+ },
321
+ {
322
+ abbr: "BUF",
323
+ nickname: "Bills",
324
+ city: "Buffalo",
325
+ fullName: "Buffalo Bills",
326
+ sport: "NFL",
327
+ aliases: [],
328
+ },
329
+ {
330
+ abbr: "CAR",
331
+ nickname: "Panthers",
332
+ city: "Carolina",
333
+ fullName: "Carolina Panthers",
334
+ sport: "NFL",
335
+ aliases: ["Charlotte"],
336
+ },
337
+ {
338
+ abbr: "CHI",
339
+ nickname: "Bears",
340
+ city: "Chicago",
341
+ fullName: "Chicago Bears",
342
+ sport: "NFL",
343
+ aliases: [],
344
+ },
345
+ {
346
+ abbr: "CIN",
347
+ nickname: "Bengals",
348
+ city: "Cincinnati",
349
+ fullName: "Cincinnati Bengals",
350
+ sport: "NFL",
351
+ aliases: ["Cincy"],
352
+ },
353
+ {
354
+ abbr: "CLE",
355
+ nickname: "Browns",
356
+ city: "Cleveland",
357
+ fullName: "Cleveland Browns",
358
+ sport: "NFL",
359
+ aliases: [],
360
+ },
361
+ {
362
+ abbr: "DAL",
363
+ nickname: "Cowboys",
364
+ city: "Dallas",
365
+ fullName: "Dallas Cowboys",
366
+ sport: "NFL",
367
+ aliases: ["America's Team"],
368
+ },
369
+ {
370
+ abbr: "DEN",
371
+ nickname: "Broncos",
372
+ city: "Denver",
373
+ fullName: "Denver Broncos",
374
+ sport: "NFL",
375
+ aliases: [],
376
+ },
377
+ {
378
+ abbr: "DET",
379
+ nickname: "Lions",
380
+ city: "Detroit",
381
+ fullName: "Detroit Lions",
382
+ sport: "NFL",
383
+ aliases: [],
384
+ },
385
+ {
386
+ abbr: "GB",
387
+ nickname: "Packers",
388
+ city: "Green Bay",
389
+ fullName: "Green Bay Packers",
390
+ sport: "NFL",
391
+ aliases: ["GNB"],
392
+ },
393
+ {
394
+ abbr: "HOU",
395
+ nickname: "Texans",
396
+ city: "Houston",
397
+ fullName: "Houston Texans",
398
+ sport: "NFL",
399
+ aliases: [],
400
+ },
401
+ {
402
+ abbr: "IND",
403
+ nickname: "Colts",
404
+ city: "Indianapolis",
405
+ fullName: "Indianapolis Colts",
406
+ sport: "NFL",
407
+ aliases: [],
408
+ },
409
+ {
410
+ abbr: "JAX",
411
+ nickname: "Jaguars",
412
+ city: "Jacksonville",
413
+ fullName: "Jacksonville Jaguars",
414
+ sport: "NFL",
415
+ aliases: ["Jags", "JAC"],
416
+ },
417
+ {
418
+ abbr: "KC",
419
+ nickname: "Chiefs",
420
+ city: "Kansas City",
421
+ fullName: "Kansas City Chiefs",
422
+ sport: "NFL",
423
+ aliases: ["KCC"],
424
+ },
425
+ {
426
+ abbr: "LV",
427
+ nickname: "Raiders",
428
+ city: "Las Vegas",
429
+ fullName: "Las Vegas Raiders",
430
+ sport: "NFL",
431
+ aliases: ["LVR", "Oakland"],
432
+ },
433
+ {
434
+ abbr: "LAC",
435
+ nickname: "Chargers",
436
+ city: "Los Angeles",
437
+ fullName: "Los Angeles Chargers",
438
+ sport: "NFL",
439
+ aliases: ["LA Chargers", "San Diego"],
440
+ },
441
+ {
442
+ abbr: "LAR",
443
+ nickname: "Rams",
444
+ city: "Los Angeles",
445
+ fullName: "Los Angeles Rams",
446
+ sport: "NFL",
447
+ aliases: ["LA Rams", "STL Rams"],
448
+ },
449
+ {
450
+ abbr: "MIA",
451
+ nickname: "Dolphins",
452
+ city: "Miami",
453
+ fullName: "Miami Dolphins",
454
+ sport: "NFL",
455
+ aliases: ["Fins"],
456
+ },
457
+ {
458
+ abbr: "MIN",
459
+ nickname: "Vikings",
460
+ city: "Minnesota",
461
+ fullName: "Minnesota Vikings",
462
+ sport: "NFL",
463
+ aliases: ["Minneapolis"],
464
+ },
465
+ {
466
+ abbr: "NE",
467
+ nickname: "Patriots",
468
+ city: "New England",
469
+ fullName: "New England Patriots",
470
+ sport: "NFL",
471
+ aliases: ["Pats", "NEP", "Boston Patriots"],
472
+ },
473
+ {
474
+ abbr: "NO",
475
+ nickname: "Saints",
476
+ city: "New Orleans",
477
+ fullName: "New Orleans Saints",
478
+ sport: "NFL",
479
+ aliases: ["NOLA", "NOS"],
480
+ },
481
+ {
482
+ abbr: "NYG",
483
+ nickname: "Giants",
484
+ city: "New York",
485
+ fullName: "New York Giants",
486
+ sport: "NFL",
487
+ aliases: ["NY Giants"],
488
+ },
489
+ {
490
+ abbr: "NYJ",
491
+ nickname: "Jets",
492
+ city: "New York",
493
+ fullName: "New York Jets",
494
+ sport: "NFL",
495
+ aliases: ["NY Jets"],
496
+ },
497
+ {
498
+ abbr: "PHI",
499
+ nickname: "Eagles",
500
+ city: "Philadelphia",
501
+ fullName: "Philadelphia Eagles",
502
+ sport: "NFL",
503
+ aliases: ["Philly"],
504
+ },
505
+ {
506
+ abbr: "PIT",
507
+ nickname: "Steelers",
508
+ city: "Pittsburgh",
509
+ fullName: "Pittsburgh Steelers",
510
+ sport: "NFL",
511
+ aliases: [],
512
+ },
513
+ {
514
+ abbr: "SF",
515
+ nickname: "49ers",
516
+ city: "San Francisco",
517
+ fullName: "San Francisco 49ers",
518
+ sport: "NFL",
519
+ aliases: ["Niners", "SFO"],
520
+ },
521
+ {
522
+ abbr: "SEA",
523
+ nickname: "Seahawks",
524
+ city: "Seattle",
525
+ fullName: "Seattle Seahawks",
526
+ sport: "NFL",
527
+ aliases: [],
528
+ },
529
+ {
530
+ abbr: "TB",
531
+ nickname: "Buccaneers",
532
+ city: "Tampa Bay",
533
+ fullName: "Tampa Bay Buccaneers",
534
+ sport: "NFL",
535
+ aliases: ["Bucs", "TBB", "Tampa"],
536
+ },
537
+ {
538
+ abbr: "TEN",
539
+ nickname: "Titans",
540
+ city: "Tennessee",
541
+ fullName: "Tennessee Titans",
542
+ sport: "NFL",
543
+ aliases: ["Nashville"],
544
+ },
545
+ {
546
+ abbr: "WAS",
547
+ nickname: "Commanders",
548
+ city: "Washington",
549
+ fullName: "Washington Commanders",
550
+ sport: "NFL",
551
+ aliases: ["DC", "WSH"],
552
+ },
553
+ // ── NHL (32) ────────────────────────────────────────────────────────────
554
+ {
555
+ abbr: "ANA",
556
+ nickname: "Ducks",
557
+ city: "Anaheim",
558
+ fullName: "Anaheim Ducks",
559
+ sport: "NHL",
560
+ aliases: [],
561
+ },
562
+ {
563
+ abbr: "BOS",
564
+ nickname: "Bruins",
565
+ city: "Boston",
566
+ fullName: "Boston Bruins",
567
+ sport: "NHL",
568
+ aliases: [],
569
+ },
570
+ {
571
+ abbr: "BUF",
572
+ nickname: "Sabres",
573
+ city: "Buffalo",
574
+ fullName: "Buffalo Sabres",
575
+ sport: "NHL",
576
+ aliases: [],
577
+ },
578
+ {
579
+ abbr: "CGY",
580
+ nickname: "Flames",
581
+ city: "Calgary",
582
+ fullName: "Calgary Flames",
583
+ sport: "NHL",
584
+ aliases: ["CAL"],
585
+ },
586
+ {
587
+ abbr: "CAR",
588
+ nickname: "Hurricanes",
589
+ city: "Carolina",
590
+ fullName: "Carolina Hurricanes",
591
+ sport: "NHL",
592
+ aliases: ["Canes", "Raleigh"],
593
+ },
594
+ {
595
+ abbr: "CHI",
596
+ nickname: "Blackhawks",
597
+ city: "Chicago",
598
+ fullName: "Chicago Blackhawks",
599
+ sport: "NHL",
600
+ aliases: ["Hawks"],
601
+ },
602
+ {
603
+ abbr: "COL",
604
+ nickname: "Avalanche",
605
+ city: "Colorado",
606
+ fullName: "Colorado Avalanche",
607
+ sport: "NHL",
608
+ aliases: ["Avs", "Denver"],
609
+ },
610
+ {
611
+ abbr: "CBJ",
612
+ nickname: "Blue Jackets",
613
+ city: "Columbus",
614
+ fullName: "Columbus Blue Jackets",
615
+ sport: "NHL",
616
+ aliases: ["Jackets"],
617
+ },
618
+ {
619
+ abbr: "DAL",
620
+ nickname: "Stars",
621
+ city: "Dallas",
622
+ fullName: "Dallas Stars",
623
+ sport: "NHL",
624
+ aliases: [],
625
+ },
626
+ {
627
+ abbr: "DET",
628
+ nickname: "Red Wings",
629
+ city: "Detroit",
630
+ fullName: "Detroit Red Wings",
631
+ sport: "NHL",
632
+ aliases: ["Wings"],
633
+ },
634
+ {
635
+ abbr: "EDM",
636
+ nickname: "Oilers",
637
+ city: "Edmonton",
638
+ fullName: "Edmonton Oilers",
639
+ sport: "NHL",
640
+ aliases: [],
641
+ },
642
+ {
643
+ abbr: "FLA",
644
+ nickname: "Panthers",
645
+ city: "Florida",
646
+ fullName: "Florida Panthers",
647
+ sport: "NHL",
648
+ aliases: ["Cats", "Sunrise"],
649
+ },
650
+ {
651
+ abbr: "LAK",
652
+ nickname: "Kings",
653
+ city: "Los Angeles",
654
+ fullName: "Los Angeles Kings",
655
+ sport: "NHL",
656
+ aliases: ["LA Kings"],
657
+ },
658
+ {
659
+ abbr: "MIN",
660
+ nickname: "Wild",
661
+ city: "Minnesota",
662
+ fullName: "Minnesota Wild",
663
+ sport: "NHL",
664
+ aliases: ["Minneapolis", "St Paul"],
665
+ },
666
+ {
667
+ abbr: "MTL",
668
+ nickname: "Canadiens",
669
+ city: "Montreal",
670
+ fullName: "Montreal Canadiens",
671
+ sport: "NHL",
672
+ aliases: ["Habs", "MON"],
673
+ },
674
+ {
675
+ abbr: "NSH",
676
+ nickname: "Predators",
677
+ city: "Nashville",
678
+ fullName: "Nashville Predators",
679
+ sport: "NHL",
680
+ aliases: ["Preds"],
681
+ },
682
+ {
683
+ abbr: "NJD",
684
+ nickname: "Devils",
685
+ city: "New Jersey",
686
+ fullName: "New Jersey Devils",
687
+ sport: "NHL",
688
+ aliases: ["NJ", "Newark"],
689
+ },
690
+ {
691
+ abbr: "NYI",
692
+ nickname: "Islanders",
693
+ city: "New York",
694
+ fullName: "New York Islanders",
695
+ sport: "NHL",
696
+ aliases: ["Isles", "NY Islanders"],
697
+ },
698
+ {
699
+ abbr: "NYR",
700
+ nickname: "Rangers",
701
+ city: "New York",
702
+ fullName: "New York Rangers",
703
+ sport: "NHL",
704
+ aliases: ["NY Rangers"],
705
+ },
706
+ {
707
+ abbr: "OTT",
708
+ nickname: "Senators",
709
+ city: "Ottawa",
710
+ fullName: "Ottawa Senators",
711
+ sport: "NHL",
712
+ aliases: ["Sens"],
713
+ },
714
+ {
715
+ abbr: "PHI",
716
+ nickname: "Flyers",
717
+ city: "Philadelphia",
718
+ fullName: "Philadelphia Flyers",
719
+ sport: "NHL",
720
+ aliases: ["Philly"],
721
+ },
722
+ {
723
+ abbr: "PIT",
724
+ nickname: "Penguins",
725
+ city: "Pittsburgh",
726
+ fullName: "Pittsburgh Penguins",
727
+ sport: "NHL",
728
+ aliases: ["Pens"],
729
+ },
730
+ {
731
+ abbr: "SJS",
732
+ nickname: "Sharks",
733
+ city: "San Jose",
734
+ fullName: "San Jose Sharks",
735
+ sport: "NHL",
736
+ aliases: ["SJ"],
737
+ },
738
+ {
739
+ abbr: "SEA",
740
+ nickname: "Kraken",
741
+ city: "Seattle",
742
+ fullName: "Seattle Kraken",
743
+ sport: "NHL",
744
+ aliases: [],
745
+ },
746
+ {
747
+ abbr: "STL",
748
+ nickname: "Blues",
749
+ city: "St. Louis",
750
+ fullName: "St. Louis Blues",
751
+ sport: "NHL",
752
+ aliases: ["Saint Louis", "St Louis"],
753
+ },
754
+ {
755
+ abbr: "TBL",
756
+ nickname: "Lightning",
757
+ city: "Tampa Bay",
758
+ fullName: "Tampa Bay Lightning",
759
+ sport: "NHL",
760
+ aliases: ["Bolts", "TB", "Tampa"],
761
+ },
762
+ {
763
+ abbr: "TOR",
764
+ nickname: "Maple Leafs",
765
+ city: "Toronto",
766
+ fullName: "Toronto Maple Leafs",
767
+ sport: "NHL",
768
+ aliases: ["Leafs"],
769
+ },
770
+ {
771
+ abbr: "VAN",
772
+ nickname: "Canucks",
773
+ city: "Vancouver",
774
+ fullName: "Vancouver Canucks",
775
+ sport: "NHL",
776
+ aliases: [],
777
+ },
778
+ {
779
+ abbr: "VGK",
780
+ nickname: "Golden Knights",
781
+ city: "Vegas",
782
+ fullName: "Vegas Golden Knights",
783
+ sport: "NHL",
784
+ aliases: ["VGS", "Las Vegas", "Knights"],
785
+ },
786
+ {
787
+ abbr: "WAS",
788
+ nickname: "Capitals",
789
+ city: "Washington",
790
+ fullName: "Washington Capitals",
791
+ sport: "NHL",
792
+ aliases: ["DC", "WSH", "Caps"],
793
+ },
794
+ {
795
+ abbr: "WPG",
796
+ nickname: "Jets",
797
+ city: "Winnipeg",
798
+ fullName: "Winnipeg Jets",
799
+ sport: "NHL",
800
+ aliases: ["WIN"],
801
+ },
802
+ {
803
+ abbr: "UTA",
804
+ nickname: "Hockey Club",
805
+ city: "Utah",
806
+ fullName: "Utah Hockey Club",
807
+ sport: "NHL",
808
+ aliases: ["UHC", "Salt Lake"],
809
+ },
810
+ // ── MLB (30) ────────────────────────────────────────────────────────────
811
+ {
812
+ abbr: "ARI",
813
+ nickname: "Diamondbacks",
814
+ city: "Arizona",
815
+ fullName: "Arizona Diamondbacks",
816
+ sport: "MLB",
817
+ aliases: ["D-backs", "Dbacks", "Phoenix"],
818
+ },
819
+ {
820
+ abbr: "ATL",
821
+ nickname: "Braves",
822
+ city: "Atlanta",
823
+ fullName: "Atlanta Braves",
824
+ sport: "MLB",
825
+ aliases: [],
826
+ },
827
+ {
828
+ abbr: "BAL",
829
+ nickname: "Orioles",
830
+ city: "Baltimore",
831
+ fullName: "Baltimore Orioles",
832
+ sport: "MLB",
833
+ aliases: ["O's"],
834
+ },
835
+ {
836
+ abbr: "BOS",
837
+ nickname: "Red Sox",
838
+ city: "Boston",
839
+ fullName: "Boston Red Sox",
840
+ sport: "MLB",
841
+ aliases: ["Sox", "BoSox"],
842
+ },
843
+ {
844
+ abbr: "CHC",
845
+ nickname: "Cubs",
846
+ city: "Chicago",
847
+ fullName: "Chicago Cubs",
848
+ sport: "MLB",
849
+ aliases: [],
850
+ },
851
+ {
852
+ abbr: "CHW",
853
+ nickname: "White Sox",
854
+ city: "Chicago",
855
+ fullName: "Chicago White Sox",
856
+ sport: "MLB",
857
+ aliases: ["CWS", "Sox"],
858
+ },
859
+ {
860
+ abbr: "CIN",
861
+ nickname: "Reds",
862
+ city: "Cincinnati",
863
+ fullName: "Cincinnati Reds",
864
+ sport: "MLB",
865
+ aliases: ["Cincy"],
866
+ },
867
+ {
868
+ abbr: "CLE",
869
+ nickname: "Guardians",
870
+ city: "Cleveland",
871
+ fullName: "Cleveland Guardians",
872
+ sport: "MLB",
873
+ aliases: ["Guards"],
874
+ },
875
+ {
876
+ abbr: "COL",
877
+ nickname: "Rockies",
878
+ city: "Colorado",
879
+ fullName: "Colorado Rockies",
880
+ sport: "MLB",
881
+ aliases: ["Denver"],
882
+ },
883
+ {
884
+ abbr: "DET",
885
+ nickname: "Tigers",
886
+ city: "Detroit",
887
+ fullName: "Detroit Tigers",
888
+ sport: "MLB",
889
+ aliases: [],
890
+ },
891
+ {
892
+ abbr: "HOU",
893
+ nickname: "Astros",
894
+ city: "Houston",
895
+ fullName: "Houston Astros",
896
+ sport: "MLB",
897
+ aliases: ["Stros"],
898
+ },
899
+ {
900
+ abbr: "KC",
901
+ nickname: "Royals",
902
+ city: "Kansas City",
903
+ fullName: "Kansas City Royals",
904
+ sport: "MLB",
905
+ aliases: ["KCR"],
906
+ },
907
+ {
908
+ abbr: "LAA",
909
+ nickname: "Angels",
910
+ city: "Los Angeles",
911
+ fullName: "Los Angeles Angels",
912
+ sport: "MLB",
913
+ aliases: ["LA Angels", "Anaheim", "Halos"],
914
+ },
915
+ {
916
+ abbr: "LAD",
917
+ nickname: "Dodgers",
918
+ city: "Los Angeles",
919
+ fullName: "Los Angeles Dodgers",
920
+ sport: "MLB",
921
+ aliases: ["LA Dodgers"],
922
+ },
923
+ {
924
+ abbr: "MIA",
925
+ nickname: "Marlins",
926
+ city: "Miami",
927
+ fullName: "Miami Marlins",
928
+ sport: "MLB",
929
+ aliases: ["Fish"],
930
+ },
931
+ {
932
+ abbr: "MIL",
933
+ nickname: "Brewers",
934
+ city: "Milwaukee",
935
+ fullName: "Milwaukee Brewers",
936
+ sport: "MLB",
937
+ aliases: ["Brew Crew"],
938
+ },
939
+ {
940
+ abbr: "MIN",
941
+ nickname: "Twins",
942
+ city: "Minnesota",
943
+ fullName: "Minnesota Twins",
944
+ sport: "MLB",
945
+ aliases: ["Minneapolis"],
946
+ },
947
+ {
948
+ abbr: "NYM",
949
+ nickname: "Mets",
950
+ city: "New York",
951
+ fullName: "New York Mets",
952
+ sport: "MLB",
953
+ aliases: ["NY Mets"],
954
+ },
955
+ {
956
+ abbr: "NYY",
957
+ nickname: "Yankees",
958
+ city: "New York",
959
+ fullName: "New York Yankees",
960
+ sport: "MLB",
961
+ aliases: ["NY Yankees", "Yanks", "Bronx Bombers"],
962
+ },
963
+ {
964
+ abbr: "OAK",
965
+ nickname: "Athletics",
966
+ city: "Oakland",
967
+ fullName: "Oakland Athletics",
968
+ sport: "MLB",
969
+ aliases: ["A's", "As"],
970
+ },
971
+ {
972
+ abbr: "PHI",
973
+ nickname: "Phillies",
974
+ city: "Philadelphia",
975
+ fullName: "Philadelphia Phillies",
976
+ sport: "MLB",
977
+ aliases: ["Philly"],
978
+ },
979
+ {
980
+ abbr: "PIT",
981
+ nickname: "Pirates",
982
+ city: "Pittsburgh",
983
+ fullName: "Pittsburgh Pirates",
984
+ sport: "MLB",
985
+ aliases: ["Bucs"],
986
+ },
987
+ {
988
+ abbr: "SD",
989
+ nickname: "Padres",
990
+ city: "San Diego",
991
+ fullName: "San Diego Padres",
992
+ sport: "MLB",
993
+ aliases: ["SDP"],
994
+ },
995
+ {
996
+ abbr: "SF",
997
+ nickname: "Giants",
998
+ city: "San Francisco",
999
+ fullName: "San Francisco Giants",
1000
+ sport: "MLB",
1001
+ aliases: ["SFG"],
1002
+ },
1003
+ {
1004
+ abbr: "SEA",
1005
+ nickname: "Mariners",
1006
+ city: "Seattle",
1007
+ fullName: "Seattle Mariners",
1008
+ sport: "MLB",
1009
+ aliases: ["M's", "Ms"],
1010
+ },
1011
+ {
1012
+ abbr: "STL",
1013
+ nickname: "Cardinals",
1014
+ city: "St. Louis",
1015
+ fullName: "St. Louis Cardinals",
1016
+ sport: "MLB",
1017
+ aliases: ["Cards", "Saint Louis", "St Louis"],
1018
+ },
1019
+ {
1020
+ abbr: "TB",
1021
+ nickname: "Rays",
1022
+ city: "Tampa Bay",
1023
+ fullName: "Tampa Bay Rays",
1024
+ sport: "MLB",
1025
+ aliases: ["TBR", "Tampa"],
1026
+ },
1027
+ {
1028
+ abbr: "TEX",
1029
+ nickname: "Rangers",
1030
+ city: "Texas",
1031
+ fullName: "Texas Rangers",
1032
+ sport: "MLB",
1033
+ aliases: ["Arlington"],
1034
+ },
1035
+ {
1036
+ abbr: "TOR",
1037
+ nickname: "Blue Jays",
1038
+ city: "Toronto",
1039
+ fullName: "Toronto Blue Jays",
1040
+ sport: "MLB",
1041
+ aliases: ["Jays"],
1042
+ },
1043
+ {
1044
+ abbr: "WAS",
1045
+ nickname: "Nationals",
1046
+ city: "Washington",
1047
+ fullName: "Washington Nationals",
1048
+ sport: "MLB",
1049
+ aliases: ["DC", "WSH", "Nats"],
1050
+ },
1051
+ ];
1052
+ /**
1053
+ * Fuzzy team search — matches query against abbreviation, nickname, city,
1054
+ * fullName, and aliases. Returns scored results sorted by match quality.
1055
+ */
1056
+ function searchTeams(query, sportFilter) {
1057
+ const q = query.toLowerCase().trim();
1058
+ if (!q)
1059
+ return [];
1060
+ const results = [];
1061
+ for (const team of TEAMS) {
1062
+ if (sportFilter && team.sport !== sportFilter)
1063
+ continue;
1064
+ let score = 0;
1065
+ const abbrL = team.abbr.toLowerCase();
1066
+ const nickL = team.nickname.toLowerCase();
1067
+ const cityL = team.city.toLowerCase();
1068
+ const fullL = team.fullName.toLowerCase();
1069
+ // Exact matches (highest priority)
1070
+ if (abbrL === q)
1071
+ score = 100;
1072
+ else if (nickL === q)
1073
+ score = 95;
1074
+ else if (fullL === q)
1075
+ score = 90;
1076
+ else if (cityL === q)
1077
+ score = 85;
1078
+ // Alias exact match
1079
+ else if (team.aliases.some((a) => a.toLowerCase() === q))
1080
+ score = 80;
1081
+ // Starts-with matches
1082
+ else if (nickL.startsWith(q))
1083
+ score = 70;
1084
+ else if (cityL.startsWith(q))
1085
+ score = 65;
1086
+ else if (abbrL.startsWith(q))
1087
+ score = 60;
1088
+ // Contains matches
1089
+ else if (fullL.includes(q))
1090
+ score = 50;
1091
+ else if (nickL.includes(q))
1092
+ score = 45;
1093
+ else if (team.aliases.some((a) => a.toLowerCase().includes(q)))
1094
+ score = 40;
1095
+ if (score > 0) {
1096
+ results.push({ ...team, score });
1097
+ }
1098
+ }
1099
+ results.sort((a, b) => b.score - a.score);
1100
+ return results;
1101
+ }
1102
+ /**
1103
+ * Resolve a query to the single best-matching team.
1104
+ * If sport is given, only matches within that sport.
1105
+ */
1106
+ function resolveTeam(query, sport) {
1107
+ const matches = searchTeams(query, sport);
1108
+ return matches.length > 0 ? matches[0] : null;
1109
+ }
55
1110
  async function apiFetch(opts) {
56
1111
  const url = new URL(opts.path, API_BASE);
57
1112
  if (opts.params) {
@@ -83,7 +1138,6 @@ async function apiFetch(opts) {
83
1138
  const remainDay = res.headers.get("X-RateLimit-Remaining-Day") || "0";
84
1139
  throw new Error(`Rate limit exceeded. Remaining: ${remainHour}/hour, ${remainDay}/day. Retry after ${retryAfter}s.`);
85
1140
  }
86
- // FINDING 5 FIX: Don't forward raw response body — may contain internal details
87
1141
  if (status === 403)
88
1142
  throw new Error("Access denied. Your subscription may have expired.");
89
1143
  if (status === 404)
@@ -97,15 +1151,59 @@ async function apiFetch(opts) {
97
1151
  }
98
1152
  return (await res.json());
99
1153
  }
1154
+ // ─── Formatting Helpers ─────────────────────────────────────────────────────
1155
+ function formatPick(p) {
1156
+ const pick = p.predicted_winner || p.predicted_pick || p.predicted_direction;
1157
+ const isHomePick = pick === p.home_team || pick === "HOME" || pick === "OVER";
1158
+ const edge = isHomePick
1159
+ ? (p.home_edge ?? p.away_edge)
1160
+ : (p.away_edge ?? p.home_edge);
1161
+ const tier = p.confidence_tier ? ` [${p.confidence_tier}]` : "";
1162
+ let line = `${p.away_team} @ ${p.home_team}`;
1163
+ line += `\n Game ID: ${p.game_id}`;
1164
+ line += `\n Pick: ${pick} (${p.confidence}% conf`;
1165
+ if (edge)
1166
+ line += `, ${edge}% edge`;
1167
+ line += `)${tier}`;
1168
+ if (p.market === "spread" && p.spread_line != null) {
1169
+ line += `\n Line: ${p.spread_line}`;
1170
+ }
1171
+ if (p.market === "totals" && p.totals_line != null) {
1172
+ line += `\n Line: ${p.totals_line}`;
1173
+ }
1174
+ if (p.home_odds) {
1175
+ line += `\n Odds: ${p.home_odds}/${p.away_odds}`;
1176
+ }
1177
+ if (p.commence_time) {
1178
+ const time = new Date(p.commence_time).toLocaleString("en-US", {
1179
+ timeZone: "America/New_York",
1180
+ weekday: "short",
1181
+ month: "short",
1182
+ day: "numeric",
1183
+ hour: "numeric",
1184
+ minute: "2-digit",
1185
+ timeZoneName: "short",
1186
+ });
1187
+ line += `\n Game: ${time}`;
1188
+ }
1189
+ return line;
1190
+ }
1191
+ function formatRateLimit(rl) {
1192
+ if (!rl)
1193
+ return "";
1194
+ return `\nRate limit: ${rl.remaining_hour}/hour, ${rl.remaining_day}/day remaining.`;
1195
+ }
100
1196
  // ─── Server Setup ───────────────────────────────────────────────────────────
101
1197
  const server = new McpServer({
102
1198
  name: "Da Vinci Bets",
103
- version: "1.0.1",
1199
+ version: "1.3.0",
104
1200
  });
105
1201
  // ─── Tool: get_predictions ──────────────────────────────────────────────────
106
- server.tool("get_predictions", "Fetch AI-powered sports predictions filtered by sport, market, and confidence. " +
107
- "Returns predictions with win probabilities, odds, edge values, and confidence tiers. " +
108
- "Covers NBA, NFL, NHL, and MLB across moneylines, spreads, and totals.", {
1202
+ server.tool("get_predictions", "Fetch AI-powered sports predictions filtered by sport, market, team, and confidence. " +
1203
+ "Returns predictions with win probabilities, odds, edge values, confidence tiers, and game IDs. " +
1204
+ "Covers NBA, NFL, NHL, and MLB across moneylines, spreads, and totals. " +
1205
+ "Use the team parameter to find predictions for a specific team (e.g., 'Lakers', 'NYY', 'Chiefs'). " +
1206
+ "Game IDs from results can be used with get_pick_card to render shareable images.", {
109
1207
  sport: z
110
1208
  .enum(["NBA", "NFL", "NHL", "MLB"])
111
1209
  .optional()
@@ -114,6 +1212,12 @@ server.tool("get_predictions", "Fetch AI-powered sports predictions filtered by
114
1212
  .enum(["moneyline", "spread", "totals"])
115
1213
  .default("moneyline")
116
1214
  .describe("Betting market type."),
1215
+ team: z
1216
+ .string()
1217
+ .optional()
1218
+ .describe("Filter by team name, abbreviation, or city. " +
1219
+ "Examples: 'Lakers', 'LAL', 'NYY', 'Chiefs', 'Boston'. " +
1220
+ "Resolved intelligently — abbreviations, nicknames, and cities all work."),
117
1221
  sort: z
118
1222
  .enum(["confidence", "edge", "game_time"])
119
1223
  .default("confidence")
@@ -132,7 +1236,7 @@ server.tool("get_predictions", "Fetch AI-powered sports predictions filtered by
132
1236
  .max(100)
133
1237
  .default(60)
134
1238
  .describe("Minimum confidence percentage (0-100)."),
135
- }, async ({ sport, market, sort, limit, min_confidence }) => {
1239
+ }, async ({ sport, market, team, sort, limit, min_confidence }) => {
136
1240
  try {
137
1241
  const params = {
138
1242
  market,
@@ -142,37 +1246,45 @@ server.tool("get_predictions", "Fetch AI-powered sports predictions filtered by
142
1246
  };
143
1247
  if (sport)
144
1248
  params.sport = sport;
1249
+ // Resolve team name to canonical form for API filter
1250
+ if (team) {
1251
+ const resolved = resolveTeam(team, sport || undefined);
1252
+ if (resolved) {
1253
+ params.team = resolved.nickname;
1254
+ if (!sport)
1255
+ params.sport = resolved.sport;
1256
+ }
1257
+ else {
1258
+ // Pass raw query — API does ILIKE matching
1259
+ params.team = team;
1260
+ }
1261
+ }
145
1262
  const data = await apiFetch({
146
1263
  path: "/api/v1/predictions",
147
1264
  params,
148
1265
  });
149
- const summary = [
150
- `Found ${data.meta?.total ?? data.predictions.length} ${market} predictions${sport ? ` for ${sport}` : ""}.`,
151
- `Rate limit: ${data.rate_limit?.remaining_hour ?? "?"}/hour, ${data.rate_limit?.remaining_day ?? "?"}/day remaining.`,
152
- "",
153
- ...data.predictions.map((p, i) => {
154
- const conf = p.confidence;
155
- const pick = p.predicted_winner || p.predicted_pick || p.predicted_direction;
156
- const edge = p.home_edge || p.away_edge;
157
- let line = `${i + 1}. ${p.away_team} @ ${p.home_team} — Pick: ${pick} (${conf}% conf`;
158
- if (edge)
159
- line += `, ${edge}% edge`;
160
- line += ")";
161
- if (market === "spread" && p.spread_line != null) {
162
- line += ` | Line: ${p.spread_line}`;
163
- }
164
- if (market === "totals" && p.totals_line != null) {
165
- line += ` | Line: ${p.totals_line}`;
166
- }
167
- if (p.home_odds) {
168
- line += ` | Odds: ${p.home_odds}/${p.away_odds}`;
169
- }
170
- return line;
171
- }),
172
- ].join("\n");
1266
+ if (data.predictions.length === 0) {
1267
+ const teamHint = team ? ` for "${team}"` : "";
1268
+ return {
1269
+ content: [
1270
+ {
1271
+ type: "text",
1272
+ text: `No ${market} predictions found${teamHint}${sport ? ` in ${sport}` : ""}. ` +
1273
+ `Games may not be scheduled, or try lowering min_confidence (currently ${min_confidence}%).`,
1274
+ },
1275
+ ],
1276
+ };
1277
+ }
1278
+ const lines = data.predictions.map((p, i) => `${i + 1}. ${formatPick(p)}`);
1279
+ const header = [
1280
+ `Found ${data.meta.total} ${market} predictions${sport ? ` for ${sport}` : ""}${team ? ` matching "${team}"` : ""}.`,
1281
+ formatRateLimit(data.rate_limit),
1282
+ ]
1283
+ .filter(Boolean)
1284
+ .join("");
173
1285
  return {
174
1286
  content: [
175
- { type: "text", text: summary },
1287
+ { type: "text", text: header + "\n\n" + lines.join("\n\n") },
176
1288
  {
177
1289
  type: "text",
178
1290
  text: "\n---\nRaw JSON:\n" + JSON.stringify(data.predictions, null, 2),
@@ -184,10 +1296,196 @@ server.tool("get_predictions", "Fetch AI-powered sports predictions filtered by
184
1296
  return errorResult(err);
185
1297
  }
186
1298
  });
1299
+ // ─── Tool: get_game ─────────────────────────────────────────────────────────
1300
+ server.tool("get_game", "Find a specific game's prediction by team name. This is the intelligent lookup tool — " +
1301
+ "give it a team name (nickname, abbreviation, or city) and it finds the matching game. " +
1302
+ "Returns the full prediction with game_id, confidence, odds, edge, and game time. " +
1303
+ "Optionally specify an opponent to find a specific matchup (e.g., team='Lakers', opponent='Celtics'). " +
1304
+ "The game_id in the response can be used with get_pick_card to render a shareable image.", {
1305
+ team: z
1306
+ .string()
1307
+ .describe("Team to look up. Accepts nickname ('Lakers'), abbreviation ('LAL'), " +
1308
+ "city ('Los Angeles'), or full name ('Los Angeles Lakers')."),
1309
+ opponent: z
1310
+ .string()
1311
+ .optional()
1312
+ .describe("Optional opponent to narrow to a specific matchup. " +
1313
+ "Example: team='Lakers', opponent='Celtics'."),
1314
+ sport: z
1315
+ .enum(["NBA", "NFL", "NHL", "MLB"])
1316
+ .optional()
1317
+ .describe("Sport hint. Auto-detected from team name if omitted."),
1318
+ market: z
1319
+ .enum(["moneyline", "spread", "totals"])
1320
+ .default("moneyline")
1321
+ .describe("Which market to look up."),
1322
+ }, async ({ team, opponent, sport, market }) => {
1323
+ try {
1324
+ // Resolve team
1325
+ const resolved = resolveTeam(team, sport || undefined);
1326
+ if (!resolved) {
1327
+ // Suggest closest matches
1328
+ const suggestions = searchTeams(team)
1329
+ .slice(0, 5)
1330
+ .map((t) => ` ${t.abbr} — ${t.fullName} (${t.sport})`)
1331
+ .join("\n");
1332
+ return {
1333
+ content: [
1334
+ {
1335
+ type: "text",
1336
+ text: `Could not resolve team "${team}". ` +
1337
+ (suggestions
1338
+ ? `Did you mean:\n${suggestions}`
1339
+ : "Try using the full team name or abbreviation."),
1340
+ },
1341
+ ],
1342
+ };
1343
+ }
1344
+ const detectedSport = sport || resolved.sport;
1345
+ // Fetch predictions for this team
1346
+ const params = {
1347
+ sport: detectedSport,
1348
+ market,
1349
+ sort: "game_time",
1350
+ limit: "10",
1351
+ min_confidence: "0",
1352
+ team: resolved.nickname,
1353
+ };
1354
+ const data = await apiFetch({
1355
+ path: "/api/v1/predictions",
1356
+ params,
1357
+ });
1358
+ let matches = data.predictions;
1359
+ // Filter by opponent if specified
1360
+ if (opponent && matches.length > 0) {
1361
+ const oppResolved = resolveTeam(opponent, detectedSport);
1362
+ const oppName = oppResolved
1363
+ ? oppResolved.nickname.toLowerCase()
1364
+ : opponent.toLowerCase();
1365
+ matches = matches.filter((p) => p.home_team.toLowerCase().includes(oppName) ||
1366
+ p.away_team.toLowerCase().includes(oppName));
1367
+ }
1368
+ if (matches.length === 0) {
1369
+ const oppHint = opponent ? ` vs ${opponent}` : "";
1370
+ return {
1371
+ content: [
1372
+ {
1373
+ type: "text",
1374
+ text: `No upcoming ${market} prediction found for ${resolved.fullName}${oppHint}. ` +
1375
+ `The team may not have games scheduled in the current prediction window.`,
1376
+ },
1377
+ ],
1378
+ };
1379
+ }
1380
+ // Return the best match (first upcoming game)
1381
+ const game = matches[0];
1382
+ const allGames = matches.length > 1
1383
+ ? `\n\n─── Other upcoming games ───\n${matches
1384
+ .slice(1)
1385
+ .map((p) => `${p.away_team} @ ${p.home_team} | Game ID: ${p.game_id}`)
1386
+ .join("\n")}`
1387
+ : "";
1388
+ return {
1389
+ content: [
1390
+ {
1391
+ type: "text",
1392
+ text: `${resolved.fullName} (${resolved.abbr}) — ${detectedSport} ${market}\n` +
1393
+ `${"═".repeat(50)}\n\n` +
1394
+ formatPick(game) +
1395
+ allGames +
1396
+ formatRateLimit(data.rate_limit) +
1397
+ `\n\nUse game_id "${game.game_id}" with get_pick_card to render a shareable image.`,
1398
+ },
1399
+ {
1400
+ type: "text",
1401
+ text: "\n---\nRaw JSON:\n" + JSON.stringify(game, null, 2),
1402
+ },
1403
+ ],
1404
+ };
1405
+ }
1406
+ catch (err) {
1407
+ return errorResult(err);
1408
+ }
1409
+ });
1410
+ // ─── Tool: lookup_teams ─────────────────────────────────────────────────────
1411
+ server.tool("lookup_teams", "Search the team registry by name, abbreviation, city, or sport. " +
1412
+ "Returns matching teams with their abbreviation, full name, sport, and aliases. " +
1413
+ "Useful for discovering team names before using get_game or get_predictions. " +
1414
+ "Omit query to list all teams for a sport. " +
1415
+ "Does not call the API — instant local lookup, no rate limit impact.", {
1416
+ query: z
1417
+ .string()
1418
+ .optional()
1419
+ .describe("Search query — team name, abbreviation, or city. " +
1420
+ "Examples: 'NY', 'Lakers', 'CHI', 'Boston'. Omit to list all teams for a sport."),
1421
+ sport: z
1422
+ .enum(["NBA", "NFL", "NHL", "MLB"])
1423
+ .optional()
1424
+ .describe("Filter to a specific sport. Omit for all sports."),
1425
+ }, async ({ query, sport }) => {
1426
+ if (!query && !sport) {
1427
+ // List summary
1428
+ const counts = { NBA: 0, NFL: 0, NHL: 0, MLB: 0 };
1429
+ for (const t of TEAMS)
1430
+ counts[t.sport]++;
1431
+ return {
1432
+ content: [
1433
+ {
1434
+ type: "text",
1435
+ text: `Da Vinci Bets covers ${TEAMS.length} teams:\n` +
1436
+ Object.entries(counts)
1437
+ .map(([s, n]) => ` ${s}: ${n} teams`)
1438
+ .join("\n") +
1439
+ `\n\nUse query or sport to search. Examples:\n` +
1440
+ ` lookup_teams(query="NY") — all NY teams\n` +
1441
+ ` lookup_teams(sport="NBA") — all NBA teams\n` +
1442
+ ` lookup_teams(query="CHI", sport="MLB") — Chicago MLB teams`,
1443
+ },
1444
+ ],
1445
+ };
1446
+ }
1447
+ let results;
1448
+ if (query) {
1449
+ results = searchTeams(query, sport || undefined);
1450
+ }
1451
+ else {
1452
+ // List all teams for the sport
1453
+ results = TEAMS.filter((t) => t.sport === sport);
1454
+ }
1455
+ if (results.length === 0) {
1456
+ return {
1457
+ content: [
1458
+ {
1459
+ type: "text",
1460
+ text: `No teams found${query ? ` matching "${query}"` : ""}${sport ? ` in ${sport}` : ""}.`,
1461
+ },
1462
+ ],
1463
+ };
1464
+ }
1465
+ const lines = results.map((t) => {
1466
+ let line = `${t.abbr.padEnd(4)} ${t.fullName} (${t.sport})`;
1467
+ if (t.aliases.length > 0) {
1468
+ line += ` — also: ${t.aliases.join(", ")}`;
1469
+ }
1470
+ return line;
1471
+ });
1472
+ const header = query
1473
+ ? `Found ${results.length} teams matching "${query}"${sport ? ` in ${sport}` : ""}:`
1474
+ : `All ${results.length} ${sport} teams:`;
1475
+ return {
1476
+ content: [
1477
+ {
1478
+ type: "text",
1479
+ text: header + "\n\n" + lines.join("\n"),
1480
+ },
1481
+ ],
1482
+ };
1483
+ });
187
1484
  // ─── Tool: get_top_picks ────────────────────────────────────────────────────
188
1485
  server.tool("get_top_picks", "Get the highest-conviction AI picks across all sports and markets. " +
189
- "Returns the top predictions sorted by confidence, great for daily briefings. " +
190
- "Note: this fetches from 3 markets in parallel, consuming 3 API requests toward your rate limit.", {
1486
+ "Returns the top predictions sorted by confidence with game IDs, great for daily briefings. " +
1487
+ "Each pick includes a game_id you can use with get_pick_card. " +
1488
+ "Note: fetches from 3 markets in parallel, consuming 3 API requests.", {
191
1489
  count: z
192
1490
  .number()
193
1491
  .int()
@@ -204,7 +1502,6 @@ server.tool("get_top_picks", "Get the highest-conviction AI picks across all spo
204
1502
  .describe("Minimum confidence percentage."),
205
1503
  }, async ({ count, min_confidence }) => {
206
1504
  try {
207
- // Fetch top picks across all markets in parallel
208
1505
  const markets = ["moneyline", "spread", "totals"];
209
1506
  const results = await Promise.allSettled(markets.map((market) => apiFetch({
210
1507
  path: "/api/v1/predictions",
@@ -228,8 +1525,7 @@ server.tool("get_top_picks", "Get the highest-conviction AI picks across all spo
228
1525
  failedMarkets.push(markets[i]);
229
1526
  }
230
1527
  }
231
- // Sort by confidence descending, take top N
232
- allPicks.sort((a, b) => Number(b.confidence || 0) - Number(a.confidence || 0));
1528
+ allPicks.sort((a, b) => Number(b.confidence) - Number(a.confidence));
233
1529
  const topN = allPicks.slice(0, count);
234
1530
  if (topN.length === 0) {
235
1531
  return {
@@ -242,39 +1538,22 @@ server.tool("get_top_picks", "Get the highest-conviction AI picks across all spo
242
1538
  };
243
1539
  }
244
1540
  const lines = topN.map((p, i) => {
245
- const conf = p.confidence;
246
- const pick = p.predicted_winner ||
247
- p.predicted_pick ||
248
- p.predicted_direction ||
249
- "N/A";
250
- const sport = p.sport;
251
- const market = p._market;
252
- let line = `${i + 1}. [${sport}/${market}] ${p.away_team} @ ${p.home_team}`;
253
- line += `\n Pick: ${pick} | ${conf}% confidence`;
254
- if (p.home_edge)
255
- line += ` | ${p.home_edge}% edge`;
256
- if (p.commence_time) {
257
- const time = new Date(String(p.commence_time)).toLocaleString("en-US", {
258
- timeZone: "America/New_York",
259
- month: "short",
260
- day: "numeric",
261
- hour: "numeric",
262
- minute: "2-digit",
263
- timeZoneName: "short",
264
- });
265
- line += `\n Game: ${time}`;
266
- }
267
- return line;
1541
+ return `${i + 1}. [${p.sport}/${p._market}] ${formatPick(p)}`;
268
1542
  });
269
- let header = `Top ${topN.length} AI Picks (≥${min_confidence}% confidence):`;
1543
+ let header = `Top ${topN.length} AI Picks (>=${min_confidence}% confidence):`;
270
1544
  if (failedMarkets.length > 0) {
271
- header += `\n⚠ Could not fetch ${failedMarkets.join(", ")} predictions. Results may be partial.`;
1545
+ header += `\nWarning: Could not fetch ${failedMarkets.join(", ")} predictions.`;
272
1546
  }
1547
+ // Collect game IDs for quick reference
1548
+ const gameIds = topN.map((p) => p.game_id);
273
1549
  return {
274
1550
  content: [
275
1551
  {
276
1552
  type: "text",
277
- text: header + "\n\n" + lines.join("\n\n"),
1553
+ text: header +
1554
+ "\n\n" +
1555
+ lines.join("\n\n") +
1556
+ `\n\n─── Game IDs (for get_pick_card) ───\n${gameIds.join("\n")}`,
278
1557
  },
279
1558
  ],
280
1559
  };
@@ -284,54 +1563,138 @@ server.tool("get_top_picks", "Get the highest-conviction AI picks across all spo
284
1563
  }
285
1564
  });
286
1565
  // ─── Tool: get_pick_card ────────────────────────────────────────────────────
287
- server.tool("get_pick_card", "Render a prediction as a shareable 1080×1350 PNG image card with ESPN logos, " +
1566
+ server.tool("get_pick_card", "Render a prediction as a shareable 1080x1350 PNG image card with ESPN logos, " +
288
1567
  "L5 form dots, confidence bar, expanded AI analysis, and injuries. " +
289
- "Powered by Browserless screenshot engine. Great for social media. " +
290
- "Requires a game_id from get_predictions results. " +
1568
+ "Returns a permanent public URL (cached in Supabase Storage) plus an inline image preview. " +
1569
+ "Use the URL for video editing, social media posts, or embedding anywhere. " +
1570
+ "Accepts a game_id from get_predictions, get_game, or get_top_picks results. " +
1571
+ "You can also pass a team name instead of game_id — it will resolve automatically. " +
291
1572
  "Partner-tier keys recommended (higher rate limits for batch generation).", {
292
1573
  game_id: z
293
1574
  .string()
294
- .regex(/^[a-zA-Z0-9_-]+$/, "game_id must be alphanumeric with _ or -")
295
- .max(100)
296
- .describe("The game_id from a prediction result. Use get_predictions first to find game IDs."),
1575
+ .optional()
1576
+ .describe("The game_id from a prediction result. Use get_predictions or get_game to find game IDs."),
1577
+ team: z
1578
+ .string()
1579
+ .optional()
1580
+ .describe("Alternative to game_id — pass a team name and the card for their next game is rendered. " +
1581
+ "Examples: 'Lakers', 'NYY'. Requires an extra API call to resolve."),
297
1582
  sport: z
298
1583
  .enum(["NBA", "NFL", "NHL", "MLB"])
299
1584
  .optional()
300
1585
  .describe("Sport for the game (helps with lookup speed)."),
301
- }, async ({ game_id, sport }) => {
1586
+ }, async ({ game_id, team, sport }) => {
302
1587
  try {
1588
+ let resolvedGameId = game_id;
1589
+ // If team given instead of game_id, resolve it
1590
+ if (!resolvedGameId && team) {
1591
+ const resolved = resolveTeam(team, sport || undefined);
1592
+ const teamName = resolved ? resolved.nickname : team;
1593
+ const teamSport = sport || resolved?.sport;
1594
+ const params = {
1595
+ market: "moneyline",
1596
+ sort: "game_time",
1597
+ limit: "1",
1598
+ min_confidence: "0",
1599
+ team: teamName,
1600
+ };
1601
+ if (teamSport)
1602
+ params.sport = teamSport;
1603
+ const data = await apiFetch({
1604
+ path: "/api/v1/predictions",
1605
+ params,
1606
+ });
1607
+ if (data.predictions.length === 0) {
1608
+ return {
1609
+ content: [
1610
+ {
1611
+ type: "text",
1612
+ text: `No upcoming game found for "${team}". Cannot render card.`,
1613
+ },
1614
+ ],
1615
+ };
1616
+ }
1617
+ resolvedGameId = data.predictions[0].game_id;
1618
+ }
1619
+ if (!resolvedGameId) {
1620
+ return {
1621
+ content: [
1622
+ {
1623
+ type: "text",
1624
+ text: "Please provide either a game_id or team name.",
1625
+ },
1626
+ ],
1627
+ };
1628
+ }
1629
+ // Validate game_id format
1630
+ if (!/^[a-zA-Z0-9_-]+$/.test(resolvedGameId)) {
1631
+ return {
1632
+ content: [
1633
+ {
1634
+ type: "text",
1635
+ text: "Invalid game_id format. Must be alphanumeric with _ or -.",
1636
+ },
1637
+ ],
1638
+ };
1639
+ }
303
1640
  const params = {};
304
1641
  if (sport)
305
1642
  params.sport = sport;
306
- const imageBuffer = await apiFetch({
307
- path: `/api/social/card-render/${encodeURIComponent(game_id)}`,
1643
+ // Get permanent public URL
1644
+ const cardData = await apiFetch({
1645
+ path: `/api/social/card-url/${encodeURIComponent(resolvedGameId)}`,
308
1646
  params,
309
- raw: true,
310
1647
  });
311
- const base64 = Buffer.from(imageBuffer).toString("base64");
312
- return {
313
- content: [
314
- {
315
- type: "image",
316
- data: base64,
317
- mimeType: "image/png",
318
- },
319
- {
320
- type: "text",
321
- text: `Pick card rendered for game ${game_id} (1080×1350 PNG, ${Math.round(imageBuffer.byteLength / 1024)}KB).`,
322
- },
323
- ],
324
- };
1648
+ // Also fetch the actual image for inline preview
1649
+ let imageContent = null;
1650
+ try {
1651
+ const imageBuffer = await apiFetch({
1652
+ path: `/api/social/card-render/${encodeURIComponent(resolvedGameId)}`,
1653
+ params,
1654
+ raw: true,
1655
+ });
1656
+ imageContent = {
1657
+ type: "image",
1658
+ data: Buffer.from(imageBuffer).toString("base64"),
1659
+ mimeType: "image/png",
1660
+ };
1661
+ }
1662
+ catch {
1663
+ // Image preview is optional
1664
+ }
1665
+ const content = [];
1666
+ if (imageContent) {
1667
+ content.push(imageContent);
1668
+ }
1669
+ const sizeKB = cardData.size
1670
+ ? `${Math.round(cardData.size / 1024)}KB`
1671
+ : "unknown size";
1672
+ content.push({
1673
+ type: "text",
1674
+ text: [
1675
+ `Pick card for game ${resolvedGameId} (1080x1350 PNG, ${sizeKB})`,
1676
+ ``,
1677
+ `Public URL: ${cardData.url}`,
1678
+ `Cached: ${cardData.cached}`,
1679
+ cardData.rendered_at ? `Rendered: ${cardData.rendered_at}` : "",
1680
+ ``,
1681
+ `Use this URL in videos, social media posts, or anywhere you need the image.`,
1682
+ ]
1683
+ .filter(Boolean)
1684
+ .join("\n"),
1685
+ });
1686
+ return { content };
325
1687
  }
326
1688
  catch (err) {
327
1689
  return errorResult(err);
328
1690
  }
329
1691
  });
330
1692
  // ─── Tool: get_batch_cards ──────────────────────────────────────────────────
331
- server.tool("get_batch_cards", "Render multiple pick cards as PNG images in one call. " +
332
- "Designed for marketing teams generating social media content. " +
333
- "Partner-tier API key required for batch operations. " +
334
- "Returns one image per game_id.", {
1693
+ server.tool("get_batch_cards", "Render multiple pick cards and return permanent public URLs for each. " +
1694
+ "Designed for marketing teams generating social media content and video assets. " +
1695
+ "Each card is cached in Supabase Storage — subsequent calls return cached URLs instantly. " +
1696
+ "Partner-tier API key recommended. Max 10 per call. " +
1697
+ "Returns URLs suitable for video editing, social posts, or any image embed.", {
335
1698
  game_ids: z
336
1699
  .array(z
337
1700
  .string()
@@ -350,26 +1713,25 @@ server.tool("get_batch_cards", "Render multiple pick cards as PNG images in one
350
1713
  if (sport)
351
1714
  params.sport = sport;
352
1715
  const results = await Promise.allSettled(game_ids.map(async (gid) => {
353
- const buf = await apiFetch({
354
- path: `/api/social/card-render/${encodeURIComponent(gid)}`,
1716
+ const data = await apiFetch({
1717
+ path: `/api/social/card-url/${encodeURIComponent(gid)}`,
355
1718
  params,
356
- raw: true,
357
1719
  });
358
- return { game_id: gid, buffer: buf };
1720
+ return data;
359
1721
  }));
360
1722
  const content = [];
1723
+ const urls = [];
361
1724
  let successCount = 0;
362
1725
  for (const r of results) {
363
1726
  if (r.status === "fulfilled") {
364
- const base64 = Buffer.from(r.value.buffer).toString("base64");
365
- content.push({
366
- type: "image",
367
- data: base64,
368
- mimeType: "image/png",
369
- });
1727
+ urls.push(r.value.url);
1728
+ const sizeKB = r.value.size
1729
+ ? `${Math.round(r.value.size / 1024)}KB`
1730
+ : "";
1731
+ const cached = r.value.cached ? " (cached)" : " (fresh render)";
370
1732
  content.push({
371
1733
  type: "text",
372
- text: `Card: ${r.value.game_id} (${Math.round(r.value.buffer.byteLength / 1024)}KB)`,
1734
+ text: `${r.value.game_id}: ${r.value.url}${sizeKB ? ` — ${sizeKB}` : ""}${cached}`,
373
1735
  });
374
1736
  successCount++;
375
1737
  }
@@ -382,8 +1744,14 @@ server.tool("get_batch_cards", "Render multiple pick cards as PNG images in one
382
1744
  }
383
1745
  content.unshift({
384
1746
  type: "text",
385
- text: `Rendered ${successCount}/${game_ids.length} pick cards.`,
1747
+ text: `Rendered ${successCount}/${game_ids.length} pick cards.\n\nPublic URLs:`,
386
1748
  });
1749
+ if (urls.length > 0) {
1750
+ content.push({
1751
+ type: "text",
1752
+ text: `\n---\nAll URLs:\n${urls.join("\n")}`,
1753
+ });
1754
+ }
387
1755
  return { content };
388
1756
  }
389
1757
  catch (err) {
@@ -392,20 +1760,17 @@ server.tool("get_batch_cards", "Render multiple pick cards as PNG images in one
392
1760
  });
393
1761
  // ─── Tool: get_performance ──────────────────────────────────────────────────
394
1762
  server.tool("get_performance", "Get a snapshot of current active predictions across sports and markets. " +
395
- "Shows prediction counts, confidence distribution, and available coverage. " +
396
- "For historical win rates, visit davincibets.io/analytics.", {
1763
+ "Shows prediction counts, confidence distribution, and available coverage.", {
397
1764
  sport: z
398
1765
  .enum(["NBA", "NFL", "NHL", "MLB"])
399
1766
  .optional()
400
- .describe("Filter performance to a specific sport."),
1767
+ .describe("Filter to a specific sport."),
401
1768
  market: z
402
1769
  .enum(["moneyline", "spread", "totals"])
403
1770
  .optional()
404
1771
  .describe("Filter to a specific market."),
405
1772
  }, async ({ sport, market }) => {
406
1773
  try {
407
- // Pull recent settled predictions to compute win rates
408
- // We use the predictions endpoint with high confidence to show quality
409
1774
  const params = {
410
1775
  sort: "confidence",
411
1776
  limit: "50",
@@ -419,7 +1784,6 @@ server.tool("get_performance", "Get a snapshot of current active predictions acr
419
1784
  path: "/api/v1/predictions",
420
1785
  params,
421
1786
  });
422
- // Summarize available predictions as a performance snapshot
423
1787
  const byMarket = {};
424
1788
  const bySport = {};
425
1789
  const confBuckets = { high: 0, medium: 0 };
@@ -428,15 +1792,14 @@ server.tool("get_performance", "Get a snapshot of current active predictions acr
428
1792
  const s = String(p.sport || "?");
429
1793
  byMarket[m] = (byMarket[m] || 0) + 1;
430
1794
  bySport[s] = (bySport[s] || 0) + 1;
431
- const conf = Number(p.confidence || 0);
432
- if (conf >= 65)
1795
+ if (p.confidence >= 65)
433
1796
  confBuckets.high++;
434
1797
  else
435
1798
  confBuckets.medium++;
436
1799
  }
437
1800
  const lines = [
438
1801
  "Da Vinci Bets — Current Predictions Snapshot",
439
- "═══════════════════════════════════════════════",
1802
+ "═".repeat(50),
440
1803
  `Total active predictions: ${data.meta?.total ?? data.predictions.length}`,
441
1804
  "",
442
1805
  "By Sport:",
@@ -446,13 +1809,9 @@ server.tool("get_performance", "Get a snapshot of current active predictions acr
446
1809
  ...Object.entries(byMarket).map(([m, n]) => ` ${m}: ${n} picks`),
447
1810
  "",
448
1811
  "Confidence Distribution:",
449
- ` High (≥65%): ${confBuckets.high} picks`,
1812
+ ` High (>=65%): ${confBuckets.high} picks`,
450
1813
  ` Medium (50-64%): ${confBuckets.medium} picks`,
451
- "",
452
- `Rate limit: ${data.rate_limit?.remaining_hour ?? "?"}/hour, ${data.rate_limit?.remaining_day ?? "?"}/day remaining.`,
453
- "",
454
- "Note: Historical win rates are available on the Da Vinci Bets analytics dashboard.",
455
- "Visit https://www.davincibets.io/analytics for full performance tracking.",
1814
+ formatRateLimit(data.rate_limit),
456
1815
  ];
457
1816
  return {
458
1817
  content: [{ type: "text", text: lines.join("\n") }],
@@ -462,9 +1821,86 @@ server.tool("get_performance", "Get a snapshot of current active predictions acr
462
1821
  return errorResult(err);
463
1822
  }
464
1823
  });
1824
+ // ─── Tool: get_track_record ─────────────────────────────────────────────────
1825
+ server.tool("get_track_record", "Get verified win rates, ROI, and historical performance from settled predictions. " +
1826
+ "Shows actual results broken down by sport, market, and confidence tier. " +
1827
+ "Includes current streak and recent settled picks. " +
1828
+ "Public endpoint — does not count against your API rate limit.", {
1829
+ sport: z
1830
+ .enum(["NBA", "NFL", "NHL", "MLB"])
1831
+ .optional()
1832
+ .describe("Filter to a specific sport. Omit for all."),
1833
+ days: z
1834
+ .number()
1835
+ .int()
1836
+ .min(7)
1837
+ .max(365)
1838
+ .default(30)
1839
+ .describe("Lookback period in days (7-365)."),
1840
+ }, async ({ sport, days }) => {
1841
+ try {
1842
+ const params = { days: String(days) };
1843
+ if (sport)
1844
+ params.sport = sport;
1845
+ const url = new URL("/api/track-record", API_BASE);
1846
+ for (const [k, v] of Object.entries(params)) {
1847
+ url.searchParams.set(k, v);
1848
+ }
1849
+ const res = await fetch(url.toString(), {
1850
+ headers: { Accept: "application/json" },
1851
+ signal: AbortSignal.timeout(15_000),
1852
+ });
1853
+ if (!res.ok) {
1854
+ throw new Error(`Track record request failed (HTTP ${res.status}).`);
1855
+ }
1856
+ const data = (await res.json());
1857
+ const breakdownLines = data.breakdown.map((b) => {
1858
+ const emoji = b.win_rate >= 55 ? "+" : b.win_rate >= 50 ? "~" : "-";
1859
+ return ` ${b.sport} ${b.market_type}: ${b.wins}-${b.losses}${b.pushes ? `-${b.pushes}P` : ""} (${b.win_rate}% WR, ${emoji}${b.roi}% ROI)`;
1860
+ });
1861
+ const tierLines = data.confidenceTiers.map((t) => ` ${t.tier}%: ${t.wins}/${t.total} (${t.win_rate}% WR)`);
1862
+ const recentLines = data.recentPicks.slice(0, 10).map((p) => {
1863
+ const icon = p.result === "win" ? "W" : p.result === "loss" ? "L" : "P";
1864
+ return ` [${icon}] ${p.sport} ${p.market_type}: ${p.pick_team}`;
1865
+ });
1866
+ const lines = [
1867
+ `Da Vinci Bets — Track Record (${data.period})`,
1868
+ "═".repeat(50),
1869
+ data.streak ? `Current Streak: ${data.streak}` : "",
1870
+ "",
1871
+ "Performance by Sport & Market:",
1872
+ ...breakdownLines,
1873
+ "",
1874
+ "By Confidence Tier:",
1875
+ ...tierLines,
1876
+ "",
1877
+ `Recent Settled Picks (last ${Math.min(data.recentPicks.length, 10)}):`,
1878
+ ...recentLines,
1879
+ ].filter(Boolean);
1880
+ return {
1881
+ content: [
1882
+ { type: "text", text: lines.join("\n") },
1883
+ {
1884
+ type: "text",
1885
+ text: "\n---\nRaw JSON:\n" +
1886
+ JSON.stringify({
1887
+ period: data.period,
1888
+ streak: data.streak,
1889
+ breakdown: data.breakdown,
1890
+ confidenceTiers: data.confidenceTiers,
1891
+ }, null, 2),
1892
+ },
1893
+ ],
1894
+ };
1895
+ }
1896
+ catch (err) {
1897
+ return errorResult(err);
1898
+ }
1899
+ });
465
1900
  // ─── Tool: get_schedule ─────────────────────────────────────────────────────
466
1901
  server.tool("get_schedule", "Get today's game schedule with predictions summary. " +
467
- "Shows which games have predictions available and their confidence levels.", {
1902
+ "Shows which games have predictions available, their confidence levels, and game IDs. " +
1903
+ "Game IDs can be used with get_pick_card or get_game for more details.", {
468
1904
  sport: z
469
1905
  .enum(["NBA", "NFL", "NHL", "MLB"])
470
1906
  .describe("Which sport's schedule to retrieve."),
@@ -486,7 +1922,7 @@ server.tool("get_schedule", "Get today's game schedule with predictions summary.
486
1922
  content: [
487
1923
  {
488
1924
  type: "text",
489
- text: `No ${sport} games found in the current prediction window. The ${sport} season may be in offseason, or no games are scheduled for the next 48 hours.`,
1925
+ text: `No ${sport} games found in the current prediction window. The ${sport} season may be in offseason, or no games are scheduled.`,
490
1926
  },
491
1927
  ],
492
1928
  };
@@ -498,7 +1934,7 @@ server.tool("get_schedule", "Get today's game schedule with predictions summary.
498
1934
  ];
499
1935
  for (const p of data.predictions) {
500
1936
  const time = p.commence_time
501
- ? new Date(String(p.commence_time)).toLocaleString("en-US", {
1937
+ ? new Date(p.commence_time).toLocaleString("en-US", {
502
1938
  timeZone: "America/New_York",
503
1939
  weekday: "short",
504
1940
  month: "short",
@@ -508,12 +1944,14 @@ server.tool("get_schedule", "Get today's game schedule with predictions summary.
508
1944
  timeZoneName: "short",
509
1945
  })
510
1946
  : "TBD";
1947
+ const tier = p.confidence_tier ? ` [${p.confidence_tier}]` : "";
511
1948
  lines.push(`${time}`);
512
1949
  lines.push(` ${p.away_team} @ ${p.home_team}`);
513
- lines.push(` Pick: ${p.predicted_winner} (${p.confidence}% conf) | Game ID: ${p.game_id}`);
1950
+ lines.push(` Pick: ${p.predicted_winner} (${p.confidence}% conf)${tier}`);
1951
+ lines.push(` Game ID: ${p.game_id}`);
514
1952
  lines.push("");
515
1953
  }
516
- lines.push("Use game_id values with get_pick_card to render shareable images.");
1954
+ lines.push("Use game IDs with get_pick_card to render shareable images, or get_game for full details.");
517
1955
  return {
518
1956
  content: [{ type: "text", text: lines.join("\n") }],
519
1957
  };
@@ -534,9 +1972,10 @@ function errorResult(err) {
534
1972
  async function main() {
535
1973
  const transport = new StdioServerTransport();
536
1974
  await server.connect(transport);
537
- console.error("Da Vinci Bets MCP Server v1.0.1 — connected via stdio");
1975
+ console.error("Da Vinci Bets MCP Server v1.3.1 — connected via stdio");
538
1976
  console.error(`API: ${API_BASE}`);
539
1977
  console.error(`Key: dvb_live_***`);
1978
+ console.error(`Teams: ${TEAMS.length} teams loaded across 4 sports`);
540
1979
  }
541
1980
  main().catch((err) => {
542
1981
  console.error("Fatal:", err);