canli-validation-mcp 0.1.1 → 0.2.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/LICENSE +21 -0
- package/README.md +30 -1
- package/package.json +13 -3
- package/src/schemas.mjs +22 -1
- package/src/server.mjs +119 -24
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Arhan Canli / Canli Capital
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -11,6 +11,11 @@ This package is published to npm as [`canli-validation-mcp`](https://www.npmjs.c
|
|
|
11
11
|
Run it with `npx`, no install step, as shown below. A local checkout is only needed to develop or
|
|
12
12
|
test this package itself; see "Local checkout" near the bottom.
|
|
13
13
|
|
|
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.
|
|
18
|
+
|
|
14
19
|
## What the API is (and is not)
|
|
15
20
|
|
|
16
21
|
The engine is the product. The service runs your submitted numbers through the same honesty
|
|
@@ -31,6 +36,7 @@ repository for the full design.
|
|
|
31
36
|
| `validate_breadth` | `POST /api/v1/validate/breadth` | yes |
|
|
32
37
|
| `get_receipt` | `GET /api/v1/receipts/{id}` | no |
|
|
33
38
|
| `service_status` | `GET /api/v1/validate/status` | no |
|
|
39
|
+
| `company_financial_history` | `GET /company-data/{cik}.json` | no |
|
|
34
40
|
|
|
35
41
|
`validate_deflated_sharpe` accepts exactly one of two input shapes, never a mix of both:
|
|
36
42
|
|
|
@@ -43,6 +49,15 @@ repository for the full design.
|
|
|
43
49
|
Sending fields from both shapes, or from neither, is rejected before any request leaves the
|
|
44
50
|
process; see `src/schemas.mjs`.
|
|
45
51
|
|
|
52
|
+
`company_financial_history` is different from the other tools: it reads the public company
|
|
53
|
+
reference at canlicapital.com, not the validation API. Give it a CIK (1 to 10 digits) to list a
|
|
54
|
+
company's available financial histories, or a CIK and a us-gaap concept such as `Revenues` to
|
|
55
|
+
get the observations, newest first (`limit` defaults to 40, maximum 200). Every observation
|
|
56
|
+
keeps its filing accession, form, filed date and unit, and the result carries the SHA-256 of the
|
|
57
|
+
original SEC response and the record's own boundary sentence: these are accounting values as
|
|
58
|
+
reported to the SEC, not market prices, returns or a recommendation. Companies and concepts
|
|
59
|
+
outside the current release return an error with the available concepts listed.
|
|
60
|
+
|
|
46
61
|
## Configuration
|
|
47
62
|
|
|
48
63
|
| Variable | Default | Meaning |
|
|
@@ -54,6 +69,13 @@ If `CANLI_KEY` is not set, call `get_key` once per session before the four valid
|
|
|
54
69
|
returns lives only in this process's memory for the life of the session; it is not written to
|
|
55
70
|
disk.
|
|
56
71
|
|
|
72
|
+
HTTP failures and API error envelopes are marked as MCP tool errors while preserving
|
|
73
|
+
the complete JSON envelope. A successful validation with a negative verdict remains
|
|
74
|
+
a normal result. Requests have a 30-second deadline covering headers and body, reject
|
|
75
|
+
redirects, and are never retried automatically. A timeout may occur after the service
|
|
76
|
+
has processed a request; check service status before deciding to submit again.
|
|
77
|
+
Non-JSON response bodies and raw network errors are omitted from tool errors.
|
|
78
|
+
|
|
57
79
|
## Install
|
|
58
80
|
|
|
59
81
|
No install step. `npx` fetches the published package on first run, so every client config below
|
|
@@ -108,7 +130,8 @@ const { tools } = await client.listTools();
|
|
|
108
130
|
console.log(tools.map((t) => t.name));
|
|
109
131
|
|
|
110
132
|
const keyResult = await client.callTool({ name: "get_key", arguments: { label: "my-agent" } });
|
|
111
|
-
|
|
133
|
+
if (keyResult.isError) throw new Error("Key setup failed; inspect the error privately.");
|
|
134
|
+
// The session retains the issued key. Avoid printing its envelope into logs.
|
|
112
135
|
|
|
113
136
|
const result = await client.callTool({
|
|
114
137
|
name: "validate_deflated_sharpe",
|
|
@@ -159,6 +182,7 @@ in place of `npx -y canli-validation-mcp` in any config above.
|
|
|
159
182
|
|
|
160
183
|
```bash
|
|
161
184
|
npm test
|
|
185
|
+
npm run test:package
|
|
162
186
|
```
|
|
163
187
|
|
|
164
188
|
Runs `node --test` over `test/*.test.mjs`: schema round-trips against the API's own OpenAPI and
|
|
@@ -169,6 +193,11 @@ file contains an em dash, and one test that spawns the actual server binary and
|
|
|
169
193
|
MCP `tools/list` and `callTool` handshake over stdio against a local HTTP stub, so the wiring is
|
|
170
194
|
proven rather than assumed.
|
|
171
195
|
|
|
196
|
+
`test:package` creates the actual npm tarball, checks its exact file list and license,
|
|
197
|
+
installs it in a temporary consumer directory, and runs the stdio test against that
|
|
198
|
+
installed entry. Dependency installation contacts npm; tool calls use only the local
|
|
199
|
+
HTTP stub. It neither publishes a package nor issues a production API key.
|
|
200
|
+
|
|
172
201
|
## Dependencies
|
|
173
202
|
|
|
174
203
|
Only `@modelcontextprotocol/sdk` (pinned exact) and `zod` (pinned exact). No other runtime
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "canli-validation-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.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",
|
|
@@ -16,11 +16,21 @@
|
|
|
16
16
|
"README.md"
|
|
17
17
|
],
|
|
18
18
|
"scripts": {
|
|
19
|
-
"test": "node --test test/*.test.mjs"
|
|
19
|
+
"test": "node --test test/*.test.mjs",
|
|
20
|
+
"test:package": "node test/package-smoke.mjs"
|
|
20
21
|
},
|
|
21
22
|
"dependencies": {
|
|
22
23
|
"@modelcontextprotocol/sdk": "1.30.0",
|
|
23
24
|
"zod": "4.5.4"
|
|
24
25
|
},
|
|
25
|
-
"mcpName": "io.github.arhancanli/canli-validation-mcp"
|
|
26
|
+
"mcpName": "io.github.arhancanli/canli-validation-mcp",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/arhancanli/canlicapital.git",
|
|
30
|
+
"directory": "mcp"
|
|
31
|
+
},
|
|
32
|
+
"homepage": "https://canlicapital.com/developers",
|
|
33
|
+
"bugs": {
|
|
34
|
+
"url": "https://github.com/arhancanli/canlicapital/issues"
|
|
35
|
+
}
|
|
26
36
|
}
|
package/src/schemas.mjs
CHANGED
|
@@ -110,6 +110,19 @@ export const getReceiptInput = z
|
|
|
110
110
|
|
|
111
111
|
export const emptyInput = z.object({}).strict();
|
|
112
112
|
|
|
113
|
+
// company_financial_history reads the public company reference, not the validation API.
|
|
114
|
+
export const companyHistoryInput = z
|
|
115
|
+
.object({
|
|
116
|
+
cik: z.string().regex(/^\d{1,10}$/, "A CIK is 1 to 10 digits"),
|
|
117
|
+
concept: z.string().regex(/^[A-Za-z][A-Za-z0-9]{0,99}$/, "A concept is a us-gaap tag such as Revenues or Assets").optional(),
|
|
118
|
+
limit: z.number().int().min(1).max(200).optional(),
|
|
119
|
+
})
|
|
120
|
+
.strict();
|
|
121
|
+
|
|
122
|
+
// The company reference carries its own boundary sentence in every record (claim_boundary),
|
|
123
|
+
// written by scripts/lib/company-reference.mjs. A test pins this copy to that source verbatim.
|
|
124
|
+
export const COMPANY_REFERENCE_BOUNDARY = "Public company accounting reference, not market prices, returns, an investment recommendation, or ALPHAC performance. Validate a separately constructed return series with the validation API; accounting values are not returns.";
|
|
125
|
+
|
|
113
126
|
// ---------------------------------------------------------------------------------------------
|
|
114
127
|
// Boundary language: verbatim sentences from api/_lib/limits.js LIMITS_TEXT. A test asserts each
|
|
115
128
|
// of these strings is still present, byte for byte, in that source array, so this module cannot
|
|
@@ -120,9 +133,16 @@ export const LIMITS_SENTENCES = Object.freeze({
|
|
|
120
133
|
scope: "This verdict is about the series exactly as submitted. The service never saw the data source, its costs, survivorship, or any lookahead in how the series was built.",
|
|
121
134
|
notAdmission: "A deflated Sharpe or overfitting probability above or below any threshold is not admission to anything and is not a forecast.",
|
|
122
135
|
unsigned: "The receipt is content-hashed and reproducible from the open-source core it names. It is not signed.",
|
|
123
|
-
quotas: "Quotas: 1000 validations per key per UTC day, 5 keys per client per UTC day, 1048576 bytes per request, 20000 observations per series, 200 variants per matrix.",
|
|
136
|
+
quotas: "Quotas: 1000 validations per key per UTC day, 5 keys per client per UTC day, 1048576 bytes per validation request, 1024 bytes per key revocation request, 20000 observations per series, 200 variants per matrix.",
|
|
124
137
|
});
|
|
125
138
|
|
|
139
|
+
// The MCP registry caps server.json's top-level description at 100 characters (its 422 reads
|
|
140
|
+
// "expected length <= 100"), shorter than any sentence above. That one description carries this
|
|
141
|
+
// clause instead: the verbatim tail of `notAdmission`, so it cannot drift from the boundary
|
|
142
|
+
// language either (test/schemas.test.mjs pins the tail, test/registry-files.test.mjs the cap).
|
|
143
|
+
export const REGISTRY_LIMITS_CLAUSE = "is not admission to anything and is not a forecast.";
|
|
144
|
+
export const REGISTRY_DESCRIPTION_MAX = 100;
|
|
145
|
+
|
|
126
146
|
// One tool description per tool, each stating (in one sentence lifted from the boundary language
|
|
127
147
|
// above) what the tool's result cannot be used to claim, so an agent sees this before it ever
|
|
128
148
|
// calls the tool, not only inside the returned envelope.
|
|
@@ -134,4 +154,5 @@ export const TOOL_DESCRIPTIONS = Object.freeze({
|
|
|
134
154
|
validate_breadth: `Book Sharpe ceiling from per-sleeve quality and average pairwise correlation, and the sleeves a target needs. ${LIMITS_SENTENCES.scope}`,
|
|
135
155
|
get_receipt: `Fetch a stored verdict by its content-hash id (GET /api/v1/receipts/{id}), immutable and cacheable. ${LIMITS_SENTENCES.unsigned}`,
|
|
136
156
|
service_status: `Service, store and quota constants for the validation API (GET /api/v1/validate/status); no key required. ${LIMITS_SENTENCES.scope}`,
|
|
157
|
+
company_financial_history: `SEC-reported financial history for one company from the canlicapital.com company reference (GET /company-data/{cik}.json). Without a concept it lists the available histories; with one it returns observations, newest first, each with its filing accession, form, filed date and unit, plus the SHA-256 of the original SEC response. No key required. ${COMPANY_REFERENCE_BOUNDARY}`,
|
|
137
158
|
});
|
package/src/server.mjs
CHANGED
|
@@ -6,10 +6,13 @@
|
|
|
6
6
|
// sentences beside it that say what the number does not establish (see schemas.mjs, LIMITS_SENTENCES
|
|
7
7
|
// and TOOL_DESCRIPTIONS). Reads CANLI_API_BASE (default https://canlicapital.com) and an optional
|
|
8
8
|
// CANLI_KEY; when CANLI_KEY is set, get_key does not call the network.
|
|
9
|
+
import { readFileSync, realpathSync } from "node:fs";
|
|
10
|
+
import { pathToFileURL } from "node:url";
|
|
9
11
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
10
12
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
11
13
|
import {
|
|
12
14
|
breadthInput,
|
|
15
|
+
companyHistoryInput,
|
|
13
16
|
deflatedSharpeInput,
|
|
14
17
|
deflatedSharpeToolShape,
|
|
15
18
|
emptyInput,
|
|
@@ -22,7 +25,8 @@ import {
|
|
|
22
25
|
|
|
23
26
|
export const DEFAULT_BASE = "https://canlicapital.com";
|
|
24
27
|
export const SERVER_NAME = "canlicapital-validation-mcp";
|
|
25
|
-
export const SERVER_VERSION = "
|
|
28
|
+
export const SERVER_VERSION = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
29
|
+
export const REQUEST_TIMEOUT_MS = 30_000;
|
|
26
30
|
|
|
27
31
|
// One place a value fails a zod schema turns into a short, readable message instead of a raw
|
|
28
32
|
// ZodError, so a thrown error reads well inside an MCP isError result.
|
|
@@ -33,12 +37,14 @@ function parseOrThrow(schema, value, label) {
|
|
|
33
37
|
throw new Error(`${label}: ${issues}`);
|
|
34
38
|
}
|
|
35
39
|
|
|
36
|
-
export function createSession({ base, fetchImpl, envKey } = {}) {
|
|
40
|
+
export function createSession({ base, fetchImpl, envKey, timeoutMs = REQUEST_TIMEOUT_MS } = {}) {
|
|
41
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0) throw new Error("Request timeout must be a positive integer");
|
|
37
42
|
return {
|
|
38
43
|
base: base ?? process.env.CANLI_API_BASE ?? DEFAULT_BASE,
|
|
39
44
|
fetchImpl: fetchImpl ?? fetch,
|
|
40
45
|
envKey: envKey ?? process.env.CANLI_KEY ?? undefined,
|
|
41
46
|
key: undefined,
|
|
47
|
+
timeoutMs,
|
|
42
48
|
};
|
|
43
49
|
}
|
|
44
50
|
|
|
@@ -46,18 +52,31 @@ async function callApi(session, { path, method = "GET", body }) {
|
|
|
46
52
|
const headers = { "Content-Type": "application/json" };
|
|
47
53
|
const key = session.key ?? session.envKey;
|
|
48
54
|
if (key) headers.Authorization = `Bearer ${key}`;
|
|
49
|
-
const
|
|
55
|
+
const signal = AbortSignal.timeout(session.timeoutMs);
|
|
56
|
+
const init = { method, headers, signal, redirect: "error" };
|
|
50
57
|
if (body !== undefined) init.body = JSON.stringify(body);
|
|
51
|
-
|
|
52
|
-
const text = await res.text();
|
|
58
|
+
let res, text;
|
|
53
59
|
try {
|
|
54
|
-
|
|
60
|
+
res = await session.fetchImpl(`${session.base}${path}`, init);
|
|
61
|
+
text = await res.text();
|
|
55
62
|
} catch {
|
|
56
|
-
throw new Error(
|
|
63
|
+
throw new Error(signal.aborted
|
|
64
|
+
? `${path} exceeded the request deadline. The server may have processed the request; no automatic retry was sent.`
|
|
65
|
+
: `${path} could not be reached or read. Check the API base and service status; no automatic retry was sent.`);
|
|
57
66
|
}
|
|
67
|
+
let envelope;
|
|
68
|
+
try {
|
|
69
|
+
envelope = JSON.parse(text);
|
|
70
|
+
} catch {
|
|
71
|
+
throw new Error(`${path} returned a non-JSON body (status ${res.status}). Response body omitted.`);
|
|
72
|
+
}
|
|
73
|
+
return { envelope, failed: res.status >= 400 || Boolean(envelope?.error) };
|
|
58
74
|
}
|
|
59
75
|
|
|
60
|
-
const asText = (envelope
|
|
76
|
+
const asText = (envelope, failed = false) => ({
|
|
77
|
+
content: [{ type: "text", text: JSON.stringify(envelope, null, 2) }],
|
|
78
|
+
...(failed ? { isError: true } : {}),
|
|
79
|
+
});
|
|
61
80
|
|
|
62
81
|
export async function toolGetKey(session, args) {
|
|
63
82
|
const { label } = parseOrThrow(getKeyInput, args, "get_key");
|
|
@@ -68,9 +87,9 @@ export async function toolGetKey(session, args) {
|
|
|
68
87
|
key_present: true,
|
|
69
88
|
});
|
|
70
89
|
}
|
|
71
|
-
const
|
|
72
|
-
if (envelope?.data?.key) session.key = envelope.data.key;
|
|
73
|
-
return asText(envelope);
|
|
90
|
+
const response = await callApi(session, { path: "/api/v1/keys", method: "POST", body: { label } });
|
|
91
|
+
if (!response.failed && response.envelope?.data?.key) session.key = response.envelope.data.key;
|
|
92
|
+
return asText(response.envelope, response.failed);
|
|
74
93
|
}
|
|
75
94
|
|
|
76
95
|
export async function toolValidateDeflatedSharpe(session, args) {
|
|
@@ -81,37 +100,108 @@ export async function toolValidateDeflatedSharpe(session, args) {
|
|
|
81
100
|
"or a return series (returns, periods_per_year, effective_independent_trials, cross_trial_sharpe_sd_annualized), never a mix of both and never neither.",
|
|
82
101
|
);
|
|
83
102
|
}
|
|
84
|
-
const
|
|
85
|
-
return asText(envelope);
|
|
103
|
+
const response = await callApi(session, { path: "/api/v1/validate/deflated-sharpe", method: "POST", body: parsed.data });
|
|
104
|
+
return asText(response.envelope, response.failed);
|
|
86
105
|
}
|
|
87
106
|
|
|
88
107
|
export async function toolValidateOverfitting(session, args) {
|
|
89
108
|
const body = parseOrThrow(overfittingInput, args, "validate_overfitting");
|
|
90
|
-
const
|
|
91
|
-
return asText(envelope);
|
|
109
|
+
const response = await callApi(session, { path: "/api/v1/validate/overfitting", method: "POST", body });
|
|
110
|
+
return asText(response.envelope, response.failed);
|
|
92
111
|
}
|
|
93
112
|
|
|
94
113
|
export async function toolValidatePaperEvidence(session, args) {
|
|
95
114
|
const body = parseOrThrow(paperEvidenceInput, args, "validate_paper_evidence");
|
|
96
|
-
const
|
|
97
|
-
return asText(envelope);
|
|
115
|
+
const response = await callApi(session, { path: "/api/v1/validate/paper-evidence", method: "POST", body });
|
|
116
|
+
return asText(response.envelope, response.failed);
|
|
98
117
|
}
|
|
99
118
|
|
|
100
119
|
export async function toolValidateBreadth(session, args) {
|
|
101
120
|
const body = parseOrThrow(breadthInput, args, "validate_breadth");
|
|
102
|
-
const
|
|
103
|
-
return asText(envelope);
|
|
121
|
+
const response = await callApi(session, { path: "/api/v1/validate/breadth", method: "POST", body });
|
|
122
|
+
return asText(response.envelope, response.failed);
|
|
104
123
|
}
|
|
105
124
|
|
|
106
125
|
export async function toolGetReceipt(session, args) {
|
|
107
126
|
const { id } = parseOrThrow(getReceiptInput, args, "get_receipt");
|
|
108
|
-
const
|
|
109
|
-
return asText(envelope);
|
|
127
|
+
const response = await callApi(session, { path: `/api/v1/receipts/${id}` });
|
|
128
|
+
return asText(response.envelope, response.failed);
|
|
110
129
|
}
|
|
111
130
|
|
|
112
131
|
export async function toolServiceStatus(session) {
|
|
113
|
-
const
|
|
114
|
-
return asText(envelope);
|
|
132
|
+
const response = await callApi(session, { path: "/api/v1/validate/status" });
|
|
133
|
+
return asText(response.envelope, response.failed);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// The company reference is a public data file, not an API envelope. The result keeps every
|
|
137
|
+
// field that says where a value came from (accession, form, filed date, unit, source hash) and
|
|
138
|
+
// the record's own claim_boundary and policy sentences, so an agent cannot quote a number
|
|
139
|
+
// without its provenance or boundary.
|
|
140
|
+
export async function toolCompanyFinancialHistory(session, args) {
|
|
141
|
+
const { cik, concept, limit = 40 } = parseOrThrow(companyHistoryInput, args, "company_financial_history");
|
|
142
|
+
const id = cik.padStart(10, "0");
|
|
143
|
+
const path = `/company-data/${id}.json`;
|
|
144
|
+
const signal = AbortSignal.timeout(session.timeoutMs);
|
|
145
|
+
let res, text;
|
|
146
|
+
try {
|
|
147
|
+
res = await session.fetchImpl(`${session.base}${path}`, { headers: { Accept: "application/json" }, signal, redirect: "error" });
|
|
148
|
+
text = await res.text();
|
|
149
|
+
} catch {
|
|
150
|
+
throw new Error(signal.aborted
|
|
151
|
+
? `${path} exceeded the request deadline; no automatic retry was sent.`
|
|
152
|
+
: `${path} could not be reached or read. Check the API base and service status; no automatic retry was sent.`);
|
|
153
|
+
}
|
|
154
|
+
if (res.status === 404) return asText({ error: { code: "not_found", message: `No company record for CIK ${id} in the current company reference release.` }, page: `${session.base}/companies` }, true);
|
|
155
|
+
if (res.status >= 400) return asText({ error: { code: "unavailable", message: `${path} returned status ${res.status}.` } }, true);
|
|
156
|
+
let record;
|
|
157
|
+
try {
|
|
158
|
+
record = JSON.parse(text);
|
|
159
|
+
} catch {
|
|
160
|
+
throw new Error(`${path} returned a non-JSON body (status ${res.status}). Response body omitted.`);
|
|
161
|
+
}
|
|
162
|
+
if (record?.schema !== "canli.company-reference.v1" || record.cik !== id || !Array.isArray(record.concepts) || typeof record.claim_boundary !== "string") {
|
|
163
|
+
return asText({ error: { code: "unexpected_record", message: `${path} did not return a canli.company-reference.v1 record for CIK ${id}.` } }, true);
|
|
164
|
+
}
|
|
165
|
+
const base = {
|
|
166
|
+
schema: "canli.mcp.company-history.v1",
|
|
167
|
+
company: { cik: record.cik, name: record.name },
|
|
168
|
+
claim_boundary: record.claim_boundary,
|
|
169
|
+
policy: record.policy,
|
|
170
|
+
source: {
|
|
171
|
+
sec_response_url: record.source_url,
|
|
172
|
+
sec_response_sha256: record.source_sha256,
|
|
173
|
+
snapshot: record.source_snapshot ? `${session.base}${record.source_snapshot}` : null,
|
|
174
|
+
fetched_at: record.fetched_at,
|
|
175
|
+
record: `${session.base}${path}`,
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
if (concept === undefined) {
|
|
179
|
+
return asText({
|
|
180
|
+
...base,
|
|
181
|
+
page: `${session.base}/companies/${id}`,
|
|
182
|
+
histories: record.concepts.map((c) => ({ concept: c.tag, label: c.label, kind: c.kind, observations: c.observations?.length ?? 0, page: `${session.base}/companies/${id}/${c.tag}` })),
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
const history = record.concepts.find((c) => c.tag === concept);
|
|
186
|
+
if (!history) {
|
|
187
|
+
return asText({ ...base, error: { code: "concept_not_found", message: `${record.name} has no ${concept} history in this release.`, available: record.concepts.map((c) => c.tag) } }, true);
|
|
188
|
+
}
|
|
189
|
+
const observations = [...(history.observations ?? [])].sort((a, b) => String(b.end).localeCompare(String(a.end))).slice(0, limit);
|
|
190
|
+
return asText({
|
|
191
|
+
...base,
|
|
192
|
+
page: `${session.base}/companies/${id}/${history.tag}`,
|
|
193
|
+
history: {
|
|
194
|
+
concept: history.tag,
|
|
195
|
+
taxonomy: history.taxonomy,
|
|
196
|
+
label: history.label,
|
|
197
|
+
kind: history.kind,
|
|
198
|
+
meaning: history.meaning,
|
|
199
|
+
units: [...new Set((history.observations ?? []).map((o) => o.unit))],
|
|
200
|
+
total_observations: history.observations?.length ?? 0,
|
|
201
|
+
returned: observations.length,
|
|
202
|
+
observations,
|
|
203
|
+
},
|
|
204
|
+
});
|
|
115
205
|
}
|
|
116
206
|
|
|
117
207
|
export function registerTools(server, session) {
|
|
@@ -150,6 +240,11 @@ export function registerTools(server, session) {
|
|
|
150
240
|
{ title: "Service status", description: TOOL_DESCRIPTIONS.service_status, inputSchema: emptyInput },
|
|
151
241
|
() => toolServiceStatus(session),
|
|
152
242
|
);
|
|
243
|
+
server.registerTool(
|
|
244
|
+
"company_financial_history",
|
|
245
|
+
{ title: "Company financial history (SEC)", description: TOOL_DESCRIPTIONS.company_financial_history, inputSchema: companyHistoryInput },
|
|
246
|
+
(args) => toolCompanyFinancialHistory(session, args),
|
|
247
|
+
);
|
|
153
248
|
}
|
|
154
249
|
|
|
155
250
|
export function createServer(session = createSession()) {
|
|
@@ -164,7 +259,7 @@ async function main() {
|
|
|
164
259
|
await server.connect(transport);
|
|
165
260
|
}
|
|
166
261
|
|
|
167
|
-
const isMain = process.argv[1] && import.meta.url ===
|
|
262
|
+
const isMain = process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href;
|
|
168
263
|
if (isMain) {
|
|
169
264
|
main().catch((err) => {
|
|
170
265
|
console.error(`${SERVER_NAME}: fatal`, err);
|