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 +3 -2
- package/dist/http.js +90 -17
- package/dist/http.js.map +1 -1
- package/dist/index.js +79 -14
- 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
|
|
@@ -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.
|
|
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
|
-
|
|
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}
|
|
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
|
|
1181
|
-
//
|
|
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:
|
|
1250
|
+
readOnlyHint: !t.writes,
|
|
1186
1251
|
destructiveHint: false,
|
|
1187
|
-
idempotentHint:
|
|
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
|