@davincibets/mcp-server 1.0.1 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -106,10 +106,33 @@ Fetch AI predictions by sport, market, and confidence.
106
106
  |-----------|------|---------|-------------|
107
107
  | `sport` | NBA, NFL, NHL, MLB | all | Filter by sport |
108
108
  | `market` | moneyline, spread, totals | moneyline | Betting market |
109
+ | `team` | string | — | Filter by team name, abbreviation, or city (e.g., `Lakers`, `LAL`, `Boston`) |
109
110
  | `sort` | confidence, edge, game_time | confidence | Sort order |
110
111
  | `limit` | 1-50 | 25 | Max results |
111
112
  | `min_confidence` | 0-100 | 60 | Minimum confidence % |
112
113
 
114
+ ### `get_game`
115
+
116
+ Find a specific game's prediction by team name. Intelligent lookup — give it a team name (nickname, abbreviation, or city) and it finds the matching game.
117
+
118
+ | Parameter | Type | Required | Description |
119
+ |-----------|------|----------|-------------|
120
+ | `team` | string | Yes | Team name, abbreviation, or city (e.g., `Lakers`, `LAL`, `Los Angeles`) |
121
+ | `opponent` | string | No | Narrow to a specific matchup (e.g., `Celtics`) |
122
+ | `sport` | NBA, NFL, NHL, MLB | No | Sport hint — auto-detected from team name if omitted |
123
+ | `market` | moneyline, spread, totals | No | Market to look up (default: moneyline) |
124
+
125
+ ### `lookup_teams`
126
+
127
+ Search the team registry by name, abbreviation, city, or sport. Instant local lookup — no API call, no rate limit impact.
128
+
129
+ | Parameter | Type | Required | Description |
130
+ |-----------|------|----------|-------------|
131
+ | `query` | string | No | Search by name, abbreviation, or city (e.g., `NY`, `CHI`, `Boston`) |
132
+ | `sport` | NBA, NFL, NHL, MLB | No | Filter to a specific sport |
133
+
134
+ Covers 124 teams across 4 sports. Omit both parameters for a summary count.
135
+
113
136
  ### `get_top_picks`
114
137
 
115
138
  Get the highest-conviction picks across all sports and markets.
@@ -129,13 +152,16 @@ Get today's game schedule with predictions for a sport.
129
152
 
130
153
  ### `get_pick_card`
131
154
 
132
- Render a prediction as a shareable 1080x1350 PNG image.
155
+ Render a prediction as a shareable 1080x1350 PNG image. Returns a permanent public URL plus an inline image preview.
133
156
 
134
157
  | Parameter | Type | Required | Description |
135
158
  |-----------|------|----------|-------------|
136
- | `game_id` | string | Yes | Game ID from predictions |
159
+ | `game_id` | string | No | Game ID from predictions |
160
+ | `team` | string | No | Alternative — pass a team name and the card for their next game is rendered |
137
161
  | `sport` | NBA, NFL, NHL, MLB | No | Sport hint |
138
162
 
163
+ Provide either `game_id` or `team`. The `team` option requires an extra API call to resolve.
164
+
139
165
  ### `get_batch_cards`
140
166
 
141
167
  Render multiple pick cards at once (up to 10). Designed for marketing teams.
@@ -154,6 +180,15 @@ Get current prediction performance snapshot.
154
180
  | `sport` | NBA, NFL, NHL, MLB | all | Filter by sport |
155
181
  | `market` | moneyline, spread, totals | all | Filter by market |
156
182
 
183
+ ### `get_track_record`
184
+
185
+ Get verified win rates, ROI, and historical performance from settled predictions. Shows actual results by sport, market, confidence tier, current streak, and recent settled picks. Public endpoint — does not count against your rate limit.
186
+
187
+ | Parameter | Type | Default | Description |
188
+ |-----------|------|---------|-------------|
189
+ | `sport` | NBA, NFL, NHL, MLB | all | Filter by sport |
190
+ | `days` | 7-365 | 30 | Lookback period in days |
191
+
157
192
  ## Rate Limits
158
193
 
159
194
  | Tier | Per Hour | Per Day | Who |
package/dist/index.d.ts 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.0
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
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.0
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,7 +23,6 @@ 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
26
  const ALLOWED_API_HOSTS = new Set([
23
27
  "www.davincibets.io",
24
28
  "davincibets.io",
@@ -52,6 +56,194 @@ if (!_rawKey.startsWith("dvb_live_")) {
52
56
  process.exit(1);
53
57
  }
54
58
  const API_KEY = _rawKey;
59
+ const TEAMS = [
60
+ // ── NBA (30) ────────────────────────────────────────────────────────────
61
+ { abbr: "ATL", nickname: "Hawks", city: "Atlanta", fullName: "Atlanta Hawks", sport: "NBA", aliases: [] },
62
+ { abbr: "BOS", nickname: "Celtics", city: "Boston", fullName: "Boston Celtics", sport: "NBA", aliases: [] },
63
+ { abbr: "BKN", nickname: "Nets", city: "Brooklyn", fullName: "Brooklyn Nets", sport: "NBA", aliases: ["BRK"] },
64
+ { abbr: "CHA", nickname: "Hornets", city: "Charlotte", fullName: "Charlotte Hornets", sport: "NBA", aliases: ["CHH"] },
65
+ { abbr: "CHI", nickname: "Bulls", city: "Chicago", fullName: "Chicago Bulls", sport: "NBA", aliases: [] },
66
+ { abbr: "CLE", nickname: "Cavaliers", city: "Cleveland", fullName: "Cleveland Cavaliers", sport: "NBA", aliases: ["Cavs"] },
67
+ { abbr: "DAL", nickname: "Mavericks", city: "Dallas", fullName: "Dallas Mavericks", sport: "NBA", aliases: ["Mavs"] },
68
+ { abbr: "DEN", nickname: "Nuggets", city: "Denver", fullName: "Denver Nuggets", sport: "NBA", aliases: [] },
69
+ { abbr: "DET", nickname: "Pistons", city: "Detroit", fullName: "Detroit Pistons", sport: "NBA", aliases: [] },
70
+ { abbr: "GSW", nickname: "Warriors", city: "Golden State", fullName: "Golden State Warriors", sport: "NBA", aliases: ["GS", "SF Warriors"] },
71
+ { abbr: "HOU", nickname: "Rockets", city: "Houston", fullName: "Houston Rockets", sport: "NBA", aliases: [] },
72
+ { abbr: "IND", nickname: "Pacers", city: "Indiana", fullName: "Indiana Pacers", sport: "NBA", aliases: ["Indianapolis"] },
73
+ { abbr: "LAC", nickname: "Clippers", city: "Los Angeles", fullName: "Los Angeles Clippers", sport: "NBA", aliases: ["LA Clippers"] },
74
+ { abbr: "LAL", nickname: "Lakers", city: "Los Angeles", fullName: "Los Angeles Lakers", sport: "NBA", aliases: ["LA Lakers"] },
75
+ { abbr: "MEM", nickname: "Grizzlies", city: "Memphis", fullName: "Memphis Grizzlies", sport: "NBA", aliases: ["Grizz"] },
76
+ { abbr: "MIA", nickname: "Heat", city: "Miami", fullName: "Miami Heat", sport: "NBA", aliases: [] },
77
+ { abbr: "MIL", nickname: "Bucks", city: "Milwaukee", fullName: "Milwaukee Bucks", sport: "NBA", aliases: [] },
78
+ { abbr: "MIN", nickname: "Timberwolves", city: "Minnesota", fullName: "Minnesota Timberwolves", sport: "NBA", aliases: ["Wolves", "TWolves", "Minneapolis"] },
79
+ { abbr: "NOP", nickname: "Pelicans", city: "New Orleans", fullName: "New Orleans Pelicans", sport: "NBA", aliases: ["NO", "NOLA", "Pels"] },
80
+ { abbr: "NYK", nickname: "Knicks", city: "New York", fullName: "New York Knicks", sport: "NBA", aliases: ["NY Knicks"] },
81
+ { abbr: "OKC", nickname: "Thunder", city: "Oklahoma City", fullName: "Oklahoma City Thunder", sport: "NBA", aliases: [] },
82
+ { abbr: "ORL", nickname: "Magic", city: "Orlando", fullName: "Orlando Magic", sport: "NBA", aliases: [] },
83
+ { abbr: "PHI", nickname: "76ers", city: "Philadelphia", fullName: "Philadelphia 76ers", sport: "NBA", aliases: ["Sixers", "Philly"] },
84
+ { abbr: "PHX", nickname: "Suns", city: "Phoenix", fullName: "Phoenix Suns", sport: "NBA", aliases: ["PHO"] },
85
+ { abbr: "POR", nickname: "Trail Blazers", city: "Portland", fullName: "Portland Trail Blazers", sport: "NBA", aliases: ["Blazers"] },
86
+ { abbr: "SAC", nickname: "Kings", city: "Sacramento", fullName: "Sacramento Kings", sport: "NBA", aliases: [] },
87
+ { abbr: "SAS", nickname: "Spurs", city: "San Antonio", fullName: "San Antonio Spurs", sport: "NBA", aliases: ["SA"] },
88
+ { abbr: "TOR", nickname: "Raptors", city: "Toronto", fullName: "Toronto Raptors", sport: "NBA", aliases: ["Raps"] },
89
+ { abbr: "UTA", nickname: "Jazz", city: "Utah", fullName: "Utah Jazz", sport: "NBA", aliases: ["SLC", "Salt Lake"] },
90
+ { abbr: "WAS", nickname: "Wizards", city: "Washington", fullName: "Washington Wizards", sport: "NBA", aliases: ["DC", "Wiz"] },
91
+ // ── NFL (32) ────────────────────────────────────────────────────────────
92
+ { abbr: "ARI", nickname: "Cardinals", city: "Arizona", fullName: "Arizona Cardinals", sport: "NFL", aliases: ["Phoenix"] },
93
+ { abbr: "ATL", nickname: "Falcons", city: "Atlanta", fullName: "Atlanta Falcons", sport: "NFL", aliases: [] },
94
+ { abbr: "BAL", nickname: "Ravens", city: "Baltimore", fullName: "Baltimore Ravens", sport: "NFL", aliases: [] },
95
+ { abbr: "BUF", nickname: "Bills", city: "Buffalo", fullName: "Buffalo Bills", sport: "NFL", aliases: [] },
96
+ { abbr: "CAR", nickname: "Panthers", city: "Carolina", fullName: "Carolina Panthers", sport: "NFL", aliases: ["Charlotte"] },
97
+ { abbr: "CHI", nickname: "Bears", city: "Chicago", fullName: "Chicago Bears", sport: "NFL", aliases: [] },
98
+ { abbr: "CIN", nickname: "Bengals", city: "Cincinnati", fullName: "Cincinnati Bengals", sport: "NFL", aliases: ["Cincy"] },
99
+ { abbr: "CLE", nickname: "Browns", city: "Cleveland", fullName: "Cleveland Browns", sport: "NFL", aliases: [] },
100
+ { abbr: "DAL", nickname: "Cowboys", city: "Dallas", fullName: "Dallas Cowboys", sport: "NFL", aliases: ["America's Team"] },
101
+ { abbr: "DEN", nickname: "Broncos", city: "Denver", fullName: "Denver Broncos", sport: "NFL", aliases: [] },
102
+ { abbr: "DET", nickname: "Lions", city: "Detroit", fullName: "Detroit Lions", sport: "NFL", aliases: [] },
103
+ { abbr: "GB", nickname: "Packers", city: "Green Bay", fullName: "Green Bay Packers", sport: "NFL", aliases: ["GNB"] },
104
+ { abbr: "HOU", nickname: "Texans", city: "Houston", fullName: "Houston Texans", sport: "NFL", aliases: [] },
105
+ { abbr: "IND", nickname: "Colts", city: "Indianapolis", fullName: "Indianapolis Colts", sport: "NFL", aliases: [] },
106
+ { abbr: "JAX", nickname: "Jaguars", city: "Jacksonville", fullName: "Jacksonville Jaguars", sport: "NFL", aliases: ["Jags", "JAC"] },
107
+ { abbr: "KC", nickname: "Chiefs", city: "Kansas City", fullName: "Kansas City Chiefs", sport: "NFL", aliases: ["KCC"] },
108
+ { abbr: "LV", nickname: "Raiders", city: "Las Vegas", fullName: "Las Vegas Raiders", sport: "NFL", aliases: ["LVR", "Oakland"] },
109
+ { abbr: "LAC", nickname: "Chargers", city: "Los Angeles", fullName: "Los Angeles Chargers", sport: "NFL", aliases: ["LA Chargers", "San Diego"] },
110
+ { abbr: "LAR", nickname: "Rams", city: "Los Angeles", fullName: "Los Angeles Rams", sport: "NFL", aliases: ["LA Rams", "STL Rams"] },
111
+ { abbr: "MIA", nickname: "Dolphins", city: "Miami", fullName: "Miami Dolphins", sport: "NFL", aliases: ["Fins"] },
112
+ { abbr: "MIN", nickname: "Vikings", city: "Minnesota", fullName: "Minnesota Vikings", sport: "NFL", aliases: ["Minneapolis"] },
113
+ { abbr: "NE", nickname: "Patriots", city: "New England", fullName: "New England Patriots", sport: "NFL", aliases: ["Pats", "NEP", "Boston Patriots"] },
114
+ { abbr: "NO", nickname: "Saints", city: "New Orleans", fullName: "New Orleans Saints", sport: "NFL", aliases: ["NOLA", "NOS"] },
115
+ { abbr: "NYG", nickname: "Giants", city: "New York", fullName: "New York Giants", sport: "NFL", aliases: ["NY Giants"] },
116
+ { abbr: "NYJ", nickname: "Jets", city: "New York", fullName: "New York Jets", sport: "NFL", aliases: ["NY Jets"] },
117
+ { abbr: "PHI", nickname: "Eagles", city: "Philadelphia", fullName: "Philadelphia Eagles", sport: "NFL", aliases: ["Philly"] },
118
+ { abbr: "PIT", nickname: "Steelers", city: "Pittsburgh", fullName: "Pittsburgh Steelers", sport: "NFL", aliases: [] },
119
+ { abbr: "SF", nickname: "49ers", city: "San Francisco", fullName: "San Francisco 49ers", sport: "NFL", aliases: ["Niners", "SFO"] },
120
+ { abbr: "SEA", nickname: "Seahawks", city: "Seattle", fullName: "Seattle Seahawks", sport: "NFL", aliases: [] },
121
+ { abbr: "TB", nickname: "Buccaneers", city: "Tampa Bay", fullName: "Tampa Bay Buccaneers", sport: "NFL", aliases: ["Bucs", "TBB", "Tampa"] },
122
+ { abbr: "TEN", nickname: "Titans", city: "Tennessee", fullName: "Tennessee Titans", sport: "NFL", aliases: ["Nashville"] },
123
+ { abbr: "WAS", nickname: "Commanders", city: "Washington", fullName: "Washington Commanders", sport: "NFL", aliases: ["DC", "WSH"] },
124
+ // ── NHL (32) ────────────────────────────────────────────────────────────
125
+ { abbr: "ANA", nickname: "Ducks", city: "Anaheim", fullName: "Anaheim Ducks", sport: "NHL", aliases: [] },
126
+ { abbr: "BOS", nickname: "Bruins", city: "Boston", fullName: "Boston Bruins", sport: "NHL", aliases: [] },
127
+ { abbr: "BUF", nickname: "Sabres", city: "Buffalo", fullName: "Buffalo Sabres", sport: "NHL", aliases: [] },
128
+ { abbr: "CGY", nickname: "Flames", city: "Calgary", fullName: "Calgary Flames", sport: "NHL", aliases: ["CAL"] },
129
+ { abbr: "CAR", nickname: "Hurricanes", city: "Carolina", fullName: "Carolina Hurricanes", sport: "NHL", aliases: ["Canes", "Raleigh"] },
130
+ { abbr: "CHI", nickname: "Blackhawks", city: "Chicago", fullName: "Chicago Blackhawks", sport: "NHL", aliases: ["Hawks"] },
131
+ { abbr: "COL", nickname: "Avalanche", city: "Colorado", fullName: "Colorado Avalanche", sport: "NHL", aliases: ["Avs", "Denver"] },
132
+ { abbr: "CBJ", nickname: "Blue Jackets", city: "Columbus", fullName: "Columbus Blue Jackets", sport: "NHL", aliases: ["Jackets"] },
133
+ { abbr: "DAL", nickname: "Stars", city: "Dallas", fullName: "Dallas Stars", sport: "NHL", aliases: [] },
134
+ { abbr: "DET", nickname: "Red Wings", city: "Detroit", fullName: "Detroit Red Wings", sport: "NHL", aliases: ["Wings"] },
135
+ { abbr: "EDM", nickname: "Oilers", city: "Edmonton", fullName: "Edmonton Oilers", sport: "NHL", aliases: [] },
136
+ { abbr: "FLA", nickname: "Panthers", city: "Florida", fullName: "Florida Panthers", sport: "NHL", aliases: ["Cats", "Sunrise"] },
137
+ { abbr: "LAK", nickname: "Kings", city: "Los Angeles", fullName: "Los Angeles Kings", sport: "NHL", aliases: ["LA Kings"] },
138
+ { abbr: "MIN", nickname: "Wild", city: "Minnesota", fullName: "Minnesota Wild", sport: "NHL", aliases: ["Minneapolis", "St Paul"] },
139
+ { abbr: "MTL", nickname: "Canadiens", city: "Montreal", fullName: "Montreal Canadiens", sport: "NHL", aliases: ["Habs", "MON"] },
140
+ { abbr: "NSH", nickname: "Predators", city: "Nashville", fullName: "Nashville Predators", sport: "NHL", aliases: ["Preds"] },
141
+ { abbr: "NJD", nickname: "Devils", city: "New Jersey", fullName: "New Jersey Devils", sport: "NHL", aliases: ["NJ", "Newark"] },
142
+ { abbr: "NYI", nickname: "Islanders", city: "New York", fullName: "New York Islanders", sport: "NHL", aliases: ["Isles", "NY Islanders"] },
143
+ { abbr: "NYR", nickname: "Rangers", city: "New York", fullName: "New York Rangers", sport: "NHL", aliases: ["NY Rangers"] },
144
+ { abbr: "OTT", nickname: "Senators", city: "Ottawa", fullName: "Ottawa Senators", sport: "NHL", aliases: ["Sens"] },
145
+ { abbr: "PHI", nickname: "Flyers", city: "Philadelphia", fullName: "Philadelphia Flyers", sport: "NHL", aliases: ["Philly"] },
146
+ { abbr: "PIT", nickname: "Penguins", city: "Pittsburgh", fullName: "Pittsburgh Penguins", sport: "NHL", aliases: ["Pens"] },
147
+ { abbr: "SJS", nickname: "Sharks", city: "San Jose", fullName: "San Jose Sharks", sport: "NHL", aliases: ["SJ"] },
148
+ { abbr: "SEA", nickname: "Kraken", city: "Seattle", fullName: "Seattle Kraken", sport: "NHL", aliases: [] },
149
+ { abbr: "STL", nickname: "Blues", city: "St. Louis", fullName: "St. Louis Blues", sport: "NHL", aliases: ["Saint Louis", "St Louis"] },
150
+ { abbr: "TBL", nickname: "Lightning", city: "Tampa Bay", fullName: "Tampa Bay Lightning", sport: "NHL", aliases: ["Bolts", "TB", "Tampa"] },
151
+ { abbr: "TOR", nickname: "Maple Leafs", city: "Toronto", fullName: "Toronto Maple Leafs", sport: "NHL", aliases: ["Leafs"] },
152
+ { abbr: "VAN", nickname: "Canucks", city: "Vancouver", fullName: "Vancouver Canucks", sport: "NHL", aliases: [] },
153
+ { abbr: "VGK", nickname: "Golden Knights", city: "Vegas", fullName: "Vegas Golden Knights", sport: "NHL", aliases: ["VGS", "Las Vegas", "Knights"] },
154
+ { abbr: "WAS", nickname: "Capitals", city: "Washington", fullName: "Washington Capitals", sport: "NHL", aliases: ["DC", "WSH", "Caps"] },
155
+ { abbr: "WPG", nickname: "Jets", city: "Winnipeg", fullName: "Winnipeg Jets", sport: "NHL", aliases: ["WIN"] },
156
+ { abbr: "UTA", nickname: "Hockey Club", city: "Utah", fullName: "Utah Hockey Club", sport: "NHL", aliases: ["UHC", "Salt Lake"] },
157
+ // ── MLB (30) ────────────────────────────────────────────────────────────
158
+ { abbr: "ARI", nickname: "Diamondbacks", city: "Arizona", fullName: "Arizona Diamondbacks", sport: "MLB", aliases: ["D-backs", "Dbacks", "Phoenix"] },
159
+ { abbr: "ATL", nickname: "Braves", city: "Atlanta", fullName: "Atlanta Braves", sport: "MLB", aliases: [] },
160
+ { abbr: "BAL", nickname: "Orioles", city: "Baltimore", fullName: "Baltimore Orioles", sport: "MLB", aliases: ["O's"] },
161
+ { abbr: "BOS", nickname: "Red Sox", city: "Boston", fullName: "Boston Red Sox", sport: "MLB", aliases: ["Sox", "BoSox"] },
162
+ { abbr: "CHC", nickname: "Cubs", city: "Chicago", fullName: "Chicago Cubs", sport: "MLB", aliases: [] },
163
+ { abbr: "CHW", nickname: "White Sox", city: "Chicago", fullName: "Chicago White Sox", sport: "MLB", aliases: ["CWS", "Sox"] },
164
+ { abbr: "CIN", nickname: "Reds", city: "Cincinnati", fullName: "Cincinnati Reds", sport: "MLB", aliases: ["Cincy"] },
165
+ { abbr: "CLE", nickname: "Guardians", city: "Cleveland", fullName: "Cleveland Guardians", sport: "MLB", aliases: ["Guards"] },
166
+ { abbr: "COL", nickname: "Rockies", city: "Colorado", fullName: "Colorado Rockies", sport: "MLB", aliases: ["Denver"] },
167
+ { abbr: "DET", nickname: "Tigers", city: "Detroit", fullName: "Detroit Tigers", sport: "MLB", aliases: [] },
168
+ { abbr: "HOU", nickname: "Astros", city: "Houston", fullName: "Houston Astros", sport: "MLB", aliases: ["Stros"] },
169
+ { abbr: "KC", nickname: "Royals", city: "Kansas City", fullName: "Kansas City Royals", sport: "MLB", aliases: ["KCR"] },
170
+ { abbr: "LAA", nickname: "Angels", city: "Los Angeles", fullName: "Los Angeles Angels", sport: "MLB", aliases: ["LA Angels", "Anaheim", "Halos"] },
171
+ { abbr: "LAD", nickname: "Dodgers", city: "Los Angeles", fullName: "Los Angeles Dodgers", sport: "MLB", aliases: ["LA Dodgers"] },
172
+ { abbr: "MIA", nickname: "Marlins", city: "Miami", fullName: "Miami Marlins", sport: "MLB", aliases: ["Fish"] },
173
+ { abbr: "MIL", nickname: "Brewers", city: "Milwaukee", fullName: "Milwaukee Brewers", sport: "MLB", aliases: ["Brew Crew"] },
174
+ { abbr: "MIN", nickname: "Twins", city: "Minnesota", fullName: "Minnesota Twins", sport: "MLB", aliases: ["Minneapolis"] },
175
+ { abbr: "NYM", nickname: "Mets", city: "New York", fullName: "New York Mets", sport: "MLB", aliases: ["NY Mets"] },
176
+ { abbr: "NYY", nickname: "Yankees", city: "New York", fullName: "New York Yankees", sport: "MLB", aliases: ["NY Yankees", "Yanks", "Bronx Bombers"] },
177
+ { abbr: "OAK", nickname: "Athletics", city: "Oakland", fullName: "Oakland Athletics", sport: "MLB", aliases: ["A's", "As"] },
178
+ { abbr: "PHI", nickname: "Phillies", city: "Philadelphia", fullName: "Philadelphia Phillies", sport: "MLB", aliases: ["Philly"] },
179
+ { abbr: "PIT", nickname: "Pirates", city: "Pittsburgh", fullName: "Pittsburgh Pirates", sport: "MLB", aliases: ["Bucs"] },
180
+ { abbr: "SD", nickname: "Padres", city: "San Diego", fullName: "San Diego Padres", sport: "MLB", aliases: ["SDP"] },
181
+ { abbr: "SF", nickname: "Giants", city: "San Francisco", fullName: "San Francisco Giants", sport: "MLB", aliases: ["SFG"] },
182
+ { abbr: "SEA", nickname: "Mariners", city: "Seattle", fullName: "Seattle Mariners", sport: "MLB", aliases: ["M's", "Ms"] },
183
+ { abbr: "STL", nickname: "Cardinals", city: "St. Louis", fullName: "St. Louis Cardinals", sport: "MLB", aliases: ["Cards", "Saint Louis", "St Louis"] },
184
+ { abbr: "TB", nickname: "Rays", city: "Tampa Bay", fullName: "Tampa Bay Rays", sport: "MLB", aliases: ["TBR", "Tampa"] },
185
+ { abbr: "TEX", nickname: "Rangers", city: "Texas", fullName: "Texas Rangers", sport: "MLB", aliases: ["Arlington"] },
186
+ { abbr: "TOR", nickname: "Blue Jays", city: "Toronto", fullName: "Toronto Blue Jays", sport: "MLB", aliases: ["Jays"] },
187
+ { abbr: "WAS", nickname: "Nationals", city: "Washington", fullName: "Washington Nationals", sport: "MLB", aliases: ["DC", "WSH", "Nats"] },
188
+ ];
189
+ /**
190
+ * Fuzzy team search — matches query against abbreviation, nickname, city,
191
+ * fullName, and aliases. Returns scored results sorted by match quality.
192
+ */
193
+ function searchTeams(query, sportFilter) {
194
+ const q = query.toLowerCase().trim();
195
+ if (!q)
196
+ return [];
197
+ const results = [];
198
+ for (const team of TEAMS) {
199
+ if (sportFilter && team.sport !== sportFilter)
200
+ continue;
201
+ let score = 0;
202
+ const abbrL = team.abbr.toLowerCase();
203
+ const nickL = team.nickname.toLowerCase();
204
+ const cityL = team.city.toLowerCase();
205
+ const fullL = team.fullName.toLowerCase();
206
+ // Exact matches (highest priority)
207
+ if (abbrL === q)
208
+ score = 100;
209
+ else if (nickL === q)
210
+ score = 95;
211
+ else if (fullL === q)
212
+ score = 90;
213
+ else if (cityL === q)
214
+ score = 85;
215
+ // Alias exact match
216
+ else if (team.aliases.some((a) => a.toLowerCase() === q))
217
+ score = 80;
218
+ // Starts-with matches
219
+ else if (nickL.startsWith(q))
220
+ score = 70;
221
+ else if (cityL.startsWith(q))
222
+ score = 65;
223
+ else if (abbrL.startsWith(q))
224
+ score = 60;
225
+ // Contains matches
226
+ else if (fullL.includes(q))
227
+ score = 50;
228
+ else if (nickL.includes(q))
229
+ score = 45;
230
+ else if (team.aliases.some((a) => a.toLowerCase().includes(q)))
231
+ score = 40;
232
+ if (score > 0) {
233
+ results.push({ ...team, score });
234
+ }
235
+ }
236
+ results.sort((a, b) => b.score - a.score);
237
+ return results;
238
+ }
239
+ /**
240
+ * Resolve a query to the single best-matching team.
241
+ * If sport is given, only matches within that sport.
242
+ */
243
+ function resolveTeam(query, sport) {
244
+ const matches = searchTeams(query, sport);
245
+ return matches.length > 0 ? matches[0] : null;
246
+ }
55
247
  async function apiFetch(opts) {
56
248
  const url = new URL(opts.path, API_BASE);
57
249
  if (opts.params) {
@@ -83,7 +275,6 @@ async function apiFetch(opts) {
83
275
  const remainDay = res.headers.get("X-RateLimit-Remaining-Day") || "0";
84
276
  throw new Error(`Rate limit exceeded. Remaining: ${remainHour}/hour, ${remainDay}/day. Retry after ${retryAfter}s.`);
85
277
  }
86
- // FINDING 5 FIX: Don't forward raw response body — may contain internal details
87
278
  if (status === 403)
88
279
  throw new Error("Access denied. Your subscription may have expired.");
89
280
  if (status === 404)
@@ -97,15 +288,56 @@ async function apiFetch(opts) {
97
288
  }
98
289
  return (await res.json());
99
290
  }
291
+ // ─── Formatting Helpers ─────────────────────────────────────────────────────
292
+ function formatPick(p) {
293
+ const pick = p.predicted_winner || p.predicted_pick || p.predicted_direction;
294
+ const edge = p.home_edge || p.away_edge;
295
+ const tier = p.confidence_tier ? ` [${p.confidence_tier}]` : "";
296
+ let line = `${p.away_team} @ ${p.home_team}`;
297
+ line += `\n Game ID: ${p.game_id}`;
298
+ line += `\n Pick: ${pick} (${p.confidence}% conf`;
299
+ if (edge)
300
+ line += `, ${edge}% edge`;
301
+ line += `)${tier}`;
302
+ if (p.market === "spread" && p.spread_line != null) {
303
+ line += `\n Line: ${p.spread_line}`;
304
+ }
305
+ if (p.market === "totals" && p.totals_line != null) {
306
+ line += `\n Line: ${p.totals_line}`;
307
+ }
308
+ if (p.home_odds) {
309
+ line += `\n Odds: ${p.home_odds}/${p.away_odds}`;
310
+ }
311
+ if (p.commence_time) {
312
+ const time = new Date(p.commence_time).toLocaleString("en-US", {
313
+ timeZone: "America/New_York",
314
+ weekday: "short",
315
+ month: "short",
316
+ day: "numeric",
317
+ hour: "numeric",
318
+ minute: "2-digit",
319
+ timeZoneName: "short",
320
+ });
321
+ line += `\n Game: ${time}`;
322
+ }
323
+ return line;
324
+ }
325
+ function formatRateLimit(rl) {
326
+ if (!rl)
327
+ return "";
328
+ return `\nRate limit: ${rl.remaining_hour}/hour, ${rl.remaining_day}/day remaining.`;
329
+ }
100
330
  // ─── Server Setup ───────────────────────────────────────────────────────────
101
331
  const server = new McpServer({
102
332
  name: "Da Vinci Bets",
103
- version: "1.0.1",
333
+ version: "1.3.0",
104
334
  });
105
335
  // ─── 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.", {
336
+ server.tool("get_predictions", "Fetch AI-powered sports predictions filtered by sport, market, team, and confidence. " +
337
+ "Returns predictions with win probabilities, odds, edge values, confidence tiers, and game IDs. " +
338
+ "Covers NBA, NFL, NHL, and MLB across moneylines, spreads, and totals. " +
339
+ "Use the team parameter to find predictions for a specific team (e.g., 'Lakers', 'NYY', 'Chiefs'). " +
340
+ "Game IDs from results can be used with get_pick_card to render shareable images.", {
109
341
  sport: z
110
342
  .enum(["NBA", "NFL", "NHL", "MLB"])
111
343
  .optional()
@@ -114,6 +346,12 @@ server.tool("get_predictions", "Fetch AI-powered sports predictions filtered by
114
346
  .enum(["moneyline", "spread", "totals"])
115
347
  .default("moneyline")
116
348
  .describe("Betting market type."),
349
+ team: z
350
+ .string()
351
+ .optional()
352
+ .describe("Filter by team name, abbreviation, or city. " +
353
+ "Examples: 'Lakers', 'LAL', 'NYY', 'Chiefs', 'Boston'. " +
354
+ "Resolved intelligently — abbreviations, nicknames, and cities all work."),
117
355
  sort: z
118
356
  .enum(["confidence", "edge", "game_time"])
119
357
  .default("confidence")
@@ -132,7 +370,7 @@ server.tool("get_predictions", "Fetch AI-powered sports predictions filtered by
132
370
  .max(100)
133
371
  .default(60)
134
372
  .describe("Minimum confidence percentage (0-100)."),
135
- }, async ({ sport, market, sort, limit, min_confidence }) => {
373
+ }, async ({ sport, market, team, sort, limit, min_confidence }) => {
136
374
  try {
137
375
  const params = {
138
376
  market,
@@ -142,40 +380,49 @@ server.tool("get_predictions", "Fetch AI-powered sports predictions filtered by
142
380
  };
143
381
  if (sport)
144
382
  params.sport = sport;
383
+ // Resolve team name to canonical form for API filter
384
+ if (team) {
385
+ const resolved = resolveTeam(team, sport || undefined);
386
+ if (resolved) {
387
+ params.team = resolved.nickname;
388
+ if (!sport)
389
+ params.sport = resolved.sport;
390
+ }
391
+ else {
392
+ // Pass raw query — API does ILIKE matching
393
+ params.team = team;
394
+ }
395
+ }
145
396
  const data = await apiFetch({
146
397
  path: "/api/v1/predictions",
147
398
  params,
148
399
  });
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");
400
+ if (data.predictions.length === 0) {
401
+ const teamHint = team ? ` for "${team}"` : "";
402
+ return {
403
+ content: [
404
+ {
405
+ type: "text",
406
+ text: `No ${market} predictions found${teamHint}${sport ? ` in ${sport}` : ""}. ` +
407
+ `Games may not be scheduled, or try lowering min_confidence (currently ${min_confidence}%).`,
408
+ },
409
+ ],
410
+ };
411
+ }
412
+ const lines = data.predictions.map((p, i) => `${i + 1}. ${formatPick(p)}`);
413
+ const header = [
414
+ `Found ${data.meta.total} ${market} predictions${sport ? ` for ${sport}` : ""}${team ? ` matching "${team}"` : ""}.`,
415
+ formatRateLimit(data.rate_limit),
416
+ ]
417
+ .filter(Boolean)
418
+ .join("");
173
419
  return {
174
420
  content: [
175
- { type: "text", text: summary },
421
+ { type: "text", text: header + "\n\n" + lines.join("\n\n") },
176
422
  {
177
423
  type: "text",
178
- text: "\n---\nRaw JSON:\n" + JSON.stringify(data.predictions, null, 2),
424
+ text: "\n---\nRaw JSON:\n" +
425
+ JSON.stringify(data.predictions, null, 2),
179
426
  },
180
427
  ],
181
428
  };
@@ -184,10 +431,196 @@ server.tool("get_predictions", "Fetch AI-powered sports predictions filtered by
184
431
  return errorResult(err);
185
432
  }
186
433
  });
434
+ // ─── Tool: get_game ─────────────────────────────────────────────────────────
435
+ server.tool("get_game", "Find a specific game's prediction by team name. This is the intelligent lookup tool — " +
436
+ "give it a team name (nickname, abbreviation, or city) and it finds the matching game. " +
437
+ "Returns the full prediction with game_id, confidence, odds, edge, and game time. " +
438
+ "Optionally specify an opponent to find a specific matchup (e.g., team='Lakers', opponent='Celtics'). " +
439
+ "The game_id in the response can be used with get_pick_card to render a shareable image.", {
440
+ team: z
441
+ .string()
442
+ .describe("Team to look up. Accepts nickname ('Lakers'), abbreviation ('LAL'), " +
443
+ "city ('Los Angeles'), or full name ('Los Angeles Lakers')."),
444
+ opponent: z
445
+ .string()
446
+ .optional()
447
+ .describe("Optional opponent to narrow to a specific matchup. " +
448
+ "Example: team='Lakers', opponent='Celtics'."),
449
+ sport: z
450
+ .enum(["NBA", "NFL", "NHL", "MLB"])
451
+ .optional()
452
+ .describe("Sport hint. Auto-detected from team name if omitted."),
453
+ market: z
454
+ .enum(["moneyline", "spread", "totals"])
455
+ .default("moneyline")
456
+ .describe("Which market to look up."),
457
+ }, async ({ team, opponent, sport, market }) => {
458
+ try {
459
+ // Resolve team
460
+ const resolved = resolveTeam(team, sport || undefined);
461
+ if (!resolved) {
462
+ // Suggest closest matches
463
+ const suggestions = searchTeams(team)
464
+ .slice(0, 5)
465
+ .map((t) => ` ${t.abbr} — ${t.fullName} (${t.sport})`)
466
+ .join("\n");
467
+ return {
468
+ content: [
469
+ {
470
+ type: "text",
471
+ text: `Could not resolve team "${team}". ` +
472
+ (suggestions
473
+ ? `Did you mean:\n${suggestions}`
474
+ : "Try using the full team name or abbreviation."),
475
+ },
476
+ ],
477
+ };
478
+ }
479
+ const detectedSport = sport || resolved.sport;
480
+ // Fetch predictions for this team
481
+ const params = {
482
+ sport: detectedSport,
483
+ market,
484
+ sort: "game_time",
485
+ limit: "10",
486
+ min_confidence: "0",
487
+ team: resolved.nickname,
488
+ };
489
+ const data = await apiFetch({
490
+ path: "/api/v1/predictions",
491
+ params,
492
+ });
493
+ let matches = data.predictions;
494
+ // Filter by opponent if specified
495
+ if (opponent && matches.length > 0) {
496
+ const oppResolved = resolveTeam(opponent, detectedSport);
497
+ const oppName = oppResolved
498
+ ? oppResolved.nickname.toLowerCase()
499
+ : opponent.toLowerCase();
500
+ matches = matches.filter((p) => p.home_team.toLowerCase().includes(oppName) ||
501
+ p.away_team.toLowerCase().includes(oppName));
502
+ }
503
+ if (matches.length === 0) {
504
+ const oppHint = opponent ? ` vs ${opponent}` : "";
505
+ return {
506
+ content: [
507
+ {
508
+ type: "text",
509
+ text: `No upcoming ${market} prediction found for ${resolved.fullName}${oppHint}. ` +
510
+ `The team may not have games scheduled in the current prediction window.`,
511
+ },
512
+ ],
513
+ };
514
+ }
515
+ // Return the best match (first upcoming game)
516
+ const game = matches[0];
517
+ const allGames = matches.length > 1
518
+ ? `\n\n─── Other upcoming games ───\n${matches
519
+ .slice(1)
520
+ .map((p) => `${p.away_team} @ ${p.home_team} | Game ID: ${p.game_id}`)
521
+ .join("\n")}`
522
+ : "";
523
+ return {
524
+ content: [
525
+ {
526
+ type: "text",
527
+ text: `${resolved.fullName} (${resolved.abbr}) — ${detectedSport} ${market}\n` +
528
+ `${"═".repeat(50)}\n\n` +
529
+ formatPick(game) +
530
+ allGames +
531
+ formatRateLimit(data.rate_limit) +
532
+ `\n\nUse game_id "${game.game_id}" with get_pick_card to render a shareable image.`,
533
+ },
534
+ {
535
+ type: "text",
536
+ text: "\n---\nRaw JSON:\n" + JSON.stringify(game, null, 2),
537
+ },
538
+ ],
539
+ };
540
+ }
541
+ catch (err) {
542
+ return errorResult(err);
543
+ }
544
+ });
545
+ // ─── Tool: lookup_teams ─────────────────────────────────────────────────────
546
+ server.tool("lookup_teams", "Search the team registry by name, abbreviation, city, or sport. " +
547
+ "Returns matching teams with their abbreviation, full name, sport, and aliases. " +
548
+ "Useful for discovering team names before using get_game or get_predictions. " +
549
+ "Omit query to list all teams for a sport. " +
550
+ "Does not call the API — instant local lookup, no rate limit impact.", {
551
+ query: z
552
+ .string()
553
+ .optional()
554
+ .describe("Search query — team name, abbreviation, or city. " +
555
+ "Examples: 'NY', 'Lakers', 'CHI', 'Boston'. Omit to list all teams for a sport."),
556
+ sport: z
557
+ .enum(["NBA", "NFL", "NHL", "MLB"])
558
+ .optional()
559
+ .describe("Filter to a specific sport. Omit for all sports."),
560
+ }, async ({ query, sport }) => {
561
+ if (!query && !sport) {
562
+ // List summary
563
+ const counts = { NBA: 0, NFL: 0, NHL: 0, MLB: 0 };
564
+ for (const t of TEAMS)
565
+ counts[t.sport]++;
566
+ return {
567
+ content: [
568
+ {
569
+ type: "text",
570
+ text: `Da Vinci Bets covers ${TEAMS.length} teams:\n` +
571
+ Object.entries(counts)
572
+ .map(([s, n]) => ` ${s}: ${n} teams`)
573
+ .join("\n") +
574
+ `\n\nUse query or sport to search. Examples:\n` +
575
+ ` lookup_teams(query="NY") — all NY teams\n` +
576
+ ` lookup_teams(sport="NBA") — all NBA teams\n` +
577
+ ` lookup_teams(query="CHI", sport="MLB") — Chicago MLB teams`,
578
+ },
579
+ ],
580
+ };
581
+ }
582
+ let results;
583
+ if (query) {
584
+ results = searchTeams(query, sport || undefined);
585
+ }
586
+ else {
587
+ // List all teams for the sport
588
+ results = TEAMS.filter((t) => t.sport === sport);
589
+ }
590
+ if (results.length === 0) {
591
+ return {
592
+ content: [
593
+ {
594
+ type: "text",
595
+ text: `No teams found${query ? ` matching "${query}"` : ""}${sport ? ` in ${sport}` : ""}.`,
596
+ },
597
+ ],
598
+ };
599
+ }
600
+ const lines = results.map((t) => {
601
+ let line = `${t.abbr.padEnd(4)} ${t.fullName} (${t.sport})`;
602
+ if (t.aliases.length > 0) {
603
+ line += ` — also: ${t.aliases.join(", ")}`;
604
+ }
605
+ return line;
606
+ });
607
+ const header = query
608
+ ? `Found ${results.length} teams matching "${query}"${sport ? ` in ${sport}` : ""}:`
609
+ : `All ${results.length} ${sport} teams:`;
610
+ return {
611
+ content: [
612
+ {
613
+ type: "text",
614
+ text: header + "\n\n" + lines.join("\n"),
615
+ },
616
+ ],
617
+ };
618
+ });
187
619
  // ─── Tool: get_top_picks ────────────────────────────────────────────────────
188
620
  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.", {
621
+ "Returns the top predictions sorted by confidence with game IDs, great for daily briefings. " +
622
+ "Each pick includes a game_id you can use with get_pick_card. " +
623
+ "Note: fetches from 3 markets in parallel, consuming 3 API requests.", {
191
624
  count: z
192
625
  .number()
193
626
  .int()
@@ -204,7 +637,6 @@ server.tool("get_top_picks", "Get the highest-conviction AI picks across all spo
204
637
  .describe("Minimum confidence percentage."),
205
638
  }, async ({ count, min_confidence }) => {
206
639
  try {
207
- // Fetch top picks across all markets in parallel
208
640
  const markets = ["moneyline", "spread", "totals"];
209
641
  const results = await Promise.allSettled(markets.map((market) => apiFetch({
210
642
  path: "/api/v1/predictions",
@@ -228,8 +660,7 @@ server.tool("get_top_picks", "Get the highest-conviction AI picks across all spo
228
660
  failedMarkets.push(markets[i]);
229
661
  }
230
662
  }
231
- // Sort by confidence descending, take top N
232
- allPicks.sort((a, b) => Number(b.confidence || 0) - Number(a.confidence || 0));
663
+ allPicks.sort((a, b) => Number(b.confidence) - Number(a.confidence));
233
664
  const topN = allPicks.slice(0, count);
234
665
  if (topN.length === 0) {
235
666
  return {
@@ -242,39 +673,22 @@ server.tool("get_top_picks", "Get the highest-conviction AI picks across all spo
242
673
  };
243
674
  }
244
675
  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;
676
+ return `${i + 1}. [${p.sport}/${p._market}] ${formatPick(p)}`;
268
677
  });
269
- let header = `Top ${topN.length} AI Picks (≥${min_confidence}% confidence):`;
678
+ let header = `Top ${topN.length} AI Picks (>=${min_confidence}% confidence):`;
270
679
  if (failedMarkets.length > 0) {
271
- header += `\n⚠ Could not fetch ${failedMarkets.join(", ")} predictions. Results may be partial.`;
680
+ header += `\nWarning: Could not fetch ${failedMarkets.join(", ")} predictions.`;
272
681
  }
682
+ // Collect game IDs for quick reference
683
+ const gameIds = topN.map((p) => p.game_id);
273
684
  return {
274
685
  content: [
275
686
  {
276
687
  type: "text",
277
- text: header + "\n\n" + lines.join("\n\n"),
688
+ text: header +
689
+ "\n\n" +
690
+ lines.join("\n\n") +
691
+ `\n\n─── Game IDs (for get_pick_card) ───\n${gameIds.join("\n")}`,
278
692
  },
279
693
  ],
280
694
  };
@@ -284,54 +698,138 @@ server.tool("get_top_picks", "Get the highest-conviction AI picks across all spo
284
698
  }
285
699
  });
286
700
  // ─── Tool: get_pick_card ────────────────────────────────────────────────────
287
- server.tool("get_pick_card", "Render a prediction as a shareable 1080×1350 PNG image card with ESPN logos, " +
701
+ server.tool("get_pick_card", "Render a prediction as a shareable 1080x1350 PNG image card with ESPN logos, " +
288
702
  "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. " +
703
+ "Returns a permanent public URL (cached in Supabase Storage) plus an inline image preview. " +
704
+ "Use the URL for video editing, social media posts, or embedding anywhere. " +
705
+ "Accepts a game_id from get_predictions, get_game, or get_top_picks results. " +
706
+ "You can also pass a team name instead of game_id — it will resolve automatically. " +
291
707
  "Partner-tier keys recommended (higher rate limits for batch generation).", {
292
708
  game_id: z
293
709
  .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."),
710
+ .optional()
711
+ .describe("The game_id from a prediction result. Use get_predictions or get_game to find game IDs."),
712
+ team: z
713
+ .string()
714
+ .optional()
715
+ .describe("Alternative to game_id — pass a team name and the card for their next game is rendered. " +
716
+ "Examples: 'Lakers', 'NYY'. Requires an extra API call to resolve."),
297
717
  sport: z
298
718
  .enum(["NBA", "NFL", "NHL", "MLB"])
299
719
  .optional()
300
720
  .describe("Sport for the game (helps with lookup speed)."),
301
- }, async ({ game_id, sport }) => {
721
+ }, async ({ game_id, team, sport }) => {
302
722
  try {
723
+ let resolvedGameId = game_id;
724
+ // If team given instead of game_id, resolve it
725
+ if (!resolvedGameId && team) {
726
+ const resolved = resolveTeam(team, sport || undefined);
727
+ const teamName = resolved ? resolved.nickname : team;
728
+ const teamSport = sport || resolved?.sport;
729
+ const params = {
730
+ market: "moneyline",
731
+ sort: "game_time",
732
+ limit: "1",
733
+ min_confidence: "0",
734
+ team: teamName,
735
+ };
736
+ if (teamSport)
737
+ params.sport = teamSport;
738
+ const data = await apiFetch({
739
+ path: "/api/v1/predictions",
740
+ params,
741
+ });
742
+ if (data.predictions.length === 0) {
743
+ return {
744
+ content: [
745
+ {
746
+ type: "text",
747
+ text: `No upcoming game found for "${team}". Cannot render card.`,
748
+ },
749
+ ],
750
+ };
751
+ }
752
+ resolvedGameId = data.predictions[0].game_id;
753
+ }
754
+ if (!resolvedGameId) {
755
+ return {
756
+ content: [
757
+ {
758
+ type: "text",
759
+ text: "Please provide either a game_id or team name.",
760
+ },
761
+ ],
762
+ };
763
+ }
764
+ // Validate game_id format
765
+ if (!/^[a-zA-Z0-9_-]+$/.test(resolvedGameId)) {
766
+ return {
767
+ content: [
768
+ {
769
+ type: "text",
770
+ text: "Invalid game_id format. Must be alphanumeric with _ or -.",
771
+ },
772
+ ],
773
+ };
774
+ }
303
775
  const params = {};
304
776
  if (sport)
305
777
  params.sport = sport;
306
- const imageBuffer = await apiFetch({
307
- path: `/api/social/card-render/${encodeURIComponent(game_id)}`,
778
+ // Get permanent public URL
779
+ const cardData = await apiFetch({
780
+ path: `/api/social/card-url/${encodeURIComponent(resolvedGameId)}`,
308
781
  params,
309
- raw: true,
310
782
  });
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
- };
783
+ // Also fetch the actual image for inline preview
784
+ let imageContent = null;
785
+ try {
786
+ const imageBuffer = await apiFetch({
787
+ path: `/api/social/card-render/${encodeURIComponent(resolvedGameId)}`,
788
+ params,
789
+ raw: true,
790
+ });
791
+ imageContent = {
792
+ type: "image",
793
+ data: Buffer.from(imageBuffer).toString("base64"),
794
+ mimeType: "image/png",
795
+ };
796
+ }
797
+ catch {
798
+ // Image preview is optional
799
+ }
800
+ const content = [];
801
+ if (imageContent) {
802
+ content.push(imageContent);
803
+ }
804
+ const sizeKB = cardData.size
805
+ ? `${Math.round(cardData.size / 1024)}KB`
806
+ : "unknown size";
807
+ content.push({
808
+ type: "text",
809
+ text: [
810
+ `Pick card for game ${resolvedGameId} (1080x1350 PNG, ${sizeKB})`,
811
+ ``,
812
+ `Public URL: ${cardData.url}`,
813
+ `Cached: ${cardData.cached}`,
814
+ cardData.rendered_at ? `Rendered: ${cardData.rendered_at}` : "",
815
+ ``,
816
+ `Use this URL in videos, social media posts, or anywhere you need the image.`,
817
+ ]
818
+ .filter(Boolean)
819
+ .join("\n"),
820
+ });
821
+ return { content };
325
822
  }
326
823
  catch (err) {
327
824
  return errorResult(err);
328
825
  }
329
826
  });
330
827
  // ─── 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.", {
828
+ server.tool("get_batch_cards", "Render multiple pick cards and return permanent public URLs for each. " +
829
+ "Designed for marketing teams generating social media content and video assets. " +
830
+ "Each card is cached in Supabase Storage — subsequent calls return cached URLs instantly. " +
831
+ "Partner-tier API key recommended. Max 10 per call. " +
832
+ "Returns URLs suitable for video editing, social posts, or any image embed.", {
335
833
  game_ids: z
336
834
  .array(z
337
835
  .string()
@@ -350,26 +848,25 @@ server.tool("get_batch_cards", "Render multiple pick cards as PNG images in one
350
848
  if (sport)
351
849
  params.sport = sport;
352
850
  const results = await Promise.allSettled(game_ids.map(async (gid) => {
353
- const buf = await apiFetch({
354
- path: `/api/social/card-render/${encodeURIComponent(gid)}`,
851
+ const data = await apiFetch({
852
+ path: `/api/social/card-url/${encodeURIComponent(gid)}`,
355
853
  params,
356
- raw: true,
357
854
  });
358
- return { game_id: gid, buffer: buf };
855
+ return data;
359
856
  }));
360
857
  const content = [];
858
+ const urls = [];
361
859
  let successCount = 0;
362
860
  for (const r of results) {
363
861
  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
- });
862
+ urls.push(r.value.url);
863
+ const sizeKB = r.value.size
864
+ ? `${Math.round(r.value.size / 1024)}KB`
865
+ : "";
866
+ const cached = r.value.cached ? " (cached)" : " (fresh render)";
370
867
  content.push({
371
868
  type: "text",
372
- text: `Card: ${r.value.game_id} (${Math.round(r.value.buffer.byteLength / 1024)}KB)`,
869
+ text: `${r.value.game_id}: ${r.value.url}${sizeKB ? ` — ${sizeKB}` : ""}${cached}`,
373
870
  });
374
871
  successCount++;
375
872
  }
@@ -382,8 +879,14 @@ server.tool("get_batch_cards", "Render multiple pick cards as PNG images in one
382
879
  }
383
880
  content.unshift({
384
881
  type: "text",
385
- text: `Rendered ${successCount}/${game_ids.length} pick cards.`,
882
+ text: `Rendered ${successCount}/${game_ids.length} pick cards.\n\nPublic URLs:`,
386
883
  });
884
+ if (urls.length > 0) {
885
+ content.push({
886
+ type: "text",
887
+ text: `\n---\nAll URLs:\n${urls.join("\n")}`,
888
+ });
889
+ }
387
890
  return { content };
388
891
  }
389
892
  catch (err) {
@@ -392,20 +895,17 @@ server.tool("get_batch_cards", "Render multiple pick cards as PNG images in one
392
895
  });
393
896
  // ─── Tool: get_performance ──────────────────────────────────────────────────
394
897
  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.", {
898
+ "Shows prediction counts, confidence distribution, and available coverage.", {
397
899
  sport: z
398
900
  .enum(["NBA", "NFL", "NHL", "MLB"])
399
901
  .optional()
400
- .describe("Filter performance to a specific sport."),
902
+ .describe("Filter to a specific sport."),
401
903
  market: z
402
904
  .enum(["moneyline", "spread", "totals"])
403
905
  .optional()
404
906
  .describe("Filter to a specific market."),
405
907
  }, async ({ sport, market }) => {
406
908
  try {
407
- // Pull recent settled predictions to compute win rates
408
- // We use the predictions endpoint with high confidence to show quality
409
909
  const params = {
410
910
  sort: "confidence",
411
911
  limit: "50",
@@ -419,7 +919,6 @@ server.tool("get_performance", "Get a snapshot of current active predictions acr
419
919
  path: "/api/v1/predictions",
420
920
  params,
421
921
  });
422
- // Summarize available predictions as a performance snapshot
423
922
  const byMarket = {};
424
923
  const bySport = {};
425
924
  const confBuckets = { high: 0, medium: 0 };
@@ -428,15 +927,14 @@ server.tool("get_performance", "Get a snapshot of current active predictions acr
428
927
  const s = String(p.sport || "?");
429
928
  byMarket[m] = (byMarket[m] || 0) + 1;
430
929
  bySport[s] = (bySport[s] || 0) + 1;
431
- const conf = Number(p.confidence || 0);
432
- if (conf >= 65)
930
+ if (p.confidence >= 65)
433
931
  confBuckets.high++;
434
932
  else
435
933
  confBuckets.medium++;
436
934
  }
437
935
  const lines = [
438
936
  "Da Vinci Bets — Current Predictions Snapshot",
439
- "═══════════════════════════════════════════════",
937
+ "═".repeat(50),
440
938
  `Total active predictions: ${data.meta?.total ?? data.predictions.length}`,
441
939
  "",
442
940
  "By Sport:",
@@ -446,13 +944,9 @@ server.tool("get_performance", "Get a snapshot of current active predictions acr
446
944
  ...Object.entries(byMarket).map(([m, n]) => ` ${m}: ${n} picks`),
447
945
  "",
448
946
  "Confidence Distribution:",
449
- ` High (≥65%): ${confBuckets.high} picks`,
947
+ ` High (>=65%): ${confBuckets.high} picks`,
450
948
  ` 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.",
949
+ formatRateLimit(data.rate_limit),
456
950
  ];
457
951
  return {
458
952
  content: [{ type: "text", text: lines.join("\n") }],
@@ -462,9 +956,86 @@ server.tool("get_performance", "Get a snapshot of current active predictions acr
462
956
  return errorResult(err);
463
957
  }
464
958
  });
959
+ // ─── Tool: get_track_record ─────────────────────────────────────────────────
960
+ server.tool("get_track_record", "Get verified win rates, ROI, and historical performance from settled predictions. " +
961
+ "Shows actual results broken down by sport, market, and confidence tier. " +
962
+ "Includes current streak and recent settled picks. " +
963
+ "Public endpoint — does not count against your API rate limit.", {
964
+ sport: z
965
+ .enum(["NBA", "NFL", "NHL", "MLB"])
966
+ .optional()
967
+ .describe("Filter to a specific sport. Omit for all."),
968
+ days: z
969
+ .number()
970
+ .int()
971
+ .min(7)
972
+ .max(365)
973
+ .default(30)
974
+ .describe("Lookback period in days (7-365)."),
975
+ }, async ({ sport, days }) => {
976
+ try {
977
+ const params = { days: String(days) };
978
+ if (sport)
979
+ params.sport = sport;
980
+ const url = new URL("/api/track-record", API_BASE);
981
+ for (const [k, v] of Object.entries(params)) {
982
+ url.searchParams.set(k, v);
983
+ }
984
+ const res = await fetch(url.toString(), {
985
+ headers: { Accept: "application/json" },
986
+ signal: AbortSignal.timeout(15_000),
987
+ });
988
+ if (!res.ok) {
989
+ throw new Error(`Track record request failed (HTTP ${res.status}).`);
990
+ }
991
+ const data = (await res.json());
992
+ const breakdownLines = data.breakdown.map((b) => {
993
+ const emoji = b.win_rate >= 55 ? "+" : b.win_rate >= 50 ? "~" : "-";
994
+ return ` ${b.sport} ${b.market_type}: ${b.wins}-${b.losses}${b.pushes ? `-${b.pushes}P` : ""} (${b.win_rate}% WR, ${emoji}${b.roi}% ROI)`;
995
+ });
996
+ const tierLines = data.confidenceTiers.map((t) => ` ${t.tier}%: ${t.wins}/${t.total} (${t.win_rate}% WR)`);
997
+ const recentLines = data.recentPicks.slice(0, 10).map((p) => {
998
+ const icon = p.result === "win" ? "W" : p.result === "loss" ? "L" : "P";
999
+ return ` [${icon}] ${p.sport} ${p.market_type}: ${p.pick_team} (${p.confidence_pct}%)`;
1000
+ });
1001
+ const lines = [
1002
+ `Da Vinci Bets — Track Record (${data.period})`,
1003
+ "═".repeat(50),
1004
+ data.streak ? `Current Streak: ${data.streak}` : "",
1005
+ "",
1006
+ "Performance by Sport & Market:",
1007
+ ...breakdownLines,
1008
+ "",
1009
+ "By Confidence Tier:",
1010
+ ...tierLines,
1011
+ "",
1012
+ `Recent Settled Picks (last ${Math.min(data.recentPicks.length, 10)}):`,
1013
+ ...recentLines,
1014
+ ].filter(Boolean);
1015
+ return {
1016
+ content: [
1017
+ { type: "text", text: lines.join("\n") },
1018
+ {
1019
+ type: "text",
1020
+ text: "\n---\nRaw JSON:\n" +
1021
+ JSON.stringify({
1022
+ period: data.period,
1023
+ streak: data.streak,
1024
+ breakdown: data.breakdown,
1025
+ confidenceTiers: data.confidenceTiers,
1026
+ }, null, 2),
1027
+ },
1028
+ ],
1029
+ };
1030
+ }
1031
+ catch (err) {
1032
+ return errorResult(err);
1033
+ }
1034
+ });
465
1035
  // ─── Tool: get_schedule ─────────────────────────────────────────────────────
466
1036
  server.tool("get_schedule", "Get today's game schedule with predictions summary. " +
467
- "Shows which games have predictions available and their confidence levels.", {
1037
+ "Shows which games have predictions available, their confidence levels, and game IDs. " +
1038
+ "Game IDs can be used with get_pick_card or get_game for more details.", {
468
1039
  sport: z
469
1040
  .enum(["NBA", "NFL", "NHL", "MLB"])
470
1041
  .describe("Which sport's schedule to retrieve."),
@@ -486,7 +1057,7 @@ server.tool("get_schedule", "Get today's game schedule with predictions summary.
486
1057
  content: [
487
1058
  {
488
1059
  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.`,
1060
+ text: `No ${sport} games found in the current prediction window. The ${sport} season may be in offseason, or no games are scheduled.`,
490
1061
  },
491
1062
  ],
492
1063
  };
@@ -498,7 +1069,7 @@ server.tool("get_schedule", "Get today's game schedule with predictions summary.
498
1069
  ];
499
1070
  for (const p of data.predictions) {
500
1071
  const time = p.commence_time
501
- ? new Date(String(p.commence_time)).toLocaleString("en-US", {
1072
+ ? new Date(p.commence_time).toLocaleString("en-US", {
502
1073
  timeZone: "America/New_York",
503
1074
  weekday: "short",
504
1075
  month: "short",
@@ -508,12 +1079,14 @@ server.tool("get_schedule", "Get today's game schedule with predictions summary.
508
1079
  timeZoneName: "short",
509
1080
  })
510
1081
  : "TBD";
1082
+ const tier = p.confidence_tier ? ` [${p.confidence_tier}]` : "";
511
1083
  lines.push(`${time}`);
512
1084
  lines.push(` ${p.away_team} @ ${p.home_team}`);
513
- lines.push(` Pick: ${p.predicted_winner} (${p.confidence}% conf) | Game ID: ${p.game_id}`);
1085
+ lines.push(` Pick: ${p.predicted_winner} (${p.confidence}% conf)${tier}`);
1086
+ lines.push(` Game ID: ${p.game_id}`);
514
1087
  lines.push("");
515
1088
  }
516
- lines.push("Use game_id values with get_pick_card to render shareable images.");
1089
+ lines.push("Use game IDs with get_pick_card to render shareable images, or get_game for full details.");
517
1090
  return {
518
1091
  content: [{ type: "text", text: lines.join("\n") }],
519
1092
  };
@@ -534,9 +1107,10 @@ function errorResult(err) {
534
1107
  async function main() {
535
1108
  const transport = new StdioServerTransport();
536
1109
  await server.connect(transport);
537
- console.error("Da Vinci Bets MCP Server v1.0.1 — connected via stdio");
1110
+ console.error("Da Vinci Bets MCP Server v1.3.0 — connected via stdio");
538
1111
  console.error(`API: ${API_BASE}`);
539
1112
  console.error(`Key: dvb_live_***`);
1113
+ console.error(`Teams: ${TEAMS.length} teams loaded across 4 sports`);
540
1114
  }
541
1115
  main().catch((err) => {
542
1116
  console.error("Fatal:", err);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@davincibets/mcp-server",
3
- "version": "1.0.1",
3
+ "version": "1.3.0",
4
4
  "description": "MCP server for Da Vinci Bets — AI sports predictions in your AI assistant",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",