canli-validation-mcp 0.3.0 → 0.3.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.
Files changed (3) hide show
  1. package/README.md +31 -10
  2. package/package.json +1 -1
  3. package/src/server.mjs +43 -10
package/README.md CHANGED
@@ -1,20 +1,19 @@
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,
7
- so the agent cannot see a number without the sentences beside it that say what the number does
8
- not establish.
4
+ gives a coding agent eight tools: issue a free key, run the four validators (deflated Sharpe,
5
+ CSCV overfitting, paper-evidence conformance, breadth ceiling), fetch a stored receipt, read
6
+ service status, and read a company's reported financial history from SEC filings. Every tool
7
+ returns the full API envelope as its result text, success or error, so the agent cannot see a
8
+ number without the sentences beside it that say what the number does not establish.
9
9
 
10
10
  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
11
+ Also listed on the official MCP Registry (`io.github.arhancanli/canli-validation-mcp`) and
12
+ [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
13
  test this package itself; see "Local checkout" near the bottom.
13
14
 
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.
15
+ This README describes the version in `package.json`. Unversioned `npx` runs npm's latest
16
+ release; `npx -y canli-validation-mcp@<version>` pins one.
18
17
 
19
18
  ## What the API is (and is not)
20
19
 
@@ -105,6 +104,28 @@ No install step. `npx` fetches the published package on first run, so every clie
105
104
  just spawns `npx -y canli-validation-mcp`. See "Local checkout" near the bottom to develop or test
106
105
  this package itself instead of running the published one.
107
106
 
107
+ ## Hosted endpoint (no install)
108
+
109
+ The same tools are served at `https://canlicapital.com/mcp` over MCP Streamable HTTP, for clients
110
+ that connect to a URL instead of spawning a process (Claude.ai connectors, ChatGPT, Cursor's remote
111
+ servers). Nothing to install, and no Node.js on your machine.
112
+
113
+ ```bash
114
+ claude mcp add --transport http canli https://canlicapital.com/mcp
115
+ ```
116
+
117
+ Without a key, requests run under a shared anonymous key, so the daily validation quota is shared
118
+ by every hosted caller. For your own quota, issue a free key (see
119
+ [/developers](https://canlicapital.com/developers#quickstart)) and send it as a header:
120
+
121
+ ```bash
122
+ claude mcp add --transport http canli https://canlicapital.com/mcp --header "Authorization: Bearer $CANLI_KEY"
123
+ ```
124
+
125
+ The endpoint is stateless. On it, `get_key` issues nothing and says which key is in use, because a
126
+ key issued there would not reach the next request. A malformed Authorization header is refused
127
+ rather than replaced with the shared key.
128
+
108
129
  ## Claude Desktop
109
130
 
110
131
  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.3.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
@@ -37,14 +37,26 @@ function parseOrThrow(schema, value, label) {
37
37
  throw new Error(`${label}: ${issues}`);
38
38
  }
39
39
 
40
- export function createSession({ base, fetchImpl, envKey, timeoutMs = REQUEST_TIMEOUT_MS } = {}) {
40
+ // An empty CANLI_KEY, or an unsubstituted template such as "${user_config.api_key}" (what a
41
+ // desktop extension host passes for an optional field the user left unset), is no key at all, not
42
+ // a key to send.
43
+ export function configuredKey(value) {
44
+ if (typeof value !== "string") return undefined;
45
+ const trimmed = value.trim();
46
+ return trimmed === "" || /^\$\{[^}]*\}$/.test(trimmed) ? undefined : trimmed;
47
+ }
48
+
49
+ export function createSession({ base, fetchImpl, envKey, timeoutMs = REQUEST_TIMEOUT_MS, hosted } = {}) {
41
50
  if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0) throw new Error("Request timeout must be a positive integer");
42
51
  return {
43
52
  base: base ?? process.env.CANLI_API_BASE ?? DEFAULT_BASE,
44
53
  fetchImpl: fetchImpl ?? fetch,
45
- envKey: envKey ?? process.env.CANLI_KEY ?? undefined,
54
+ envKey: envKey ?? configuredKey(process.env.CANLI_KEY),
46
55
  key: undefined,
47
56
  timeoutMs,
57
+ // Set by the hosted endpoint (api/mcp.js): which key a request runs under, so get_key can say
58
+ // so instead of issuing a key the next stateless request would never see.
59
+ hosted: hosted ?? undefined,
48
60
  };
49
61
  }
50
62
 
@@ -99,6 +111,21 @@ export function columnarObservations(observations) {
99
111
 
100
112
  export async function toolGetKey(session, args) {
101
113
  const { label } = parseOrThrow(getKeyInput, args, "get_key");
114
+ if (session.hosted) {
115
+ // The hosted endpoint is stateless: a key issued here would be gone by the next request, and
116
+ // every hosted caller shares the platform's egress address and so its per-client issuance quota.
117
+ const ownKey = "get a free key at https://canlicapital.com/developers#quickstart and send it as 'Authorization: Bearer <key>' to this endpoint";
118
+ const notes = {
119
+ caller: "This hosted session runs under the key in your Authorization header; no new key was issued.",
120
+ 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}.`,
121
+ none: `This hosted endpoint has no shared key configured, so validations need your own key: ${ownKey}.`,
122
+ };
123
+ return asText({
124
+ note: notes[session.hosted.keySource] ?? notes.none,
125
+ key_source: session.hosted.keySource,
126
+ key_present: session.hosted.keySource !== "none",
127
+ });
128
+ }
102
129
  if (session.envKey) {
103
130
  return asText({
104
131
  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.",
@@ -223,45 +250,51 @@ export async function toolCompanyFinancialHistory(session, args) {
223
250
  });
224
251
  }
225
252
 
253
+ // MCP tool annotations: every tool reaches the canlicapital.com API (openWorldHint). Reads are
254
+ // marked read-only; a validation stores a receipt and get_key creates a key, so neither is
255
+ // read-only, and nothing any tool does modifies or deletes a caller's data.
256
+ const READ_ONLY = { readOnlyHint: true, destructiveHint: false, openWorldHint: true };
257
+ const WRITES_RECEIPT = { readOnlyHint: false, destructiveHint: false, openWorldHint: true };
258
+
226
259
  export function registerTools(server, session) {
227
260
  server.registerTool(
228
261
  "get_key",
229
- { title: "Get a free validation key", description: TOOL_DESCRIPTIONS.get_key, inputSchema: getKeyInput },
262
+ { 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
263
  (args) => toolGetKey(session, args),
231
264
  );
232
265
  server.registerTool(
233
266
  "validate_deflated_sharpe",
234
- { title: "Validate deflated Sharpe", description: TOOL_DESCRIPTIONS.validate_deflated_sharpe, inputSchema: deflatedSharpeToolShape },
267
+ { title: "Validate deflated Sharpe", annotations: { title: "Validate deflated Sharpe", ...WRITES_RECEIPT }, description: TOOL_DESCRIPTIONS.validate_deflated_sharpe, inputSchema: deflatedSharpeToolShape },
235
268
  (args) => toolValidateDeflatedSharpe(session, args),
236
269
  );
237
270
  server.registerTool(
238
271
  "validate_overfitting",
239
- { title: "Validate overfitting (CSCV)", description: TOOL_DESCRIPTIONS.validate_overfitting, inputSchema: overfittingInput },
272
+ { title: "Validate overfitting (CSCV)", annotations: { title: "Validate overfitting (CSCV)", ...WRITES_RECEIPT }, description: TOOL_DESCRIPTIONS.validate_overfitting, inputSchema: overfittingInput },
240
273
  (args) => toolValidateOverfitting(session, args),
241
274
  );
242
275
  server.registerTool(
243
276
  "validate_paper_evidence",
244
- { title: "Validate paper evidence", description: TOOL_DESCRIPTIONS.validate_paper_evidence, inputSchema: paperEvidenceInput },
277
+ { title: "Validate paper evidence", annotations: { title: "Validate paper evidence", ...WRITES_RECEIPT }, description: TOOL_DESCRIPTIONS.validate_paper_evidence, inputSchema: paperEvidenceInput },
245
278
  (args) => toolValidatePaperEvidence(session, args),
246
279
  );
247
280
  server.registerTool(
248
281
  "validate_breadth",
249
- { title: "Validate breadth ceiling", description: TOOL_DESCRIPTIONS.validate_breadth, inputSchema: breadthInput },
282
+ { title: "Validate breadth ceiling", annotations: { title: "Validate breadth ceiling", ...WRITES_RECEIPT }, description: TOOL_DESCRIPTIONS.validate_breadth, inputSchema: breadthInput },
250
283
  (args) => toolValidateBreadth(session, args),
251
284
  );
252
285
  server.registerTool(
253
286
  "get_receipt",
254
- { title: "Get a receipt", description: TOOL_DESCRIPTIONS.get_receipt, inputSchema: getReceiptInput },
287
+ { title: "Get a receipt", annotations: { title: "Get a receipt", ...READ_ONLY }, description: TOOL_DESCRIPTIONS.get_receipt, inputSchema: getReceiptInput },
255
288
  (args) => toolGetReceipt(session, args),
256
289
  );
257
290
  server.registerTool(
258
291
  "service_status",
259
- { title: "Service status", description: TOOL_DESCRIPTIONS.service_status, inputSchema: emptyInput },
292
+ { title: "Service status", annotations: { title: "Service status", ...READ_ONLY }, description: TOOL_DESCRIPTIONS.service_status, inputSchema: emptyInput },
260
293
  () => toolServiceStatus(session),
261
294
  );
262
295
  server.registerTool(
263
296
  "company_financial_history",
264
- { title: "Company financial history (SEC)", description: TOOL_DESCRIPTIONS.company_financial_history, inputSchema: companyHistoryInput },
297
+ { title: "Company financial history (SEC)", annotations: { title: "Company financial history (SEC)", ...READ_ONLY }, description: TOOL_DESCRIPTIONS.company_financial_history, inputSchema: companyHistoryInput },
265
298
  (args) => toolCompanyFinancialHistory(session, args),
266
299
  );
267
300
  }