@davincibets/mcp-server 1.0.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 +211 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +543 -0
- package/package.json +47 -0
package/README.md
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# Da Vinci Bets MCP Server
|
|
2
|
+
|
|
3
|
+
Give your AI assistant native access to AI-powered sports predictions.
|
|
4
|
+
|
|
5
|
+
Works with **Claude Desktop**, **Claude Code**, **Cursor**, **Windsurf**, and any MCP-compatible client.
|
|
6
|
+
|
|
7
|
+
## Quick Start
|
|
8
|
+
|
|
9
|
+
### 1. Get your API key
|
|
10
|
+
|
|
11
|
+
Go to [davincibets.io/settings](https://www.davincibets.io/settings) → **API** tab → Generate Key.
|
|
12
|
+
|
|
13
|
+
Your key looks like: `dvb_live_a3f2...` — copy it immediately (shown once).
|
|
14
|
+
|
|
15
|
+
### 2. Add to your AI assistant
|
|
16
|
+
|
|
17
|
+
#### Claude Desktop
|
|
18
|
+
|
|
19
|
+
Edit `~/.config/Claude/claude_desktop_config.json` (Mac/Linux) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
{
|
|
23
|
+
"mcpServers": {
|
|
24
|
+
"davinci-bets": {
|
|
25
|
+
"command": "npx",
|
|
26
|
+
"args": ["-y", "@davincibets/mcp-server@latest"],
|
|
27
|
+
"env": {
|
|
28
|
+
"DVB_API_KEY": "dvb_live_your_key_here"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
#### Claude Code
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
claude mcp add davinci-bets -- npx -y @davincibets/mcp-server@latest
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Then set the environment variable:
|
|
42
|
+
```bash
|
|
43
|
+
export DVB_API_KEY="dvb_live_your_key_here"
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Or add to your project's `.mcp.json`:
|
|
47
|
+
|
|
48
|
+
```json
|
|
49
|
+
{
|
|
50
|
+
"mcpServers": {
|
|
51
|
+
"davinci-bets": {
|
|
52
|
+
"command": "npx",
|
|
53
|
+
"args": ["-y", "@davincibets/mcp-server@latest"],
|
|
54
|
+
"env": {
|
|
55
|
+
"DVB_API_KEY": "dvb_live_your_key_here"
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
#### Cursor
|
|
63
|
+
|
|
64
|
+
Open Settings → MCP Servers → Add:
|
|
65
|
+
|
|
66
|
+
- **Name:** `davinci-bets`
|
|
67
|
+
- **Command:** `npx`
|
|
68
|
+
- **Args:** `-y @davincibets/mcp-server@latest`
|
|
69
|
+
- **Env:** `DVB_API_KEY=dvb_live_your_key_here`
|
|
70
|
+
|
|
71
|
+
#### Windsurf
|
|
72
|
+
|
|
73
|
+
Add to your Windsurf MCP config:
|
|
74
|
+
|
|
75
|
+
```json
|
|
76
|
+
{
|
|
77
|
+
"mcpServers": {
|
|
78
|
+
"davinci-bets": {
|
|
79
|
+
"command": "npx",
|
|
80
|
+
"args": ["-y", "@davincibets/mcp-server@latest"],
|
|
81
|
+
"env": {
|
|
82
|
+
"DVB_API_KEY": "dvb_live_your_key_here"
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### 3. Ask your AI
|
|
90
|
+
|
|
91
|
+
Once connected, you can ask naturally:
|
|
92
|
+
|
|
93
|
+
- *"What are today's highest confidence NBA picks?"*
|
|
94
|
+
- *"Show me NFL spread predictions for this week"*
|
|
95
|
+
- *"Get the top 5 picks across all sports"*
|
|
96
|
+
- *"Generate a pick card image for the Lakers game"*
|
|
97
|
+
- *"What's the MLB schedule with predictions?"*
|
|
98
|
+
|
|
99
|
+
## Tools
|
|
100
|
+
|
|
101
|
+
### `get_predictions`
|
|
102
|
+
|
|
103
|
+
Fetch AI predictions by sport, market, and confidence.
|
|
104
|
+
|
|
105
|
+
| Parameter | Type | Default | Description |
|
|
106
|
+
|-----------|------|---------|-------------|
|
|
107
|
+
| `sport` | NBA, NFL, NHL, MLB | all | Filter by sport |
|
|
108
|
+
| `market` | moneyline, spread, totals | moneyline | Betting market |
|
|
109
|
+
| `sort` | confidence, edge, game_time | confidence | Sort order |
|
|
110
|
+
| `limit` | 1-50 | 25 | Max results |
|
|
111
|
+
| `min_confidence` | 0-100 | 60 | Minimum confidence % |
|
|
112
|
+
|
|
113
|
+
### `get_top_picks`
|
|
114
|
+
|
|
115
|
+
Get the highest-conviction picks across all sports and markets.
|
|
116
|
+
|
|
117
|
+
| Parameter | Type | Default | Description |
|
|
118
|
+
|-----------|------|---------|-------------|
|
|
119
|
+
| `count` | 1-20 | 10 | Number of top picks |
|
|
120
|
+
| `min_confidence` | 0-100 | 65 | Minimum confidence % |
|
|
121
|
+
|
|
122
|
+
### `get_schedule`
|
|
123
|
+
|
|
124
|
+
Get today's game schedule with predictions for a sport.
|
|
125
|
+
|
|
126
|
+
| Parameter | Type | Required | Description |
|
|
127
|
+
|-----------|------|----------|-------------|
|
|
128
|
+
| `sport` | NBA, NFL, NHL, MLB | Yes | Which sport |
|
|
129
|
+
|
|
130
|
+
### `get_pick_card`
|
|
131
|
+
|
|
132
|
+
Render a prediction as a shareable 1080x1350 PNG image.
|
|
133
|
+
|
|
134
|
+
| Parameter | Type | Required | Description |
|
|
135
|
+
|-----------|------|----------|-------------|
|
|
136
|
+
| `game_id` | string | Yes | Game ID from predictions |
|
|
137
|
+
| `sport` | NBA, NFL, NHL, MLB | No | Sport hint |
|
|
138
|
+
|
|
139
|
+
### `get_batch_cards`
|
|
140
|
+
|
|
141
|
+
Render multiple pick cards at once (up to 10). Designed for marketing teams.
|
|
142
|
+
|
|
143
|
+
| Parameter | Type | Required | Description |
|
|
144
|
+
|-----------|------|----------|-------------|
|
|
145
|
+
| `game_ids` | string[] | Yes | Array of game IDs (max 10) |
|
|
146
|
+
| `sport` | NBA, NFL, NHL, MLB | No | Sport hint |
|
|
147
|
+
|
|
148
|
+
### `get_performance`
|
|
149
|
+
|
|
150
|
+
Get current prediction performance snapshot.
|
|
151
|
+
|
|
152
|
+
| Parameter | Type | Default | Description |
|
|
153
|
+
|-----------|------|---------|-------------|
|
|
154
|
+
| `sport` | NBA, NFL, NHL, MLB | all | Filter by sport |
|
|
155
|
+
| `market` | moneyline, spread, totals | all | Filter by market |
|
|
156
|
+
|
|
157
|
+
## Rate Limits
|
|
158
|
+
|
|
159
|
+
| Tier | Per Hour | Per Day | Who |
|
|
160
|
+
|------|----------|---------|-----|
|
|
161
|
+
| Personal | 50 | 500 | $8/mo subscribers |
|
|
162
|
+
| Partner | 500 | 10,000 | Marketing agencies (DVB-Social) |
|
|
163
|
+
| Internal | Unlimited | Unlimited | Da Vinci Bets systems |
|
|
164
|
+
|
|
165
|
+
Rate limit info is included in prediction responses. When limits are hit, the server returns a clear error with retry timing.
|
|
166
|
+
|
|
167
|
+
## For Marketing Teams (Partner Tier)
|
|
168
|
+
|
|
169
|
+
DVB-Social and marketing partners get elevated rate limits for batch content generation:
|
|
170
|
+
|
|
171
|
+
```
|
|
172
|
+
# Example workflow in Claude:
|
|
173
|
+
1. "Get today's NBA moneyline predictions"
|
|
174
|
+
2. "Generate pick cards for all games above 70% confidence"
|
|
175
|
+
3. "Also get the NHL schedule and render those cards too"
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
The `get_batch_cards` tool can render up to 10 cards per call — ideal for daily social media pipelines.
|
|
179
|
+
|
|
180
|
+
Contact support@davincibets.io for partner API key provisioning.
|
|
181
|
+
|
|
182
|
+
## Environment Variables
|
|
183
|
+
|
|
184
|
+
| Variable | Required | Description |
|
|
185
|
+
|----------|----------|-------------|
|
|
186
|
+
| `DVB_API_KEY` | Yes | Your API key (`dvb_live_xxx`) |
|
|
187
|
+
| `DVB_API_URL` | No | Override API base URL (default: `https://www.davincibets.io`) |
|
|
188
|
+
|
|
189
|
+
## Requirements
|
|
190
|
+
|
|
191
|
+
- Node.js 18+
|
|
192
|
+
- A Da Vinci Bets premium subscription ($8/mo)
|
|
193
|
+
- An API key from Settings → API tab
|
|
194
|
+
|
|
195
|
+
## Development
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
# Clone and install
|
|
199
|
+
cd packages/mcp-server
|
|
200
|
+
npm install
|
|
201
|
+
|
|
202
|
+
# Run locally
|
|
203
|
+
DVB_API_KEY=dvb_live_xxx npm run dev
|
|
204
|
+
|
|
205
|
+
# Build
|
|
206
|
+
npm run build
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## License
|
|
210
|
+
|
|
211
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Da Vinci Bets MCP Server
|
|
4
|
+
*
|
|
5
|
+
* Gives any AI assistant (Claude, Cursor, Windsurf, etc.) native access
|
|
6
|
+
* to Da Vinci Bets predictions via the user's personal API key.
|
|
7
|
+
*
|
|
8
|
+
* Tools:
|
|
9
|
+
* get_predictions — Fetch AI predictions by sport, market, confidence
|
|
10
|
+
* get_top_picks — Get highest-conviction picks across all sports
|
|
11
|
+
* get_pick_card — Render a pick as a shareable PNG (partner tier)
|
|
12
|
+
* get_performance — Win rates and track record stats
|
|
13
|
+
*
|
|
14
|
+
* Auth: DVB_API_KEY env var (dvb_live_xxx from Settings → API tab)
|
|
15
|
+
* Docs: https://www.davincibets.io/features
|
|
16
|
+
*/
|
|
17
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Da Vinci Bets MCP Server
|
|
4
|
+
*
|
|
5
|
+
* Gives any AI assistant (Claude, Cursor, Windsurf, etc.) native access
|
|
6
|
+
* to Da Vinci Bets predictions via the user's personal API key.
|
|
7
|
+
*
|
|
8
|
+
* Tools:
|
|
9
|
+
* get_predictions — Fetch AI predictions by sport, market, confidence
|
|
10
|
+
* get_top_picks — Get highest-conviction picks across all sports
|
|
11
|
+
* get_pick_card — Render a pick as a shareable PNG (partner tier)
|
|
12
|
+
* get_performance — Win rates and track record stats
|
|
13
|
+
*
|
|
14
|
+
* Auth: DVB_API_KEY env var (dvb_live_xxx from Settings → API tab)
|
|
15
|
+
* Docs: https://www.davincibets.io/features
|
|
16
|
+
*/
|
|
17
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
18
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
19
|
+
import { z } from "zod";
|
|
20
|
+
// ─── Configuration ──────────────────────────────────────────────────────────
|
|
21
|
+
// FINDING 4 FIX: Validate DVB_API_URL to prevent key exfiltration via SSRF
|
|
22
|
+
const ALLOWED_API_HOSTS = new Set([
|
|
23
|
+
"www.davincibets.io",
|
|
24
|
+
"davincibets.io",
|
|
25
|
+
"localhost",
|
|
26
|
+
"127.0.0.1",
|
|
27
|
+
]);
|
|
28
|
+
const _rawUrl = process.env.DVB_API_URL || "https://www.davincibets.io";
|
|
29
|
+
try {
|
|
30
|
+
const parsed = new URL(_rawUrl);
|
|
31
|
+
if (!ALLOWED_API_HOSTS.has(parsed.hostname)) {
|
|
32
|
+
console.error(`ERROR: DVB_API_URL host "${parsed.hostname}" is not allowed.\n` +
|
|
33
|
+
`Permitted: ${[...ALLOWED_API_HOSTS].join(", ")}`);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
console.error(`ERROR: DVB_API_URL "${_rawUrl}" is not a valid URL.`);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
const API_BASE = _rawUrl;
|
|
42
|
+
const _rawKey = process.env.DVB_API_KEY;
|
|
43
|
+
if (!_rawKey) {
|
|
44
|
+
console.error("ERROR: DVB_API_KEY is required.\n" +
|
|
45
|
+
"Generate one at https://www.davincibets.io/settings (API tab).\n" +
|
|
46
|
+
'Set it in your MCP config: "env": { "DVB_API_KEY": "dvb_live_xxx" }');
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
if (!_rawKey.startsWith("dvb_live_")) {
|
|
50
|
+
console.error('ERROR: DVB_API_KEY must start with "dvb_live_".\n' +
|
|
51
|
+
"Generate a valid key at https://www.davincibets.io/settings");
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
const API_KEY = _rawKey;
|
|
55
|
+
async function apiFetch(opts) {
|
|
56
|
+
const url = new URL(opts.path, API_BASE);
|
|
57
|
+
if (opts.params) {
|
|
58
|
+
for (const [k, v] of Object.entries(opts.params)) {
|
|
59
|
+
if (v)
|
|
60
|
+
url.searchParams.set(k, v);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
let res;
|
|
64
|
+
try {
|
|
65
|
+
res = await fetch(url.toString(), {
|
|
66
|
+
headers: {
|
|
67
|
+
Authorization: `Bearer ${API_KEY}`,
|
|
68
|
+
Accept: opts.raw ? "image/png" : "application/json",
|
|
69
|
+
},
|
|
70
|
+
signal: AbortSignal.timeout(opts.raw ? 30_000 : 15_000),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
throw new Error("Unable to reach Da Vinci Bets API. Check your internet connection.");
|
|
75
|
+
}
|
|
76
|
+
if (!res.ok) {
|
|
77
|
+
const status = res.status;
|
|
78
|
+
if (status === 401)
|
|
79
|
+
throw new Error("Invalid or expired API key. Regenerate at davincibets.io/settings.");
|
|
80
|
+
if (status === 429) {
|
|
81
|
+
const retryAfter = res.headers.get("Retry-After") || "60";
|
|
82
|
+
const remainHour = res.headers.get("X-RateLimit-Remaining-Hour") || "0";
|
|
83
|
+
const remainDay = res.headers.get("X-RateLimit-Remaining-Day") || "0";
|
|
84
|
+
throw new Error(`Rate limit exceeded. Remaining: ${remainHour}/hour, ${remainDay}/day. Retry after ${retryAfter}s.`);
|
|
85
|
+
}
|
|
86
|
+
// FINDING 5 FIX: Don't forward raw response body — may contain internal details
|
|
87
|
+
if (status === 403)
|
|
88
|
+
throw new Error("Access denied. Your subscription may have expired.");
|
|
89
|
+
if (status === 404)
|
|
90
|
+
throw new Error("Resource not found.");
|
|
91
|
+
if (status >= 500)
|
|
92
|
+
throw new Error("Da Vinci Bets API is temporarily unavailable. Try again shortly.");
|
|
93
|
+
throw new Error(`API request failed (HTTP ${status}).`);
|
|
94
|
+
}
|
|
95
|
+
if (opts.raw) {
|
|
96
|
+
return (await res.arrayBuffer());
|
|
97
|
+
}
|
|
98
|
+
return (await res.json());
|
|
99
|
+
}
|
|
100
|
+
// ─── Server Setup ───────────────────────────────────────────────────────────
|
|
101
|
+
const server = new McpServer({
|
|
102
|
+
name: "Da Vinci Bets",
|
|
103
|
+
version: "1.0.0",
|
|
104
|
+
});
|
|
105
|
+
// ─── Tool: get_predictions ──────────────────────────────────────────────────
|
|
106
|
+
server.tool("get_predictions", "Fetch AI-powered sports predictions filtered by sport, market, and confidence. " +
|
|
107
|
+
"Returns predictions with win probabilities, odds, edge values, and confidence tiers. " +
|
|
108
|
+
"Covers NBA, NFL, NHL, and MLB across moneylines, spreads, and totals.", {
|
|
109
|
+
sport: z
|
|
110
|
+
.enum(["NBA", "NFL", "NHL", "MLB"])
|
|
111
|
+
.optional()
|
|
112
|
+
.describe("Filter by sport. Omit for all sports."),
|
|
113
|
+
market: z
|
|
114
|
+
.enum(["moneyline", "spread", "totals"])
|
|
115
|
+
.default("moneyline")
|
|
116
|
+
.describe("Betting market type."),
|
|
117
|
+
sort: z
|
|
118
|
+
.enum(["confidence", "edge", "game_time"])
|
|
119
|
+
.default("confidence")
|
|
120
|
+
.describe("Sort order for results."),
|
|
121
|
+
limit: z
|
|
122
|
+
.number()
|
|
123
|
+
.int()
|
|
124
|
+
.min(1)
|
|
125
|
+
.max(50)
|
|
126
|
+
.default(25)
|
|
127
|
+
.describe("Maximum number of predictions to return."),
|
|
128
|
+
min_confidence: z
|
|
129
|
+
.number()
|
|
130
|
+
.int()
|
|
131
|
+
.min(0)
|
|
132
|
+
.max(100)
|
|
133
|
+
.default(60)
|
|
134
|
+
.describe("Minimum confidence percentage (0-100)."),
|
|
135
|
+
}, async ({ sport, market, sort, limit, min_confidence }) => {
|
|
136
|
+
try {
|
|
137
|
+
const params = {
|
|
138
|
+
market,
|
|
139
|
+
sort,
|
|
140
|
+
limit: String(limit),
|
|
141
|
+
min_confidence: String(min_confidence),
|
|
142
|
+
};
|
|
143
|
+
if (sport)
|
|
144
|
+
params.sport = sport;
|
|
145
|
+
const data = await apiFetch({
|
|
146
|
+
path: "/api/v1/predictions",
|
|
147
|
+
params,
|
|
148
|
+
});
|
|
149
|
+
const summary = [
|
|
150
|
+
`Found ${data.meta?.total ?? data.predictions.length} ${market} predictions${sport ? ` for ${sport}` : ""}.`,
|
|
151
|
+
`Rate limit: ${data.rate_limit?.remaining_hour ?? "?"}/hour, ${data.rate_limit?.remaining_day ?? "?"}/day remaining.`,
|
|
152
|
+
"",
|
|
153
|
+
...data.predictions.map((p, i) => {
|
|
154
|
+
const conf = p.confidence;
|
|
155
|
+
const pick = p.predicted_winner || p.predicted_pick || p.predicted_direction;
|
|
156
|
+
const edge = p.home_edge || p.away_edge;
|
|
157
|
+
let line = `${i + 1}. ${p.away_team} @ ${p.home_team} — Pick: ${pick} (${conf}% conf`;
|
|
158
|
+
if (edge)
|
|
159
|
+
line += `, ${edge}% edge`;
|
|
160
|
+
line += ")";
|
|
161
|
+
if (market === "spread" && p.spread_line != null) {
|
|
162
|
+
line += ` | Line: ${p.spread_line}`;
|
|
163
|
+
}
|
|
164
|
+
if (market === "totals" && p.totals_line != null) {
|
|
165
|
+
line += ` | Line: ${p.totals_line}`;
|
|
166
|
+
}
|
|
167
|
+
if (p.home_odds) {
|
|
168
|
+
line += ` | Odds: ${p.home_odds}/${p.away_odds}`;
|
|
169
|
+
}
|
|
170
|
+
return line;
|
|
171
|
+
}),
|
|
172
|
+
].join("\n");
|
|
173
|
+
return {
|
|
174
|
+
content: [
|
|
175
|
+
{ type: "text", text: summary },
|
|
176
|
+
{
|
|
177
|
+
type: "text",
|
|
178
|
+
text: "\n---\nRaw JSON:\n" + JSON.stringify(data.predictions, null, 2),
|
|
179
|
+
},
|
|
180
|
+
],
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
catch (err) {
|
|
184
|
+
return errorResult(err);
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
// ─── Tool: get_top_picks ────────────────────────────────────────────────────
|
|
188
|
+
server.tool("get_top_picks", "Get the highest-conviction AI picks across all sports and markets. " +
|
|
189
|
+
"Returns the top predictions sorted by confidence, great for daily briefings. " +
|
|
190
|
+
"Note: this fetches from 3 markets in parallel, consuming 3 API requests toward your rate limit.", {
|
|
191
|
+
count: z
|
|
192
|
+
.number()
|
|
193
|
+
.int()
|
|
194
|
+
.min(1)
|
|
195
|
+
.max(20)
|
|
196
|
+
.default(10)
|
|
197
|
+
.describe("Number of top picks to return."),
|
|
198
|
+
min_confidence: z
|
|
199
|
+
.number()
|
|
200
|
+
.int()
|
|
201
|
+
.min(0)
|
|
202
|
+
.max(100)
|
|
203
|
+
.default(65)
|
|
204
|
+
.describe("Minimum confidence percentage."),
|
|
205
|
+
}, async ({ count, min_confidence }) => {
|
|
206
|
+
try {
|
|
207
|
+
// Fetch top picks across all markets in parallel
|
|
208
|
+
const markets = ["moneyline", "spread", "totals"];
|
|
209
|
+
const results = await Promise.allSettled(markets.map((market) => apiFetch({
|
|
210
|
+
path: "/api/v1/predictions",
|
|
211
|
+
params: {
|
|
212
|
+
market,
|
|
213
|
+
sort: "confidence",
|
|
214
|
+
limit: String(count),
|
|
215
|
+
min_confidence: String(min_confidence),
|
|
216
|
+
},
|
|
217
|
+
})));
|
|
218
|
+
const allPicks = [];
|
|
219
|
+
const failedMarkets = [];
|
|
220
|
+
for (let i = 0; i < results.length; i++) {
|
|
221
|
+
const r = results[i];
|
|
222
|
+
if (r.status === "fulfilled" && r.value.predictions) {
|
|
223
|
+
for (const p of r.value.predictions) {
|
|
224
|
+
allPicks.push({ ...p, _market: markets[i] });
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
failedMarkets.push(markets[i]);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
// Sort by confidence descending, take top N
|
|
232
|
+
allPicks.sort((a, b) => Number(b.confidence || 0) - Number(a.confidence || 0));
|
|
233
|
+
const topN = allPicks.slice(0, count);
|
|
234
|
+
if (topN.length === 0) {
|
|
235
|
+
return {
|
|
236
|
+
content: [
|
|
237
|
+
{
|
|
238
|
+
type: "text",
|
|
239
|
+
text: `No picks found above ${min_confidence}% confidence. Games may not be scheduled today, or try lowering the threshold.`,
|
|
240
|
+
},
|
|
241
|
+
],
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
const lines = topN.map((p, i) => {
|
|
245
|
+
const conf = p.confidence;
|
|
246
|
+
const pick = p.predicted_winner ||
|
|
247
|
+
p.predicted_pick ||
|
|
248
|
+
p.predicted_direction ||
|
|
249
|
+
"N/A";
|
|
250
|
+
const sport = p.sport;
|
|
251
|
+
const market = p._market;
|
|
252
|
+
let line = `${i + 1}. [${sport}/${market}] ${p.away_team} @ ${p.home_team}`;
|
|
253
|
+
line += `\n Pick: ${pick} | ${conf}% confidence`;
|
|
254
|
+
if (p.home_edge)
|
|
255
|
+
line += ` | ${p.home_edge}% edge`;
|
|
256
|
+
if (p.commence_time) {
|
|
257
|
+
const time = new Date(String(p.commence_time)).toLocaleString("en-US", {
|
|
258
|
+
timeZone: "America/New_York",
|
|
259
|
+
month: "short",
|
|
260
|
+
day: "numeric",
|
|
261
|
+
hour: "numeric",
|
|
262
|
+
minute: "2-digit",
|
|
263
|
+
timeZoneName: "short",
|
|
264
|
+
});
|
|
265
|
+
line += `\n Game: ${time}`;
|
|
266
|
+
}
|
|
267
|
+
return line;
|
|
268
|
+
});
|
|
269
|
+
let header = `Top ${topN.length} AI Picks (≥${min_confidence}% confidence):`;
|
|
270
|
+
if (failedMarkets.length > 0) {
|
|
271
|
+
header += `\n⚠ Could not fetch ${failedMarkets.join(", ")} predictions. Results may be partial.`;
|
|
272
|
+
}
|
|
273
|
+
return {
|
|
274
|
+
content: [
|
|
275
|
+
{
|
|
276
|
+
type: "text",
|
|
277
|
+
text: header + "\n\n" + lines.join("\n\n"),
|
|
278
|
+
},
|
|
279
|
+
],
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
catch (err) {
|
|
283
|
+
return errorResult(err);
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
// ─── Tool: get_pick_card ────────────────────────────────────────────────────
|
|
287
|
+
server.tool("get_pick_card", "Render a prediction as a shareable 1080×1350 PNG image card. " +
|
|
288
|
+
"Returns the card image directly. Great for social media posts and marketing. " +
|
|
289
|
+
"Requires a game_id from get_predictions results. " +
|
|
290
|
+
"Partner-tier keys recommended (higher rate limits for batch image generation).", {
|
|
291
|
+
game_id: z
|
|
292
|
+
.string()
|
|
293
|
+
.regex(/^[a-zA-Z0-9_-]+$/, "game_id must be alphanumeric with _ or -")
|
|
294
|
+
.max(100)
|
|
295
|
+
.describe("The game_id from a prediction result. Use get_predictions first to find game IDs."),
|
|
296
|
+
sport: z
|
|
297
|
+
.enum(["NBA", "NFL", "NHL", "MLB"])
|
|
298
|
+
.optional()
|
|
299
|
+
.describe("Sport for the game (helps with lookup speed)."),
|
|
300
|
+
}, async ({ game_id, sport }) => {
|
|
301
|
+
try {
|
|
302
|
+
const params = {};
|
|
303
|
+
if (sport)
|
|
304
|
+
params.sport = sport;
|
|
305
|
+
const imageBuffer = await apiFetch({
|
|
306
|
+
path: `/api/card-image/${encodeURIComponent(game_id)}`,
|
|
307
|
+
params,
|
|
308
|
+
raw: true,
|
|
309
|
+
});
|
|
310
|
+
const base64 = Buffer.from(imageBuffer).toString("base64");
|
|
311
|
+
return {
|
|
312
|
+
content: [
|
|
313
|
+
{
|
|
314
|
+
type: "image",
|
|
315
|
+
data: base64,
|
|
316
|
+
mimeType: "image/png",
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
type: "text",
|
|
320
|
+
text: `Pick card rendered for game ${game_id} (1080×1350 PNG, ${Math.round(imageBuffer.byteLength / 1024)}KB).`,
|
|
321
|
+
},
|
|
322
|
+
],
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
catch (err) {
|
|
326
|
+
return errorResult(err);
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
// ─── Tool: get_batch_cards ──────────────────────────────────────────────────
|
|
330
|
+
server.tool("get_batch_cards", "Render multiple pick cards as PNG images in one call. " +
|
|
331
|
+
"Designed for marketing teams generating social media content. " +
|
|
332
|
+
"Partner-tier API key required for batch operations. " +
|
|
333
|
+
"Returns one image per game_id.", {
|
|
334
|
+
game_ids: z
|
|
335
|
+
.array(z
|
|
336
|
+
.string()
|
|
337
|
+
.regex(/^[a-zA-Z0-9_-]+$/, "game_id must be alphanumeric")
|
|
338
|
+
.max(100))
|
|
339
|
+
.min(1)
|
|
340
|
+
.max(10)
|
|
341
|
+
.describe("Array of game_ids to render. Max 10 per call."),
|
|
342
|
+
sport: z
|
|
343
|
+
.enum(["NBA", "NFL", "NHL", "MLB"])
|
|
344
|
+
.optional()
|
|
345
|
+
.describe("Sport hint for faster lookups."),
|
|
346
|
+
}, async ({ game_ids, sport }) => {
|
|
347
|
+
try {
|
|
348
|
+
const params = {};
|
|
349
|
+
if (sport)
|
|
350
|
+
params.sport = sport;
|
|
351
|
+
const results = await Promise.allSettled(game_ids.map(async (gid) => {
|
|
352
|
+
const buf = await apiFetch({
|
|
353
|
+
path: `/api/card-image/${encodeURIComponent(gid)}`,
|
|
354
|
+
params,
|
|
355
|
+
raw: true,
|
|
356
|
+
});
|
|
357
|
+
return { game_id: gid, buffer: buf };
|
|
358
|
+
}));
|
|
359
|
+
const content = [];
|
|
360
|
+
let successCount = 0;
|
|
361
|
+
for (const r of results) {
|
|
362
|
+
if (r.status === "fulfilled") {
|
|
363
|
+
const base64 = Buffer.from(r.value.buffer).toString("base64");
|
|
364
|
+
content.push({
|
|
365
|
+
type: "image",
|
|
366
|
+
data: base64,
|
|
367
|
+
mimeType: "image/png",
|
|
368
|
+
});
|
|
369
|
+
content.push({
|
|
370
|
+
type: "text",
|
|
371
|
+
text: `Card: ${r.value.game_id} (${Math.round(r.value.buffer.byteLength / 1024)}KB)`,
|
|
372
|
+
});
|
|
373
|
+
successCount++;
|
|
374
|
+
}
|
|
375
|
+
else {
|
|
376
|
+
content.push({
|
|
377
|
+
type: "text",
|
|
378
|
+
text: `Failed: ${r.reason instanceof Error ? r.reason.message : String(r.reason)}`,
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
content.unshift({
|
|
383
|
+
type: "text",
|
|
384
|
+
text: `Rendered ${successCount}/${game_ids.length} pick cards.`,
|
|
385
|
+
});
|
|
386
|
+
return { content };
|
|
387
|
+
}
|
|
388
|
+
catch (err) {
|
|
389
|
+
return errorResult(err);
|
|
390
|
+
}
|
|
391
|
+
});
|
|
392
|
+
// ─── Tool: get_performance ──────────────────────────────────────────────────
|
|
393
|
+
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.", {
|
|
396
|
+
sport: z
|
|
397
|
+
.enum(["NBA", "NFL", "NHL", "MLB"])
|
|
398
|
+
.optional()
|
|
399
|
+
.describe("Filter performance to a specific sport."),
|
|
400
|
+
market: z
|
|
401
|
+
.enum(["moneyline", "spread", "totals"])
|
|
402
|
+
.optional()
|
|
403
|
+
.describe("Filter to a specific market."),
|
|
404
|
+
}, async ({ sport, market }) => {
|
|
405
|
+
try {
|
|
406
|
+
// Pull recent settled predictions to compute win rates
|
|
407
|
+
// We use the predictions endpoint with high confidence to show quality
|
|
408
|
+
const params = {
|
|
409
|
+
sort: "confidence",
|
|
410
|
+
limit: "50",
|
|
411
|
+
min_confidence: "50",
|
|
412
|
+
};
|
|
413
|
+
if (sport)
|
|
414
|
+
params.sport = sport;
|
|
415
|
+
if (market)
|
|
416
|
+
params.market = market;
|
|
417
|
+
const data = await apiFetch({
|
|
418
|
+
path: "/api/v1/predictions",
|
|
419
|
+
params,
|
|
420
|
+
});
|
|
421
|
+
// Summarize available predictions as a performance snapshot
|
|
422
|
+
const byMarket = {};
|
|
423
|
+
const bySport = {};
|
|
424
|
+
const confBuckets = { high: 0, medium: 0 };
|
|
425
|
+
for (const p of data.predictions) {
|
|
426
|
+
const m = String(p.market || "moneyline");
|
|
427
|
+
const s = String(p.sport || "?");
|
|
428
|
+
byMarket[m] = (byMarket[m] || 0) + 1;
|
|
429
|
+
bySport[s] = (bySport[s] || 0) + 1;
|
|
430
|
+
const conf = Number(p.confidence || 0);
|
|
431
|
+
if (conf >= 65)
|
|
432
|
+
confBuckets.high++;
|
|
433
|
+
else
|
|
434
|
+
confBuckets.medium++;
|
|
435
|
+
}
|
|
436
|
+
const lines = [
|
|
437
|
+
"Da Vinci Bets — Current Predictions Snapshot",
|
|
438
|
+
"═══════════════════════════════════════════════",
|
|
439
|
+
`Total active predictions: ${data.meta?.total ?? data.predictions.length}`,
|
|
440
|
+
"",
|
|
441
|
+
"By Sport:",
|
|
442
|
+
...Object.entries(bySport).map(([s, n]) => ` ${s}: ${n} picks`),
|
|
443
|
+
"",
|
|
444
|
+
"By Market:",
|
|
445
|
+
...Object.entries(byMarket).map(([m, n]) => ` ${m}: ${n} picks`),
|
|
446
|
+
"",
|
|
447
|
+
"Confidence Distribution:",
|
|
448
|
+
` High (≥65%): ${confBuckets.high} picks`,
|
|
449
|
+
` 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.",
|
|
455
|
+
];
|
|
456
|
+
return {
|
|
457
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
catch (err) {
|
|
461
|
+
return errorResult(err);
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
// ─── Tool: get_schedule ─────────────────────────────────────────────────────
|
|
465
|
+
server.tool("get_schedule", "Get today's game schedule with predictions summary. " +
|
|
466
|
+
"Shows which games have predictions available and their confidence levels.", {
|
|
467
|
+
sport: z
|
|
468
|
+
.enum(["NBA", "NFL", "NHL", "MLB"])
|
|
469
|
+
.describe("Which sport's schedule to retrieve."),
|
|
470
|
+
}, async ({ sport }) => {
|
|
471
|
+
try {
|
|
472
|
+
const data = await apiFetch({
|
|
473
|
+
path: "/api/v1/predictions",
|
|
474
|
+
params: {
|
|
475
|
+
sport,
|
|
476
|
+
market: "moneyline",
|
|
477
|
+
sort: "game_time",
|
|
478
|
+
limit: "50",
|
|
479
|
+
min_confidence: "0",
|
|
480
|
+
},
|
|
481
|
+
});
|
|
482
|
+
const total = data.meta?.total ?? data.predictions.length;
|
|
483
|
+
if (total === 0) {
|
|
484
|
+
return {
|
|
485
|
+
content: [
|
|
486
|
+
{
|
|
487
|
+
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 for the next 48 hours.`,
|
|
489
|
+
},
|
|
490
|
+
],
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
const lines = [
|
|
494
|
+
`${sport} Schedule — ${total} games with predictions`,
|
|
495
|
+
"─".repeat(50),
|
|
496
|
+
"",
|
|
497
|
+
];
|
|
498
|
+
for (const p of data.predictions) {
|
|
499
|
+
const time = p.commence_time
|
|
500
|
+
? new Date(String(p.commence_time)).toLocaleString("en-US", {
|
|
501
|
+
timeZone: "America/New_York",
|
|
502
|
+
weekday: "short",
|
|
503
|
+
month: "short",
|
|
504
|
+
day: "numeric",
|
|
505
|
+
hour: "numeric",
|
|
506
|
+
minute: "2-digit",
|
|
507
|
+
timeZoneName: "short",
|
|
508
|
+
})
|
|
509
|
+
: "TBD";
|
|
510
|
+
lines.push(`${time}`);
|
|
511
|
+
lines.push(` ${p.away_team} @ ${p.home_team}`);
|
|
512
|
+
lines.push(` Pick: ${p.predicted_winner} (${p.confidence}% conf) | Game ID: ${p.game_id}`);
|
|
513
|
+
lines.push("");
|
|
514
|
+
}
|
|
515
|
+
lines.push("Use game_id values with get_pick_card to render shareable images.");
|
|
516
|
+
return {
|
|
517
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
catch (err) {
|
|
521
|
+
return errorResult(err);
|
|
522
|
+
}
|
|
523
|
+
});
|
|
524
|
+
// ─── Error Helper ───────────────────────────────────────────────────────────
|
|
525
|
+
function errorResult(err) {
|
|
526
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
527
|
+
return {
|
|
528
|
+
content: [{ type: "text", text: `Error: ${message}` }],
|
|
529
|
+
isError: true,
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
// ─── Start Server ───────────────────────────────────────────────────────────
|
|
533
|
+
async function main() {
|
|
534
|
+
const transport = new StdioServerTransport();
|
|
535
|
+
await server.connect(transport);
|
|
536
|
+
console.error("Da Vinci Bets MCP Server v1.0.0 — connected via stdio");
|
|
537
|
+
console.error(`API: ${API_BASE}`);
|
|
538
|
+
console.error(`Key: dvb_live_***`);
|
|
539
|
+
}
|
|
540
|
+
main().catch((err) => {
|
|
541
|
+
console.error("Fatal:", err);
|
|
542
|
+
process.exit(1);
|
|
543
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@davincibets/mcp-server",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server for Da Vinci Bets — AI sports predictions in your AI assistant",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"davincibets-mcp": "./dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsc",
|
|
17
|
+
"dev": "tsx src/index.ts",
|
|
18
|
+
"prepublishOnly": "npm run build"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"mcp",
|
|
22
|
+
"model-context-protocol",
|
|
23
|
+
"ai",
|
|
24
|
+
"claude",
|
|
25
|
+
"sports-betting",
|
|
26
|
+
"predictions",
|
|
27
|
+
"davincibets"
|
|
28
|
+
],
|
|
29
|
+
"author": "Da Vinci Bets <support@davincibets.io>",
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "https://github.com/DaVinciii/davincibets-mcp"
|
|
34
|
+
},
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=18.0.0"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@modelcontextprotocol/sdk": "^1.7.0",
|
|
40
|
+
"zod": "^3.24.0"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@types/node": "^22.0.0",
|
|
44
|
+
"tsx": "^4.7.0",
|
|
45
|
+
"typescript": "^5.7.0"
|
|
46
|
+
}
|
|
47
|
+
}
|