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 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 seven tools: issue a free key, run the four validators (deflated Sharpe,
5
- CSCV overfitting, paper-evidence conformance, breadth ceiling), fetch a stored receipt, and read
6
- service status. Every tool returns the full API envelope as its result text, success or error,
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
- Run it with `npx`, no install step, as shown below. A local checkout is only needed to develop or
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
- Release checkpoint, September 20, 2026: npm's latest version is 0.1.1. This checkout
15
- prepares 0.1.2; its HTTP error flags, request deadlines and redirect handling described
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.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
- export function createSession({ base, fetchImpl, envKey, timeoutMs = REQUEST_TIMEOUT_MS } = {}) {
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 ?? undefined,
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
  }