@cyanheads/secedgar-mcp-server 0.13.0 → 0.14.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 +40 -9
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp-server/prompts/definitions/company-analysis.prompt.js +2 -2
- package/dist/mcp-server/prompts/definitions/company-analysis.prompt.js.map +1 -1
- package/dist/mcp-server/resources/definitions/concepts.resource.d.ts.map +1 -1
- package/dist/mcp-server/resources/definitions/concepts.resource.js +6 -1
- package/dist/mcp-server/resources/definitions/concepts.resource.js.map +1 -1
- package/dist/mcp-server/resources/definitions/filing-types.resource.d.ts.map +1 -1
- package/dist/mcp-server/resources/definitions/filing-types.resource.js +18 -2
- package/dist/mcp-server/resources/definitions/filing-types.resource.js.map +1 -1
- package/dist/mcp-server/tools/definitions/compare-companies.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/compare-companies.tool.js +27 -4
- package/dist/mcp-server/tools/definitions/compare-companies.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/find-holders.tool.d.ts +61 -0
- package/dist/mcp-server/tools/definitions/find-holders.tool.d.ts.map +1 -0
- package/dist/mcp-server/tools/definitions/find-holders.tool.js +396 -0
- package/dist/mcp-server/tools/definitions/find-holders.tool.js.map +1 -0
- package/dist/mcp-server/tools/definitions/get-filing.tool.d.ts +9 -0
- package/dist/mcp-server/tools/definitions/get-filing.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/get-filing.tool.js +91 -6
- package/dist/mcp-server/tools/definitions/get-filing.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/get-financials.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/get-financials.tool.js +39 -14
- package/dist/mcp-server/tools/definitions/get-financials.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/get-institutional-holdings.tool.d.ts +3 -2
- package/dist/mcp-server/tools/definitions/get-institutional-holdings.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/get-institutional-holdings.tool.js +11 -6
- package/dist/mcp-server/tools/definitions/get-institutional-holdings.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/get-material-events.tool.d.ts +63 -0
- package/dist/mcp-server/tools/definitions/get-material-events.tool.d.ts.map +1 -0
- package/dist/mcp-server/tools/definitions/get-material-events.tool.js +380 -0
- package/dist/mcp-server/tools/definitions/get-material-events.tool.js.map +1 -0
- package/dist/mcp-server/tools/definitions/get-snapshot.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/get-snapshot.tool.js +34 -4
- package/dist/mcp-server/tools/definitions/get-snapshot.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/search-concepts.tool.d.ts +1 -0
- package/dist/mcp-server/tools/definitions/search-concepts.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/search-concepts.tool.js +9 -1
- package/dist/mcp-server/tools/definitions/search-concepts.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/search-filings.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/search-filings.tool.js +3 -15
- package/dist/mcp-server/tools/definitions/search-filings.tool.js.map +1 -1
- package/dist/services/edgar/concept-map.d.ts +3 -1
- package/dist/services/edgar/concept-map.d.ts.map +1 -1
- package/dist/services/edgar/concept-map.js +171 -5
- package/dist/services/edgar/concept-map.js.map +1 -1
- package/dist/services/edgar/concept-series.d.ts +98 -11
- package/dist/services/edgar/concept-series.d.ts.map +1 -1
- package/dist/services/edgar/concept-series.js +186 -19
- package/dist/services/edgar/concept-series.js.map +1 -1
- package/dist/services/edgar/edgar-api-service.d.ts +7 -0
- package/dist/services/edgar/edgar-api-service.d.ts.map +1 -1
- package/dist/services/edgar/edgar-api-service.js +13 -0
- package/dist/services/edgar/edgar-api-service.js.map +1 -1
- package/dist/services/edgar/eight-k-items.d.ts +62 -0
- package/dist/services/edgar/eight-k-items.d.ts.map +1 -0
- package/dist/services/edgar/eight-k-items.js +131 -0
- package/dist/services/edgar/eight-k-items.js.map +1 -0
- package/dist/services/edgar/fiscal-periods.d.ts +10 -5
- package/dist/services/edgar/fiscal-periods.d.ts.map +1 -1
- package/dist/services/edgar/fiscal-periods.js +33 -10
- package/dist/services/edgar/fiscal-periods.js.map +1 -1
- package/dist/services/edgar/types.d.ts +39 -0
- package/dist/services/edgar/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/server.json +3 -3
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Reverse 13F lookup — which institutions reported holding an issuer.
|
|
3
|
+
* Searches the EDGAR full-text index scoped to 13F-HR within one quarter's filing
|
|
4
|
+
* window and returns the filer list, each row chaining into
|
|
5
|
+
* secedgar_get_institutional_holdings for the actual position.
|
|
6
|
+
* @module mcp-server/tools/definitions/find-holders
|
|
7
|
+
*/
|
|
8
|
+
import { tool, z } from '@cyanheads/mcp-ts-core';
|
|
9
|
+
import { JsonRpcErrorCode } from '@cyanheads/mcp-ts-core/errors';
|
|
10
|
+
import { getCanvasBridge, toDatasetField } from '../../../services/canvas-bridge/canvas-bridge.js';
|
|
11
|
+
import { cleanDisplayName, getEdgarApiService } from '../../../services/edgar/edgar-api-service.js';
|
|
12
|
+
/**
|
|
13
|
+
* Hits returned by one EFTS request. The endpoint ignores the `size` parameter
|
|
14
|
+
* and always answers with a full page, so paging is done with `from` alone.
|
|
15
|
+
*/
|
|
16
|
+
const EFTS_PAGE_SIZE = 100;
|
|
17
|
+
/**
|
|
18
|
+
* Pages fetched in one call — 500 filer rows. Measured against the Q1 2026
|
|
19
|
+
* filing window (2026-04-01 → 2026-06-30): Apple (CUSIP 037833100) draws 6,652
|
|
20
|
+
* 13F-HR filings, so the budget returns a 500-row sample with the exact total
|
|
21
|
+
* still reported and `dataset.truncated` set; Liberty Live Holdings (530909100)
|
|
22
|
+
* draws 246 and Louisiana-Pacific (546347105) 451, both complete. Five requests
|
|
23
|
+
* at ~0.4s each keeps a call around two seconds and well inside SEC's 10 req/s
|
|
24
|
+
* limit, and the list is unranked either way — a deeper sample buys no ordering
|
|
25
|
+
* the caller can use.
|
|
26
|
+
*/
|
|
27
|
+
const FETCH_PAGE_BUDGET = 5;
|
|
28
|
+
/** Days after a quarter end by which a 13F-HR is due. */
|
|
29
|
+
const FILING_DEADLINE_DAYS = 45;
|
|
30
|
+
const QUARTER_END_SUFFIX = ['03-31', '06-30', '09-30', '12-31'];
|
|
31
|
+
/** Shift a YYYY-MM-DD date by whole days, staying in UTC. */
|
|
32
|
+
function addDays(date, days) {
|
|
33
|
+
const d = new Date(`${date}T00:00:00Z`);
|
|
34
|
+
d.setUTCDate(d.getUTCDate() + days);
|
|
35
|
+
return d.toISOString().slice(0, 10);
|
|
36
|
+
}
|
|
37
|
+
/** Calendar quarter-end date (YYYY-MM-DD) for a `YYYY-QN` string. */
|
|
38
|
+
function quarterEndDate(quarter) {
|
|
39
|
+
const [year, q] = quarter.split('-Q');
|
|
40
|
+
return `${year}-${QUARTER_END_SUFFIX[Number(q) - 1]}`;
|
|
41
|
+
}
|
|
42
|
+
/** End date of the quarter after `quarter` — the close of its filing window. */
|
|
43
|
+
function nextQuarterEndDate(quarter) {
|
|
44
|
+
const [year, q] = quarter.split('-Q');
|
|
45
|
+
const n = Number(q);
|
|
46
|
+
return n === 4 ? `${Number(year) + 1}-03-31` : `${year}-${QUARTER_END_SUFFIX[n]}`;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Newest reporting quarter whose 13F-HR deadline has already passed as of
|
|
50
|
+
* `today` — the default target, so an unqualified call returns a quarter that
|
|
51
|
+
* institutions have actually filed for rather than an empty in-flight one.
|
|
52
|
+
*/
|
|
53
|
+
function latestClosedQuarter(today) {
|
|
54
|
+
const cutoff = addDays(today, -FILING_DEADLINE_DAYS);
|
|
55
|
+
const year = Number(cutoff.slice(0, 4));
|
|
56
|
+
// Suffixes ascend, so the last one at or before the cutoff is the newest closed quarter.
|
|
57
|
+
const quarter = QUARTER_END_SUFFIX.findLastIndex((suffix) => `${year}-${suffix}` <= cutoff) + 1;
|
|
58
|
+
return quarter > 0 ? `${year}-Q${quarter}` : `${year - 1}-Q4`;
|
|
59
|
+
}
|
|
60
|
+
function hitToHolder(hit) {
|
|
61
|
+
const displayName = hit._source.display_names?.[0] ?? '';
|
|
62
|
+
return {
|
|
63
|
+
accession_number: hit._source.adsh || hit._id.split(':')[0] || hit._id,
|
|
64
|
+
filer_cik: hit._source.ciks?.[0] ?? '',
|
|
65
|
+
filer_name: cleanDisplayName(displayName),
|
|
66
|
+
filing_date: hit._source.file_date,
|
|
67
|
+
form: hit._source.form ?? undefined,
|
|
68
|
+
period_ending: hit._source.period_ending ?? undefined,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
export const findHoldersTool = tool('secedgar_find_holders', {
|
|
72
|
+
title: 'Find Holders',
|
|
73
|
+
description: 'Find which institutional managers reported holding an issuer, by searching 13F-HR information tables for one reporting quarter. This is the reverse direction of secedgar_get_institutional_holdings: that tool takes a manager and returns its portfolio, this one takes an issuer and returns its managers — pass a returned filer_cik plus the same quarter to read the actual position. Searching by cusip is the precise path, matching the identifier the information table itself carries; without it the issuer name is matched as a phrase against the filing text, which both over-matches (unrelated issuers sharing a word) and under-matches (managers writing the name differently), so prefer cusip whenever one is known. A CUSIP cannot be derived from a ticker here — read one off any 13F information table returned by secedgar_get_institutional_holdings. The returned list is unranked: the search index scores by text relevance, which carries no signal about position size, and no ordering by shares or market value is available without opening each filing. Managers holding under $100M in 13(f) securities are exempt from filing at all.',
|
|
74
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
75
|
+
errors: [
|
|
76
|
+
{
|
|
77
|
+
reason: 'issuer_not_found',
|
|
78
|
+
code: JsonRpcErrorCode.NotFound,
|
|
79
|
+
when: 'No cusip was given and the issuer does not resolve to a known EDGAR company',
|
|
80
|
+
recovery: 'Confirm the issuer with secedgar_company_search, or pass the cusip parameter to search the information tables directly.',
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
reason: 'ambiguous_issuer',
|
|
84
|
+
code: JsonRpcErrorCode.ValidationError,
|
|
85
|
+
when: 'The issuer name matches several EDGAR companies and no cusip was given',
|
|
86
|
+
recovery: 'Retry with the ticker or the 10-digit CIK of the intended issuer from the matches list.',
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
input: z.object({
|
|
90
|
+
issuer: z
|
|
91
|
+
.string()
|
|
92
|
+
.trim()
|
|
93
|
+
.min(1, 'Issuer cannot be blank')
|
|
94
|
+
.describe('The portfolio company whose holders you want — a ticker ("AAPL"), a 10-digit CIK ("0000320193"), or a company name. Without cusip, this resolves to the company\'s EDGAR-conformed name and that name is phrase-matched against 13F information tables, so it must identify one company. With cusip supplied, it is used only to label the result.'),
|
|
95
|
+
cusip: z
|
|
96
|
+
.union([
|
|
97
|
+
z.literal(''),
|
|
98
|
+
z
|
|
99
|
+
.string()
|
|
100
|
+
.regex(/^[0-9A-Za-z]{9}$/, 'Expected a 9-character CUSIP or CINS identifier')
|
|
101
|
+
.describe('9-character CUSIP/CINS'),
|
|
102
|
+
])
|
|
103
|
+
.optional()
|
|
104
|
+
.describe('The issuer\'s 9-character CUSIP (e.g. "037833100" for Apple common stock; foreign issuers use a CINS starting with a letter, e.g. "H1467J104"). The precise match key — information tables identify every position by CUSIP, so this avoids the name-phrase misses. Each share class has its own CUSIP, so a multi-class issuer needs one call per class. Read a CUSIP off the holdings returned by secedgar_get_institutional_holdings.'),
|
|
105
|
+
quarter: z
|
|
106
|
+
.union([
|
|
107
|
+
z.literal(''),
|
|
108
|
+
z
|
|
109
|
+
.string()
|
|
110
|
+
.regex(/^\d{4}-Q[1-4]$/, 'Expected YYYY-QN, e.g. 2026-Q1')
|
|
111
|
+
.describe('YYYY-QN'),
|
|
112
|
+
])
|
|
113
|
+
.optional()
|
|
114
|
+
.describe('Reporting quarter to search, "YYYY-QN" (e.g. "2026-Q1"). Omit for the newest quarter whose 45-day filing deadline has passed — the applied quarter and its filing window are echoed in the response. A quarter still inside its deadline returns nothing, because the filings do not exist yet.'),
|
|
115
|
+
limit: z
|
|
116
|
+
.number()
|
|
117
|
+
.int()
|
|
118
|
+
.min(1)
|
|
119
|
+
.max(100)
|
|
120
|
+
.default(20)
|
|
121
|
+
.describe('Filer rows returned inline. The full fetched set (up to 500 rows) is materialized as a dataframe when a canvas is available. Default 20.'),
|
|
122
|
+
}),
|
|
123
|
+
output: z.object({
|
|
124
|
+
issuer: z.string().describe('The issuer input, echoed.'),
|
|
125
|
+
resolved_issuer_name: z
|
|
126
|
+
.string()
|
|
127
|
+
.optional()
|
|
128
|
+
.describe('EDGAR-conformed company name the issuer resolved to, and the phrase that was searched. Absent when cusip was supplied (no company lookup runs).'),
|
|
129
|
+
resolved_issuer_cik: z
|
|
130
|
+
.string()
|
|
131
|
+
.optional()
|
|
132
|
+
.describe('CIK of the resolved issuer, zero-padded to 10 digits. Absent when cusip was supplied.'),
|
|
133
|
+
search_mode: z
|
|
134
|
+
.enum(['cusip', 'name'])
|
|
135
|
+
.describe('Which key matched the information tables. "cusip" matches the identifier the table itself carries; "name" phrase-matches the filing text and is looser in both directions.'),
|
|
136
|
+
search_key: z.string().describe('The exact term searched — the CUSIP, or the quoted phrase.'),
|
|
137
|
+
quarter: z
|
|
138
|
+
.string()
|
|
139
|
+
.describe('Reporting quarter searched, "YYYY-QN" — the requested one, or the applied default.'),
|
|
140
|
+
filed_from: z.string().describe('Start of the filing window searched (YYYY-MM-DD).'),
|
|
141
|
+
filed_to: z.string().describe('End of the filing window searched (YYYY-MM-DD).'),
|
|
142
|
+
total_filings: z
|
|
143
|
+
.number()
|
|
144
|
+
.describe("Total 13F-HR filings matching the search key inside the filing window, as reported by the index. A slight over-count of this quarter's holders on two counts, both of which the returned rows correct for: a few percent are amendments restating an older quarter, and a few more are managers amending their own report for this quarter, which puts them in the window twice."),
|
|
145
|
+
total_is_exact: z
|
|
146
|
+
.boolean()
|
|
147
|
+
.describe('False when total_filings is a lower bound (the index capped the count).'),
|
|
148
|
+
fetched: z
|
|
149
|
+
.number()
|
|
150
|
+
.describe('Filings retrieved from the index, capped by the fetch budget of 500. Equals total_filings when the whole window fit inside the budget.'),
|
|
151
|
+
holders_in_quarter: z
|
|
152
|
+
.number()
|
|
153
|
+
.describe('Distinct managers among the fetched filings reporting this quarter as their period — the set paged by limit and materialized on the dataframe. Lower than fetched by the filings dropped as amendments restating other quarters, and by managers that amended this quarter (kept once, at their latest filing).'),
|
|
154
|
+
holders: z
|
|
155
|
+
.array(z
|
|
156
|
+
.object({
|
|
157
|
+
filer_name: z
|
|
158
|
+
.string()
|
|
159
|
+
.describe('Institutional manager that filed, with ticker/CIK parentheticals stripped.'),
|
|
160
|
+
filer_cik: z
|
|
161
|
+
.string()
|
|
162
|
+
.describe("Filer CIK, zero-padded to 10 digits. Pass as ticker_or_cik to secedgar_get_institutional_holdings for this manager's positions."),
|
|
163
|
+
accession_number: z
|
|
164
|
+
.string()
|
|
165
|
+
.describe('Accession number of the 13F-HR — pass to secedgar_get_filing.'),
|
|
166
|
+
filing_date: z.string().describe('Date the 13F-HR was submitted (YYYY-MM-DD).'),
|
|
167
|
+
form: z
|
|
168
|
+
.string()
|
|
169
|
+
.optional()
|
|
170
|
+
.describe('Form type, "13F-HR" or "13F-HR/A" for an amendment. Absent when the index carries no form tag.'),
|
|
171
|
+
})
|
|
172
|
+
.describe('One institutional manager reporting a position in this issuer.'))
|
|
173
|
+
.describe('One page of filers, capped at limit. Order carries no position-size meaning — see the ordering note.'),
|
|
174
|
+
dataset: z
|
|
175
|
+
.object({
|
|
176
|
+
name: z
|
|
177
|
+
.string()
|
|
178
|
+
.describe('Dataframe handle (df_XXXXX_XXXXX) — pass to secedgar_dataframe_query.'),
|
|
179
|
+
row_count: z.number().describe('Rows materialized in the dataframe.'),
|
|
180
|
+
expires_at: z.string().describe('ISO 8601 expiry timestamp.'),
|
|
181
|
+
truncated: z
|
|
182
|
+
.boolean()
|
|
183
|
+
.describe('True when more filers exist beyond the fetch budget — total_filings exceeds fetched.'),
|
|
184
|
+
})
|
|
185
|
+
.optional()
|
|
186
|
+
.describe('Canvas dataframe holding every fetched filer row, each carrying the issuer key and quarter so it joins across issuers and quarters. Absent when the result fits inline, canvas is unavailable, or materialization failed. Query with secedgar_dataframe_query.'),
|
|
187
|
+
}),
|
|
188
|
+
enrichment: {
|
|
189
|
+
ordering: z
|
|
190
|
+
.string()
|
|
191
|
+
.describe('How the holder list is ordered, and what that ordering does not mean.'),
|
|
192
|
+
notice: z
|
|
193
|
+
.string()
|
|
194
|
+
.optional()
|
|
195
|
+
.describe('Guidance when the search returned no filers — names the likely cause.'),
|
|
196
|
+
truncated: z.boolean().optional().describe('True when the inline holders list was capped.'),
|
|
197
|
+
shown: z.number().optional().describe('Number of filers shown inline.'),
|
|
198
|
+
cap: z.number().optional().describe('The limit cap applied.'),
|
|
199
|
+
},
|
|
200
|
+
async handler(input, ctx) {
|
|
201
|
+
const api = getEdgarApiService();
|
|
202
|
+
const cusip = input.cusip?.toUpperCase() || undefined;
|
|
203
|
+
// With a CUSIP in hand the information tables are matched directly, so no
|
|
204
|
+
// company lookup runs and the issuer input is carried through as a label.
|
|
205
|
+
let resolvedName;
|
|
206
|
+
let resolvedCik;
|
|
207
|
+
if (!cusip) {
|
|
208
|
+
const resolved = await api.resolveCik(input.issuer);
|
|
209
|
+
const candidates = Array.isArray(resolved) ? resolved : [resolved];
|
|
210
|
+
if (candidates.length > 1) {
|
|
211
|
+
const shown = candidates.slice(0, 10);
|
|
212
|
+
const list = shown.map((c) => `${c.ticker ?? c.cik} (${c.name ?? 'Unknown'})`).join(', ');
|
|
213
|
+
throw ctx.fail('ambiguous_issuer', `'${input.issuer}' matches multiple EDGAR companies: ${list}.`, {
|
|
214
|
+
...ctx.recoveryFor('ambiguous_issuer'),
|
|
215
|
+
matches: shown.map((c) => ({ cik: c.cik, name: c.name, ticker: c.ticker })),
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
const match = candidates[0];
|
|
219
|
+
// A bare numeric CIK absent from the ticker cache resolves without a name,
|
|
220
|
+
// leaving nothing to phrase-match — that is a miss, not a silent broadening.
|
|
221
|
+
if (!match?.name) {
|
|
222
|
+
throw ctx.fail('issuer_not_found', `No EDGAR company name found for issuer '${input.issuer}', so there is nothing to match against 13F information tables.`, { ...ctx.recoveryFor('issuer_not_found') });
|
|
223
|
+
}
|
|
224
|
+
resolvedName = match.name;
|
|
225
|
+
resolvedCik = match.cik;
|
|
226
|
+
}
|
|
227
|
+
const quarter = input.quarter || latestClosedQuarter(new Date().toISOString().slice(0, 10));
|
|
228
|
+
const quarterEnd = quarterEndDate(quarter);
|
|
229
|
+
// The window runs from the day after the reporting quarter to the end of the
|
|
230
|
+
// next one. It covers the 45-day deadline with room for late filers, and stops
|
|
231
|
+
// before the following quarter's originals start arriving — those report a
|
|
232
|
+
// different period and would otherwise be counted as this quarter's holders.
|
|
233
|
+
const filedFrom = addDays(quarterEnd, 1);
|
|
234
|
+
const filedTo = nextQuarterEndDate(quarter);
|
|
235
|
+
const searchKey = cusip ?? `"${resolvedName}"`;
|
|
236
|
+
const searchMode = cusip ? 'cusip' : 'name';
|
|
237
|
+
// EFTS answers with a full page regardless of the requested size, so the
|
|
238
|
+
// budget is expressed in pages and `from` walks the window.
|
|
239
|
+
/**
|
|
240
|
+
* One row per manager, keyed by filer CIK. A manager that amended its report
|
|
241
|
+
* for this same quarter files twice inside one window — 7 of Cracker Barrel's
|
|
242
|
+
* 256 Q1 2026 filings — and listing it twice would inflate a holder count by
|
|
243
|
+
* a few percent. The later filing wins: the amendment supersedes the original
|
|
244
|
+
* and its accession is the one worth reading.
|
|
245
|
+
*/
|
|
246
|
+
const byFiler = new Map();
|
|
247
|
+
const seen = new Set();
|
|
248
|
+
let total = 0;
|
|
249
|
+
let totalIsExact = true;
|
|
250
|
+
let hitsFetched = 0;
|
|
251
|
+
for (let page = 0; page < FETCH_PAGE_BUDGET; page++) {
|
|
252
|
+
const response = await api.searchFilings({
|
|
253
|
+
query: searchKey,
|
|
254
|
+
forms: ['13F-HR'],
|
|
255
|
+
startDate: filedFrom,
|
|
256
|
+
endDate: filedTo,
|
|
257
|
+
from: page * EFTS_PAGE_SIZE,
|
|
258
|
+
size: EFTS_PAGE_SIZE,
|
|
259
|
+
});
|
|
260
|
+
if (page === 0) {
|
|
261
|
+
total = response.hits.total.value;
|
|
262
|
+
totalIsExact = response.hits.total.relation === 'eq';
|
|
263
|
+
}
|
|
264
|
+
const hits = response.hits.hits;
|
|
265
|
+
hitsFetched += hits.length;
|
|
266
|
+
for (const hit of hits) {
|
|
267
|
+
const row = hitToHolder(hit);
|
|
268
|
+
// One filing can match on several documents; the filer is listed once.
|
|
269
|
+
if (seen.has(row.accession_number))
|
|
270
|
+
continue;
|
|
271
|
+
seen.add(row.accession_number);
|
|
272
|
+
// Amendments to older quarters keep arriving inside any filing window —
|
|
273
|
+
// roughly 6% of the Q1 2026 window — so the reported period is what
|
|
274
|
+
// decides membership, not the filing date.
|
|
275
|
+
if (row.period_ending !== quarterEnd)
|
|
276
|
+
continue;
|
|
277
|
+
const existing = byFiler.get(row.filer_cik);
|
|
278
|
+
if (!existing || row.filing_date > existing.filing_date) {
|
|
279
|
+
byFiler.set(row.filer_cik, row);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
if (hits.length < EFTS_PAGE_SIZE || hitsFetched >= total)
|
|
283
|
+
break;
|
|
284
|
+
}
|
|
285
|
+
const rows = [...byFiler.values()];
|
|
286
|
+
const truncated = hitsFetched < total;
|
|
287
|
+
let dataset;
|
|
288
|
+
const bridge = getCanvasBridge();
|
|
289
|
+
if (bridge && rows.length > input.limit) {
|
|
290
|
+
const registered = await bridge.registerDataframe(ctx, {
|
|
291
|
+
rows: rows.map((row) => ({
|
|
292
|
+
issuer: input.issuer,
|
|
293
|
+
issuer_cusip: cusip ?? null,
|
|
294
|
+
issuer_cik: resolvedCik ?? null,
|
|
295
|
+
quarter,
|
|
296
|
+
reporting_period: quarterEnd,
|
|
297
|
+
filer_name: row.filer_name,
|
|
298
|
+
filer_cik: row.filer_cik,
|
|
299
|
+
accession_number: row.accession_number,
|
|
300
|
+
filing_date: row.filing_date,
|
|
301
|
+
form: row.form ?? null,
|
|
302
|
+
})),
|
|
303
|
+
sourceTool: 'secedgar_find_holders',
|
|
304
|
+
queryParams: {
|
|
305
|
+
issuer: input.issuer,
|
|
306
|
+
cusip,
|
|
307
|
+
quarter,
|
|
308
|
+
filed_from: filedFrom,
|
|
309
|
+
filed_to: filedTo,
|
|
310
|
+
search_mode: searchMode,
|
|
311
|
+
},
|
|
312
|
+
truncated,
|
|
313
|
+
});
|
|
314
|
+
if (registered)
|
|
315
|
+
dataset = { ...toDatasetField(registered), truncated };
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* The budget slices the window in relevance order, so once it binds the
|
|
319
|
+
* result is a sample rather than a short list — an aggregate over it is an
|
|
320
|
+
* aggregate over that sample, which is worth saying before someone counts it.
|
|
321
|
+
*/
|
|
322
|
+
const sampleNote = truncated
|
|
323
|
+
? ` The fetch budget stopped at ${hitsFetched} of ${total} matching filings, and the index served them in that same relevance order, so this is a sample of the holders rather than the first ones by any measure.`
|
|
324
|
+
: '';
|
|
325
|
+
ctx.enrich({
|
|
326
|
+
ordering: `Filers are listed in EDGAR search-relevance order, which reflects text match strength only — it is not a ranking by shares held or market value.${sampleNote} Read a manager's actual position with secedgar_get_institutional_holdings using its filer_cik and quarter "${quarter}".`,
|
|
327
|
+
});
|
|
328
|
+
const searchedTerm = searchMode === 'cusip' ? `CUSIP ${searchKey}` : `the phrase ${searchKey}`;
|
|
329
|
+
if (total === 0) {
|
|
330
|
+
const deadline = addDays(quarterEnd, FILING_DEADLINE_DAYS);
|
|
331
|
+
const cause = deadline > new Date().toISOString().slice(0, 10)
|
|
332
|
+
? `13F-HR filings for ${quarter} are not due until ${deadline}, so few or none exist yet — search an earlier quarter.`
|
|
333
|
+
: searchMode === 'cusip'
|
|
334
|
+
? 'Verify the CUSIP identifies a 13(f)-reportable security and covers the intended share class, or drop cusip to phrase-match the issuer name instead.'
|
|
335
|
+
: 'The issuer name is phrase-matched against filing text and managers may write it differently — pass cusip for an exact match, read one off any secedgar_get_institutional_holdings result.';
|
|
336
|
+
ctx.enrich.notice(`No 13F-HR filings matched ${searchedTerm} filed ${filedFrom} to ${filedTo}. ${cause}`);
|
|
337
|
+
}
|
|
338
|
+
else if (rows.length === 0) {
|
|
339
|
+
ctx.enrich.notice(`${total} 13F-HR filings matched ${searchedTerm} in the ${filedFrom} to ${filedTo} window, but none of the ${hitsFetched} fetched report ${quarter} (period ending ${quarterEnd}) — they are amendments restating other quarters. Search the quarter those amendments cover, or a later one.`);
|
|
340
|
+
}
|
|
341
|
+
else if (rows.length > input.limit) {
|
|
342
|
+
ctx.enrich.truncated({
|
|
343
|
+
shown: input.limit,
|
|
344
|
+
cap: input.limit,
|
|
345
|
+
guidance: truncated
|
|
346
|
+
? `${total} filings matched in the window; the fetch budget of ${FETCH_PAGE_BUDGET * EFTS_PAGE_SIZE} retrieved ${hitsFetched}, resolving to ${rows.length} managers reporting ${quarter}. Query that set with secedgar_dataframe_query, or narrow to a single share class with cusip.`
|
|
347
|
+
: 'Query the full matched set with secedgar_dataframe_query.',
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
ctx.log.info('Holder search completed', {
|
|
351
|
+
issuer: input.issuer,
|
|
352
|
+
searchMode,
|
|
353
|
+
quarter,
|
|
354
|
+
total,
|
|
355
|
+
fetched: hitsFetched,
|
|
356
|
+
inQuarter: rows.length,
|
|
357
|
+
datasetName: dataset?.name,
|
|
358
|
+
});
|
|
359
|
+
return {
|
|
360
|
+
issuer: input.issuer,
|
|
361
|
+
resolved_issuer_name: resolvedName,
|
|
362
|
+
resolved_issuer_cik: resolvedCik,
|
|
363
|
+
search_mode: searchMode,
|
|
364
|
+
search_key: searchKey,
|
|
365
|
+
quarter,
|
|
366
|
+
filed_from: filedFrom,
|
|
367
|
+
filed_to: filedTo,
|
|
368
|
+
total_filings: total,
|
|
369
|
+
total_is_exact: totalIsExact,
|
|
370
|
+
fetched: hitsFetched,
|
|
371
|
+
holders_in_quarter: rows.length,
|
|
372
|
+
holders: rows.slice(0, input.limit),
|
|
373
|
+
dataset,
|
|
374
|
+
};
|
|
375
|
+
},
|
|
376
|
+
format: (result) => {
|
|
377
|
+
const lines = [
|
|
378
|
+
`**13F holders of ${result.issuer}** — ${result.quarter}, filed ${result.filed_from} to ${result.filed_to}`,
|
|
379
|
+
];
|
|
380
|
+
if (result.resolved_issuer_name) {
|
|
381
|
+
lines.push(`Resolved issuer: ${result.resolved_issuer_name} (CIK ${result.resolved_issuer_cik})`);
|
|
382
|
+
}
|
|
383
|
+
lines.push(`Matched by ${result.search_mode}: ${result.search_key}`, `${result.total_filings} filings in the window (${result.total_is_exact ? 'exact count' : 'lower bound'}); ${result.fetched} fetched, ${result.holders_in_quarter} managers reporting ${result.quarter}, ${result.holders.length} shown`);
|
|
384
|
+
for (const h of result.holders) {
|
|
385
|
+
lines.push(`- ${h.filer_name} (CIK ${h.filer_cik}) — ${h.form ?? '13F-HR'} ${h.filing_date} [${h.accession_number}]`);
|
|
386
|
+
}
|
|
387
|
+
if (result.dataset) {
|
|
388
|
+
const truncatedNote = result.dataset.truncated
|
|
389
|
+
? ' (truncated — more filers exist beyond the fetch budget)'
|
|
390
|
+
: '';
|
|
391
|
+
lines.push(`\nDataset: ${result.dataset.name} (${result.dataset.row_count} rows, expires ${result.dataset.expires_at})${truncatedNote} — query with secedgar_dataframe_query.`);
|
|
392
|
+
}
|
|
393
|
+
return [{ type: 'text', text: lines.join('\n') }];
|
|
394
|
+
},
|
|
395
|
+
});
|
|
396
|
+
//# sourceMappingURL=find-holders.tool.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"find-holders.tool.js","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/find-holders.tool.ts"],"names":[],"mappings":"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"}
|
|
@@ -27,24 +27,28 @@ export declare const getFilingTool: import("@cyanheads/mcp-ts-core").ToolDefinit
|
|
|
27
27
|
type: z.ZodString;
|
|
28
28
|
description: z.ZodOptional<z.ZodString>;
|
|
29
29
|
size: z.ZodOptional<z.ZodNumber>;
|
|
30
|
+
binary: z.ZodOptional<z.ZodBoolean>;
|
|
30
31
|
}, z.core.$strip>>;
|
|
31
32
|
exhibits: z.ZodArray<z.ZodObject<{
|
|
32
33
|
name: z.ZodString;
|
|
33
34
|
type: z.ZodString;
|
|
34
35
|
description: z.ZodOptional<z.ZodString>;
|
|
35
36
|
size: z.ZodOptional<z.ZodNumber>;
|
|
37
|
+
binary: z.ZodOptional<z.ZodBoolean>;
|
|
36
38
|
}, z.core.$strip>>;
|
|
37
39
|
auxiliary: z.ZodArray<z.ZodObject<{
|
|
38
40
|
name: z.ZodString;
|
|
39
41
|
type: z.ZodString;
|
|
40
42
|
description: z.ZodOptional<z.ZodString>;
|
|
41
43
|
size: z.ZodOptional<z.ZodNumber>;
|
|
44
|
+
binary: z.ZodOptional<z.ZodBoolean>;
|
|
42
45
|
}, z.core.$strip>>;
|
|
43
46
|
xbrl: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
44
47
|
name: z.ZodString;
|
|
45
48
|
type: z.ZodString;
|
|
46
49
|
description: z.ZodOptional<z.ZodString>;
|
|
47
50
|
size: z.ZodOptional<z.ZodNumber>;
|
|
51
|
+
binary: z.ZodOptional<z.ZodBoolean>;
|
|
48
52
|
}, z.core.$strip>>>;
|
|
49
53
|
}, z.core.$strip>;
|
|
50
54
|
content: z.ZodString;
|
|
@@ -66,6 +70,11 @@ export declare const getFilingTool: import("@cyanheads/mcp-ts-core").ToolDefinit
|
|
|
66
70
|
readonly code: JsonRpcErrorCode.NotFound;
|
|
67
71
|
readonly when: "Filing index lists items but no fetchable primary document was found";
|
|
68
72
|
readonly recovery: "Set document to one of the filenames listed in the error message.";
|
|
73
|
+
}, {
|
|
74
|
+
readonly reason: "binary_document";
|
|
75
|
+
readonly code: JsonRpcErrorCode.ValidationError;
|
|
76
|
+
readonly when: "The requested document is a binary entry (scanned image, PDF, archive) with no text to return";
|
|
77
|
+
readonly recovery: "Pick a text document (.htm, .txt, .xml) from the list in the error message.";
|
|
69
78
|
}, {
|
|
70
79
|
readonly reason: "filing_not_found";
|
|
71
80
|
readonly code: JsonRpcErrorCode.NotFound;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"get-filing.tool.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/get-filing.tool.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAQ,CAAC,EAAE,MAAM,wBAAwB,CAAC;AACjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;
|
|
1
|
+
{"version":3,"file":"get-filing.tool.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/get-filing.tool.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAQ,CAAC,EAAE,MAAM,wBAAwB,CAAC;AACjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AA+GjE,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAkcxB,CAAC"}
|
|
@@ -30,12 +30,16 @@ const documentEntrySchema = z
|
|
|
30
30
|
name: z.string().describe('Document filename within the filing archive.'),
|
|
31
31
|
type: z
|
|
32
32
|
.string()
|
|
33
|
-
.describe('SEC document type from the submission header (e.g., "10-K", "EX-21.1", "GRAPHIC", "XML"). When the submission header is unavailable, falls back to a label inferred from the filename: known XBRL artifacts ("XBRL-LINKBASE", "XBRL-INSTANCE", etc.), "exhibit" for common exhibit filename patterns (ex-21.htm, exhibit21, dex991), and "unknown" for everything else.'),
|
|
33
|
+
.describe('SEC document type from the submission header (e.g., "10-K", "EX-21.1", "GRAPHIC", "XML"). When the submission header is unavailable, falls back to a label inferred from the filename: known XBRL artifacts ("XBRL-LINKBASE", "XBRL-INSTANCE", etc.), "exhibit" for common exhibit filename patterns (ex-21.htm, exhibit21, dex991), "GRAPHIC"/"PDF"/"BINARY" for known binary file extensions, and "unknown" for everything else.'),
|
|
34
34
|
description: z
|
|
35
35
|
.string()
|
|
36
36
|
.optional()
|
|
37
37
|
.describe('Human-readable description (e.g., "Annual Report", "Subsidiaries of the Registrant"). Absent when SEC published none for this entry.'),
|
|
38
38
|
size: z.number().optional().describe('File size in bytes.'),
|
|
39
|
+
binary: z
|
|
40
|
+
.boolean()
|
|
41
|
+
.optional()
|
|
42
|
+
.describe('Present and true when the entry holds binary bytes — a scanned page or logo, a PDF exhibit, a packaged archive or spreadsheet. These cannot be converted to text and are rejected by the document input. Absent for readable entries.'),
|
|
39
43
|
})
|
|
40
44
|
.describe('One document entry from the filing.');
|
|
41
45
|
export const getFilingTool = tool('secedgar_get_filing', {
|
|
@@ -54,6 +58,12 @@ export const getFilingTool = tool('secedgar_get_filing', {
|
|
|
54
58
|
when: 'Filing index lists items but no fetchable primary document was found',
|
|
55
59
|
recovery: 'Set document to one of the filenames listed in the error message.',
|
|
56
60
|
},
|
|
61
|
+
{
|
|
62
|
+
reason: 'binary_document',
|
|
63
|
+
code: JsonRpcErrorCode.ValidationError,
|
|
64
|
+
when: 'The requested document is a binary entry (scanned image, PDF, archive) with no text to return',
|
|
65
|
+
recovery: 'Pick a text document (.htm, .txt, .xml) from the list in the error message.',
|
|
66
|
+
},
|
|
57
67
|
{
|
|
58
68
|
reason: 'filing_not_found',
|
|
59
69
|
code: JsonRpcErrorCode.NotFound,
|
|
@@ -93,7 +103,7 @@ export const getFilingTool = tool('secedgar_get_filing', {
|
|
|
93
103
|
document: z
|
|
94
104
|
.string()
|
|
95
105
|
.optional()
|
|
96
|
-
.describe('Specific document filename within the filing (e.g., "ex-21.htm" for subsidiaries list). Default: the primary document. Available documents listed in the response metadata.'),
|
|
106
|
+
.describe('Specific document filename within the filing (e.g., "ex-21.htm" for subsidiaries list). Default: the primary document. Available documents are listed in the response metadata under documents; entries marked binary hold no text and are rejected.'),
|
|
97
107
|
include_xbrl: z
|
|
98
108
|
.boolean()
|
|
99
109
|
.default(false)
|
|
@@ -152,7 +162,7 @@ export const getFilingTool = tool('secedgar_get_filing', {
|
|
|
152
162
|
.optional()
|
|
153
163
|
.describe('XBRL viewer artifacts and machine-readable taxonomy files. Only present when include_xbrl=true.'),
|
|
154
164
|
})
|
|
155
|
-
.describe('Filing documents grouped by category.
|
|
165
|
+
.describe('Filing documents grouped by category. Every name is a valid document input EXCEPT entries carrying binary: true — scanned pages, PDFs, packaged archives and spreadsheets, which hold no text and are rejected with a binary_document error. Scans can outnumber readable documents in a filing, so read the flag before picking a name. XBRL viewer artifacts are suppressed by default; setting include_xbrl=true surfaces them under the xbrl bucket.'),
|
|
156
166
|
content: z.string().describe('Document text content for this page window.'),
|
|
157
167
|
content_truncated: z.boolean().describe('True if content was truncated at content_limit.'),
|
|
158
168
|
content_total_length: z.number().describe('Full document length before any truncation.'),
|
|
@@ -210,6 +220,19 @@ export const getFilingTool = tool('secedgar_get_filing', {
|
|
|
210
220
|
recovery: { hint },
|
|
211
221
|
});
|
|
212
222
|
}
|
|
223
|
+
if (resolved.kind === 'binary_document') {
|
|
224
|
+
const catalogBlock = renderDocumentCatalog(resolved.documents);
|
|
225
|
+
const primaryName = resolved.documents.primary[0]?.name;
|
|
226
|
+
const hint = primaryName
|
|
227
|
+
? `Use document="${primaryName}" (the primary) or another entry not marked binary. Binary entries hold image, PDF, or archive bytes and have no text to return.`
|
|
228
|
+
: 'Pick an entry not marked binary from the list above — binary entries hold image, PDF, or archive bytes and have no text to return.';
|
|
229
|
+
throw ctx.fail('binary_document', `Document '${resolved.requestedDocument}' is a ${resolved.documentType} entry and holds no readable text.${catalogBlock}`, {
|
|
230
|
+
requested_document: resolved.requestedDocument,
|
|
231
|
+
document_type: resolved.documentType,
|
|
232
|
+
documents: resolved.documents,
|
|
233
|
+
recovery: { hint },
|
|
234
|
+
});
|
|
235
|
+
}
|
|
213
236
|
if (resolved.kind === 'no_documents') {
|
|
214
237
|
const catalogBlock = renderDocumentCatalog(resolved.documents);
|
|
215
238
|
throw ctx.fail('no_documents', `No primary document found in filing ${accn}.${catalogBlock}`, {
|
|
@@ -388,8 +411,16 @@ function renderDocumentCatalog(documents) {
|
|
|
388
411
|
const addCategory = (label, entries) => {
|
|
389
412
|
if (entries.length === 0)
|
|
390
413
|
return;
|
|
391
|
-
|
|
392
|
-
|
|
414
|
+
/**
|
|
415
|
+
* Readable entries first. A sample this small is otherwise consumed by scan
|
|
416
|
+
* images on filings that carry hundreds of them, leaving the caller with a
|
|
417
|
+
* recovery list of names it cannot actually pass back. Stable sort, so order
|
|
418
|
+
* within each half is the filing index's own.
|
|
419
|
+
*/
|
|
420
|
+
const shown = [...entries]
|
|
421
|
+
.sort((a, b) => Number(a.binary ?? false) - Number(b.binary ?? false))
|
|
422
|
+
.slice(0, MAX_DOCUMENTS_IN_ERROR);
|
|
423
|
+
const names = shown.map((d) => `${d.name} [${d.type}${d.binary ? ', binary' : ''}]`).join(', ');
|
|
393
424
|
const omitted = entries.length - shown.length;
|
|
394
425
|
const tail = omitted > 0 ? ` (+${omitted} more)` : '';
|
|
395
426
|
sections.push(`${label} (${entries.length} total): ${names}${tail}`);
|
|
@@ -432,6 +463,26 @@ async function resolveFilingArchive(api, accessionNumber, providedCik, requested
|
|
|
432
463
|
const targetName = requestedDocument ?? filingPrimaryName;
|
|
433
464
|
if (!items.some((item) => item.name === targetName))
|
|
434
465
|
continue;
|
|
466
|
+
/**
|
|
467
|
+
* Reject a binary target BEFORE the body is fetched. `filingToExtract` runs
|
|
468
|
+
* `html-to-text` over whatever bytes arrive and never inspects a content
|
|
469
|
+
* type, so a `.jpg` would come back as its own decoded payload with a
|
|
470
|
+
* plausible length and no error (#96). The check is filename-only — the
|
|
471
|
+
* canonical GRAPHIC type lives in the submission header, which this path
|
|
472
|
+
* has not fetched yet, and requiring it here would trade the parallel
|
|
473
|
+
* header/submissions fetch for a sequential one on every call.
|
|
474
|
+
*/
|
|
475
|
+
const binaryType = binaryTypeFromName(targetName);
|
|
476
|
+
if (binaryType) {
|
|
477
|
+
const documents = categorizeDocuments(items, filingPrimaryName, null, false);
|
|
478
|
+
return {
|
|
479
|
+
ok: false,
|
|
480
|
+
kind: 'binary_document',
|
|
481
|
+
requestedDocument: targetName,
|
|
482
|
+
documentType: binaryType,
|
|
483
|
+
documents,
|
|
484
|
+
};
|
|
485
|
+
}
|
|
435
486
|
const html = await api.tryGetFilingDocument(cik, accessionNumber, targetName);
|
|
436
487
|
if (!html)
|
|
437
488
|
continue;
|
|
@@ -520,6 +571,7 @@ function categorizeDocuments(items, primaryName, headers, includeXbrl) {
|
|
|
520
571
|
type,
|
|
521
572
|
description: header?.description,
|
|
522
573
|
size: item.size ? Number.parseInt(item.size, 10) || undefined : undefined,
|
|
574
|
+
...(isBinaryDocument(item.name, type) ? { binary: true } : {}),
|
|
523
575
|
};
|
|
524
576
|
if (item.name === primaryName) {
|
|
525
577
|
primary.push(entry);
|
|
@@ -565,6 +617,35 @@ function isXbrlArtifact(name, type) {
|
|
|
565
617
|
* `d123456dex991.htm` / `aapl-20230930xex21d1.htm` (printer "dex"/"xex" + number).
|
|
566
618
|
*/
|
|
567
619
|
const EXHIBIT_NAME_PATTERN = /(?:^|[^a-z])ex[-_.]?\d|exhibit[-_.]?\d|\d[dx]ex[-_.]?\d/i;
|
|
620
|
+
/**
|
|
621
|
+
* Binary file extensions EDGAR filings carry, mapped to the type label reported
|
|
622
|
+
* for them. Images use SEC's own canonical TYPE (`GRAPHIC`) so header-derived and
|
|
623
|
+
* inferred types read identically. The set is deliberately extension-driven: it
|
|
624
|
+
* is the only signal available before the document body is fetched, and it works
|
|
625
|
+
* for a binary entry in any bucket — a PDF exhibit is typed `EX-99.*` in the
|
|
626
|
+
* submission header and lands under exhibits, not among the graphics.
|
|
627
|
+
*/
|
|
628
|
+
const BINARY_EXTENSIONS = [
|
|
629
|
+
[/\.(?:jpe?g|gif|png|bmp|tiff?)$/i, 'GRAPHIC'],
|
|
630
|
+
[/\.pdf$/i, 'PDF'],
|
|
631
|
+
[/\.(?:zip|xlsx?)$/i, 'BINARY'],
|
|
632
|
+
];
|
|
633
|
+
/** Type label for a binary filename, or undefined when the name reads as text. */
|
|
634
|
+
function binaryTypeFromName(name) {
|
|
635
|
+
for (const [pattern, label] of BINARY_EXTENSIONS) {
|
|
636
|
+
if (pattern.test(name))
|
|
637
|
+
return label;
|
|
638
|
+
}
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
/**
|
|
642
|
+
* Whether an entry holds bytes rather than text. The filename is authoritative
|
|
643
|
+
* for the fetch guard; the header type adds the cases a filer named oddly, since
|
|
644
|
+
* SEC types every scanned page `GRAPHIC` regardless of extension.
|
|
645
|
+
*/
|
|
646
|
+
function isBinaryDocument(name, type) {
|
|
647
|
+
return binaryTypeFromName(name) !== undefined || type.toUpperCase() === 'GRAPHIC';
|
|
648
|
+
}
|
|
568
649
|
/** Fallback type label when the submission header is unavailable. */
|
|
569
650
|
function inferTypeFromName(name) {
|
|
570
651
|
if (/^R\d+\.htm$/i.test(name))
|
|
@@ -587,7 +668,10 @@ function inferTypeFromName(name) {
|
|
|
587
668
|
return 'FINANCIAL-REPORT';
|
|
588
669
|
if (EXHIBIT_NAME_PATTERN.test(name))
|
|
589
670
|
return 'exhibit';
|
|
590
|
-
|
|
671
|
+
// Last, so the named XBRL artifacts and exhibit filename patterns above keep
|
|
672
|
+
// their more specific labels. An entry's `binary` flag is derived separately,
|
|
673
|
+
// so a PDF exhibit is still marked unreadable while reading as an exhibit.
|
|
674
|
+
return binaryTypeFromName(name) ?? 'unknown';
|
|
591
675
|
}
|
|
592
676
|
function formatDocumentSection(docs) {
|
|
593
677
|
const sections = [];
|
|
@@ -630,6 +714,7 @@ function formatDocList(entries) {
|
|
|
630
714
|
.map((d) => {
|
|
631
715
|
const tail = [
|
|
632
716
|
d.type,
|
|
717
|
+
d.binary ? 'binary' : null,
|
|
633
718
|
d.size !== undefined ? `${d.size}B` : null,
|
|
634
719
|
d.description ? `"${d.description}"` : null,
|
|
635
720
|
]
|