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.
- package/README.md +31 -10
- package/package.json +1 -1
- 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
|
|
5
|
-
CSCV overfitting, paper-evidence conformance, breadth ceiling), fetch a stored receipt,
|
|
6
|
-
service status
|
|
7
|
-
|
|
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
|
-
|
|
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
|
-
|
|
15
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
}
|