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.
- package/README.md +34 -0
- package/package.json +1 -1
- package/src/server.mjs +40 -15
package/README.md
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/canli-validation-mcp)
|
|
4
4
|
[](https://scorecard.dev/viewer/?uri=github.com/arhancanli/canli-validation-mcp)
|
|
5
|
+
[](https://www.bestpractices.dev/projects/14954)
|
|
5
6
|
[](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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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),
|