canli-validation-mcp 0.7.0 → 0.7.1
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 +31 -1
- package/package.json +1 -1
- package/src/server.mjs +32 -9
package/README.md
CHANGED
|
@@ -5,6 +5,34 @@
|
|
|
5
5
|
[](https://www.bestpractices.dev/projects/14954)
|
|
6
6
|
[](https://glama.ai/mcp/servers/arhancanli/canli-validation-mcp)
|
|
7
7
|
|
|
8
|
+
**Is your best backtest real, or just the luckiest of the variants you tried?** This MCP server
|
|
9
|
+
lets Claude, Cursor or any MCP client answer that with the standard corrections: the deflated
|
|
10
|
+
Sharpe ratio, the CSCV probability of backtest overfitting, the minimum track record length, the
|
|
11
|
+
haircut Sharpe ratio, and luck-equivalent trials. Free and MIT-licensed.
|
|
12
|
+
|
|
13
|
+
## Quick start
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# Claude Code, computed on your machine: no key, nothing sent anywhere
|
|
17
|
+
claude mcp add canli-local --env CANLI_LOCAL=1 -- npx -y canli-validation-mcp
|
|
18
|
+
|
|
19
|
+
# the same tools with signed, stored receipts from the free API (a free key is issued on first use)
|
|
20
|
+
claude mcp add canli -- npx -y canli-validation-mcp
|
|
21
|
+
|
|
22
|
+
# nothing to install: the hosted endpoint
|
|
23
|
+
claude mcp add --transport http canli https://canlicapital.com/mcp
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Then ask, for example: *"I tried 229 variants and kept the best: an annualised Sharpe of
|
|
27
|
+
1.5 over 730 daily returns (365 a year), skew -0.5, kurtosis 5, and the variants'
|
|
28
|
+
Sharpe ratios spread by 0.57. Is it real?"* The assistant calls `validate_deflated_sharpe`: the
|
|
29
|
+
best of 229 skill-less variants would reach 1.60 by luck alone, so the probability that the
|
|
30
|
+
Sharpe is above zero falls from 98.1% to 44.4% once the search is counted. Claude Desktop and any
|
|
31
|
+
other stdio client (Cursor, VS Code) run the same `npx` command; see "Claude Desktop" and
|
|
32
|
+
"Generic stdio client" below.
|
|
33
|
+
|
|
34
|
+
## What it does
|
|
35
|
+
|
|
8
36
|
An MCP (Model Context Protocol) server over canlicapital.com's free, keyed validation API. It
|
|
9
37
|
gives a coding agent fourteen tools: issue a free key, run the eight validators (deflated Sharpe,
|
|
10
38
|
CSCV overfitting, paper-evidence conformance, breadth ceiling, minimum track record length,
|
|
@@ -165,7 +193,9 @@ claude mcp add --transport http canli https://canlicapital.com/mcp
|
|
|
165
193
|
```
|
|
166
194
|
|
|
167
195
|
Without a key, requests run under a shared anonymous key, so the daily validation quota is shared
|
|
168
|
-
by every hosted caller.
|
|
196
|
+
by every hosted caller. When that shared quota is used up for the day, validations are still
|
|
197
|
+
answered, computed by the same code on the hosted endpoint, but without a stored receipt; the
|
|
198
|
+
result says so. For your own quota, issue a free key (see
|
|
169
199
|
[/developers](https://canlicapital.com/developers#quickstart)) and send it as a header:
|
|
170
200
|
|
|
171
201
|
```bash
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "canli-validation-mcp",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "MCP server for canlicapital.com's free validation API: deflated Sharpe, CSCV overfitting, paper-evidence conformance, and breadth ceiling, each returned as the full API envelope so the boundary language cannot be dropped.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
package/src/server.mjs
CHANGED
|
@@ -132,6 +132,29 @@ async function callApi(session, { path, method = "GET", body }) {
|
|
|
132
132
|
return { envelope, failed: res.status >= 400 || Boolean(envelope?.error) };
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
+
// The hosted endpoint's shared anonymous key has one daily quota for every caller who connects
|
|
136
|
+
// without a key of their own. When it is used up, the answer is still computed, by the same code
|
|
137
|
+
// the API runs (src/local, byte for byte), on the hosted endpoint; what the caller loses is the
|
|
138
|
+
// stored, signed receipt, and the note says how to get one. A caller's own key, or a missing
|
|
139
|
+
// shared key, still gets the API's refusal unchanged.
|
|
140
|
+
export const SHARED_QUOTA_NOTE = "The shared anonymous quota of this hosted endpoint is used up for today (it resets at 00:00 UTC), so this result was computed by the same code on the hosted endpoint and no receipt was stored. For a receipt and a quota of your own, get a free key at https://canlicapital.com/developers#quickstart and send it as 'Authorization: Bearer <key>'. To run with no quota at all, on your own machine: npx -y canli-validation-mcp with CANLI_LOCAL=1.";
|
|
141
|
+
|
|
142
|
+
async function validateRemote(session, tool, path, body) {
|
|
143
|
+
let response = await callApi(session, { path, method: "POST", body });
|
|
144
|
+
// stdio without CANLI_KEY: the first validation used to come back 401 and the model had to work
|
|
145
|
+
// out that get_key comes first. Issue the free key once, as get_key would, and retry once.
|
|
146
|
+
if (response.failed && response.envelope?.error?.code === "unauthorized" && !session.hosted && !session.key && !session.envKey) {
|
|
147
|
+
const issued = await callApi(session, { path: "/api/v1/keys", method: "POST", body: { label: "auto" } });
|
|
148
|
+
if (!issued.failed && issued.envelope?.data?.key) {
|
|
149
|
+
session.key = issued.envelope.data.key;
|
|
150
|
+
response = await callApi(session, { path, method: "POST", body });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (!response.failed || session.hosted?.keySource !== "shared" || response.envelope?.error?.code !== "quota_exhausted") return response;
|
|
154
|
+
const fallback = computeLocally(tool, body);
|
|
155
|
+
return { ...fallback, envelope: { ...fallback.envelope, computed: "hosted_without_receipt", note: SHARED_QUOTA_NOTE } };
|
|
156
|
+
}
|
|
157
|
+
|
|
135
158
|
// Compact context (0.3.0): minified JSON. Indentation is whitespace an agent pays for in tokens and
|
|
136
159
|
// never reads; every field, boundary sentence and provenance value is kept. Measured on the live
|
|
137
160
|
// Apple StockholdersEquity record (README, "Compact context"): 2,214 -> 1,560 tokens minified,
|
|
@@ -230,56 +253,56 @@ export async function toolValidateDeflatedSharpe(session, args) {
|
|
|
230
253
|
);
|
|
231
254
|
}
|
|
232
255
|
if (session.local) { const local = computeLocally("validate_deflated_sharpe", parsed.data); return validationText(session, local); }
|
|
233
|
-
const response = await
|
|
256
|
+
const response = await validateRemote(session, "validate_deflated_sharpe", "/api/v1/validate/deflated-sharpe", parsed.data);
|
|
234
257
|
return validationText(session, response);
|
|
235
258
|
}
|
|
236
259
|
|
|
237
260
|
export async function toolValidateOverfitting(session, args) {
|
|
238
261
|
const body = parseOrThrow(overfittingInput, args, "validate_overfitting");
|
|
239
262
|
if (session.local) { const local = computeLocally("validate_overfitting", body); return validationText(session, local); }
|
|
240
|
-
const response = await
|
|
263
|
+
const response = await validateRemote(session, "validate_overfitting", "/api/v1/validate/overfitting", body);
|
|
241
264
|
return validationText(session, response);
|
|
242
265
|
}
|
|
243
266
|
|
|
244
267
|
export async function toolValidatePaperEvidence(session, args) {
|
|
245
268
|
const body = parseOrThrow(paperEvidenceInput, args, "validate_paper_evidence");
|
|
246
269
|
if (session.local) { const local = computeLocally("validate_paper_evidence", body); return validationText(session, local); }
|
|
247
|
-
const response = await
|
|
270
|
+
const response = await validateRemote(session, "validate_paper_evidence", "/api/v1/validate/paper-evidence", body);
|
|
248
271
|
return validationText(session, response);
|
|
249
272
|
}
|
|
250
273
|
|
|
251
274
|
export async function toolValidateBreadth(session, args) {
|
|
252
275
|
const body = parseOrThrow(breadthInput, args, "validate_breadth");
|
|
253
276
|
if (session.local) { const local = computeLocally("validate_breadth", body); return validationText(session, local); }
|
|
254
|
-
const response = await
|
|
277
|
+
const response = await validateRemote(session, "validate_breadth", "/api/v1/validate/breadth", body);
|
|
255
278
|
return validationText(session, response);
|
|
256
279
|
}
|
|
257
280
|
|
|
258
281
|
export async function toolValidateTrackRecord(session, args) {
|
|
259
282
|
const body = parseOrThrow(trackRecordInput, args, "validate_track_record");
|
|
260
283
|
if (session.local) { const local = computeLocally("validate_track_record", body); return validationText(session, local); }
|
|
261
|
-
const response = await
|
|
284
|
+
const response = await validateRemote(session, "validate_track_record", "/api/v1/validate/track-record", body);
|
|
262
285
|
return validationText(session, response);
|
|
263
286
|
}
|
|
264
287
|
|
|
265
288
|
export async function toolValidateBacktestLength(session, args) {
|
|
266
289
|
const body = parseOrThrow(backtestLengthInput, args, "validate_backtest_length");
|
|
267
290
|
if (session.local) { const local = computeLocally("validate_backtest_length", body); return validationText(session, local); }
|
|
268
|
-
const response = await
|
|
291
|
+
const response = await validateRemote(session, "validate_backtest_length", "/api/v1/validate/backtest-length", body);
|
|
269
292
|
return validationText(session, response);
|
|
270
293
|
}
|
|
271
294
|
|
|
272
295
|
export async function toolValidateHaircutSharpe(session, args) {
|
|
273
296
|
const body = parseOrThrow(haircutSharpeInput, args, "validate_haircut_sharpe");
|
|
274
297
|
if (session.local) { const local = computeLocally("validate_haircut_sharpe", body); return validationText(session, local); }
|
|
275
|
-
const response = await
|
|
298
|
+
const response = await validateRemote(session, "validate_haircut_sharpe", "/api/v1/validate/haircut-sharpe", body);
|
|
276
299
|
return validationText(session, response);
|
|
277
300
|
}
|
|
278
301
|
|
|
279
302
|
export async function toolValidateLuckTrials(session, args) {
|
|
280
303
|
const body = parseOrThrow(luckTrialsInput, args, "validate_luck_trials");
|
|
281
304
|
if (session.local) { const local = computeLocally("validate_luck_trials", body); return validationText(session, local); }
|
|
282
|
-
const response = await
|
|
305
|
+
const response = await validateRemote(session, "validate_luck_trials", "/api/v1/validate/luck-trials", body);
|
|
283
306
|
return validationText(session, response);
|
|
284
307
|
}
|
|
285
308
|
|
|
@@ -287,7 +310,7 @@ export async function toolValidateLuckTrials(session, args) {
|
|
|
287
310
|
// through the API (one validation of quota, one receipt).
|
|
288
311
|
async function runValidator(session, tool, path, body) {
|
|
289
312
|
if (session.local) return computeLocally(tool, body);
|
|
290
|
-
return
|
|
313
|
+
return validateRemote(session, tool, path, body);
|
|
291
314
|
}
|
|
292
315
|
|
|
293
316
|
const sameJson = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|