@pipeworx/mcp-edgar 0.3.0 → 0.3.2
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 +2 -2
- package/package.json +1 -1
- package/server.json +1 -1
- package/src/index.ts +501 -45
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
EDGAR MCP — SEC EDGAR public APIs (free, no auth)
|
|
4
4
|
|
|
5
|
-
Part of [Pipeworx](https://pipeworx.io) — an MCP gateway connecting AI agents to
|
|
5
|
+
Part of [Pipeworx](https://pipeworx.io) — an MCP gateway connecting AI agents to 1337+ live data sources.
|
|
6
6
|
|
|
7
7
|
## Tools
|
|
8
8
|
|
|
@@ -23,7 +23,7 @@ Add to your MCP client (Claude Desktop, Cursor, Windsurf, etc.):
|
|
|
23
23
|
}
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
-
Or connect to the full Pipeworx gateway for access to all
|
|
26
|
+
Or connect to the full Pipeworx gateway for access to all 1337+ data sources:
|
|
27
27
|
|
|
28
28
|
```json
|
|
29
29
|
{
|
package/package.json
CHANGED
package/server.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "io.github.pipeworx-io/edgar",
|
|
4
4
|
"title": "Edgar",
|
|
5
5
|
"description": "EDGAR MCP — SEC EDGAR public APIs (free, no auth)",
|
|
6
|
-
"version": "0.3.
|
|
6
|
+
"version": "0.3.2",
|
|
7
7
|
"websiteUrl": "https://pipeworx.io/packs/edgar",
|
|
8
8
|
"repository": {
|
|
9
9
|
"url": "https://github.com/pipeworx-io/mcp-edgar",
|
package/src/index.ts
CHANGED
|
@@ -16,6 +16,124 @@ interface McpToolExport {
|
|
|
16
16
|
provider?: string;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
// Reusable entity-resolution helpers for MCP packs. SELF-CONTAINED — no internal
|
|
20
|
+
// imports — so publish-pack.sh can inline it into standalone pack builds the same
|
|
21
|
+
// way it inlines the McpToolExport type.
|
|
22
|
+
//
|
|
23
|
+
// Recurring failure mode across financial packs: callers pass a company NAME
|
|
24
|
+
// ("Apple", "apple inc") where a ticker / CIK / provider symbol is expected, and
|
|
25
|
+
// the pack 404s or throws "not found". `rankMatches` is a generic name-ranker any
|
|
26
|
+
// pack can run over its OWN list (US tickers, B3 tickers, drug names, airports…);
|
|
27
|
+
// `resolveSecEntity` wraps it around the SEC company_tickers.json universe, shared
|
|
28
|
+
// by the packs that key on CIK (edgar, sec).
|
|
29
|
+
|
|
30
|
+
type MatchKind = 'exact' | 'prefix' | 'word' | 'substring';
|
|
31
|
+
|
|
32
|
+
interface RankedMatch<T> {
|
|
33
|
+
item: T;
|
|
34
|
+
kind: MatchKind;
|
|
35
|
+
score: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const normalize = (s: string): string =>
|
|
39
|
+
s.toUpperCase().replace(/[.,]/g, '').replace(/\s+/g, ' ').trim();
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Rank `items` by how well their name matches `query`:
|
|
43
|
+
* exact (4) > prefix (3) > whole-word (2) > substring (1). Ties break by shortest
|
|
44
|
+
* name — the primary entity (e.g. "Apple Inc." over "Apple Hospitality REIT").
|
|
45
|
+
* Returns only items that match at all, best first. Pure (no I/O).
|
|
46
|
+
*/
|
|
47
|
+
function rankMatches<T>(
|
|
48
|
+
query: string,
|
|
49
|
+
items: T[],
|
|
50
|
+
getName: (item: T) => string,
|
|
51
|
+
): RankedMatch<T>[] {
|
|
52
|
+
const q = normalize(query);
|
|
53
|
+
if (!q) return [];
|
|
54
|
+
const scored: { item: T; kind: MatchKind; score: number; len: number }[] = [];
|
|
55
|
+
for (const item of items) {
|
|
56
|
+
const name = getName(item);
|
|
57
|
+
const n = normalize(name);
|
|
58
|
+
let kind: MatchKind | null = null;
|
|
59
|
+
let score = 0;
|
|
60
|
+
if (n === q) { kind = 'exact'; score = 4; }
|
|
61
|
+
else if (n.startsWith(q)) { kind = 'prefix'; score = 3; }
|
|
62
|
+
else if (n.includes(` ${q} `) || n.endsWith(` ${q}`)) { kind = 'word'; score = 2; }
|
|
63
|
+
else if (n.includes(q)) { kind = 'substring'; score = 1; }
|
|
64
|
+
if (kind) scored.push({ item, kind, score, len: name.length });
|
|
65
|
+
}
|
|
66
|
+
scored.sort((a, b) => b.score - a.score || a.len - b.len);
|
|
67
|
+
return scored.map(({ item, kind, score }) => ({ item, kind, score }));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
interface SecTickerRow { cik_str: number; ticker: string; title: string }
|
|
71
|
+
|
|
72
|
+
interface SecEntity {
|
|
73
|
+
ticker: string;
|
|
74
|
+
cik: string;
|
|
75
|
+
cik_padded: string;
|
|
76
|
+
company_name: string;
|
|
77
|
+
matched_by: 'ticker' | 'company_name';
|
|
78
|
+
alternatives?: { ticker: string; company_name: string; cik: string }[];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const SEC_TICKERS_URL = 'https://www.sec.gov/files/company_tickers.json';
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Resolve a ticker OR company name to its SEC identity (CIK + canonical name).
|
|
85
|
+
* Exact ticker first (the common, unambiguous case), then fuzzy company-name
|
|
86
|
+
* fallback so "Apple" / "APPLE" → AAPL's CIK. Throws if nothing matches.
|
|
87
|
+
*
|
|
88
|
+
* `headers` lets callers pass their pack's SEC User-Agent — www.sec.gov requires
|
|
89
|
+
* a UA. `fetchImpl` defaults to global fetch (override in tests).
|
|
90
|
+
*/
|
|
91
|
+
async function resolveSecEntity(
|
|
92
|
+
query: string,
|
|
93
|
+
opts: { fetchImpl?: typeof fetch; headers?: Record<string, string> } = {},
|
|
94
|
+
): Promise<SecEntity> {
|
|
95
|
+
if (typeof query !== 'string' || !query.trim()) {
|
|
96
|
+
throw new Error('Required argument is missing or empty. Pass a ticker like "AAPL" or a company name like "Apple".');
|
|
97
|
+
}
|
|
98
|
+
const doFetch = opts.fetchImpl ?? fetch;
|
|
99
|
+
const res = await doFetch(SEC_TICKERS_URL, { headers: opts.headers });
|
|
100
|
+
if (!res.ok) throw new Error(`SEC ticker lookup error: ${res.status}`);
|
|
101
|
+
const data = (await res.json()) as Record<string, SecTickerRow>;
|
|
102
|
+
const rows = Object.values(data);
|
|
103
|
+
|
|
104
|
+
// 1) Exact ticker match — the common, unambiguous case.
|
|
105
|
+
const q = query.toUpperCase().trim();
|
|
106
|
+
for (const r of rows) {
|
|
107
|
+
if (r.ticker === q) return toEntity(r, 'ticker');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// 2) Company-name fallback.
|
|
111
|
+
const ranked = rankMatches(query, rows, (r) => r.title);
|
|
112
|
+
if (ranked.length) {
|
|
113
|
+
const best = toEntity(ranked[0].item, 'company_name');
|
|
114
|
+
const alts = ranked.slice(1, 4).map((m) => ({
|
|
115
|
+
ticker: m.item.ticker,
|
|
116
|
+
company_name: m.item.title,
|
|
117
|
+
cik: String(m.item.cik_str),
|
|
118
|
+
}));
|
|
119
|
+
if (alts.length) best.alternatives = alts;
|
|
120
|
+
return best;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
throw new Error(`No SEC company matches "${query}". Pass a ticker like "AAPL" or a company name like "Apple Inc.".`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function toEntity(r: SecTickerRow, matched_by: 'ticker' | 'company_name'): SecEntity {
|
|
127
|
+
return {
|
|
128
|
+
ticker: r.ticker,
|
|
129
|
+
cik: String(r.cik_str),
|
|
130
|
+
cik_padded: String(r.cik_str).padStart(10, '0'),
|
|
131
|
+
company_name: r.title,
|
|
132
|
+
matched_by,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
|
|
19
137
|
/**
|
|
20
138
|
* EDGAR MCP — SEC EDGAR public APIs (free, no auth)
|
|
21
139
|
*
|
|
@@ -25,6 +143,7 @@ interface McpToolExport {
|
|
|
25
143
|
* - edgar_company_facts: get structured XBRL financial data for a company
|
|
26
144
|
* - edgar_company_concept: get a specific financial metric over time
|
|
27
145
|
* - edgar_ticker_to_cik: look up CIK from ticker symbol
|
|
146
|
+
* - edgar_filing_documents: list the documents inside one filing by accession number
|
|
28
147
|
*
|
|
29
148
|
* Note: SEC requires a descriptive User-Agent header per their guidelines.
|
|
30
149
|
*/
|
|
@@ -86,7 +205,7 @@ const tools: McpToolExport['tools'] = [
|
|
|
86
205
|
properties: {
|
|
87
206
|
cik: {
|
|
88
207
|
type: 'string',
|
|
89
|
-
description: '
|
|
208
|
+
description: 'Ticker ("NVDA") or CIK number ("320193"). Tickers are auto-resolved to CIKs internally.',
|
|
90
209
|
},
|
|
91
210
|
},
|
|
92
211
|
required: ['cik'],
|
|
@@ -164,18 +283,66 @@ const tools: McpToolExport['tools'] = [
|
|
|
164
283
|
{
|
|
165
284
|
name: 'edgar_ticker_to_cik',
|
|
166
285
|
description:
|
|
167
|
-
'Resolve a US stock ticker (e.g. "TSLA") to the SEC\'s 10-digit CIK identifier — required by every other SEC tool. Call THIS FIRST when you have a ticker and need to use edgar_company_concept, edgar_company_filings, edgar_company_facts, sec_8k_recent, or any other SEC-keyed tool. Returns {cik, cik_padded, company_name}. Cheap, no rate limit concerns. Most other tools also accept tickers directly and call this internally — only use it explicitly when you want the CIK as data.',
|
|
286
|
+
'Resolve a US stock ticker (e.g. "TSLA") OR a company name (e.g. "Tesla", "Apple Inc") to the SEC\'s 10-digit CIK identifier — required by every other SEC tool. Call THIS FIRST when you have a ticker/name and need to use edgar_company_concept, edgar_company_filings, edgar_company_facts, sec_8k_recent, or any other SEC-keyed tool. Returns {cik, cik_padded, company_name, ticker, matched_by}; when matched by name it also returns `alternatives` for disambiguation. Cheap, no rate limit concerns. Most other tools also accept tickers/names directly and call this internally — only use it explicitly when you want the CIK as data.',
|
|
168
287
|
inputSchema: {
|
|
169
288
|
type: 'object' as const,
|
|
170
289
|
properties: {
|
|
171
290
|
ticker: {
|
|
172
291
|
type: 'string',
|
|
173
|
-
description: 'Stock ticker symbol (e.g., "AAPL", "MSFT", "TSLA")',
|
|
292
|
+
description: 'Stock ticker symbol (e.g., "AAPL", "MSFT", "TSLA") or company name (e.g., "Apple", "Microsoft")',
|
|
174
293
|
},
|
|
175
294
|
},
|
|
176
295
|
required: ['ticker'],
|
|
177
296
|
},
|
|
178
297
|
},
|
|
298
|
+
{
|
|
299
|
+
name: 'edgar_xbrl_frames',
|
|
300
|
+
description:
|
|
301
|
+
'Compare ONE financial metric across ALL public companies for a single period (SEC XBRL "frames"). PREFER OVER WEB SEARCH for "which companies had the most revenue/net income/assets in <year>", "rank companies by <metric>", cross-company financial comparison. concept is a US-GAAP tag (e.g. "Revenues", "NetIncomeLoss", "Assets", "ResearchAndDevelopmentExpense", "CashAndCashEquivalentsAtCarryingValue"). period is a calendar frame: "CY2023" (annual), "CY2023Q1" (quarter), or "CY2023Q1I" (instant/balance-sheet, period-end). Returns companies + values, sorted descending by default. Differs from edgar_company_concept (one company over time) — this is one period across every filer.',
|
|
302
|
+
inputSchema: {
|
|
303
|
+
type: 'object' as const,
|
|
304
|
+
properties: {
|
|
305
|
+
concept: { type: 'string', description: 'US-GAAP (or dei) tag, e.g. "Revenues", "NetIncomeLoss", "Assets", "ResearchAndDevelopmentExpense".' },
|
|
306
|
+
period: { type: 'string', description: 'Calendar frame: "CY2023" (annual duration), "CY2023Q1" (quarterly duration), or "CY2023Q1I" (instant, balance-sheet items at period end).' },
|
|
307
|
+
unit: { type: 'string', description: 'Unit of measure (default "USD"). Use "shares" for share counts, "USD-per-shares" for per-share.' },
|
|
308
|
+
taxonomy: { type: 'string', description: 'Taxonomy: "us-gaap" (default) or "dei".' },
|
|
309
|
+
sort: { type: 'string', description: '"desc" (default, largest first) or "asc".', enum: ['desc', 'asc'] },
|
|
310
|
+
limit: { type: 'number', description: 'Max companies to return (1-200, default 25).' },
|
|
311
|
+
},
|
|
312
|
+
required: ['concept', 'period'],
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
name: 'edgar_filing_documents',
|
|
317
|
+
description:
|
|
318
|
+
'AUTHORITATIVE list of the SEC filing documents inside ONE specific filing, by accession number. Retrieve a filing / its contents / attachments: pass the accession (e.g. "0000320193-25-000079", with or without dashes) plus the filer\'s ticker ("AAPL") or CIK ("320193"). Returns every document in the filing folder — the primary document (10-K / 10-Q / 8-K body), all exhibits, and XBRL files — each with name, type, size, and a direct https URL, plus the filing\'s form type, filing date, and human -index.html page. Set include_primary_text:true to also pull the primary document\'s text (HTML stripped to plaintext, ~40k chars). Use to list a 10-K / 10-Q / 8-K\'s exhibits, retrieve filing contents/attachments, or fetch the text of a filing. You can pass an exact accession, OR just a ticker + form_type to auto-resolve the latest matching filing (no accession lookup needed). Examples: edgar_filing_documents({ticker: "NVDA", form_type: "10-K"}) for the documents in NVIDIA\'s latest annual report; edgar_filing_documents({accession: "0000320193-25-000079", ticker: "AAPL", include_primary_text: true}) for a specific filing\'s text.',
|
|
319
|
+
inputSchema: {
|
|
320
|
+
type: 'object' as const,
|
|
321
|
+
properties: {
|
|
322
|
+
accession: {
|
|
323
|
+
type: 'string',
|
|
324
|
+
description: 'Optional SEC accession number of a specific filing, with or without dashes (e.g. "0000320193-25-000079"). Omit it to auto-resolve the latest filing — pass form_type instead.',
|
|
325
|
+
},
|
|
326
|
+
ticker: {
|
|
327
|
+
type: 'string',
|
|
328
|
+
description: 'The filer\'s ticker (e.g. "AAPL", "NVDA") or company name. Provide this OR cik. Tickers are auto-resolved to CIKs.',
|
|
329
|
+
},
|
|
330
|
+
cik: {
|
|
331
|
+
type: 'string',
|
|
332
|
+
description: 'The filer\'s CIK number (e.g. "320193"). Provide this OR ticker.',
|
|
333
|
+
},
|
|
334
|
+
form_type: {
|
|
335
|
+
type: 'string',
|
|
336
|
+
description: 'When accession is omitted, the form type of the latest filing to fetch, e.g. "10-K", "10-Q", "8-K", "DEF 14A". Omit both accession and form_type to get the single most recent filing of any type.',
|
|
337
|
+
},
|
|
338
|
+
include_primary_text: {
|
|
339
|
+
type: 'boolean',
|
|
340
|
+
description: 'When true, also fetch the primary document and return its text (HTML stripped to plaintext, truncated to ~40,000 chars). Default false.',
|
|
341
|
+
},
|
|
342
|
+
},
|
|
343
|
+
required: [],
|
|
344
|
+
},
|
|
345
|
+
},
|
|
179
346
|
];
|
|
180
347
|
|
|
181
348
|
// ── Helpers ─────────────────────────────────────────────────────────
|
|
@@ -251,31 +418,11 @@ async function searchFilings(
|
|
|
251
418
|
};
|
|
252
419
|
}
|
|
253
420
|
|
|
421
|
+
// Resolves a ticker OR company name ("AAPL", "Apple", "apple inc") to its SEC
|
|
422
|
+
// identity. Delegates to the shared resolver so the ticker/name-resolution logic
|
|
423
|
+
// stays consistent across every SEC-keyed pack (edgar, sec).
|
|
254
424
|
async function tickerToCik(ticker: string) {
|
|
255
|
-
|
|
256
|
-
throw new Error('Required argument "ticker" is missing or empty. Pass a ticker symbol like "AAPL" or "MSFT".');
|
|
257
|
-
}
|
|
258
|
-
const res = await fetch('https://www.sec.gov/files/company_tickers.json', { headers: SEC_HEADERS });
|
|
259
|
-
if (!res.ok) throw new Error(`SEC ticker lookup error: ${res.status}`);
|
|
260
|
-
|
|
261
|
-
const data = (await res.json()) as Record<
|
|
262
|
-
string,
|
|
263
|
-
{ cik_str: number; ticker: string; title: string }
|
|
264
|
-
>;
|
|
265
|
-
|
|
266
|
-
const upperTicker = ticker.toUpperCase().trim();
|
|
267
|
-
for (const entry of Object.values(data)) {
|
|
268
|
-
if (entry.ticker === upperTicker) {
|
|
269
|
-
return {
|
|
270
|
-
ticker: entry.ticker,
|
|
271
|
-
cik: String(entry.cik_str),
|
|
272
|
-
cik_padded: String(entry.cik_str).padStart(10, '0'),
|
|
273
|
-
company_name: entry.title,
|
|
274
|
-
};
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
throw new Error(`Ticker "${ticker}" not found in SEC company tickers`);
|
|
425
|
+
return resolveSecEntity(ticker, { headers: SEC_HEADERS });
|
|
279
426
|
}
|
|
280
427
|
|
|
281
428
|
async function resolveCik(tickerOrCik: string): Promise<string> {
|
|
@@ -356,7 +503,171 @@ async function companyFilings(tickerOrCik: string, formType?: string, limit?: nu
|
|
|
356
503
|
};
|
|
357
504
|
}
|
|
358
505
|
|
|
359
|
-
|
|
506
|
+
// ── Filing documents (list docs inside one filing by accession) ─────
|
|
507
|
+
|
|
508
|
+
// Normalize an accession number (dashed or undashed) to both forms.
|
|
509
|
+
// SEC accessions are 18 digits shaped 10-2-6 (e.g. 0000320193-25-000079).
|
|
510
|
+
function normalizeAccession(raw: string): { dashed: string; nodash: string } | null {
|
|
511
|
+
const digits = String(raw ?? '').replace(/\D/g, '');
|
|
512
|
+
if (digits.length !== 18) return null;
|
|
513
|
+
const dashed = `${digits.slice(0, 10)}-${digits.slice(10, 12)}-${digits.slice(12)}`;
|
|
514
|
+
return { dashed, nodash: digits };
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Strip an HTML document to readable plaintext. Workers have no DOMParser;
|
|
518
|
+
// drop script/style blocks, remove tags, decode entities, collapse whitespace.
|
|
519
|
+
function htmlToText(html: string): string {
|
|
520
|
+
return (decodeEntities(
|
|
521
|
+
html
|
|
522
|
+
.replace(/<(script|style)[\s\S]*?<\/\1>/gi, ' ')
|
|
523
|
+
.replace(/<[^>]+>/g, ' '),
|
|
524
|
+
) ?? '')
|
|
525
|
+
.replace(/[ \t\f\v]+/g, ' ')
|
|
526
|
+
.replace(/\s*\n\s*/g, '\n')
|
|
527
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
528
|
+
.trim();
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
async function filingDocuments(
|
|
532
|
+
accession: string | undefined,
|
|
533
|
+
tickerOrCik: string,
|
|
534
|
+
includePrimaryText?: boolean,
|
|
535
|
+
wantFormTypeArg?: string,
|
|
536
|
+
) {
|
|
537
|
+
if (typeof tickerOrCik !== 'string' || !tickerOrCik.trim()) {
|
|
538
|
+
throw new Error(
|
|
539
|
+
'A ticker or CIK is required. Pass ticker ("AAPL") or cik ("320193"), plus either an accession or a form_type (e.g. edgar_filing_documents({ticker: "NVDA", form_type: "10-K"}) for the latest 10-K).',
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
const cik = await resolveCik(tickerOrCik);
|
|
543
|
+
// Accession is optional: when omitted, resolve the latest filing matching
|
|
544
|
+
// form_type (or the single most recent filing of any type) so one-shot
|
|
545
|
+
// questions like "documents in NVIDIA's latest 10-K" work without the caller
|
|
546
|
+
// first having to look up an accession number.
|
|
547
|
+
let acc = accession ? normalizeAccession(accession) : null;
|
|
548
|
+
if (accession && !acc) {
|
|
549
|
+
throw new Error(
|
|
550
|
+
`Invalid accession "${accession}". A SEC accession number is 18 digits shaped 10-2-6 (e.g. "0000320193-25-000079", with or without dashes). Get accession numbers from edgar_company_filings, or omit accession and pass form_type to auto-resolve the latest.`,
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
if (!acc) {
|
|
554
|
+
const wantForm = wantFormTypeArg?.trim().toUpperCase();
|
|
555
|
+
const subRes = await fetch(`${DATA_BASE}/submissions/CIK${padCik(cik)}.json`, { headers: SEC_HEADERS });
|
|
556
|
+
if (!subRes.ok) throw new Error(`SEC submissions lookup failed (HTTP ${subRes.status}) resolving the latest filing for ${tickerOrCik}.`);
|
|
557
|
+
const sub = (await subRes.json()) as { filings?: { recent?: { accessionNumber?: string[]; form?: string[] } } };
|
|
558
|
+
const r = sub.filings?.recent;
|
|
559
|
+
const forms = r?.form ?? [];
|
|
560
|
+
const accs = r?.accessionNumber ?? [];
|
|
561
|
+
const idx = wantForm ? forms.findIndex((f) => f.toUpperCase() === wantForm) : 0;
|
|
562
|
+
if (idx < 0 || !accs[idx]) {
|
|
563
|
+
throw new Error(
|
|
564
|
+
`No ${wantForm ?? 'recent'} filing found for ${tickerOrCik} in the recent index. Use edgar_company_filings({ticker:"${tickerOrCik}"${wantForm ? `, form_type:"${wantForm}"` : ''}}) to list available filings, then pass an accession.`,
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
acc = normalizeAccession(accs[idx]);
|
|
568
|
+
if (!acc) throw new Error('Failed to resolve a valid accession from the submissions index.');
|
|
569
|
+
}
|
|
570
|
+
const cikNoZeros = String(Number(cik.replace(/\D/g, ''))); // archive path uses CIK with no leading zeros
|
|
571
|
+
const paddedCik = padCik(cik);
|
|
572
|
+
|
|
573
|
+
// Match the accession in the filer's submissions to enrich with form/date/primaryDoc.
|
|
574
|
+
let company: string | null = null;
|
|
575
|
+
let formType: string | null = null;
|
|
576
|
+
let filingDate: string | null = null;
|
|
577
|
+
let primaryDocument: string | null = null;
|
|
578
|
+
let primaryDocDescription: string | null = null;
|
|
579
|
+
try {
|
|
580
|
+
const subRes = await fetch(`${DATA_BASE}/submissions/CIK${paddedCik}.json`, { headers: SEC_HEADERS });
|
|
581
|
+
if (subRes.ok) {
|
|
582
|
+
const sub = (await subRes.json()) as {
|
|
583
|
+
name?: string;
|
|
584
|
+
filings?: {
|
|
585
|
+
recent?: {
|
|
586
|
+
accessionNumber?: string[];
|
|
587
|
+
form?: string[];
|
|
588
|
+
filingDate?: string[];
|
|
589
|
+
primaryDocument?: string[];
|
|
590
|
+
primaryDocDescription?: string[];
|
|
591
|
+
};
|
|
592
|
+
};
|
|
593
|
+
};
|
|
594
|
+
company = sub.name ?? null;
|
|
595
|
+
const r = sub.filings?.recent;
|
|
596
|
+
if (r?.accessionNumber) {
|
|
597
|
+
const i = r.accessionNumber.indexOf(acc.dashed);
|
|
598
|
+
if (i >= 0) {
|
|
599
|
+
formType = r.form?.[i] ?? null;
|
|
600
|
+
filingDate = r.filingDate?.[i] ?? null;
|
|
601
|
+
primaryDocument = r.primaryDocument?.[i] ?? null;
|
|
602
|
+
primaryDocDescription = r.primaryDocDescription?.[i] ?? null;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
} catch {
|
|
607
|
+
// Metadata enrichment is best-effort; the document directory below is the core payload.
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
const folder = `https://www.sec.gov/Archives/edgar/data/${cikNoZeros}/${acc.nodash}`;
|
|
611
|
+
const idxRes = await fetch(`${folder}/index.json`, { headers: SEC_HEADERS });
|
|
612
|
+
if (idxRes.status === 404) {
|
|
613
|
+
throw new Error(
|
|
614
|
+
`No filing found for accession ${acc.dashed} — that accession number does not exist in SEC EDGAR (double-check the digits; it may belong to a different filer). Use edgar_company_filings({ticker_or_cik: "${tickerOrCik}"}) to list this company's real accession numbers.`,
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
if (!idxRes.ok) throw new Error(`SEC EDGAR filing index error: ${idxRes.status}`);
|
|
618
|
+
const idx = (await idxRes.json()) as {
|
|
619
|
+
directory?: { item?: { name: string; type?: string; size?: string | number; 'last-modified'?: string }[] };
|
|
620
|
+
};
|
|
621
|
+
|
|
622
|
+
const items = idx.directory?.item ?? [];
|
|
623
|
+
const documents = items.map((it) => ({
|
|
624
|
+
name: it.name,
|
|
625
|
+
type: it.type ?? null,
|
|
626
|
+
...(it.name === primaryDocument && primaryDocDescription ? { description: primaryDocDescription } : {}),
|
|
627
|
+
size: it.size !== undefined && it.size !== '' ? Number(it.size) : null,
|
|
628
|
+
last_modified: it['last-modified'] ?? null,
|
|
629
|
+
url: `${folder}/${it.name}`,
|
|
630
|
+
}));
|
|
631
|
+
|
|
632
|
+
const result: Record<string, unknown> = {
|
|
633
|
+
accession: acc.dashed,
|
|
634
|
+
cik: cikNoZeros,
|
|
635
|
+
company,
|
|
636
|
+
form_type: formType,
|
|
637
|
+
filing_date: filingDate,
|
|
638
|
+
primary_document: primaryDocument,
|
|
639
|
+
filing_url: `${folder}/${acc.dashed}-index.html`,
|
|
640
|
+
documents,
|
|
641
|
+
};
|
|
642
|
+
|
|
643
|
+
if (includePrimaryText && primaryDocument) {
|
|
644
|
+
try {
|
|
645
|
+
const docUrl = `${folder}/${primaryDocument}`;
|
|
646
|
+
const docRes = await fetch(docUrl, { headers: { 'User-Agent': SEC_HEADERS['User-Agent'] } });
|
|
647
|
+
if (docRes.ok) {
|
|
648
|
+
const body = await docRes.text();
|
|
649
|
+
const isHtml = /\.x?html?$/i.test(primaryDocument) || /^\s*</.test(body);
|
|
650
|
+
const text = isHtml ? htmlToText(body) : body;
|
|
651
|
+
result.primary_text = text.length > 40000 ? text.slice(0, 40000) + '\n…[truncated]' : text;
|
|
652
|
+
} else {
|
|
653
|
+
result.primary_text = null;
|
|
654
|
+
result.primary_text_error = `fetch ${docRes.status}`;
|
|
655
|
+
}
|
|
656
|
+
} catch (e) {
|
|
657
|
+
result.primary_text = null;
|
|
658
|
+
result.primary_text_error = String(e);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
return result;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
async function companyFacts(cikOrTicker: string) {
|
|
666
|
+
// Was the only EDGAR tool that skipped resolveCik() — it called padCik()
|
|
667
|
+
// directly, so a ticker ("NVDA") got its letters stripped -> "0000000000" ->
|
|
668
|
+
// 404, and a missing arg crashed on .replace of undefined. Route through
|
|
669
|
+
// resolveCik like every sibling: validates the arg and resolves tickers.
|
|
670
|
+
const cik = await resolveCik(cikOrTicker);
|
|
360
671
|
const paddedCik = padCik(cik);
|
|
361
672
|
const res = await fetch(`${DATA_BASE}/api/xbrl/companyfacts/CIK${paddedCik}.json`, {
|
|
362
673
|
headers: SEC_HEADERS,
|
|
@@ -465,6 +776,22 @@ const CONCEPT_CANDIDATES: Record<string, string[]> = {
|
|
|
465
776
|
'CashCashEquivalentsRestrictedCashAndRestrictedCashEquivalents',
|
|
466
777
|
],
|
|
467
778
|
longtermdebt: ['LongTermDebt', 'LongTermDebtNoncurrent'],
|
|
779
|
+
// Common concepts an LLM asks for that were MISSING → 404 (net income was
|
|
780
|
+
// covered but these weren't). Keys are bare-alphanumeric (the lookup strips
|
|
781
|
+
// spaces/punctuation), so "total assets"/"earnings per share" etc. all match.
|
|
782
|
+
assets: ['Assets'],
|
|
783
|
+
totalassets: ['Assets'],
|
|
784
|
+
liabilities: ['Liabilities'],
|
|
785
|
+
totalliabilities: ['Liabilities'],
|
|
786
|
+
stockholdersequity: ['StockholdersEquity', 'StockholdersEquityIncludingPortionAttributableToNoncontrollingInterest'],
|
|
787
|
+
equity: ['StockholdersEquity', 'StockholdersEquityIncludingPortionAttributableToNoncontrollingInterest'],
|
|
788
|
+
eps: ['EarningsPerShareDiluted', 'EarningsPerShareBasic'],
|
|
789
|
+
earningspershare: ['EarningsPerShareDiluted', 'EarningsPerShareBasic'],
|
|
790
|
+
earningspersharediluted: ['EarningsPerShareDiluted', 'EarningsPerShareBasic'],
|
|
791
|
+
earningspersharebasic: ['EarningsPerShareBasic', 'EarningsPerShareDiluted'],
|
|
792
|
+
grossprofit: ['GrossProfit'],
|
|
793
|
+
operatingincome: ['OperatingIncomeLoss'],
|
|
794
|
+
operatingincomeloss: ['OperatingIncomeLoss'],
|
|
468
795
|
// ── XBRL-name fallbacks (drove 80-of-84 EDGAR errors in 48h analytics) ──
|
|
469
796
|
longtermdebtnoncurrent: ['LongTermDebtNoncurrent', 'LongTermDebt'],
|
|
470
797
|
salesrevenuenet: ['SalesRevenueNet', 'Revenues', 'RevenueFromContractWithCustomerExcludingAssessedTax'],
|
|
@@ -493,6 +820,31 @@ const CONCEPT_CANDIDATES: Record<string, string[]> = {
|
|
|
493
820
|
],
|
|
494
821
|
};
|
|
495
822
|
|
|
823
|
+
// Annual-report forms. 10-K/10-K/A = domestic; 20-F/40-F (+ amendments) =
|
|
824
|
+
// foreign private issuers (IFRS filers). The concept reader filtered to 10-K
|
|
825
|
+
// only, which silently dropped every foreign filer's data even when the tag
|
|
826
|
+
// existed → 404s / empty results.
|
|
827
|
+
const ANNUAL_FORMS = new Set(['10-K', '10-K/A', '20-F', '20-F/A', '40-F', '40-F/A']);
|
|
828
|
+
|
|
829
|
+
// ifrs-full taxonomy fallback for foreign private issuers (they report under
|
|
830
|
+
// ifrs-full, not us-gaap). Keyed by the same normalized metric name; tried only
|
|
831
|
+
// when the us-gaap candidates all miss, so domestic lookups pay no extra call.
|
|
832
|
+
const IFRS_FALLBACK: Record<string, string[]> = {
|
|
833
|
+
revenue: ['Revenue', 'RevenueFromContractsWithCustomers'],
|
|
834
|
+
revenues: ['Revenue', 'RevenueFromContractsWithCustomers'],
|
|
835
|
+
revenuefromcontractwithcustomerexcludingassessedtax: ['Revenue', 'RevenueFromContractsWithCustomers'],
|
|
836
|
+
salesrevenuenet: ['Revenue', 'RevenueFromContractsWithCustomers'],
|
|
837
|
+
netincome: ['ProfitLoss', 'ProfitLossAttributableToOwnersOfParent'],
|
|
838
|
+
netincomeloss: ['ProfitLoss', 'ProfitLossAttributableToOwnersOfParent'],
|
|
839
|
+
profitloss: ['ProfitLoss', 'ProfitLossAttributableToOwnersOfParent'],
|
|
840
|
+
cash: ['CashAndCashEquivalents'],
|
|
841
|
+
cashandcashequivalentsatcarryingvalue: ['CashAndCashEquivalents'],
|
|
842
|
+
assets: ['Assets'],
|
|
843
|
+
liabilities: ['Liabilities'],
|
|
844
|
+
stockholdersequity: ['Equity', 'EquityAttributableToOwnersOfParent'],
|
|
845
|
+
equity: ['Equity', 'EquityAttributableToOwnersOfParent'],
|
|
846
|
+
};
|
|
847
|
+
|
|
496
848
|
async function companyConcept(cikOrTicker: string, concept: string) {
|
|
497
849
|
// Auto-resolve ticker → CIK so callers can pass "AAPL" not "320193".
|
|
498
850
|
// Production analytics: 11% errors on edgar largely from LLMs passing
|
|
@@ -500,7 +852,11 @@ async function companyConcept(cikOrTicker: string, concept: string) {
|
|
|
500
852
|
const cik = await resolveCik(cikOrTicker);
|
|
501
853
|
const paddedCik = padCik(cik);
|
|
502
854
|
|
|
503
|
-
|
|
855
|
+
// Normalize the lookup key to bare alphanumerics so natural-language input
|
|
856
|
+
// ("net income", "Long-Term Debt", "earnings per share") matches the no-space
|
|
857
|
+
// map keys — the #1 edgar_company_concept error was "net income" (with a space)
|
|
858
|
+
// missing the `netincome` key → 404.
|
|
859
|
+
const candidates = CONCEPT_CANDIDATES[concept.trim().toLowerCase().replace(/[^a-z0-9]/g, '')] ?? [concept];
|
|
504
860
|
|
|
505
861
|
type ConceptDoc = {
|
|
506
862
|
cik: number;
|
|
@@ -523,25 +879,68 @@ async function companyConcept(cikOrTicker: string, concept: string) {
|
|
|
523
879
|
// $26.9B/FY2022 figure; freshest-data-wins returns the correct $215.9B.
|
|
524
880
|
let lastStatus = 0;
|
|
525
881
|
const found: { doc: ConceptDoc; latestEnd: string }[] = [];
|
|
526
|
-
|
|
527
|
-
const
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
for (const
|
|
536
|
-
|
|
537
|
-
|
|
882
|
+
const tryNamespace = async (ns: string, tags: string[]) => {
|
|
883
|
+
for (const candidate of tags) {
|
|
884
|
+
const r = await fetch(
|
|
885
|
+
`${DATA_BASE}/api/xbrl/companyconcept/CIK${paddedCik}/${ns}/${encodeURIComponent(candidate)}.json`,
|
|
886
|
+
{ headers: SEC_HEADERS },
|
|
887
|
+
);
|
|
888
|
+
if (!r.ok) { lastStatus = r.status; continue; }
|
|
889
|
+
const doc = (await r.json()) as ConceptDoc;
|
|
890
|
+
let latestEnd = '';
|
|
891
|
+
for (const values of Object.values(doc.units)) {
|
|
892
|
+
for (const e of values) {
|
|
893
|
+
if (!ANNUAL_FORMS.has(e.form)) continue;
|
|
894
|
+
if ((e.end ?? '') > latestEnd) latestEnd = e.end ?? '';
|
|
895
|
+
}
|
|
538
896
|
}
|
|
897
|
+
found.push({ doc, latestEnd });
|
|
898
|
+
}
|
|
899
|
+
};
|
|
900
|
+
await tryNamespace('us-gaap', candidates);
|
|
901
|
+
// Only try the ifrs-full taxonomy when us-gaap returned nothing — foreign
|
|
902
|
+
// private issuers file under ifrs-full (form 20-F/40-F). Domestic (us-gaap)
|
|
903
|
+
// lookups short-circuit here and pay no extra request.
|
|
904
|
+
if (found.length === 0) {
|
|
905
|
+
const ifrs = IFRS_FALLBACK[concept.trim().toLowerCase().replace(/[^a-z0-9]/g, '')] ?? [];
|
|
906
|
+
if (ifrs.length) await tryNamespace('ifrs-full', ifrs);
|
|
907
|
+
}
|
|
908
|
+
// Fallback: the filer tags this metric under a name not in our candidate list
|
|
909
|
+
// (very common for debt / lease / segment concepts — e.g. Ford dropped
|
|
910
|
+
// LongTermDebtNoncurrent, many filers use LongTermDebtAndCapitalLease...). Pull
|
|
911
|
+
// the full facts once and find the freshest us-gaap tag whose NAME matches the
|
|
912
|
+
// concept keywords, preferring balance-sheet stock tags over cash-flow tags.
|
|
913
|
+
if (found.length === 0) {
|
|
914
|
+
const norm = (s: string) => s.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
915
|
+
const tokens = candidates.map(norm).filter((t) => t.length >= 3);
|
|
916
|
+
const factsRes = await fetch(`${DATA_BASE}/api/xbrl/companyfacts/CIK${paddedCik}.json`, { headers: SEC_HEADERS });
|
|
917
|
+
if (factsRes.ok && tokens.length) {
|
|
918
|
+
const facts = (await factsRes.json()) as { entityName?: string; facts?: { 'us-gaap'?: Record<string, { label?: string; description?: string; units: Record<string, ConceptDoc['units'][string]> }> } };
|
|
919
|
+
const gaap = facts.facts?.['us-gaap'] ?? {};
|
|
920
|
+
const FLOW = /^(ProceedsFrom|RepaymentsOf|PaymentsOf|IncreaseDecrease|AmortizationOf|Gain|Loss)/;
|
|
921
|
+
const matches: { isFlow: boolean; latestEnd: string; doc: ConceptDoc }[] = [];
|
|
922
|
+
for (const [tag, entry] of Object.entries(gaap)) {
|
|
923
|
+
if (!tokens.some((t) => norm(tag).includes(t))) continue;
|
|
924
|
+
let latestEnd = '';
|
|
925
|
+
for (const values of Object.values(entry.units)) {
|
|
926
|
+
for (const e of values) {
|
|
927
|
+
if (e.form !== '10-K' && e.form !== '10-K/A') continue;
|
|
928
|
+
if ((e.end ?? '') > latestEnd) latestEnd = e.end ?? '';
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
matches.push({
|
|
932
|
+
isFlow: FLOW.test(tag),
|
|
933
|
+
latestEnd,
|
|
934
|
+
doc: { cik: Number(cik), entityName: facts.entityName ?? '', tag, taxonomy: 'us-gaap', label: entry.label ?? tag, description: entry.description ?? '', units: entry.units },
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
matches.sort((a, b) => Number(a.isFlow) - Number(b.isFlow) || (b.latestEnd ?? '').localeCompare(a.latestEnd ?? ''));
|
|
938
|
+
if (matches.length) found.push({ doc: matches[0].doc, latestEnd: matches[0].latestEnd });
|
|
539
939
|
}
|
|
540
|
-
found.push({ doc, latestEnd });
|
|
541
940
|
}
|
|
542
941
|
if (found.length === 0) {
|
|
543
942
|
throw new Error(
|
|
544
|
-
`SEC EDGAR company concept error: ${lastStatus} — none of the candidate concepts ${JSON.stringify(candidates)} exist for CIK ${paddedCik}. Use edgar_company_facts({cik: "${cikOrTicker}"}) to see every concept this filer reports.`,
|
|
943
|
+
`SEC EDGAR company concept error: ${lastStatus} — none of the candidate concepts ${JSON.stringify(candidates)} exist for CIK ${paddedCik}, and no us-gaap tag matched. Use edgar_company_facts({cik: "${cikOrTicker}"}) to see every concept this filer reports.`,
|
|
545
944
|
);
|
|
546
945
|
}
|
|
547
946
|
found.sort((a, b) => (b.latestEnd ?? '').localeCompare(a.latestEnd ?? ''));
|
|
@@ -564,7 +963,7 @@ async function companyConcept(cikOrTicker: string, concept: string) {
|
|
|
564
963
|
const dedup = new Map<string, { entry: Raw; unit: string }>();
|
|
565
964
|
for (const [unit, values] of Object.entries(data.units)) {
|
|
566
965
|
for (const e of values) {
|
|
567
|
-
if (e.form
|
|
966
|
+
if (!ANNUAL_FORMS.has(e.form)) continue; // 10-K (+/A) domestic, 20-F/40-F (+/A) foreign
|
|
568
967
|
const key = `${unit}|${e.fy ?? ''}|${e.fp ?? 'FY'}|${e.end ?? ''}`;
|
|
569
968
|
const prior = dedup.get(key);
|
|
570
969
|
if (!prior || (e.filed ?? '') > (prior.entry.filed ?? '')) {
|
|
@@ -980,12 +1379,69 @@ async function callTool(name: string, args: Record<string, unknown>): Promise<un
|
|
|
980
1379
|
case 'edgar_fund_holdings':
|
|
981
1380
|
return fundHoldings(args.ticker as string, args.limit as number | undefined);
|
|
982
1381
|
case 'edgar_company_concept':
|
|
983
|
-
|
|
1382
|
+
// Accept `ticker_or_cik` too: the schema/handler use `cik` (which takes a
|
|
1383
|
+
// ticker or CIK) but the sibling tools use `ticker_or_cik` and the error
|
|
1384
|
+
// message names it — agents naturally pass it and were rejected.
|
|
1385
|
+
return companyConcept((args.cik ?? args.ticker_or_cik) as string, args.concept as string);
|
|
1386
|
+
case 'edgar_filing_documents':
|
|
1387
|
+
return filingDocuments(
|
|
1388
|
+
args.accession as string | undefined,
|
|
1389
|
+
(args.cik ?? args.ticker ?? args.ticker_or_cik) as string,
|
|
1390
|
+
args.include_primary_text as boolean | undefined,
|
|
1391
|
+
args.form_type as string | undefined,
|
|
1392
|
+
);
|
|
984
1393
|
case 'edgar_ticker_to_cik':
|
|
985
1394
|
return tickerToCik(args.ticker as string);
|
|
1395
|
+
case 'edgar_xbrl_frames':
|
|
1396
|
+
return xbrlFrames(args);
|
|
986
1397
|
default:
|
|
987
1398
|
throw new Error(`Unknown tool: ${name}`);
|
|
988
1399
|
}
|
|
989
1400
|
}
|
|
990
1401
|
|
|
1402
|
+
interface FrameDatum { cik?: number; entityName?: string; val?: number; start?: string; end?: string; fy?: number; fp?: string; form?: string }
|
|
1403
|
+
|
|
1404
|
+
async function xbrlFrames(args: Record<string, unknown>) {
|
|
1405
|
+
const concept = String(args.concept ?? '').trim().replace(/[^A-Za-z0-9]/g, '');
|
|
1406
|
+
if (!concept) throw new Error('Required argument "concept" is missing (a US-GAAP tag, e.g. "Revenues", "NetIncomeLoss", "Assets").');
|
|
1407
|
+
const period = String(args.period ?? '').trim().toUpperCase().replace(/[^A-Z0-9]/g, '');
|
|
1408
|
+
if (!/^CY\d{4}(Q[1-4]I?)?$/.test(period)) {
|
|
1409
|
+
throw new Error('Required argument "period" must be a calendar frame: "CY2023" (annual), "CY2023Q1" (quarter), or "CY2023Q1I" (instant). Got: ' + (args.period ?? ''));
|
|
1410
|
+
}
|
|
1411
|
+
const taxonomy = String(args.taxonomy ?? 'us-gaap').trim().toLowerCase() === 'dei' ? 'dei' : 'us-gaap';
|
|
1412
|
+
const unit = String(args.unit ?? 'USD').trim() || 'USD';
|
|
1413
|
+
const sortDesc = String(args.sort ?? 'desc').toLowerCase() !== 'asc';
|
|
1414
|
+
const limit = Math.min(200, Math.max(1, Number(args.limit) || 25));
|
|
1415
|
+
|
|
1416
|
+
const url = `${DATA_BASE}/api/xbrl/frames/${taxonomy}/${encodeURIComponent(concept)}/${encodeURIComponent(unit)}/${period}.json`;
|
|
1417
|
+
const res = await fetch(url, { headers: SEC_HEADERS });
|
|
1418
|
+
if (res.status === 404) {
|
|
1419
|
+
return { error: 'not_found', concept, period, unit, taxonomy, message: `No SEC frame for ${taxonomy}/${concept}/${unit}/${period}. Check the tag spelling/casing, unit (USD vs shares), and period type (add "I" for balance-sheet/instant items like Assets).` };
|
|
1420
|
+
}
|
|
1421
|
+
if (!res.ok) throw new Error(`SEC frames error: ${res.status}`);
|
|
1422
|
+
const data = (await res.json()) as { taxonomy?: string; tag?: string; uom?: string; label?: string; description?: string; data?: FrameDatum[] };
|
|
1423
|
+
const rows = (data.data ?? []).slice().sort((a, b) => sortDesc ? (b.val ?? 0) - (a.val ?? 0) : (a.val ?? 0) - (b.val ?? 0));
|
|
1424
|
+
|
|
1425
|
+
return {
|
|
1426
|
+
concept,
|
|
1427
|
+
period,
|
|
1428
|
+
unit,
|
|
1429
|
+
taxonomy,
|
|
1430
|
+
label: data.label ?? null,
|
|
1431
|
+
companies_reporting: rows.length,
|
|
1432
|
+
returned: Math.min(rows.length, limit),
|
|
1433
|
+
sort: sortDesc ? 'desc' : 'asc',
|
|
1434
|
+
source: 'SEC EDGAR XBRL frames (data.sec.gov)',
|
|
1435
|
+
note: 'Values are AS-FILED in each company\'s XBRL submission for this calendar frame. Extreme outliers are often filer reporting errors (wrong units/scale) — cross-check surprising top values with edgar_company_concept before treating a ranking as authoritative.',
|
|
1436
|
+
companies: rows.slice(0, limit).map((r) => ({
|
|
1437
|
+
cik: r.cik ?? null,
|
|
1438
|
+
entity: r.entityName ?? null,
|
|
1439
|
+
value: r.val ?? null,
|
|
1440
|
+
period_start: r.start ?? null,
|
|
1441
|
+
period_end: r.end ?? null,
|
|
1442
|
+
form: r.form ?? null,
|
|
1443
|
+
})),
|
|
1444
|
+
};
|
|
1445
|
+
}
|
|
1446
|
+
|
|
991
1447
|
export default { tools, callTool, meter: { credits: 10 } } satisfies McpToolExport;
|