propline-mcp 0.37.0 → 0.39.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 +3 -2
- package/dist/http.js +104 -21
- package/dist/http.js.map +1 -1
- package/dist/index.js +93 -18
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -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
|
|
@@ -151,7 +152,12 @@ var PropLineClient = class {
|
|
|
151
152
|
getOddsClosing(sportKey, eventId, opts = {}) {
|
|
152
153
|
return this.request(
|
|
153
154
|
`/v1/sports/${sportKey}/events/${eventId}/odds/closing`,
|
|
154
|
-
{
|
|
155
|
+
{
|
|
156
|
+
markets: opts.markets,
|
|
157
|
+
bookmakers: opts.bookmakers,
|
|
158
|
+
period: opts.period,
|
|
159
|
+
opening_window: opts.openingWindow
|
|
160
|
+
}
|
|
155
161
|
);
|
|
156
162
|
}
|
|
157
163
|
/**
|
|
@@ -173,6 +179,20 @@ var PropLineClient = class {
|
|
|
173
179
|
{ bookmaker, legs }
|
|
174
180
|
);
|
|
175
181
|
}
|
|
182
|
+
/**
|
|
183
|
+
* Register a free PropLine key for an email (POST /v1/auth/register).
|
|
184
|
+
* The key is EMAILED to that address and never returned — the response
|
|
185
|
+
* is only a status message. `forward` carries the end user's IP plus the
|
|
186
|
+
* shared secret on the hosted server, so the API's per-IP signup throttle
|
|
187
|
+
* counts that user rather than the MCP machine.
|
|
188
|
+
*/
|
|
189
|
+
registerFreeKey(email, source, forward) {
|
|
190
|
+
const headers = forward ? {
|
|
191
|
+
"X-PropLine-Forward-Secret": forward.secret,
|
|
192
|
+
"X-PropLine-Client-IP": forward.clientIp
|
|
193
|
+
} : {};
|
|
194
|
+
return this.postRequest("/v1/auth/register", { email, source }, headers);
|
|
195
|
+
}
|
|
176
196
|
// ----- Bulk exports -----
|
|
177
197
|
/**
|
|
178
198
|
* Full line-movement tick history as CSV text (Backfill pass / Enterprise
|
|
@@ -301,14 +321,19 @@ var PropLineClient = class {
|
|
|
301
321
|
};
|
|
302
322
|
|
|
303
323
|
// src/server.ts
|
|
304
|
-
var VERSION = "0.
|
|
324
|
+
var VERSION = "0.39.0";
|
|
305
325
|
var DEMO_KEY = "be2b8487fcfacb1fbc292a8aa925a84c";
|
|
306
326
|
var apiKey = process.env.PROPLINE_API_KEY;
|
|
307
327
|
var baseUrl = process.env.PROPLINE_BASE_URL;
|
|
308
328
|
var usingDemoKey = !apiKey;
|
|
309
329
|
var requestClient = new AsyncLocalStorage();
|
|
310
|
-
function withClient(c, demo, fn) {
|
|
311
|
-
return requestClient.run({ client: c, demo }, fn);
|
|
330
|
+
function withClient(c, demo, fn, meta = {}) {
|
|
331
|
+
return requestClient.run({ client: c, demo, clientIp: meta.clientIp }, fn);
|
|
332
|
+
}
|
|
333
|
+
var forwardSecret = process.env.PROPLINE_MCP_FORWARD_SECRET;
|
|
334
|
+
function callerIsDemo() {
|
|
335
|
+
const scoped = requestClient.getStore();
|
|
336
|
+
return scoped ? scoped.demo : usingDemoKey;
|
|
312
337
|
}
|
|
313
338
|
var _client = null;
|
|
314
339
|
function client() {
|
|
@@ -321,10 +346,9 @@ function client() {
|
|
|
321
346
|
}
|
|
322
347
|
function demoKeyNote() {
|
|
323
348
|
const scoped = requestClient.getStore();
|
|
324
|
-
|
|
325
|
-
if (!demo) return null;
|
|
349
|
+
if (!callerIsDemo()) return null;
|
|
326
350
|
const ref = scoped ? "mcp-hosted" : "mcp";
|
|
327
|
-
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}
|
|
351
|
+
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).`;
|
|
328
352
|
}
|
|
329
353
|
function filterByMinEv(res, minEvPct) {
|
|
330
354
|
if (typeof minEvPct !== "number" || Number.isNaN(minEvPct)) return res;
|
|
@@ -509,7 +533,7 @@ var tools = [
|
|
|
509
533
|
{
|
|
510
534
|
name: "propline_get_odds_closing",
|
|
511
535
|
title: "Get opening & closing lines",
|
|
512
|
-
description: "Hobby+ endpoint. Returns the OPENING and CLOSING line per (book, market, outcome) for an event. Closing = the last snapshot at or before commence_time (price/point/closing_at); opening = the first snapshot
|
|
536
|
+
description: "Hobby+ endpoint. Returns the OPENING and CLOSING line per (book, market, outcome) for an event. Closing = the last snapshot at or before commence_time (price/point/closing_at); opening = the first snapshot PropLine holds for the outcome, however far before kickoff the book posted it (opening_price/opening_point/opening_at); pass opening_window to limit that lookback. Canonical CLV-tracking helper; one call returns both data points your bet should be measured against, instead of fetching full history and post-processing. Compare the POINTS as well as the prices \u2014 on spreads and totals the number moves as much as the price, so a price-only comparison mis-measures those markets. opening_age_seconds says how long before kickoff the opener was recorded: the archive starts 2026-04, so a small value means PropLine started polling late and this is not the book's true open. Free tier returns redacted structure with upgrade pointer.",
|
|
513
537
|
inputSchema: {
|
|
514
538
|
type: "object",
|
|
515
539
|
properties: {
|
|
@@ -523,6 +547,10 @@ var tools = [
|
|
|
523
547
|
period: {
|
|
524
548
|
type: "string",
|
|
525
549
|
description: "Game-period filter. Omitted = full-game markets only. Canonical codes (q1..q4, h1/h2, p1..p3, i1..i9, f3/f5/f7), comma-separated, or 'all'."
|
|
550
|
+
},
|
|
551
|
+
opening_window: {
|
|
552
|
+
type: ["string", "number"],
|
|
553
|
+
description: "Limit the opening lookback to this many days before kickoff (1-3650), or 'all' (default: the first snapshot held). 14 matches the resolved-props export's opening columns."
|
|
526
554
|
}
|
|
527
555
|
},
|
|
528
556
|
required: ["sport_key", "event_id"],
|
|
@@ -534,7 +562,8 @@ var tools = [
|
|
|
534
562
|
{
|
|
535
563
|
markets: args.markets,
|
|
536
564
|
bookmakers: args.bookmakers,
|
|
537
|
-
period: args.period
|
|
565
|
+
period: args.period,
|
|
566
|
+
openingWindow: args.opening_window === void 0 ? void 0 : String(args.opening_window)
|
|
538
567
|
}
|
|
539
568
|
)
|
|
540
569
|
},
|
|
@@ -764,7 +793,7 @@ var tools = [
|
|
|
764
793
|
{
|
|
765
794
|
name: "propline_get_nhl_daily_goals_total",
|
|
766
795
|
title: "Get NHL daily goals total",
|
|
767
|
-
description: "Free-tier endpoint. Returns the synthetic daily NHL goals total (hockey's equivalent of the MLB Grand Salami) for a given
|
|
796
|
+
description: "Free-tier endpoint. Returns the synthetic daily NHL goals total (hockey's equivalent of the MLB Grand Salami) for a given US Eastern date \u2014 total goals scored across every NHL game on the slate (including OT/SO) plus each book's implied Daily Goals Total line (median of per-game primary totals across our NHL books). No retail sportsbook quotes this as a single market. Useful for: 'what's the total goal line for tonight's full NHL slate', 'did the Daily Goals Total go over yesterday', 'historical NHL daily-goals results for backtesting'.",
|
|
768
797
|
inputSchema: {
|
|
769
798
|
type: "object",
|
|
770
799
|
properties: {
|
|
@@ -1158,11 +1187,57 @@ var tools = [
|
|
|
1158
1187
|
sinceSeq: args.since_seq,
|
|
1159
1188
|
limit: args.limit
|
|
1160
1189
|
})
|
|
1190
|
+
},
|
|
1191
|
+
{
|
|
1192
|
+
name: "propline_create_free_api_key",
|
|
1193
|
+
title: "Create a free PropLine API key",
|
|
1194
|
+
// Not read-only: it creates an account and sends an email.
|
|
1195
|
+
writes: true,
|
|
1196
|
+
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).",
|
|
1197
|
+
inputSchema: {
|
|
1198
|
+
type: "object",
|
|
1199
|
+
properties: {
|
|
1200
|
+
email: {
|
|
1201
|
+
type: "string",
|
|
1202
|
+
description: "The user's own email address, as they gave it."
|
|
1203
|
+
}
|
|
1204
|
+
},
|
|
1205
|
+
required: ["email"],
|
|
1206
|
+
additionalProperties: false
|
|
1207
|
+
},
|
|
1208
|
+
handler: async (args) => {
|
|
1209
|
+
const email = String(args.email ?? "").trim();
|
|
1210
|
+
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
|
1211
|
+
throw new Error("email must be a valid email address the user gave you");
|
|
1212
|
+
}
|
|
1213
|
+
if (!callerIsDemo()) {
|
|
1214
|
+
return {
|
|
1215
|
+
status: "already_keyed",
|
|
1216
|
+
message: "This connection already uses a personal PropLine key, so no new key was created. Manage it at https://prop-line.com/dashboard."
|
|
1217
|
+
};
|
|
1218
|
+
}
|
|
1219
|
+
const scoped = requestClient.getStore();
|
|
1220
|
+
const hosted = Boolean(scoped);
|
|
1221
|
+
const forward = hosted && forwardSecret && scoped?.clientIp ? { clientIp: scoped.clientIp, secret: forwardSecret } : void 0;
|
|
1222
|
+
const res = await client().registerFreeKey(
|
|
1223
|
+
email,
|
|
1224
|
+
hosted ? "mcp-hosted" : "mcp",
|
|
1225
|
+
forward
|
|
1226
|
+
);
|
|
1227
|
+
return {
|
|
1228
|
+
status: "sent",
|
|
1229
|
+
email,
|
|
1230
|
+
tier: res.tier,
|
|
1231
|
+
daily_limit: res.daily_limit,
|
|
1232
|
+
message: res.message,
|
|
1233
|
+
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."
|
|
1234
|
+
};
|
|
1235
|
+
}
|
|
1161
1236
|
}
|
|
1162
1237
|
];
|
|
1163
|
-
function withDemoNote(text) {
|
|
1238
|
+
function withDemoNote(text, toolName) {
|
|
1164
1239
|
const blocks = [{ type: "text", text }];
|
|
1165
|
-
const note = demoKeyNote();
|
|
1240
|
+
const note = toolName === "propline_create_free_api_key" ? null : demoKeyNote();
|
|
1166
1241
|
if (note) blocks.push({ type: "text", text: note });
|
|
1167
1242
|
return blocks;
|
|
1168
1243
|
}
|
|
@@ -1177,14 +1252,14 @@ function createServer() {
|
|
|
1177
1252
|
title: t.title,
|
|
1178
1253
|
description: t.description,
|
|
1179
1254
|
inputSchema: t.inputSchema,
|
|
1180
|
-
// Every PropLine tool is a READ of the odds API
|
|
1181
|
-
//
|
|
1255
|
+
// Every PropLine tool is a READ of the odds API except the one marked
|
|
1256
|
+
// `writes` (propline_create_free_api_key creates an account + email). Directories (Claude connectors, Cursor)
|
|
1182
1257
|
// require these hints; clients use them to skip confirmation prompts.
|
|
1183
1258
|
annotations: {
|
|
1184
1259
|
title: t.title,
|
|
1185
|
-
readOnlyHint:
|
|
1260
|
+
readOnlyHint: !t.writes,
|
|
1186
1261
|
destructiveHint: false,
|
|
1187
|
-
idempotentHint:
|
|
1262
|
+
idempotentHint: !t.writes,
|
|
1188
1263
|
openWorldHint: true
|
|
1189
1264
|
}
|
|
1190
1265
|
}))
|
|
@@ -1201,13 +1276,13 @@ function createServer() {
|
|
|
1201
1276
|
const data = await tool.handler(req.params.arguments ?? {});
|
|
1202
1277
|
const text = typeof data === "string" ? data : JSON.stringify(data, null, 2);
|
|
1203
1278
|
return {
|
|
1204
|
-
content: withDemoNote(text)
|
|
1279
|
+
content: withDemoNote(text, tool.name)
|
|
1205
1280
|
};
|
|
1206
1281
|
} catch (err) {
|
|
1207
1282
|
const msg = err instanceof PropLineHTTPError ? `PropLine API error ${err.statusCode}: ${err.body.slice(0, 500)}` : err instanceof Error ? err.message : String(err);
|
|
1208
1283
|
return {
|
|
1209
1284
|
isError: true,
|
|
1210
|
-
content: withDemoNote(msg)
|
|
1285
|
+
content: withDemoNote(msg, tool.name)
|
|
1211
1286
|
};
|
|
1212
1287
|
}
|
|
1213
1288
|
});
|
|
@@ -1231,6 +1306,11 @@ function extractApiKey(req) {
|
|
|
1231
1306
|
if (q && q.trim()) return { key: q.trim(), demo: false };
|
|
1232
1307
|
return { key: DEMO_KEY, demo: true };
|
|
1233
1308
|
}
|
|
1309
|
+
function endUserIp(req) {
|
|
1310
|
+
const fly = req.headers["fly-client-ip"];
|
|
1311
|
+
if (typeof fly === "string" && fly.trim()) return fly.trim();
|
|
1312
|
+
return req.socket.remoteAddress ?? void 0;
|
|
1313
|
+
}
|
|
1234
1314
|
var CORS_HEADERS = {
|
|
1235
1315
|
"Access-Control-Allow-Origin": "*",
|
|
1236
1316
|
"Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
|
|
@@ -1256,7 +1336,9 @@ async function handleMcp(req, res) {
|
|
|
1256
1336
|
});
|
|
1257
1337
|
if (demo) res.setHeader("X-PropLine-Demo-Key", "1");
|
|
1258
1338
|
await server.connect(transport);
|
|
1259
|
-
await withClient(client2, demo, () => transport.handleRequest(req, res)
|
|
1339
|
+
await withClient(client2, demo, () => transport.handleRequest(req, res), {
|
|
1340
|
+
clientIp: endUserIp(req)
|
|
1341
|
+
});
|
|
1260
1342
|
}
|
|
1261
1343
|
var manifest = () => ({
|
|
1262
1344
|
name: "propline-mcp",
|
|
@@ -1312,6 +1394,7 @@ httpServer.listen(PORT, () => {
|
|
|
1312
1394
|
);
|
|
1313
1395
|
});
|
|
1314
1396
|
export {
|
|
1397
|
+
endUserIp,
|
|
1315
1398
|
extractApiKey
|
|
1316
1399
|
};
|
|
1317
1400
|
//# sourceMappingURL=http.js.map
|