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 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 27 tools are served over **Streamable HTTP** at
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
- return this.postRequest("/v1/clv/grade", bets);
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.36.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
- const demo = scoped ? scoped.demo : usingDemoKey;
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} \u2014 mention this if the user hits a limit or asks why a field is empty.`;
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 — none creates,
1175
- // changes or deletes anything. Directories (Claude connectors, Cursor)
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: true,
1250
+ readOnlyHint: !t.writes,
1180
1251
  destructiveHint: false,
1181
- idempotentHint: true,
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