propline-mcp 0.37.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
@@ -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
@@ -173,6 +174,20 @@ var PropLineClient = class {
173
174
  { bookmaker, legs }
174
175
  );
175
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
+ }
176
191
  // ----- Bulk exports -----
177
192
  /**
178
193
  * Full line-movement tick history as CSV text (Backfill pass / Enterprise
@@ -301,14 +316,19 @@ var PropLineClient = class {
301
316
  };
302
317
 
303
318
  // src/server.ts
304
- var VERSION = "0.37.0";
319
+ var VERSION = "0.38.0";
305
320
  var DEMO_KEY = "be2b8487fcfacb1fbc292a8aa925a84c";
306
321
  var apiKey = process.env.PROPLINE_API_KEY;
307
322
  var baseUrl = process.env.PROPLINE_BASE_URL;
308
323
  var usingDemoKey = !apiKey;
309
324
  var requestClient = new AsyncLocalStorage();
310
- function withClient(c, demo, fn) {
311
- 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;
312
332
  }
313
333
  var _client = null;
314
334
  function client() {
@@ -321,10 +341,9 @@ function client() {
321
341
  }
322
342
  function demoKeyNote() {
323
343
  const scoped = requestClient.getStore();
324
- const demo = scoped ? scoped.demo : usingDemoKey;
325
- if (!demo) return null;
344
+ if (!callerIsDemo()) return null;
326
345
  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} \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).`;
328
347
  }
329
348
  function filterByMinEv(res, minEvPct) {
330
349
  if (typeof minEvPct !== "number" || Number.isNaN(minEvPct)) return res;
@@ -1158,11 +1177,57 @@ var tools = [
1158
1177
  sinceSeq: args.since_seq,
1159
1178
  limit: args.limit
1160
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
+ }
1161
1226
  }
1162
1227
  ];
1163
- function withDemoNote(text) {
1228
+ function withDemoNote(text, toolName) {
1164
1229
  const blocks = [{ type: "text", text }];
1165
- const note = demoKeyNote();
1230
+ const note = toolName === "propline_create_free_api_key" ? null : demoKeyNote();
1166
1231
  if (note) blocks.push({ type: "text", text: note });
1167
1232
  return blocks;
1168
1233
  }
@@ -1177,14 +1242,14 @@ function createServer() {
1177
1242
  title: t.title,
1178
1243
  description: t.description,
1179
1244
  inputSchema: t.inputSchema,
1180
- // Every PropLine tool is a READ of the odds API — none creates,
1181
- // 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)
1182
1247
  // require these hints; clients use them to skip confirmation prompts.
1183
1248
  annotations: {
1184
1249
  title: t.title,
1185
- readOnlyHint: true,
1250
+ readOnlyHint: !t.writes,
1186
1251
  destructiveHint: false,
1187
- idempotentHint: true,
1252
+ idempotentHint: !t.writes,
1188
1253
  openWorldHint: true
1189
1254
  }
1190
1255
  }))
@@ -1201,13 +1266,13 @@ function createServer() {
1201
1266
  const data = await tool.handler(req.params.arguments ?? {});
1202
1267
  const text = typeof data === "string" ? data : JSON.stringify(data, null, 2);
1203
1268
  return {
1204
- content: withDemoNote(text)
1269
+ content: withDemoNote(text, tool.name)
1205
1270
  };
1206
1271
  } catch (err) {
1207
1272
  const msg = err instanceof PropLineHTTPError ? `PropLine API error ${err.statusCode}: ${err.body.slice(0, 500)}` : err instanceof Error ? err.message : String(err);
1208
1273
  return {
1209
1274
  isError: true,
1210
- content: withDemoNote(msg)
1275
+ content: withDemoNote(msg, tool.name)
1211
1276
  };
1212
1277
  }
1213
1278
  });
@@ -1231,6 +1296,11 @@ function extractApiKey(req) {
1231
1296
  if (q && q.trim()) return { key: q.trim(), demo: false };
1232
1297
  return { key: DEMO_KEY, demo: true };
1233
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
+ }
1234
1304
  var CORS_HEADERS = {
1235
1305
  "Access-Control-Allow-Origin": "*",
1236
1306
  "Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
@@ -1256,7 +1326,9 @@ async function handleMcp(req, res) {
1256
1326
  });
1257
1327
  if (demo) res.setHeader("X-PropLine-Demo-Key", "1");
1258
1328
  await server.connect(transport);
1259
- await withClient(client2, demo, () => transport.handleRequest(req, res));
1329
+ await withClient(client2, demo, () => transport.handleRequest(req, res), {
1330
+ clientIp: endUserIp(req)
1331
+ });
1260
1332
  }
1261
1333
  var manifest = () => ({
1262
1334
  name: "propline-mcp",
@@ -1312,6 +1384,7 @@ httpServer.listen(PORT, () => {
1312
1384
  );
1313
1385
  });
1314
1386
  export {
1387
+ endUserIp,
1315
1388
  extractApiKey
1316
1389
  };
1317
1390
  //# sourceMappingURL=http.js.map