@davincibets/mcp-server 1.0.0 → 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 +704 -129
- 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.
|
|
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,53 +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
|
|
288
|
-
"
|
|
289
|
-
"
|
|
290
|
-
"
|
|
701
|
+
server.tool("get_pick_card", "Render a prediction as a shareable 1080x1350 PNG image card with ESPN logos, " +
|
|
702
|
+
"L5 form dots, confidence bar, expanded AI analysis, and injuries. " +
|
|
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. " +
|
|
707
|
+
"Partner-tier keys recommended (higher rate limits for batch generation).", {
|
|
291
708
|
game_id: z
|
|
292
709
|
.string()
|
|
293
|
-
.
|
|
294
|
-
.
|
|
295
|
-
|
|
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."),
|
|
296
717
|
sport: z
|
|
297
718
|
.enum(["NBA", "NFL", "NHL", "MLB"])
|
|
298
719
|
.optional()
|
|
299
720
|
.describe("Sport for the game (helps with lookup speed)."),
|
|
300
|
-
}, async ({ game_id, sport }) => {
|
|
721
|
+
}, async ({ game_id, team, sport }) => {
|
|
301
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
|
+
}
|
|
302
775
|
const params = {};
|
|
303
776
|
if (sport)
|
|
304
777
|
params.sport = sport;
|
|
305
|
-
|
|
306
|
-
|
|
778
|
+
// Get permanent public URL
|
|
779
|
+
const cardData = await apiFetch({
|
|
780
|
+
path: `/api/social/card-url/${encodeURIComponent(resolvedGameId)}`,
|
|
307
781
|
params,
|
|
308
|
-
raw: true,
|
|
309
782
|
});
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
}
|
|
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 };
|
|
324
822
|
}
|
|
325
823
|
catch (err) {
|
|
326
824
|
return errorResult(err);
|
|
327
825
|
}
|
|
328
826
|
});
|
|
329
827
|
// ─── Tool: get_batch_cards ──────────────────────────────────────────────────
|
|
330
|
-
server.tool("get_batch_cards", "Render multiple pick cards
|
|
331
|
-
"Designed for marketing teams generating social media content. " +
|
|
332
|
-
"
|
|
333
|
-
"
|
|
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.", {
|
|
334
833
|
game_ids: z
|
|
335
834
|
.array(z
|
|
336
835
|
.string()
|
|
@@ -349,26 +848,25 @@ server.tool("get_batch_cards", "Render multiple pick cards as PNG images in one
|
|
|
349
848
|
if (sport)
|
|
350
849
|
params.sport = sport;
|
|
351
850
|
const results = await Promise.allSettled(game_ids.map(async (gid) => {
|
|
352
|
-
const
|
|
353
|
-
path: `/api/card-
|
|
851
|
+
const data = await apiFetch({
|
|
852
|
+
path: `/api/social/card-url/${encodeURIComponent(gid)}`,
|
|
354
853
|
params,
|
|
355
|
-
raw: true,
|
|
356
854
|
});
|
|
357
|
-
return
|
|
855
|
+
return data;
|
|
358
856
|
}));
|
|
359
857
|
const content = [];
|
|
858
|
+
const urls = [];
|
|
360
859
|
let successCount = 0;
|
|
361
860
|
for (const r of results) {
|
|
362
861
|
if (r.status === "fulfilled") {
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
});
|
|
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)";
|
|
369
867
|
content.push({
|
|
370
868
|
type: "text",
|
|
371
|
-
text:
|
|
869
|
+
text: `${r.value.game_id}: ${r.value.url}${sizeKB ? ` — ${sizeKB}` : ""}${cached}`,
|
|
372
870
|
});
|
|
373
871
|
successCount++;
|
|
374
872
|
}
|
|
@@ -381,8 +879,14 @@ server.tool("get_batch_cards", "Render multiple pick cards as PNG images in one
|
|
|
381
879
|
}
|
|
382
880
|
content.unshift({
|
|
383
881
|
type: "text",
|
|
384
|
-
text: `Rendered ${successCount}/${game_ids.length} pick cards
|
|
882
|
+
text: `Rendered ${successCount}/${game_ids.length} pick cards.\n\nPublic URLs:`,
|
|
385
883
|
});
|
|
884
|
+
if (urls.length > 0) {
|
|
885
|
+
content.push({
|
|
886
|
+
type: "text",
|
|
887
|
+
text: `\n---\nAll URLs:\n${urls.join("\n")}`,
|
|
888
|
+
});
|
|
889
|
+
}
|
|
386
890
|
return { content };
|
|
387
891
|
}
|
|
388
892
|
catch (err) {
|
|
@@ -391,20 +895,17 @@ server.tool("get_batch_cards", "Render multiple pick cards as PNG images in one
|
|
|
391
895
|
});
|
|
392
896
|
// ─── Tool: get_performance ──────────────────────────────────────────────────
|
|
393
897
|
server.tool("get_performance", "Get a snapshot of current active predictions across sports and markets. " +
|
|
394
|
-
"Shows prediction counts, confidence distribution, and available coverage.
|
|
395
|
-
"For historical win rates, visit davincibets.io/analytics.", {
|
|
898
|
+
"Shows prediction counts, confidence distribution, and available coverage.", {
|
|
396
899
|
sport: z
|
|
397
900
|
.enum(["NBA", "NFL", "NHL", "MLB"])
|
|
398
901
|
.optional()
|
|
399
|
-
.describe("Filter
|
|
902
|
+
.describe("Filter to a specific sport."),
|
|
400
903
|
market: z
|
|
401
904
|
.enum(["moneyline", "spread", "totals"])
|
|
402
905
|
.optional()
|
|
403
906
|
.describe("Filter to a specific market."),
|
|
404
907
|
}, async ({ sport, market }) => {
|
|
405
908
|
try {
|
|
406
|
-
// Pull recent settled predictions to compute win rates
|
|
407
|
-
// We use the predictions endpoint with high confidence to show quality
|
|
408
909
|
const params = {
|
|
409
910
|
sort: "confidence",
|
|
410
911
|
limit: "50",
|
|
@@ -418,7 +919,6 @@ server.tool("get_performance", "Get a snapshot of current active predictions acr
|
|
|
418
919
|
path: "/api/v1/predictions",
|
|
419
920
|
params,
|
|
420
921
|
});
|
|
421
|
-
// Summarize available predictions as a performance snapshot
|
|
422
922
|
const byMarket = {};
|
|
423
923
|
const bySport = {};
|
|
424
924
|
const confBuckets = { high: 0, medium: 0 };
|
|
@@ -427,15 +927,14 @@ server.tool("get_performance", "Get a snapshot of current active predictions acr
|
|
|
427
927
|
const s = String(p.sport || "?");
|
|
428
928
|
byMarket[m] = (byMarket[m] || 0) + 1;
|
|
429
929
|
bySport[s] = (bySport[s] || 0) + 1;
|
|
430
|
-
|
|
431
|
-
if (conf >= 65)
|
|
930
|
+
if (p.confidence >= 65)
|
|
432
931
|
confBuckets.high++;
|
|
433
932
|
else
|
|
434
933
|
confBuckets.medium++;
|
|
435
934
|
}
|
|
436
935
|
const lines = [
|
|
437
936
|
"Da Vinci Bets — Current Predictions Snapshot",
|
|
438
|
-
"
|
|
937
|
+
"═".repeat(50),
|
|
439
938
|
`Total active predictions: ${data.meta?.total ?? data.predictions.length}`,
|
|
440
939
|
"",
|
|
441
940
|
"By Sport:",
|
|
@@ -445,13 +944,9 @@ server.tool("get_performance", "Get a snapshot of current active predictions acr
|
|
|
445
944
|
...Object.entries(byMarket).map(([m, n]) => ` ${m}: ${n} picks`),
|
|
446
945
|
"",
|
|
447
946
|
"Confidence Distribution:",
|
|
448
|
-
` High (
|
|
947
|
+
` High (>=65%): ${confBuckets.high} picks`,
|
|
449
948
|
` Medium (50-64%): ${confBuckets.medium} picks`,
|
|
450
|
-
|
|
451
|
-
`Rate limit: ${data.rate_limit?.remaining_hour ?? "?"}/hour, ${data.rate_limit?.remaining_day ?? "?"}/day remaining.`,
|
|
452
|
-
"",
|
|
453
|
-
"Note: Historical win rates are available on the Da Vinci Bets analytics dashboard.",
|
|
454
|
-
"Visit https://www.davincibets.io/analytics for full performance tracking.",
|
|
949
|
+
formatRateLimit(data.rate_limit),
|
|
455
950
|
];
|
|
456
951
|
return {
|
|
457
952
|
content: [{ type: "text", text: lines.join("\n") }],
|
|
@@ -461,9 +956,86 @@ server.tool("get_performance", "Get a snapshot of current active predictions acr
|
|
|
461
956
|
return errorResult(err);
|
|
462
957
|
}
|
|
463
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
|
+
});
|
|
464
1035
|
// ─── Tool: get_schedule ─────────────────────────────────────────────────────
|
|
465
1036
|
server.tool("get_schedule", "Get today's game schedule with predictions summary. " +
|
|
466
|
-
"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.", {
|
|
467
1039
|
sport: z
|
|
468
1040
|
.enum(["NBA", "NFL", "NHL", "MLB"])
|
|
469
1041
|
.describe("Which sport's schedule to retrieve."),
|
|
@@ -485,7 +1057,7 @@ server.tool("get_schedule", "Get today's game schedule with predictions summary.
|
|
|
485
1057
|
content: [
|
|
486
1058
|
{
|
|
487
1059
|
type: "text",
|
|
488
|
-
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.`,
|
|
489
1061
|
},
|
|
490
1062
|
],
|
|
491
1063
|
};
|
|
@@ -497,7 +1069,7 @@ server.tool("get_schedule", "Get today's game schedule with predictions summary.
|
|
|
497
1069
|
];
|
|
498
1070
|
for (const p of data.predictions) {
|
|
499
1071
|
const time = p.commence_time
|
|
500
|
-
? new Date(
|
|
1072
|
+
? new Date(p.commence_time).toLocaleString("en-US", {
|
|
501
1073
|
timeZone: "America/New_York",
|
|
502
1074
|
weekday: "short",
|
|
503
1075
|
month: "short",
|
|
@@ -507,12 +1079,14 @@ server.tool("get_schedule", "Get today's game schedule with predictions summary.
|
|
|
507
1079
|
timeZoneName: "short",
|
|
508
1080
|
})
|
|
509
1081
|
: "TBD";
|
|
1082
|
+
const tier = p.confidence_tier ? ` [${p.confidence_tier}]` : "";
|
|
510
1083
|
lines.push(`${time}`);
|
|
511
1084
|
lines.push(` ${p.away_team} @ ${p.home_team}`);
|
|
512
|
-
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}`);
|
|
513
1087
|
lines.push("");
|
|
514
1088
|
}
|
|
515
|
-
lines.push("Use
|
|
1089
|
+
lines.push("Use game IDs with get_pick_card to render shareable images, or get_game for full details.");
|
|
516
1090
|
return {
|
|
517
1091
|
content: [{ type: "text", text: lines.join("\n") }],
|
|
518
1092
|
};
|
|
@@ -533,9 +1107,10 @@ function errorResult(err) {
|
|
|
533
1107
|
async function main() {
|
|
534
1108
|
const transport = new StdioServerTransport();
|
|
535
1109
|
await server.connect(transport);
|
|
536
|
-
console.error("Da Vinci Bets MCP Server v1.
|
|
1110
|
+
console.error("Da Vinci Bets MCP Server v1.3.0 — connected via stdio");
|
|
537
1111
|
console.error(`API: ${API_BASE}`);
|
|
538
1112
|
console.error(`Key: dvb_live_***`);
|
|
1113
|
+
console.error(`Teams: ${TEAMS.length} teams loaded across 4 sports`);
|
|
539
1114
|
}
|
|
540
1115
|
main().catch((err) => {
|
|
541
1116
|
console.error("Fatal:", err);
|