canli-validation-mcp 0.6.0 → 0.7.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.
Files changed (3) hide show
  1. package/README.md +34 -0
  2. package/package.json +1 -1
  3. package/src/server.mjs +40 -15
package/README.md CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  [![npm](https://img.shields.io/npm/v/canli-validation-mcp)](https://www.npmjs.com/package/canli-validation-mcp)
4
4
  [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/arhancanli/canli-validation-mcp/badge)](https://scorecard.dev/viewer/?uri=github.com/arhancanli/canli-validation-mcp)
5
+ [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/14954/badge)](https://www.bestpractices.dev/projects/14954)
5
6
  [![Glama score](https://glama.ai/mcp/servers/arhancanli/canli-validation-mcp/badges/score.svg)](https://glama.ai/mcp/servers/arhancanli/canli-validation-mcp)
6
7
 
7
8
  An MCP (Model Context Protocol) server over canlicapital.com's free, keyed validation API. It
@@ -133,6 +134,7 @@ an array of objects.
133
134
  | `CANLI_API_BASE` | `https://canlicapital.com` | Where the API lives. Point it at a preview deployment for testing. |
134
135
  | `CANLI_KEY` | unset | A key already issued from `POST /api/v1/keys`. When set, `get_key` sends no request and reports the key is already configured; every other tool sends it as `Authorization: Bearer <key>`. |
135
136
  | `CANLI_FULL_ENVELOPE` | unset | `1` or `true` returns each validation's full API envelope instead of the compact result (below). |
137
+ | `CANLI_TOOLSETS` | all | Which tools to list: a comma-separated choice of `validate`, `receipts`, `company` and `status`, or `all`. An unknown name is refused at startup. See "Toolsets" below. |
136
138
  | `CANLI_LOCAL` | unset | `1` or `true` runs the eight validators on this machine (private local mode, below): no key, no network, no receipt. |
137
139
 
138
140
  If `CANLI_KEY` is not set and local mode is off, call `get_key` once per session before the validators. The key it
@@ -170,6 +172,9 @@ by every hosted caller. For your own quota, issue a free key (see
170
172
  claude mcp add --transport http canli https://canlicapital.com/mcp --header "Authorization: Bearer $CANLI_KEY"
171
173
  ```
172
174
 
175
+ Add `?toolsets=` to the URL to list only some tools (see "Toolsets" below), for example
176
+ `https://canlicapital.com/mcp?toolsets=company`.
177
+
173
178
  The endpoint is stateless. On it, `get_key` issues nothing and says which key is in use, because a
174
179
  key issued there would not reach the next request. A malformed Authorization header is refused
175
180
  rather than replaced with the shared key.
@@ -279,6 +284,35 @@ describes the service rather than the answer, and an agent pays for every token
279
284
  call. It stays in the stored receipt, which `get_receipt` returns in full, and in `service_status`.
280
285
  On a breadth result this is about half the text. Set `CANLI_FULL_ENVELOPE=1` to receive every field.
281
286
 
287
+ ## Toolsets (tokens)
288
+
289
+ A client sends the model the whole tool list on every turn, and it is most of each turn's prompt:
290
+ a validation result is a few hundred tokens, the list of all fourteen tools several thousand. A
291
+ client that needs one kind of tool can list only that kind, with `CANLI_TOOLSETS` (stdio) or
292
+ `?toolsets=` (hosted endpoint). The default is every tool.
293
+
294
+ | toolset | tools |
295
+ |---|---|
296
+ | `validate` | `get_key`, the eight validators, `audit_backtest` |
297
+ | `receipts` | `get_receipt`, `verify_receipt` |
298
+ | `company` | `company_financial_history` |
299
+ | `status` | `service_status` |
300
+
301
+ Measured with `bench/tool_list_tokens.py` (tokenizer: tiktoken `o200k_base`; other models'
302
+ tokenizers give different absolute counts), in the shape an OpenAI-style client sends the list:
303
+
304
+ | CANLI_TOOLSETS | tools | tokens per turn | of all |
305
+ |---|---|---|---|
306
+ | `all` | 14 | 3,888 | 100% |
307
+ | `validate` | 10 | 3,154 | 81% |
308
+ | `receipts` | 2 | 340 | 9% |
309
+ | `company` | 1 | 302 | 8% |
310
+ | `status` | 1 | 98 | 3% |
311
+
312
+ Providers cache a tool list that is identical from turn to turn and bill the cached part at a
313
+ fraction of the price (`test/tool-list-stable.test.mjs` keeps each list byte-stable); a smaller list
314
+ costs less either way.
315
+
282
316
  ## Local checkout
283
317
 
284
318
  Only needed to develop or test this package itself, not to run the published one.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "canli-validation-mcp",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
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
@@ -66,7 +66,29 @@ export function configuredLocal(value) {
66
66
  return v === "1" || v?.toLowerCase() === "true";
67
67
  }
68
68
 
69
- export function createSession({ base, fetchImpl, envKey, timeoutMs = REQUEST_TIMEOUT_MS, hosted, local, fullEnvelope, receiptKeys } = {}) {
69
+ // Toolsets: which tools the server lists. The tool list is re-sent to the model on every turn and
70
+ // is most of each turn's prompt (README, "Toolsets"; bench/tool_list_tokens.py measures it), so a
71
+ // client that needs one kind of tool can load only that kind. Default: all.
72
+ export const TOOLSETS = Object.freeze({
73
+ validate: Object.freeze(["get_key", "validate_deflated_sharpe", "validate_overfitting", "validate_paper_evidence", "validate_breadth", "validate_track_record", "validate_backtest_length", "validate_haircut_sharpe", "validate_luck_trials", "audit_backtest"]),
74
+ receipts: Object.freeze(["get_receipt", "verify_receipt"]),
75
+ company: Object.freeze(["company_financial_history"]),
76
+ status: Object.freeze(["service_status"]),
77
+ });
78
+
79
+ // CANLI_TOOLSETS=validate,company (or ?toolsets= on the hosted endpoint): a comma-separated list of
80
+ // TOOLSETS names, or "all". Empty or unsubstituted means all; an unknown name is refused, so a typo
81
+ // never silently leaves a client without the tools it asked for.
82
+ export function configuredToolsets(value) {
83
+ const v = configuredKey(value);
84
+ if (!v || v.trim().toLowerCase() === "all") return Object.keys(TOOLSETS);
85
+ const names = [...new Set(v.split(",").map((s) => s.trim().toLowerCase()).filter(Boolean))];
86
+ const unknown = names.filter((n) => !Object.hasOwn(TOOLSETS, n));
87
+ if (unknown.length || !names.length) throw new Error(`Unknown toolset ${unknown.join(", ") || "(none)"}; choose from ${Object.keys(TOOLSETS).join(", ")} or all`);
88
+ return names;
89
+ }
90
+
91
+ export function createSession({ base, fetchImpl, envKey, timeoutMs = REQUEST_TIMEOUT_MS, hosted, local, fullEnvelope, receiptKeys, toolsets } = {}) {
70
92
  if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0) throw new Error("Request timeout must be a positive integer");
71
93
  return {
72
94
  base: base ?? process.env.CANLI_API_BASE ?? DEFAULT_BASE,
@@ -81,6 +103,7 @@ export function createSession({ base, fetchImpl, envKey, timeoutMs = REQUEST_TIM
81
103
  fullEnvelope: fullEnvelope ?? configuredFullEnvelope(process.env.CANLI_FULL_ENVELOPE),
82
104
  // Tests pass their own keys; everyone else verifies against the bundled published keys.
83
105
  receiptKeys: receiptKeys ?? undefined,
106
+ toolsets: toolsets ?? configuredToolsets(process.env.CANLI_TOOLSETS),
84
107
  };
85
108
  }
86
109
 
@@ -476,72 +499,74 @@ const READ_ONLY = { readOnlyHint: true, destructiveHint: false, openWorldHint: t
476
499
  const WRITES_RECEIPT = { readOnlyHint: false, destructiveHint: false, openWorldHint: true };
477
500
 
478
501
  export function registerTools(server, session) {
479
- server.registerTool(
502
+ const enabled = new Set((session.toolsets ?? Object.keys(TOOLSETS)).flatMap((name) => TOOLSETS[name]));
503
+ const register = (name, ...rest) => { if (enabled.has(name)) server.registerTool(name, ...rest); };
504
+ register(
480
505
  "get_key",
481
506
  { title: "Get a free validation key", annotations: { title: "Get a free validation key", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }, description: TOOL_DESCRIPTIONS.get_key, inputSchema: getKeyInput },
482
507
  (args) => toolGetKey(session, args),
483
508
  );
484
- server.registerTool(
509
+ register(
485
510
  "validate_deflated_sharpe",
486
511
  { title: "Validate deflated Sharpe", annotations: { title: "Validate deflated Sharpe", ...WRITES_RECEIPT }, description: TOOL_DESCRIPTIONS.validate_deflated_sharpe, inputSchema: deflatedSharpeToolShape },
487
512
  (args) => toolValidateDeflatedSharpe(session, args),
488
513
  );
489
- server.registerTool(
514
+ register(
490
515
  "validate_overfitting",
491
516
  { title: "Validate overfitting (CSCV)", annotations: { title: "Validate overfitting (CSCV)", ...WRITES_RECEIPT }, description: TOOL_DESCRIPTIONS.validate_overfitting, inputSchema: overfittingInput },
492
517
  (args) => toolValidateOverfitting(session, args),
493
518
  );
494
- server.registerTool(
519
+ register(
495
520
  "validate_paper_evidence",
496
521
  { title: "Validate paper evidence", annotations: { title: "Validate paper evidence", ...WRITES_RECEIPT }, description: TOOL_DESCRIPTIONS.validate_paper_evidence, inputSchema: paperEvidenceInput },
497
522
  (args) => toolValidatePaperEvidence(session, args),
498
523
  );
499
- server.registerTool(
524
+ register(
500
525
  "validate_breadth",
501
526
  { title: "Validate breadth ceiling", annotations: { title: "Validate breadth ceiling", ...WRITES_RECEIPT }, description: TOOL_DESCRIPTIONS.validate_breadth, inputSchema: breadthInput },
502
527
  (args) => toolValidateBreadth(session, args),
503
528
  );
504
- server.registerTool(
529
+ register(
505
530
  "validate_track_record",
506
531
  { title: "Minimum track record length", annotations: { title: "Minimum track record length", ...WRITES_RECEIPT }, description: TOOL_DESCRIPTIONS.validate_track_record, inputSchema: trackRecordInput },
507
532
  (args) => toolValidateTrackRecord(session, args),
508
533
  );
509
- server.registerTool(
534
+ register(
510
535
  "validate_backtest_length",
511
536
  { title: "Minimum backtest length", annotations: { title: "Minimum backtest length", ...WRITES_RECEIPT }, description: TOOL_DESCRIPTIONS.validate_backtest_length, inputSchema: backtestLengthInput },
512
537
  (args) => toolValidateBacktestLength(session, args),
513
538
  );
514
- server.registerTool(
539
+ register(
515
540
  "validate_haircut_sharpe",
516
541
  { title: "Haircut Sharpe ratio", annotations: { title: "Haircut Sharpe ratio", ...WRITES_RECEIPT }, description: TOOL_DESCRIPTIONS.validate_haircut_sharpe, inputSchema: haircutSharpeInput },
517
542
  (args) => toolValidateHaircutSharpe(session, args),
518
543
  );
519
- server.registerTool(
544
+ register(
520
545
  "validate_luck_trials",
521
546
  { title: "Luck-equivalent trials", annotations: { title: "Luck-equivalent trials", ...WRITES_RECEIPT }, description: TOOL_DESCRIPTIONS.validate_luck_trials, inputSchema: luckTrialsInput },
522
547
  (args) => toolValidateLuckTrials(session, args),
523
548
  );
524
- server.registerTool(
549
+ register(
525
550
  "audit_backtest",
526
551
  { title: "Audit a backtest", annotations: { title: "Audit a backtest", ...WRITES_RECEIPT }, description: TOOL_DESCRIPTIONS.audit_backtest, inputSchema: auditBacktestToolShape },
527
552
  (args) => toolAuditBacktest(session, args),
528
553
  );
529
- server.registerTool(
554
+ register(
530
555
  "get_receipt",
531
556
  { title: "Get a receipt", annotations: { title: "Get a receipt", ...READ_ONLY }, description: TOOL_DESCRIPTIONS.get_receipt, inputSchema: getReceiptInput },
532
557
  (args) => toolGetReceipt(session, args),
533
558
  );
534
- server.registerTool(
559
+ register(
535
560
  "verify_receipt",
536
561
  { title: "Verify a receipt", annotations: { title: "Verify a receipt", ...READ_ONLY }, description: TOOL_DESCRIPTIONS.verify_receipt, inputSchema: verifyReceiptToolShape },
537
562
  (args) => toolVerifyReceipt(session, args),
538
563
  );
539
- server.registerTool(
564
+ register(
540
565
  "service_status",
541
566
  { title: "Service status", annotations: { title: "Service status", ...READ_ONLY }, description: TOOL_DESCRIPTIONS.service_status, inputSchema: emptyInput },
542
567
  () => toolServiceStatus(session),
543
568
  );
544
- server.registerTool(
569
+ register(
545
570
  "company_financial_history",
546
571
  { title: "Company financial history (SEC)", annotations: { title: "Company financial history (SEC)", ...READ_ONLY }, description: TOOL_DESCRIPTIONS.company_financial_history, inputSchema: companyHistoryToolShape },
547
572
  (args) => toolCompanyFinancialHistory(session, args),