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 CHANGED
@@ -5,6 +5,34 @@
5
5
  [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/14954/badge)](https://www.bestpractices.dev/projects/14954)
6
6
  [![Glama score](https://glama.ai/mcp/servers/arhancanli/canli-validation-mcp/badges/score.svg)](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. For your own quota, issue a free key (see
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.0",
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 callApi(session, { path: "/api/v1/validate/deflated-sharpe", method: "POST", body: parsed.data });
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 callApi(session, { path: "/api/v1/validate/overfitting", method: "POST", body });
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 callApi(session, { path: "/api/v1/validate/paper-evidence", method: "POST", body });
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 callApi(session, { path: "/api/v1/validate/breadth", method: "POST", body });
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 callApi(session, { path: "/api/v1/validate/track-record", method: "POST", body });
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 callApi(session, { path: "/api/v1/validate/backtest-length", method: "POST", body });
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 callApi(session, { path: "/api/v1/validate/haircut-sharpe", method: "POST", body });
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 callApi(session, { path: "/api/v1/validate/luck-trials", method: "POST", body });
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 callApi(session, { path, method: "POST", body });
313
+ return validateRemote(session, tool, path, body);
291
314
  }
292
315
 
293
316
  const sameJson = (a, b) => JSON.stringify(a) === JSON.stringify(b);