canli-validation-mcp 0.3.0 → 0.4.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 +31 -8
- package/package.json +1 -1
- package/src/schemas.mjs +17 -0
- package/src/server.mjs +55 -10
package/README.md
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
# canli-validation-mcp
|
|
2
2
|
|
|
3
3
|
An MCP (Model Context Protocol) server over canlicapital.com's free, keyed validation API. It
|
|
4
|
-
gives a coding agent
|
|
5
|
-
CSCV overfitting, paper-evidence conformance, breadth ceiling
|
|
6
|
-
|
|
4
|
+
gives a coding agent nine tools: issue a free key, run the five validators (deflated Sharpe,
|
|
5
|
+
CSCV overfitting, paper-evidence conformance, breadth ceiling, minimum track record length),
|
|
6
|
+
fetch a stored receipt, read service status, and read a company's reported financial history
|
|
7
|
+
from SEC filings. Every tool returns the full API envelope as its result text, success or error,
|
|
7
8
|
so the agent cannot see a number without the sentences beside it that say what the number does
|
|
8
9
|
not establish.
|
|
9
10
|
|
|
10
11
|
This package is published to npm as [`canli-validation-mcp`](https://www.npmjs.com/package/canli-validation-mcp).
|
|
11
|
-
|
|
12
|
+
Also listed on the official MCP Registry (`io.github.arhancanli/canli-validation-mcp`) and
|
|
13
|
+
[cursor.directory](https://cursor.directory/plugins/canli-validation-mcp-1). Run it with `npx`, no install step, as shown below. A local checkout is only needed to develop or
|
|
12
14
|
test this package itself; see "Local checkout" near the bottom.
|
|
13
15
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
below are candidate changes until that version is published. Running unversioned
|
|
17
|
-
`npx` uses npm's latest release, not this checkout.
|
|
16
|
+
This README describes the version in `package.json`. Unversioned `npx` runs npm's latest
|
|
17
|
+
release; `npx -y canli-validation-mcp@<version>` pins one.
|
|
18
18
|
|
|
19
19
|
## What the API is (and is not)
|
|
20
20
|
|
|
@@ -34,6 +34,7 @@ repository for the full design.
|
|
|
34
34
|
| `validate_overfitting` | `POST /api/v1/validate/overfitting` | yes |
|
|
35
35
|
| `validate_paper_evidence` | `POST /api/v1/validate/paper-evidence` | yes |
|
|
36
36
|
| `validate_breadth` | `POST /api/v1/validate/breadth` | yes |
|
|
37
|
+
| `validate_track_record` | `POST /api/v1/validate/track-record` | yes |
|
|
37
38
|
| `get_receipt` | `GET /api/v1/receipts/{id}` | no |
|
|
38
39
|
| `service_status` | `GET /api/v1/validate/status` | no |
|
|
39
40
|
| `company_financial_history` | `GET /company-data/{cik}.json` | no |
|
|
@@ -105,6 +106,28 @@ No install step. `npx` fetches the published package on first run, so every clie
|
|
|
105
106
|
just spawns `npx -y canli-validation-mcp`. See "Local checkout" near the bottom to develop or test
|
|
106
107
|
this package itself instead of running the published one.
|
|
107
108
|
|
|
109
|
+
## Hosted endpoint (no install)
|
|
110
|
+
|
|
111
|
+
The same tools are served at `https://canlicapital.com/mcp` over MCP Streamable HTTP, for clients
|
|
112
|
+
that connect to a URL instead of spawning a process (Claude.ai connectors, ChatGPT, Cursor's remote
|
|
113
|
+
servers). Nothing to install, and no Node.js on your machine.
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
claude mcp add --transport http canli https://canlicapital.com/mcp
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Without a key, requests run under a shared anonymous key, so the daily validation quota is shared
|
|
120
|
+
by every hosted caller. For your own quota, issue a free key (see
|
|
121
|
+
[/developers](https://canlicapital.com/developers#quickstart)) and send it as a header:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
claude mcp add --transport http canli https://canlicapital.com/mcp --header "Authorization: Bearer $CANLI_KEY"
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
The endpoint is stateless. On it, `get_key` issues nothing and says which key is in use, because a
|
|
128
|
+
key issued there would not reach the next request. A malformed Authorization header is refused
|
|
129
|
+
rather than replaced with the shared key.
|
|
130
|
+
|
|
108
131
|
## Claude Desktop
|
|
109
132
|
|
|
110
133
|
Add to `claude_desktop_config.json` (Settings, Developer, Edit Config):
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "canli-validation-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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/schemas.mjs
CHANGED
|
@@ -92,6 +92,22 @@ export const breadthInput = z
|
|
|
92
92
|
})
|
|
93
93
|
.strict();
|
|
94
94
|
|
|
95
|
+
// ---------------------------------------------------------------------------------------------
|
|
96
|
+
// validate_track_record
|
|
97
|
+
// ---------------------------------------------------------------------------------------------
|
|
98
|
+
|
|
99
|
+
export const trackRecordInput = z
|
|
100
|
+
.object({
|
|
101
|
+
observed_sharpe_annualized: z.number().min(-10).max(10),
|
|
102
|
+
periods_per_year: z.number().min(1).max(10000),
|
|
103
|
+
skew: z.number().min(-20).max(20),
|
|
104
|
+
non_excess_kurtosis: z.number().min(1).max(100),
|
|
105
|
+
benchmark_sharpe_annualized: z.number().min(-10).max(10).optional(),
|
|
106
|
+
confidence: z.number().gt(0).lt(1).optional(),
|
|
107
|
+
observations: z.number().int().min(2).max(1000000).optional(),
|
|
108
|
+
})
|
|
109
|
+
.strict();
|
|
110
|
+
|
|
95
111
|
// ---------------------------------------------------------------------------------------------
|
|
96
112
|
// get_key / get_receipt
|
|
97
113
|
// ---------------------------------------------------------------------------------------------
|
|
@@ -151,6 +167,7 @@ export const TOOL_DESCRIPTIONS = Object.freeze({
|
|
|
151
167
|
validate_deflated_sharpe: `Probabilistic and deflated Sharpe from the seven contract inputs, or from a return series plus the trials and dispersion behind it, never both. ${LIMITS_SENTENCES.notAdmission}`,
|
|
152
168
|
validate_overfitting: `Probability of backtest overfitting by CSCV over the returns of every variant tried. ${LIMITS_SENTENCES.notAdmission}`,
|
|
153
169
|
validate_paper_evidence: `Conformance of a performance record against the canli.paper-evidence.v0 standard. ${LIMITS_SENTENCES.scope}`,
|
|
170
|
+
validate_track_record: `Minimum track record length for an observed Sharpe to clear a benchmark Sharpe (default 0) at a confidence level (default 0.95), and, when observations is sent, the probabilistic Sharpe of that record against the benchmark. ${LIMITS_SENTENCES.notAdmission}`,
|
|
154
171
|
validate_breadth: `Book Sharpe ceiling from per-sleeve quality and average pairwise correlation, and the sleeves a target needs. ${LIMITS_SENTENCES.scope}`,
|
|
155
172
|
get_receipt: `Fetch a stored verdict by its content-hash id (GET /api/v1/receipts/{id}), immutable and cacheable. ${LIMITS_SENTENCES.unsigned}`,
|
|
156
173
|
service_status: `Service, store and quota constants for the validation API (GET /api/v1/validate/status); no key required. ${LIMITS_SENTENCES.scope}`,
|
package/src/server.mjs
CHANGED
|
@@ -12,6 +12,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
12
12
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
13
13
|
import {
|
|
14
14
|
breadthInput,
|
|
15
|
+
trackRecordInput,
|
|
15
16
|
companyHistoryInput,
|
|
16
17
|
deflatedSharpeInput,
|
|
17
18
|
deflatedSharpeToolShape,
|
|
@@ -37,14 +38,26 @@ function parseOrThrow(schema, value, label) {
|
|
|
37
38
|
throw new Error(`${label}: ${issues}`);
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
|
|
41
|
+
// An empty CANLI_KEY, or an unsubstituted template such as "${user_config.api_key}" (what a
|
|
42
|
+
// desktop extension host passes for an optional field the user left unset), is no key at all, not
|
|
43
|
+
// a key to send.
|
|
44
|
+
export function configuredKey(value) {
|
|
45
|
+
if (typeof value !== "string") return undefined;
|
|
46
|
+
const trimmed = value.trim();
|
|
47
|
+
return trimmed === "" || /^\$\{[^}]*\}$/.test(trimmed) ? undefined : trimmed;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function createSession({ base, fetchImpl, envKey, timeoutMs = REQUEST_TIMEOUT_MS, hosted } = {}) {
|
|
41
51
|
if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0) throw new Error("Request timeout must be a positive integer");
|
|
42
52
|
return {
|
|
43
53
|
base: base ?? process.env.CANLI_API_BASE ?? DEFAULT_BASE,
|
|
44
54
|
fetchImpl: fetchImpl ?? fetch,
|
|
45
|
-
envKey: envKey ?? process.env.CANLI_KEY
|
|
55
|
+
envKey: envKey ?? configuredKey(process.env.CANLI_KEY),
|
|
46
56
|
key: undefined,
|
|
47
57
|
timeoutMs,
|
|
58
|
+
// Set by the hosted endpoint (api/mcp.js): which key a request runs under, so get_key can say
|
|
59
|
+
// so instead of issuing a key the next stateless request would never see.
|
|
60
|
+
hosted: hosted ?? undefined,
|
|
48
61
|
};
|
|
49
62
|
}
|
|
50
63
|
|
|
@@ -99,6 +112,21 @@ export function columnarObservations(observations) {
|
|
|
99
112
|
|
|
100
113
|
export async function toolGetKey(session, args) {
|
|
101
114
|
const { label } = parseOrThrow(getKeyInput, args, "get_key");
|
|
115
|
+
if (session.hosted) {
|
|
116
|
+
// The hosted endpoint is stateless: a key issued here would be gone by the next request, and
|
|
117
|
+
// every hosted caller shares the platform's egress address and so its per-client issuance quota.
|
|
118
|
+
const ownKey = "get a free key at https://canlicapital.com/developers#quickstart and send it as 'Authorization: Bearer <key>' to this endpoint";
|
|
119
|
+
const notes = {
|
|
120
|
+
caller: "This hosted session runs under the key in your Authorization header; no new key was issued.",
|
|
121
|
+
shared: `This hosted session runs under a shared anonymous key with a shared daily quota; no new key was issued. For your own quota, ${ownKey}.`,
|
|
122
|
+
none: `This hosted endpoint has no shared key configured, so validations need your own key: ${ownKey}.`,
|
|
123
|
+
};
|
|
124
|
+
return asText({
|
|
125
|
+
note: notes[session.hosted.keySource] ?? notes.none,
|
|
126
|
+
key_source: session.hosted.keySource,
|
|
127
|
+
key_present: session.hosted.keySource !== "none",
|
|
128
|
+
});
|
|
129
|
+
}
|
|
102
130
|
if (session.envKey) {
|
|
103
131
|
return asText({
|
|
104
132
|
note: "CANLI_KEY is set in the environment; no new key was issued and no request was sent. Using the configured key for this session.",
|
|
@@ -141,6 +169,12 @@ export async function toolValidateBreadth(session, args) {
|
|
|
141
169
|
return asText(response.envelope, response.failed);
|
|
142
170
|
}
|
|
143
171
|
|
|
172
|
+
export async function toolValidateTrackRecord(session, args) {
|
|
173
|
+
const body = parseOrThrow(trackRecordInput, args, "validate_track_record");
|
|
174
|
+
const response = await callApi(session, { path: "/api/v1/validate/track-record", method: "POST", body });
|
|
175
|
+
return asText(response.envelope, response.failed);
|
|
176
|
+
}
|
|
177
|
+
|
|
144
178
|
export async function toolGetReceipt(session, args) {
|
|
145
179
|
const { id } = parseOrThrow(getReceiptInput, args, "get_receipt");
|
|
146
180
|
const response = await callApi(session, { path: `/api/v1/receipts/${id}` });
|
|
@@ -223,45 +257,56 @@ export async function toolCompanyFinancialHistory(session, args) {
|
|
|
223
257
|
});
|
|
224
258
|
}
|
|
225
259
|
|
|
260
|
+
// MCP tool annotations: every tool reaches the canlicapital.com API (openWorldHint). Reads are
|
|
261
|
+
// marked read-only; a validation stores a receipt and get_key creates a key, so neither is
|
|
262
|
+
// read-only, and nothing any tool does modifies or deletes a caller's data.
|
|
263
|
+
const READ_ONLY = { readOnlyHint: true, destructiveHint: false, openWorldHint: true };
|
|
264
|
+
const WRITES_RECEIPT = { readOnlyHint: false, destructiveHint: false, openWorldHint: true };
|
|
265
|
+
|
|
226
266
|
export function registerTools(server, session) {
|
|
227
267
|
server.registerTool(
|
|
228
268
|
"get_key",
|
|
229
|
-
{ title: "Get a free validation key", description: TOOL_DESCRIPTIONS.get_key, inputSchema: getKeyInput },
|
|
269
|
+
{ 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 },
|
|
230
270
|
(args) => toolGetKey(session, args),
|
|
231
271
|
);
|
|
232
272
|
server.registerTool(
|
|
233
273
|
"validate_deflated_sharpe",
|
|
234
|
-
{ title: "Validate deflated Sharpe", description: TOOL_DESCRIPTIONS.validate_deflated_sharpe, inputSchema: deflatedSharpeToolShape },
|
|
274
|
+
{ title: "Validate deflated Sharpe", annotations: { title: "Validate deflated Sharpe", ...WRITES_RECEIPT }, description: TOOL_DESCRIPTIONS.validate_deflated_sharpe, inputSchema: deflatedSharpeToolShape },
|
|
235
275
|
(args) => toolValidateDeflatedSharpe(session, args),
|
|
236
276
|
);
|
|
237
277
|
server.registerTool(
|
|
238
278
|
"validate_overfitting",
|
|
239
|
-
{ title: "Validate overfitting (CSCV)", description: TOOL_DESCRIPTIONS.validate_overfitting, inputSchema: overfittingInput },
|
|
279
|
+
{ title: "Validate overfitting (CSCV)", annotations: { title: "Validate overfitting (CSCV)", ...WRITES_RECEIPT }, description: TOOL_DESCRIPTIONS.validate_overfitting, inputSchema: overfittingInput },
|
|
240
280
|
(args) => toolValidateOverfitting(session, args),
|
|
241
281
|
);
|
|
242
282
|
server.registerTool(
|
|
243
283
|
"validate_paper_evidence",
|
|
244
|
-
{ title: "Validate paper evidence", description: TOOL_DESCRIPTIONS.validate_paper_evidence, inputSchema: paperEvidenceInput },
|
|
284
|
+
{ title: "Validate paper evidence", annotations: { title: "Validate paper evidence", ...WRITES_RECEIPT }, description: TOOL_DESCRIPTIONS.validate_paper_evidence, inputSchema: paperEvidenceInput },
|
|
245
285
|
(args) => toolValidatePaperEvidence(session, args),
|
|
246
286
|
);
|
|
247
287
|
server.registerTool(
|
|
248
288
|
"validate_breadth",
|
|
249
|
-
{ title: "Validate breadth ceiling", description: TOOL_DESCRIPTIONS.validate_breadth, inputSchema: breadthInput },
|
|
289
|
+
{ title: "Validate breadth ceiling", annotations: { title: "Validate breadth ceiling", ...WRITES_RECEIPT }, description: TOOL_DESCRIPTIONS.validate_breadth, inputSchema: breadthInput },
|
|
250
290
|
(args) => toolValidateBreadth(session, args),
|
|
251
291
|
);
|
|
292
|
+
server.registerTool(
|
|
293
|
+
"validate_track_record",
|
|
294
|
+
{ title: "Minimum track record length", annotations: { title: "Minimum track record length", ...WRITES_RECEIPT }, description: TOOL_DESCRIPTIONS.validate_track_record, inputSchema: trackRecordInput },
|
|
295
|
+
(args) => toolValidateTrackRecord(session, args),
|
|
296
|
+
);
|
|
252
297
|
server.registerTool(
|
|
253
298
|
"get_receipt",
|
|
254
|
-
{ title: "Get a receipt", description: TOOL_DESCRIPTIONS.get_receipt, inputSchema: getReceiptInput },
|
|
299
|
+
{ title: "Get a receipt", annotations: { title: "Get a receipt", ...READ_ONLY }, description: TOOL_DESCRIPTIONS.get_receipt, inputSchema: getReceiptInput },
|
|
255
300
|
(args) => toolGetReceipt(session, args),
|
|
256
301
|
);
|
|
257
302
|
server.registerTool(
|
|
258
303
|
"service_status",
|
|
259
|
-
{ title: "Service status", description: TOOL_DESCRIPTIONS.service_status, inputSchema: emptyInput },
|
|
304
|
+
{ title: "Service status", annotations: { title: "Service status", ...READ_ONLY }, description: TOOL_DESCRIPTIONS.service_status, inputSchema: emptyInput },
|
|
260
305
|
() => toolServiceStatus(session),
|
|
261
306
|
);
|
|
262
307
|
server.registerTool(
|
|
263
308
|
"company_financial_history",
|
|
264
|
-
{ title: "Company financial history (SEC)", description: TOOL_DESCRIPTIONS.company_financial_history, inputSchema: companyHistoryInput },
|
|
309
|
+
{ title: "Company financial history (SEC)", annotations: { title: "Company financial history (SEC)", ...READ_ONLY }, description: TOOL_DESCRIPTIONS.company_financial_history, inputSchema: companyHistoryInput },
|
|
265
310
|
(args) => toolCompanyFinancialHistory(session, args),
|
|
266
311
|
);
|
|
267
312
|
}
|