propline-mcp 0.36.0 → 0.38.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 +4 -3
- package/dist/http.js +99 -20
- package/dist/http.js.map +1 -1
- package/dist/index.js +88 -17
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -30,7 +30,7 @@ The model uses these tools transparently:
|
|
|
30
30
|
| `propline_get_odds` | Live odds — bulk by sport or full props per event. Accepts `period` (q1/h1/p1/f5/…) to scope to game-period markets. |
|
|
31
31
|
| `propline_get_odds_history` | Hobby+: snapshot history per outcome; supports `period` (q1/h1/…) plus time-window filters (from/to, relative_from/relative_to, interval, changes_only) |
|
|
32
32
|
| `propline_get_odds_closing` | Hobby+: opening **and** closing line per (book, market, outcome) — CLV helper. Accepts `period` to scope to a specific game period. |
|
|
33
|
-
| `propline_grade_clv` | Hobby+: grade **placed** bets against their closing lines. Returns closing price, de-vigged closing fair (`fair_source` = sharpest book at close, not yours), `clv_pct` (price-vs-price, vig-blind) **and** `ev_vs_close_pct` (the honest number), plus the graded result once the game settles. Fail-closed matching; unstarted events come back `closing_is_final: false` and are excluded from the averages. |
|
|
33
|
+
| `propline_grade_clv` | Hobby+: grade **placed** bets against their closing lines. Returns closing price, de-vigged closing fair (`fair_source` = sharpest book at close, not yours), `clv_pct` (price-vs-price, vig-blind) **and** `ev_vs_close_pct` (the honest number), plus the graded result once the game settles. Fail-closed matching; unstarted events come back `closing_is_final: false` and are excluded from the averages. `devig`: multiplicative or shin. |
|
|
34
34
|
| `propline_price_sgp` | Hobby+: price a same-game parlay at the **book's own** correlated odds (FanDuel, DraftKings, BetOnline, LowVig — or `bookmaker: "all"` for every book side by side with `best_bookmaker`). Returns `sgp_price`, `independent_price` (product of the live single-leg prices) and `correlation_factor` (their ratio). Legs are named exactly as `/odds` names an outcome; matching is fail-closed. |
|
|
35
35
|
| `propline_export_odds_history` | Backfill-pass / Enterprise: bulk line-movement tick history (every snapshot, per book) for a sport. Requires a `since`/`until` window; result capped to 200 rows (use the REST endpoint directly for the full file). |
|
|
36
36
|
| `propline_get_futures` | Season-long futures — championship/division/conference winners, MVP + awards, season win totals — across Bovada/FanDuel/DraftKings/Pinnacle/Kalshi (free) |
|
|
@@ -51,10 +51,11 @@ The model uses these tools transparently:
|
|
|
51
51
|
| `propline_get_best_line` | Hobby+: cross-book line shopping — best price per (market, player, line) across all comparable books, `all_prices` sorted best-first; optional `bookmakers` filter |
|
|
52
52
|
| `propline_list_webhooks` | Streaming Lite+: list webhook subscriptions (read-only, secrets masked) |
|
|
53
53
|
| `propline_get_webhook_deliveries` | Streaming Lite+: recent delivery attempts for a webhook — status, HTTP code, attempts, payload; `before_id` pages backwards. The "why isn't my webhook firing" tool |
|
|
54
|
+
| `propline_create_free_api_key` | Sign the user up for a free personal key from inside the chat. Takes the email **the user gives**; the key is emailed to them (never returned), with a ready-made connector URL to reconnect. The only tool that is not read-only |
|
|
54
55
|
|
|
55
56
|
## Hosted endpoint (no install)
|
|
56
57
|
|
|
57
|
-
The same
|
|
58
|
+
The same 29 tools are served over **Streamable HTTP** at
|
|
58
59
|
|
|
59
60
|
```
|
|
60
61
|
https://mcp.prop-line.com/mcp
|
|
@@ -91,7 +92,7 @@ npx -y propline-mcp
|
|
|
91
92
|
|
|
92
93
|
Your agent can immediately pull live odds, scores, and stats. The demo key is free-tier and shared — paid features (resolution, +EV, history, exports) return a redacted teaser, and limits are pooled across everyone. For full access and your own limits, set `PROPLINE_API_KEY` (below). Get a free personal key at [prop-line.com](https://prop-line.com/?ref=mcp).
|
|
93
94
|
|
|
94
|
-
While the demo key is in use, every tool result carries a second content block noting the pooling and redaction, so the assistant can explain an empty field or a 429 accurately. It disappears the moment you set your own key.
|
|
95
|
+
While the demo key is in use, every tool result carries a second content block noting the pooling and redaction, so the assistant can explain an empty field or a 429 accurately. It disappears the moment you set your own key. The note also tells the assistant it can offer `propline_create_free_api_key`, so a user can get their own key without leaving the chat.
|
|
95
96
|
|
|
96
97
|
## Install (with your own key)
|
|
97
98
|
|
package/dist/http.js
CHANGED
|
@@ -74,7 +74,7 @@ var PropLineClient = class {
|
|
|
74
74
|
* endpoint on this server is a GET with query params; folding a body into
|
|
75
75
|
* that signature would make the common case harder to read.
|
|
76
76
|
*/
|
|
77
|
-
async postRequest(path, body) {
|
|
77
|
+
async postRequest(path, body, extraHeaders = {}) {
|
|
78
78
|
const url = new URL(this.baseUrl + path);
|
|
79
79
|
const controller = new AbortController();
|
|
80
80
|
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
@@ -85,7 +85,8 @@ var PropLineClient = class {
|
|
|
85
85
|
"X-API-Key": this.apiKey,
|
|
86
86
|
Accept: "application/json",
|
|
87
87
|
"Content-Type": "application/json",
|
|
88
|
-
"User-Agent": "propline-mcp/0.1.0"
|
|
88
|
+
"User-Agent": "propline-mcp/0.1.0",
|
|
89
|
+
...extraHeaders
|
|
89
90
|
},
|
|
90
91
|
body: JSON.stringify(body),
|
|
91
92
|
signal: controller.signal
|
|
@@ -159,8 +160,9 @@ var PropLineClient = class {
|
|
|
159
160
|
* See the propline_grade_clv tool description for the semantics that
|
|
160
161
|
* matter when presenting the result.
|
|
161
162
|
*/
|
|
162
|
-
gradeClv(bets) {
|
|
163
|
-
|
|
163
|
+
gradeClv(bets, devig) {
|
|
164
|
+
const qs = devig ? `?devig=${encodeURIComponent(devig)}` : "";
|
|
165
|
+
return this.postRequest(`/v1/clv/grade${qs}`, bets);
|
|
164
166
|
}
|
|
165
167
|
/**
|
|
166
168
|
* Price a same-game parlay at the book's own correlated odds (FanDuel
|
|
@@ -172,6 +174,20 @@ var PropLineClient = class {
|
|
|
172
174
|
{ bookmaker, legs }
|
|
173
175
|
);
|
|
174
176
|
}
|
|
177
|
+
/**
|
|
178
|
+
* Register a free PropLine key for an email (POST /v1/auth/register).
|
|
179
|
+
* The key is EMAILED to that address and never returned — the response
|
|
180
|
+
* is only a status message. `forward` carries the end user's IP plus the
|
|
181
|
+
* shared secret on the hosted server, so the API's per-IP signup throttle
|
|
182
|
+
* counts that user rather than the MCP machine.
|
|
183
|
+
*/
|
|
184
|
+
registerFreeKey(email, source, forward) {
|
|
185
|
+
const headers = forward ? {
|
|
186
|
+
"X-PropLine-Forward-Secret": forward.secret,
|
|
187
|
+
"X-PropLine-Client-IP": forward.clientIp
|
|
188
|
+
} : {};
|
|
189
|
+
return this.postRequest("/v1/auth/register", { email, source }, headers);
|
|
190
|
+
}
|
|
175
191
|
// ----- Bulk exports -----
|
|
176
192
|
/**
|
|
177
193
|
* Full line-movement tick history as CSV text (Backfill pass / Enterprise
|
|
@@ -300,14 +316,19 @@ var PropLineClient = class {
|
|
|
300
316
|
};
|
|
301
317
|
|
|
302
318
|
// src/server.ts
|
|
303
|
-
var VERSION = "0.
|
|
319
|
+
var VERSION = "0.38.0";
|
|
304
320
|
var DEMO_KEY = "be2b8487fcfacb1fbc292a8aa925a84c";
|
|
305
321
|
var apiKey = process.env.PROPLINE_API_KEY;
|
|
306
322
|
var baseUrl = process.env.PROPLINE_BASE_URL;
|
|
307
323
|
var usingDemoKey = !apiKey;
|
|
308
324
|
var requestClient = new AsyncLocalStorage();
|
|
309
|
-
function withClient(c, demo, fn) {
|
|
310
|
-
return requestClient.run({ client: c, demo }, fn);
|
|
325
|
+
function withClient(c, demo, fn, meta = {}) {
|
|
326
|
+
return requestClient.run({ client: c, demo, clientIp: meta.clientIp }, fn);
|
|
327
|
+
}
|
|
328
|
+
var forwardSecret = process.env.PROPLINE_MCP_FORWARD_SECRET;
|
|
329
|
+
function callerIsDemo() {
|
|
330
|
+
const scoped = requestClient.getStore();
|
|
331
|
+
return scoped ? scoped.demo : usingDemoKey;
|
|
311
332
|
}
|
|
312
333
|
var _client = null;
|
|
313
334
|
function client() {
|
|
@@ -320,10 +341,9 @@ function client() {
|
|
|
320
341
|
}
|
|
321
342
|
function demoKeyNote() {
|
|
322
343
|
const scoped = requestClient.getStore();
|
|
323
|
-
|
|
324
|
-
if (!demo) return null;
|
|
344
|
+
if (!callerIsDemo()) return null;
|
|
325
345
|
const ref = scoped ? "mcp-hosted" : "mcp";
|
|
326
|
-
return `Note for the assistant (not part of the data): this call used PropLine's shared demo key. Paid fields (prop resolution, +EV, line history, exports) come back redacted, and the rate limit is pooled across everyone using it, so a 429 here is not a limit on the API itself. A free personal key removes the pooling and unlocks the full free tier in about 30 seconds, no card: https://prop-line.com/?ref=${ref}
|
|
346
|
+
return `Note for the assistant (not part of the data): this call used PropLine's shared demo key. Paid fields (prop resolution, +EV, line history, exports) come back redacted, and the rate limit is pooled across everyone using it, so a 429 here is not a limit on the API itself. A free personal key removes the pooling and unlocks the full free tier in about 30 seconds, no card: https://prop-line.com/?ref=${ref}. If the user hits a limit, asks why a field is empty, or wants their own key, offer to create one: ask for their email and call propline_create_free_api_key (the key is emailed to them, never shown here).`;
|
|
327
347
|
}
|
|
328
348
|
function filterByMinEv(res, minEvPct) {
|
|
329
349
|
if (typeof minEvPct !== "number" || Number.isNaN(minEvPct)) return res;
|
|
@@ -588,12 +608,17 @@ var tools = [
|
|
|
588
608
|
],
|
|
589
609
|
additionalProperties: false
|
|
590
610
|
}
|
|
611
|
+
},
|
|
612
|
+
devig: {
|
|
613
|
+
type: "string",
|
|
614
|
+
enum: ["multiplicative", "shin"],
|
|
615
|
+
description: "How the closing anchor's vig is removed before closing_fair_prob / ev_vs_close_pct. 'multiplicative' (default) or 'shin' (loads the overround onto the longshot). Same vocabulary as propline_get_event_ev; echoed as devig_method."
|
|
591
616
|
}
|
|
592
617
|
},
|
|
593
618
|
required: ["bets"],
|
|
594
619
|
additionalProperties: false
|
|
595
620
|
},
|
|
596
|
-
handler: (args) => client().gradeClv(args.bets)
|
|
621
|
+
handler: (args) => client().gradeClv(args.bets, args.devig)
|
|
597
622
|
},
|
|
598
623
|
{
|
|
599
624
|
name: "propline_price_sgp",
|
|
@@ -1152,11 +1177,57 @@ var tools = [
|
|
|
1152
1177
|
sinceSeq: args.since_seq,
|
|
1153
1178
|
limit: args.limit
|
|
1154
1179
|
})
|
|
1180
|
+
},
|
|
1181
|
+
{
|
|
1182
|
+
name: "propline_create_free_api_key",
|
|
1183
|
+
title: "Create a free PropLine API key",
|
|
1184
|
+
// Not read-only: it creates an account and sends an email.
|
|
1185
|
+
writes: true,
|
|
1186
|
+
description: "Create a free personal PropLine API key for the user and EMAIL it to them. Use this when the user wants their own key \u2014 e.g. they hit a shared-demo-key rate limit, a paid field came back redacted, or they ask how to get a key. Only call it with an email address the user explicitly gave you for this purpose in this conversation; never guess, reuse one from elsewhere, or sign up a third party. The key is never returned here \u2014 it goes to that inbox, with instructions to reconnect this assistant using it. Free tier: 1,000 requests/day, no card. If the address already has a key, the key is re-sent (at most once a day).",
|
|
1187
|
+
inputSchema: {
|
|
1188
|
+
type: "object",
|
|
1189
|
+
properties: {
|
|
1190
|
+
email: {
|
|
1191
|
+
type: "string",
|
|
1192
|
+
description: "The user's own email address, as they gave it."
|
|
1193
|
+
}
|
|
1194
|
+
},
|
|
1195
|
+
required: ["email"],
|
|
1196
|
+
additionalProperties: false
|
|
1197
|
+
},
|
|
1198
|
+
handler: async (args) => {
|
|
1199
|
+
const email = String(args.email ?? "").trim();
|
|
1200
|
+
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
|
1201
|
+
throw new Error("email must be a valid email address the user gave you");
|
|
1202
|
+
}
|
|
1203
|
+
if (!callerIsDemo()) {
|
|
1204
|
+
return {
|
|
1205
|
+
status: "already_keyed",
|
|
1206
|
+
message: "This connection already uses a personal PropLine key, so no new key was created. Manage it at https://prop-line.com/dashboard."
|
|
1207
|
+
};
|
|
1208
|
+
}
|
|
1209
|
+
const scoped = requestClient.getStore();
|
|
1210
|
+
const hosted = Boolean(scoped);
|
|
1211
|
+
const forward = hosted && forwardSecret && scoped?.clientIp ? { clientIp: scoped.clientIp, secret: forwardSecret } : void 0;
|
|
1212
|
+
const res = await client().registerFreeKey(
|
|
1213
|
+
email,
|
|
1214
|
+
hosted ? "mcp-hosted" : "mcp",
|
|
1215
|
+
forward
|
|
1216
|
+
);
|
|
1217
|
+
return {
|
|
1218
|
+
status: "sent",
|
|
1219
|
+
email,
|
|
1220
|
+
tier: res.tier,
|
|
1221
|
+
daily_limit: res.daily_limit,
|
|
1222
|
+
message: res.message,
|
|
1223
|
+
next_steps: hosted ? "Tell the user to check their inbox (and Junk). The email has a ready-made connector URL (https://mcp.prop-line.com/mcp?apiKey=...) and a Claude Code command. Once they reconnect with it, this session stops using the shared demo key." : "Tell the user to check their inbox (and Junk), then set PROPLINE_API_KEY to the emailed key in this MCP server's config and restart it."
|
|
1224
|
+
};
|
|
1225
|
+
}
|
|
1155
1226
|
}
|
|
1156
1227
|
];
|
|
1157
|
-
function withDemoNote(text) {
|
|
1228
|
+
function withDemoNote(text, toolName) {
|
|
1158
1229
|
const blocks = [{ type: "text", text }];
|
|
1159
|
-
const note = demoKeyNote();
|
|
1230
|
+
const note = toolName === "propline_create_free_api_key" ? null : demoKeyNote();
|
|
1160
1231
|
if (note) blocks.push({ type: "text", text: note });
|
|
1161
1232
|
return blocks;
|
|
1162
1233
|
}
|
|
@@ -1171,14 +1242,14 @@ function createServer() {
|
|
|
1171
1242
|
title: t.title,
|
|
1172
1243
|
description: t.description,
|
|
1173
1244
|
inputSchema: t.inputSchema,
|
|
1174
|
-
// Every PropLine tool is a READ of the odds API
|
|
1175
|
-
//
|
|
1245
|
+
// Every PropLine tool is a READ of the odds API except the one marked
|
|
1246
|
+
// `writes` (propline_create_free_api_key creates an account + email). Directories (Claude connectors, Cursor)
|
|
1176
1247
|
// require these hints; clients use them to skip confirmation prompts.
|
|
1177
1248
|
annotations: {
|
|
1178
1249
|
title: t.title,
|
|
1179
|
-
readOnlyHint:
|
|
1250
|
+
readOnlyHint: !t.writes,
|
|
1180
1251
|
destructiveHint: false,
|
|
1181
|
-
idempotentHint:
|
|
1252
|
+
idempotentHint: !t.writes,
|
|
1182
1253
|
openWorldHint: true
|
|
1183
1254
|
}
|
|
1184
1255
|
}))
|
|
@@ -1195,13 +1266,13 @@ function createServer() {
|
|
|
1195
1266
|
const data = await tool.handler(req.params.arguments ?? {});
|
|
1196
1267
|
const text = typeof data === "string" ? data : JSON.stringify(data, null, 2);
|
|
1197
1268
|
return {
|
|
1198
|
-
content: withDemoNote(text)
|
|
1269
|
+
content: withDemoNote(text, tool.name)
|
|
1199
1270
|
};
|
|
1200
1271
|
} catch (err) {
|
|
1201
1272
|
const msg = err instanceof PropLineHTTPError ? `PropLine API error ${err.statusCode}: ${err.body.slice(0, 500)}` : err instanceof Error ? err.message : String(err);
|
|
1202
1273
|
return {
|
|
1203
1274
|
isError: true,
|
|
1204
|
-
content: withDemoNote(msg)
|
|
1275
|
+
content: withDemoNote(msg, tool.name)
|
|
1205
1276
|
};
|
|
1206
1277
|
}
|
|
1207
1278
|
});
|
|
@@ -1225,6 +1296,11 @@ function extractApiKey(req) {
|
|
|
1225
1296
|
if (q && q.trim()) return { key: q.trim(), demo: false };
|
|
1226
1297
|
return { key: DEMO_KEY, demo: true };
|
|
1227
1298
|
}
|
|
1299
|
+
function endUserIp(req) {
|
|
1300
|
+
const fly = req.headers["fly-client-ip"];
|
|
1301
|
+
if (typeof fly === "string" && fly.trim()) return fly.trim();
|
|
1302
|
+
return req.socket.remoteAddress ?? void 0;
|
|
1303
|
+
}
|
|
1228
1304
|
var CORS_HEADERS = {
|
|
1229
1305
|
"Access-Control-Allow-Origin": "*",
|
|
1230
1306
|
"Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
|
|
@@ -1250,7 +1326,9 @@ async function handleMcp(req, res) {
|
|
|
1250
1326
|
});
|
|
1251
1327
|
if (demo) res.setHeader("X-PropLine-Demo-Key", "1");
|
|
1252
1328
|
await server.connect(transport);
|
|
1253
|
-
await withClient(client2, demo, () => transport.handleRequest(req, res)
|
|
1329
|
+
await withClient(client2, demo, () => transport.handleRequest(req, res), {
|
|
1330
|
+
clientIp: endUserIp(req)
|
|
1331
|
+
});
|
|
1254
1332
|
}
|
|
1255
1333
|
var manifest = () => ({
|
|
1256
1334
|
name: "propline-mcp",
|
|
@@ -1306,6 +1384,7 @@ httpServer.listen(PORT, () => {
|
|
|
1306
1384
|
);
|
|
1307
1385
|
});
|
|
1308
1386
|
export {
|
|
1387
|
+
endUserIp,
|
|
1309
1388
|
extractApiKey
|
|
1310
1389
|
};
|
|
1311
1390
|
//# sourceMappingURL=http.js.map
|