@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 +37 -2
- package/dist/index.d.ts +8 -3
- package/dist/index.js +702 -128
- package/package.json +1 -1
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 |
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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:
|
|
421
|
+
{ type: "text", text: header + "\n\n" + lines.join("\n\n") },
|
|
176
422
|
{
|
|
177
423
|
type: "text",
|
|
178
|
-
text: "\n---\nRaw JSON:\n" +
|
|
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
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
678
|
+
let header = `Top ${topN.length} AI Picks (>=${min_confidence}% confidence):`;
|
|
270
679
|
if (failedMarkets.length > 0) {
|
|
271
|
-
header += `\
|
|
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 +
|
|
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
|
|
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
|
-
"
|
|
290
|
-
"
|
|
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
|
-
.
|
|
295
|
-
.
|
|
296
|
-
|
|
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
|
-
|
|
307
|
-
|
|
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
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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
|
|
332
|
-
"Designed for marketing teams generating social media content. " +
|
|
333
|
-
"
|
|
334
|
-
"
|
|
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
|
|
354
|
-
path: `/api/social/card-
|
|
851
|
+
const data = await apiFetch({
|
|
852
|
+
path: `/api/social/card-url/${encodeURIComponent(gid)}`,
|
|
355
853
|
params,
|
|
356
|
-
raw: true,
|
|
357
854
|
});
|
|
358
|
-
return
|
|
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
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
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 (
|
|
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
|
|
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
|
|
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(
|
|
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)
|
|
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
|
|
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
|
|
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);
|