@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 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
@@ -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
+ }