@cyanheads/secedgar-mcp-server 0.12.3 → 0.12.5
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/Dockerfile +1 -1
- package/LICENSE +1 -1
- package/README.md +10 -7
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp-server/prompts/definitions/company-analysis.prompt.js +1 -1
- package/dist/mcp-server/prompts/definitions/company-analysis.prompt.js.map +1 -1
- package/dist/mcp-server/tools/definitions/fetch-frames.tool.d.ts +7 -3
- package/dist/mcp-server/tools/definitions/fetch-frames.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/fetch-frames.tool.js +36 -5
- package/dist/mcp-server/tools/definitions/fetch-frames.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/get-filing.tool.d.ts +2 -2
- package/dist/mcp-server/tools/definitions/get-filing.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/get-filing.tool.js +87 -11
- package/dist/mcp-server/tools/definitions/get-filing.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/get-financials.tool.d.ts +3 -3
- package/dist/mcp-server/tools/definitions/get-financials.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/get-financials.tool.js +10 -2
- package/dist/mcp-server/tools/definitions/get-financials.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/get-insider-transactions.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/get-insider-transactions.tool.js +19 -2
- package/dist/mcp-server/tools/definitions/get-insider-transactions.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/get-institutional-holdings.tool.d.ts +6 -3
- 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 +33 -5
- package/dist/mcp-server/tools/definitions/get-institutional-holdings.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/search-concepts.tool.d.ts +8 -8
- package/dist/mcp-server/tools/definitions/search-filings.tool.d.ts +29 -2
- package/dist/mcp-server/tools/definitions/search-filings.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/search-filings.tool.js +356 -28
- package/dist/mcp-server/tools/definitions/search-filings.tool.js.map +1 -1
- package/dist/services/canvas-bridge/canvas-bridge.d.ts.map +1 -1
- package/dist/services/canvas-bridge/canvas-bridge.js.map +1 -1
- package/dist/services/edgar/edgar-api-service.d.ts +34 -1
- package/dist/services/edgar/edgar-api-service.d.ts.map +1 -1
- package/dist/services/edgar/edgar-api-service.js +79 -0
- package/dist/services/edgar/edgar-api-service.js.map +1 -1
- package/dist/services/edgar/mirror/edgar-mirror.d.ts.map +1 -1
- package/dist/services/edgar/types.d.ts +24 -0
- package/dist/services/edgar/types.d.ts.map +1 -1
- package/package.json +9 -9
- package/server.json +3 -3
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview
|
|
2
|
+
* @fileoverview Search EDGAR filings since 1993 — EFTS full-text for 2001-present,
|
|
3
|
+
* archive-backed browse (submissions history + quarterly full-index) for pre-2001
|
|
4
|
+
* date ranges.
|
|
3
5
|
* @module mcp-server/tools/definitions/search-filings
|
|
4
6
|
*/
|
|
5
7
|
import { z } from '@cyanheads/mcp-ts-core';
|
|
@@ -12,8 +14,8 @@ export declare const searchFilingsTool: import("@cyanheads/mcp-ts-core").ToolDef
|
|
|
12
14
|
limit: z.ZodDefault<z.ZodNumber>;
|
|
13
15
|
offset: z.ZodDefault<z.ZodNumber>;
|
|
14
16
|
sort: z.ZodDefault<z.ZodEnum<{
|
|
15
|
-
filing_date_desc: "filing_date_desc";
|
|
16
17
|
filing_date_asc: "filing_date_asc";
|
|
18
|
+
filing_date_desc: "filing_date_desc";
|
|
17
19
|
relevance: "relevance";
|
|
18
20
|
}>>;
|
|
19
21
|
}, z.core.$strip>, z.ZodObject<{
|
|
@@ -30,6 +32,11 @@ export declare const searchFilingsTool: import("@cyanheads/mcp-ts-core").ToolDef
|
|
|
30
32
|
file_description: z.ZodOptional<z.ZodString>;
|
|
31
33
|
sic: z.ZodOptional<z.ZodString>;
|
|
32
34
|
location: z.ZodOptional<z.ZodString>;
|
|
35
|
+
source: z.ZodOptional<z.ZodEnum<{
|
|
36
|
+
efts: "efts";
|
|
37
|
+
"full-index": "full-index";
|
|
38
|
+
submissions: "submissions";
|
|
39
|
+
}>>;
|
|
33
40
|
}, z.core.$strip>>;
|
|
34
41
|
form_distribution: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNumber>>;
|
|
35
42
|
dataset: z.ZodOptional<z.ZodObject<{
|
|
@@ -53,11 +60,31 @@ export declare const searchFilingsTool: import("@cyanheads/mcp-ts-core").ToolDef
|
|
|
53
60
|
readonly code: JsonRpcErrorCode.ValidationError;
|
|
54
61
|
readonly when: "A cik: targeting token in the query is not a 1-10 digit number";
|
|
55
62
|
readonly recovery: "Pass a numeric CIK such as cik:320193 — find it with secedgar_company_search if unknown.";
|
|
63
|
+
}, {
|
|
64
|
+
readonly reason: "entity_not_found";
|
|
65
|
+
readonly code: JsonRpcErrorCode.NotFound;
|
|
66
|
+
readonly when: "A cik: targeting token on a pre-2001 date range names a CIK with no EDGAR submissions history";
|
|
67
|
+
readonly recovery: "Confirm the CIK with secedgar_company_search — an accession-number prefix names the filing agent, not the issuer.";
|
|
56
68
|
}, {
|
|
57
69
|
readonly reason: "missing_criteria";
|
|
58
70
|
readonly code: JsonRpcErrorCode.ValidationError;
|
|
59
71
|
readonly when: "Neither a full-text query nor a forms filter was provided (a date range cannot stand alone)";
|
|
60
72
|
readonly recovery: "Provide search terms, or browse by form type (forms: [\"S-1\"]) or entity (ticker:AAPL / cik:320193 in the query) — optionally narrowed by date.";
|
|
73
|
+
}, {
|
|
74
|
+
readonly reason: "straddling_date_range";
|
|
75
|
+
readonly code: JsonRpcErrorCode.ValidationError;
|
|
76
|
+
readonly when: "The date range crosses 2001-01-01, spanning both the EFTS full-text era and the pre-2001 archives";
|
|
77
|
+
readonly recovery: "Split at 2001-01-01: run one search for the ≥2001 side and another for the <2001 side, then combine the results.";
|
|
78
|
+
}, {
|
|
79
|
+
readonly reason: "pre2001_full_text_unscoped";
|
|
80
|
+
readonly code: JsonRpcErrorCode.ValidationError;
|
|
81
|
+
readonly when: "A pre-2001 date range carries free-text terms with no entity scope — no pre-2001 full-text index exists";
|
|
82
|
+
readonly recovery: "Add ticker: or cik: entity scope to the query, or drop the text terms to browse pre-2001 filings by form and date.";
|
|
83
|
+
}, {
|
|
84
|
+
readonly reason: "pre2001_full_text_scoped";
|
|
85
|
+
readonly code: JsonRpcErrorCode.ValidationError;
|
|
86
|
+
readonly when: "A pre-2001 date range carries free-text terms alongside entity scope — local document matching is not yet supported";
|
|
87
|
+
readonly recovery: "Drop the text terms to browse this entity's pre-2001 filings by form and date.";
|
|
61
88
|
}], {
|
|
62
89
|
readonly effectiveQuery: z.ZodString;
|
|
63
90
|
readonly notice: z.ZodOptional<z.ZodString>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"search-filings.tool.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/search-filings.tool.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"search-filings.tool.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/search-filings.tool.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAsB,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,EACL,gBAAgB,EAIjB,MAAM,+BAA+B,CAAC;AA6cvC,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA+f5B,CAAC"}
|
|
@@ -1,11 +1,33 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview
|
|
2
|
+
* @fileoverview Search EDGAR filings since 1993 — EFTS full-text for 2001-present,
|
|
3
|
+
* archive-backed browse (submissions history + quarterly full-index) for pre-2001
|
|
4
|
+
* date ranges.
|
|
3
5
|
* @module mcp-server/tools/definitions/search-filings
|
|
4
6
|
*/
|
|
5
7
|
import { tool, z } from '@cyanheads/mcp-ts-core';
|
|
6
|
-
import { JsonRpcErrorCode, validationError } from '@cyanheads/mcp-ts-core/errors';
|
|
8
|
+
import { JsonRpcErrorCode, McpError, notFound, validationError, } from '@cyanheads/mcp-ts-core/errors';
|
|
7
9
|
import { getCanvasBridge, toDatasetField } from '../../../services/canvas-bridge/canvas-bridge.js';
|
|
8
|
-
import { getEdgarApiService } from '../../../services/edgar/edgar-api-service.js';
|
|
10
|
+
import { getEdgarApiService, quartersInRange, selectArchivePages, } from '../../../services/edgar/edgar-api-service.js';
|
|
11
|
+
/**
|
|
12
|
+
* The EFTS full-text index only covers filings from this date onward, even
|
|
13
|
+
* though the archives reach 1993. A date range entirely before this floor is
|
|
14
|
+
* routed to the archive-backed paths; a range straddling it is rejected (#77).
|
|
15
|
+
*/
|
|
16
|
+
const EFTS_FULLTEXT_FLOOR = '2001-01-01';
|
|
17
|
+
/**
|
|
18
|
+
* Cap on submissions archive pages fetched in one call (pre-2001 entity-scoped
|
|
19
|
+
* arm) — bounds latency and the rate-limited request budget. Mirrors the same
|
|
20
|
+
* constant in company-search.tool.ts. Pre-2001 pages are a filer's oldest, so
|
|
21
|
+
* the cap rarely binds; when it does, `dataset.truncated` discloses it.
|
|
22
|
+
*/
|
|
23
|
+
const ARCHIVE_PAGE_SCAN_CAP = 10;
|
|
24
|
+
/**
|
|
25
|
+
* Cap on quarterly full-index files fetched in one unscoped pre-2001 browse.
|
|
26
|
+
* Each `master.idx` is a multi-MB whole-quarter download and is not
|
|
27
|
+
* form-filterable server-side, so this arm is the heaviest — the cap bounds it
|
|
28
|
+
* hard, and `dataset.truncated` discloses when older quarters went unscanned.
|
|
29
|
+
*/
|
|
30
|
+
const FULL_INDEX_QUARTER_SCAN_CAP = 8;
|
|
9
31
|
function eftsHitToRow(hit) {
|
|
10
32
|
const displayName = hit._source.display_names?.[0] || '';
|
|
11
33
|
return {
|
|
@@ -19,8 +41,47 @@ function eftsHitToRow(hit) {
|
|
|
19
41
|
file_description: hit._source.file_description ?? null,
|
|
20
42
|
sic: hit._source.sics?.[0] ?? null,
|
|
21
43
|
location: hit._source.biz_locations?.[0] ?? null,
|
|
44
|
+
source: 'efts',
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/** Map a fully-keyed {@link SearchRow} to an inline output row (nulls → omitted optionals). */
|
|
48
|
+
function toInlineResult(row) {
|
|
49
|
+
return {
|
|
50
|
+
accession_number: row.accession_number,
|
|
51
|
+
...(row.form != null && { form: row.form }),
|
|
52
|
+
filing_date: row.filing_date,
|
|
53
|
+
...(row.period_ending != null && { period_ending: row.period_ending }),
|
|
54
|
+
company_name: row.company_name,
|
|
55
|
+
cik: row.cik ?? '',
|
|
56
|
+
...(row.ticker != null && { ticker: row.ticker }),
|
|
57
|
+
...(row.file_description != null && { file_description: row.file_description }),
|
|
58
|
+
...(row.sic != null && { sic: row.sic }),
|
|
59
|
+
...(row.location != null && { location: row.location }),
|
|
60
|
+
source: row.source,
|
|
22
61
|
};
|
|
23
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* True when a filing's form matches a requested form, counting amendments
|
|
65
|
+
* (requested "10-K" matches "10-K" and "10-K/A"), mirroring EFTS forms
|
|
66
|
+
* semantics so the archive-backed arms honor the same forms contract as the
|
|
67
|
+
* post-2001 full-text path.
|
|
68
|
+
*/
|
|
69
|
+
function formMatches(filingForm, requestedForms) {
|
|
70
|
+
const f = filingForm.toUpperCase();
|
|
71
|
+
return requestedForms.some((req) => {
|
|
72
|
+
const r = req.toUpperCase();
|
|
73
|
+
return f === r || f.startsWith(`${r}/`);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
/** Count filings by form, dropping the field when nothing carried a form. */
|
|
77
|
+
function buildFormDistribution(forms) {
|
|
78
|
+
const dist = {};
|
|
79
|
+
for (const form of forms) {
|
|
80
|
+
if (form)
|
|
81
|
+
dist[form] = (dist[form] ?? 0) + 1;
|
|
82
|
+
}
|
|
83
|
+
return Object.keys(dist).length > 0 ? dist : undefined;
|
|
84
|
+
}
|
|
24
85
|
/**
|
|
25
86
|
* EDGAR's `display_names[0]` embeds the ticker(s) and CIK in trailing parentheticals
|
|
26
87
|
* (e.g., "Apple Inc. (AAPL) (CIK 0000320193)"). Strip them so consumers see a clean
|
|
@@ -84,8 +145,203 @@ async function resolveEntityTargeting(query, ctx) {
|
|
|
84
145
|
}
|
|
85
146
|
return { query };
|
|
86
147
|
}
|
|
148
|
+
/**
|
|
149
|
+
* Shared tail for the archive-backed arms: sort the fully-matched rows, slice
|
|
150
|
+
* the inline window, recompute the form distribution, register the full set to
|
|
151
|
+
* the canvas (source-tagged), and emit the query echo / boundary notices.
|
|
152
|
+
* `scanTruncated` (a scan cap bound before the range was exhausted) drives both
|
|
153
|
+
* `total_is_exact` and the dataframe `truncated` flag.
|
|
154
|
+
*/
|
|
155
|
+
async function finishArchiveResult(ctx, args) {
|
|
156
|
+
// Full-index and submissions have no relevance score, so relevance collapses
|
|
157
|
+
// to date-descending; only an explicit ascending request reorders.
|
|
158
|
+
const sorted = [...args.matched].sort((a, b) => args.sort === 'filing_date_asc'
|
|
159
|
+
? a.filing_date.localeCompare(b.filing_date)
|
|
160
|
+
: b.filing_date.localeCompare(a.filing_date));
|
|
161
|
+
const total = sorted.length;
|
|
162
|
+
const totalIsExact = !args.scanTruncated;
|
|
163
|
+
const sliced = sorted.slice(args.offset, args.offset + args.limit);
|
|
164
|
+
const results = sliced.map(toInlineResult);
|
|
165
|
+
const formDistribution = buildFormDistribution(sorted.map((row) => row.form));
|
|
166
|
+
let dataset;
|
|
167
|
+
const bridge = getCanvasBridge();
|
|
168
|
+
if (bridge && sorted.length > args.limit) {
|
|
169
|
+
const registered = await bridge.registerDataframe(ctx, {
|
|
170
|
+
rows: sorted,
|
|
171
|
+
sourceTool: 'secedgar_search_filings',
|
|
172
|
+
queryParams: args.queryParams,
|
|
173
|
+
truncated: args.scanTruncated,
|
|
174
|
+
});
|
|
175
|
+
if (registered)
|
|
176
|
+
dataset = { ...toDatasetField(registered), truncated: args.scanTruncated };
|
|
177
|
+
}
|
|
178
|
+
ctx.enrich.echo(args.effectiveQuery);
|
|
179
|
+
if (total === 0) {
|
|
180
|
+
ctx.enrich.notice(`No filings matched ${args.zeroHitCriteria}. ${args.coverageNote}`);
|
|
181
|
+
}
|
|
182
|
+
else if (results.length === 0 && args.offset >= sorted.length) {
|
|
183
|
+
ctx.enrich.notice(`Offset (${args.offset}) exceeds the ${sorted.length} matched filings. ${total} filings matched — lower the offset or query the full set via secedgar_dataframe_query.`);
|
|
184
|
+
}
|
|
185
|
+
else if (total > results.length) {
|
|
186
|
+
ctx.enrich.truncated({
|
|
187
|
+
shown: results.length,
|
|
188
|
+
cap: args.limit,
|
|
189
|
+
guidance: args.scanTruncated
|
|
190
|
+
? `${args.coverageNote} The scan hit its cap before exhausting the range — older filings may exist beyond the scanned window. Narrow the range, or query the dataframe.`
|
|
191
|
+
: `${args.coverageNote} Query the full matched set via secedgar_dataframe_query, or page with offset.`,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
return {
|
|
195
|
+
total,
|
|
196
|
+
total_is_exact: totalIsExact,
|
|
197
|
+
results,
|
|
198
|
+
form_distribution: formDistribution,
|
|
199
|
+
dataset,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Pre-2001 entity-scoped arm: resolve the CIK's full submissions history (the
|
|
204
|
+
* `recent` window plus the `filings.files[]` archive pages reused from #78) and
|
|
205
|
+
* filter by form + date. Reuses `getSubmissions`/`selectArchivePages`/
|
|
206
|
+
* `fetchArchivePage` as-is. Rows are tagged `source: 'submissions'`; the
|
|
207
|
+
* EFTS-only fields (period_ending, ticker, file_description, sic, location) are
|
|
208
|
+
* left null for a uniform archive-row shape.
|
|
209
|
+
*/
|
|
210
|
+
async function serveSubmissionsArchive(ctx, args) {
|
|
211
|
+
const api = getEdgarApiService();
|
|
212
|
+
// A `cik:` targeting token is only shape-validated (1-10 digits), never checked
|
|
213
|
+
// against the ticker cache, so a well-formed filing-agent/transmitter CIK reaches
|
|
214
|
+
// the submissions feed and 404s. Resolve it here to tell a cache-hit registrant
|
|
215
|
+
// from a bare CIK, then convert only the bare-CIK 404 into the declared contract
|
|
216
|
+
// error — no raw SEC URL (#93). A registrant that 404s is an EDGAR-side problem
|
|
217
|
+
// and propagates unchanged, same reasoning as #55 and #76. The lookup is confined
|
|
218
|
+
// to this pre-2001 arm (already multi-fetch) so the EFTS path stays untouched.
|
|
219
|
+
const resolved = await api.resolveCik(args.entityCik);
|
|
220
|
+
const cacheMatch = Array.isArray(resolved) ? resolved[0] : resolved;
|
|
221
|
+
const isBareCikFallback = !cacheMatch?.name && !cacheMatch?.ticker;
|
|
222
|
+
let submissions;
|
|
223
|
+
try {
|
|
224
|
+
submissions = await api.getSubmissions(args.entityCik);
|
|
225
|
+
}
|
|
226
|
+
catch (err) {
|
|
227
|
+
if (isBareCikFallback && err instanceof McpError && err.code === JsonRpcErrorCode.NotFound) {
|
|
228
|
+
ctx.log.debug('CIK has no submissions feed', { cik: args.entityCik });
|
|
229
|
+
throw notFound(`No EDGAR filer found for CIK ${args.entityCik}. If this looks like an accession-number prefix, it's the filing agent, not the issuer — use secedgar_company_search to find the company.`, { reason: 'entity_not_found', ...ctx.recoveryFor('entity_not_found') }, { cause: err });
|
|
230
|
+
}
|
|
231
|
+
throw err;
|
|
232
|
+
}
|
|
233
|
+
const companyName = submissions.name;
|
|
234
|
+
const forms = args.forms?.length ? args.forms : undefined;
|
|
235
|
+
const rows = [];
|
|
236
|
+
const pushBlock = (block) => {
|
|
237
|
+
for (let i = 0; i < block.accessionNumber.length; i++) {
|
|
238
|
+
const form = block.form[i] ?? '';
|
|
239
|
+
const filingDate = block.filingDate[i] ?? '';
|
|
240
|
+
if (filingDate < args.startDate || filingDate > args.endDate)
|
|
241
|
+
continue;
|
|
242
|
+
if (forms && !formMatches(form, forms))
|
|
243
|
+
continue;
|
|
244
|
+
rows.push({
|
|
245
|
+
accession_number: block.accessionNumber[i] ?? '',
|
|
246
|
+
form: form || null,
|
|
247
|
+
filing_date: filingDate,
|
|
248
|
+
period_ending: null,
|
|
249
|
+
company_name: companyName,
|
|
250
|
+
cik: args.entityCik,
|
|
251
|
+
ticker: null,
|
|
252
|
+
file_description: null,
|
|
253
|
+
sic: null,
|
|
254
|
+
location: null,
|
|
255
|
+
source: 'submissions',
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
pushBlock(submissions.filings.recent);
|
|
260
|
+
// Older filings live in the archive pages covering the requested pre-2001 range.
|
|
261
|
+
const pages = selectArchivePages(submissions.filings.files, args.startDate, args.endDate);
|
|
262
|
+
const pageLimit = Math.min(pages.length, ARCHIVE_PAGE_SCAN_CAP);
|
|
263
|
+
const scanTruncated = pages.length > pageLimit;
|
|
264
|
+
for (let i = 0; i < pageLimit; i++) {
|
|
265
|
+
const page = pages[i];
|
|
266
|
+
if (!page)
|
|
267
|
+
break;
|
|
268
|
+
pushBlock(await api.fetchArchivePage(page.name));
|
|
269
|
+
}
|
|
270
|
+
const formsNote = forms ? `, forms ${forms.join(', ')}` : '';
|
|
271
|
+
return finishArchiveResult(ctx, {
|
|
272
|
+
matched: rows,
|
|
273
|
+
sort: args.sort,
|
|
274
|
+
limit: args.limit,
|
|
275
|
+
offset: args.offset,
|
|
276
|
+
scanTruncated,
|
|
277
|
+
queryParams: {
|
|
278
|
+
entity_cik: args.entityCik,
|
|
279
|
+
forms: args.forms,
|
|
280
|
+
start_date: args.startDate,
|
|
281
|
+
end_date: args.endDate,
|
|
282
|
+
source: 'submissions',
|
|
283
|
+
},
|
|
284
|
+
effectiveQuery: `(pre-2001 archive: CIK ${args.entityCik}${formsNote}, ${args.startDate} to ${args.endDate})`,
|
|
285
|
+
coverageNote: 'Served from the entity submissions history — full-text search covers 2001-present only.',
|
|
286
|
+
zeroHitCriteria: `CIK ${args.entityCik}${forms ? `, forms [${forms.join(', ')}]` : ''}, dates ${args.startDate} to ${args.endDate}`,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Pre-2001 unscoped browse arm: fetch the quarterly full-index (`master.idx`)
|
|
291
|
+
* files spanning the range and filter by form + date client-side (the index is
|
|
292
|
+
* not form-filterable server-side). Bounded by FULL_INDEX_QUARTER_SCAN_CAP —
|
|
293
|
+
* the heaviest arm. Rows are tagged `source: 'full-index'`.
|
|
294
|
+
*/
|
|
295
|
+
async function serveFullIndexBrowse(ctx, args) {
|
|
296
|
+
const api = getEdgarApiService();
|
|
297
|
+
const quarters = quartersInRange(args.startDate, args.endDate);
|
|
298
|
+
const quarterLimit = Math.min(quarters.length, FULL_INDEX_QUARTER_SCAN_CAP);
|
|
299
|
+
const scanTruncated = quarters.length > quarterLimit;
|
|
300
|
+
const rows = [];
|
|
301
|
+
for (let i = 0; i < quarterLimit; i++) {
|
|
302
|
+
const quarter = quarters[i];
|
|
303
|
+
if (!quarter)
|
|
304
|
+
break;
|
|
305
|
+
const entries = await api.fetchFullIndexQuarter(quarter.year, quarter.quarter);
|
|
306
|
+
for (const entry of entries) {
|
|
307
|
+
if (entry.filingDate < args.startDate || entry.filingDate > args.endDate)
|
|
308
|
+
continue;
|
|
309
|
+
if (!formMatches(entry.form, args.forms))
|
|
310
|
+
continue;
|
|
311
|
+
rows.push({
|
|
312
|
+
accession_number: entry.accessionNumber,
|
|
313
|
+
form: entry.form || null,
|
|
314
|
+
filing_date: entry.filingDate,
|
|
315
|
+
period_ending: null,
|
|
316
|
+
company_name: entry.companyName,
|
|
317
|
+
cik: entry.cik,
|
|
318
|
+
ticker: null,
|
|
319
|
+
file_description: null,
|
|
320
|
+
sic: null,
|
|
321
|
+
location: null,
|
|
322
|
+
source: 'full-index',
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return finishArchiveResult(ctx, {
|
|
327
|
+
matched: rows,
|
|
328
|
+
sort: args.sort,
|
|
329
|
+
limit: args.limit,
|
|
330
|
+
offset: args.offset,
|
|
331
|
+
scanTruncated,
|
|
332
|
+
queryParams: {
|
|
333
|
+
forms: args.forms,
|
|
334
|
+
start_date: args.startDate,
|
|
335
|
+
end_date: args.endDate,
|
|
336
|
+
source: 'full-index',
|
|
337
|
+
},
|
|
338
|
+
effectiveQuery: `(pre-2001 full-index browse: forms ${args.forms.join(', ')}, ${args.startDate} to ${args.endDate})`,
|
|
339
|
+
coverageNote: 'Served from the quarterly EDGAR full-index — full-text search covers 2001-present only.',
|
|
340
|
+
zeroHitCriteria: `forms [${args.forms.join(', ')}], dates ${args.startDate} to ${args.endDate}`,
|
|
341
|
+
});
|
|
342
|
+
}
|
|
87
343
|
export const searchFilingsTool = tool('secedgar_search_filings', {
|
|
88
|
-
description: 'Search
|
|
344
|
+
description: 'Search EDGAR filings since 1993. Full-text search covers 2001-present (the EFTS index floor); pre-2001 date ranges (to 1993) are served from the archives by form and entity/date — pre-2001 full-text matching requires entity scope (ticker:/cik:). Supports exact phrases, boolean operators, wildcards, and entity targeting (ticker:AAPL or cik:320193 in query).',
|
|
89
345
|
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
90
346
|
// Agent-facing context for the success path — the query as EDGAR executed it and
|
|
91
347
|
// an optional notice for empty results. Populated via ctx.enrich so it reaches
|
|
@@ -121,12 +377,36 @@ export const searchFilingsTool = tool('secedgar_search_filings', {
|
|
|
121
377
|
when: 'A cik: targeting token in the query is not a 1-10 digit number',
|
|
122
378
|
recovery: 'Pass a numeric CIK such as cik:320193 — find it with secedgar_company_search if unknown.',
|
|
123
379
|
},
|
|
380
|
+
{
|
|
381
|
+
reason: 'entity_not_found',
|
|
382
|
+
code: JsonRpcErrorCode.NotFound,
|
|
383
|
+
when: 'A cik: targeting token on a pre-2001 date range names a CIK with no EDGAR submissions history',
|
|
384
|
+
recovery: 'Confirm the CIK with secedgar_company_search — an accession-number prefix names the filing agent, not the issuer.',
|
|
385
|
+
},
|
|
124
386
|
{
|
|
125
387
|
reason: 'missing_criteria',
|
|
126
388
|
code: JsonRpcErrorCode.ValidationError,
|
|
127
389
|
when: 'Neither a full-text query nor a forms filter was provided (a date range cannot stand alone)',
|
|
128
390
|
recovery: 'Provide search terms, or browse by form type (forms: ["S-1"]) or entity (ticker:AAPL / cik:320193 in the query) — optionally narrowed by date.',
|
|
129
391
|
},
|
|
392
|
+
{
|
|
393
|
+
reason: 'straddling_date_range',
|
|
394
|
+
code: JsonRpcErrorCode.ValidationError,
|
|
395
|
+
when: 'The date range crosses 2001-01-01, spanning both the EFTS full-text era and the pre-2001 archives',
|
|
396
|
+
recovery: 'Split at 2001-01-01: run one search for the ≥2001 side and another for the <2001 side, then combine the results.',
|
|
397
|
+
},
|
|
398
|
+
{
|
|
399
|
+
reason: 'pre2001_full_text_unscoped',
|
|
400
|
+
code: JsonRpcErrorCode.ValidationError,
|
|
401
|
+
when: 'A pre-2001 date range carries free-text terms with no entity scope — no pre-2001 full-text index exists',
|
|
402
|
+
recovery: 'Add ticker: or cik: entity scope to the query, or drop the text terms to browse pre-2001 filings by form and date.',
|
|
403
|
+
},
|
|
404
|
+
{
|
|
405
|
+
reason: 'pre2001_full_text_scoped',
|
|
406
|
+
code: JsonRpcErrorCode.ValidationError,
|
|
407
|
+
when: 'A pre-2001 date range carries free-text terms alongside entity scope — local document matching is not yet supported',
|
|
408
|
+
recovery: "Drop the text terms to browse this entity's pre-2001 filings by form and date.",
|
|
409
|
+
},
|
|
130
410
|
],
|
|
131
411
|
input: z.object({
|
|
132
412
|
query: z
|
|
@@ -139,7 +419,7 @@ export const searchFilingsTool = tool('secedgar_search_filings', {
|
|
|
139
419
|
.describe('Full-text search query. Supports: exact phrases ("material weakness"), boolean operators (revenue OR income), exclusion (-preliminary), wildcard suffix (account*), entity targeting (ticker:AAPL or cik:320193 in the query). Terms are AND\'d by default.'),
|
|
140
420
|
])
|
|
141
421
|
.optional()
|
|
142
|
-
.describe('Full-text search query. Optional — omit (or pass "") to browse by form type and/or entity instead, e.g. every S-1 in a date window, or a company\'s filings via ticker:/cik:. A date range alone is not a valid search; pair it with forms or entity targeting. When present, supports exact phrases ("material weakness"), boolean operators (revenue OR income), exclusion (-preliminary), wildcard suffix (account*), and entity targeting (ticker:AAPL or cik:320193 in the query); terms are AND\'d by default.'),
|
|
422
|
+
.describe('Full-text search query. Optional — omit (or pass "") to browse by form type and/or entity instead, e.g. every S-1 in a date window, or a company\'s filings via ticker:/cik:. A date range alone is not a valid search; pair it with forms or entity targeting. Full-text terms match only filings from 2001 onward (the EFTS index floor); for a pre-2001 date range, drop the text terms (browse by form/date) or add ticker:/cik: entity scope. When present, supports exact phrases ("material weakness"), boolean operators (revenue OR income), exclusion (-preliminary), wildcard suffix (account*), and entity targeting (ticker:AAPL or cik:320193 in the query); terms are AND\'d by default.'),
|
|
143
423
|
forms: z
|
|
144
424
|
.array(z.string())
|
|
145
425
|
.optional()
|
|
@@ -175,13 +455,15 @@ export const searchFilingsTool = tool('secedgar_search_filings', {
|
|
|
175
455
|
sort: z
|
|
176
456
|
.enum(['filing_date_desc', 'filing_date_asc', 'relevance'])
|
|
177
457
|
.default('filing_date_desc')
|
|
178
|
-
.describe('Result ordering. "filing_date_desc" (default) returns most recent first. "filing_date_asc" returns oldest first. "relevance" returns SEC\'s native search-score order, which weights term match strength over recency. Date sorts re-order the top 100 hits returned by the search index — for broad queries with more than 100 matches and no entity targeting, date-newest filings may sit outside that window. Entity targeting (ticker:/cik:) or a narrower query keeps matches inside the window when absolute recency matters. On the no-query browse path (forms/entity only), EFTS has no relevance signal — every hit scores null — and returns filings in natural date-descending order, so all sort modes effectively yield newest-first.'),
|
|
458
|
+
.describe('Result ordering. "filing_date_desc" (default) returns most recent first. "filing_date_asc" returns oldest first. "relevance" returns SEC\'s native search-score order, which weights term match strength over recency. Date sorts re-order the top 100 hits returned by the search index — for broad queries with more than 100 matches and no entity targeting, date-newest filings may sit outside that window. Entity targeting (ticker:/cik:) or a narrower query keeps matches inside the window when absolute recency matters. On the no-query browse path (forms/entity only), EFTS has no relevance signal — every hit scores null — and returns filings in natural date-descending order, so all sort modes effectively yield newest-first. Pre-2001 archive results carry no relevance score either, so relevance collapses to date-descending there.'),
|
|
179
459
|
}),
|
|
180
460
|
output: z.object({
|
|
181
461
|
total: z
|
|
182
462
|
.number()
|
|
183
|
-
.describe("Total matching filings (capped at 10,000
|
|
184
|
-
total_is_exact: z
|
|
463
|
+
.describe("Total matching filings. On the full-text (2001+) path this is capped at 10,000; entity targeting (ticker:/cik:) scopes server-side via the EFTS ciks param, so it is the entity's exact match count up to the cap. On a pre-2001 archive path it is the exact count within the scanned window (see total_is_exact)."),
|
|
464
|
+
total_is_exact: z
|
|
465
|
+
.boolean()
|
|
466
|
+
.describe('False when total is a lower bound — the full-text path hit its 10,000 cap, or a pre-2001 archive scan hit its page/quarter cap before exhausting the range.'),
|
|
185
467
|
results: z
|
|
186
468
|
.array(z
|
|
187
469
|
.object({
|
|
@@ -196,7 +478,7 @@ export const searchFilingsTool = tool('secedgar_search_filings', {
|
|
|
196
478
|
period_ending: z
|
|
197
479
|
.string()
|
|
198
480
|
.optional()
|
|
199
|
-
.describe('Period the filing reports on (YYYY-MM-DD). Absent for filings without a reporting period (e.g., proxy statements, ownership reports).'),
|
|
481
|
+
.describe('Period the filing reports on (YYYY-MM-DD). Absent for filings without a reporting period (e.g., proxy statements, ownership reports) and for all pre-2001 archive-sourced rows (source submissions/full-index), which carry no period field.'),
|
|
200
482
|
company_name: z
|
|
201
483
|
.string()
|
|
202
484
|
.describe('Filing entity, with ticker/CIK parentheticals stripped.'),
|
|
@@ -204,21 +486,25 @@ export const searchFilingsTool = tool('secedgar_search_filings', {
|
|
|
204
486
|
ticker: z
|
|
205
487
|
.string()
|
|
206
488
|
.optional()
|
|
207
|
-
.describe('Primary ticker symbol parsed from the EFTS display name. Absent for private filers, foreign filers without a US listing,
|
|
489
|
+
.describe('Primary ticker symbol parsed from the EFTS display name. Absent for private filers, foreign filers without a US listing, filings whose display name omits the ticker parenthetical, and all pre-2001 archive-sourced rows. For multi-class issuers (e.g., BRK-A / BRK-B), this is the first class listed.'),
|
|
208
490
|
file_description: z
|
|
209
491
|
.string()
|
|
210
492
|
.optional()
|
|
211
|
-
.describe('SEC-provided description of the matching document (e.g., "EX-99.1"). Absent when SEC published none.'),
|
|
493
|
+
.describe('SEC-provided description of the matching document (e.g., "EX-99.1"). Absent when SEC published none, and for pre-2001 archive-sourced rows.'),
|
|
212
494
|
sic: z
|
|
213
495
|
.string()
|
|
214
496
|
.optional()
|
|
215
|
-
.describe('SIC industry code for the filer. Absent for filers without a classification.'),
|
|
497
|
+
.describe('SIC industry code for the filer. Absent for filers without a classification, and for pre-2001 archive-sourced rows.'),
|
|
216
498
|
location: z
|
|
217
499
|
.string()
|
|
218
500
|
.optional()
|
|
219
|
-
.describe('Business location (state or country code). Absent when SEC has no location for this filer.'),
|
|
501
|
+
.describe('Business location (state or country code). Absent when SEC has no location for this filer, and for pre-2001 archive-sourced rows.'),
|
|
502
|
+
source: z
|
|
503
|
+
.enum(['efts', 'submissions', 'full-index'])
|
|
504
|
+
.optional()
|
|
505
|
+
.describe('Which EDGAR backend served this row: "efts" (2001+ full-text index), "submissions" (a pre-2001 entity-scoped filing history), or "full-index" (a pre-2001 unscoped quarterly index browse). Provenance is carried into the canvas dataframe as a `source` column.'),
|
|
220
506
|
})
|
|
221
|
-
.describe('One matching filing hit
|
|
507
|
+
.describe('One matching filing hit.'))
|
|
222
508
|
.describe('Matching filings.'),
|
|
223
509
|
form_distribution: z
|
|
224
510
|
.record(z.string(), z.number())
|
|
@@ -233,10 +519,10 @@ export const searchFilingsTool = tool('secedgar_search_filings', {
|
|
|
233
519
|
expires_at: z.string().describe('ISO 8601 expiry timestamp.'),
|
|
234
520
|
truncated: z
|
|
235
521
|
.boolean()
|
|
236
|
-
.describe('True when
|
|
522
|
+
.describe('True when more matches exist beyond the materialized set — the full-text window was exceeded, or a pre-2001 archive scan hit its cap. Each row carries a `source` column so provenance survives into secedgar_dataframe_query.'),
|
|
237
523
|
})
|
|
238
524
|
.optional()
|
|
239
|
-
.describe('Canvas dataframe holding the hits
|
|
525
|
+
.describe('Canvas dataframe holding the fetched hits (full-text window, or the full pre-2001 archive match set), each tagged with its `source`. Absent when total ≤ inline limit, canvas is unavailable, or materialization failed. Query with secedgar_dataframe_query SQL.'),
|
|
240
526
|
}),
|
|
241
527
|
async handler(input, ctx) {
|
|
242
528
|
// Validate date range: both or neither
|
|
@@ -255,6 +541,48 @@ export const searchFilingsTool = tool('secedgar_search_filings', {
|
|
|
255
541
|
// server-side ciks param. Throws typed validation errors on bad tokens (#61).
|
|
256
542
|
// Default to '' so a browse request (undefined query) resolves cleanly.
|
|
257
543
|
const { query, entityCik } = await resolveEntityTargeting(input.query ?? '', ctx);
|
|
544
|
+
const api = getEdgarApiService();
|
|
545
|
+
// Date-based routing at the EFTS full-text floor (2001-01-01). EFTS only
|
|
546
|
+
// indexes filings from 2001; a range entirely before it is served from the
|
|
547
|
+
// archives, a range crossing it is rejected with a split instruction (#77).
|
|
548
|
+
const startDate = input.start_date || undefined;
|
|
549
|
+
const endDate = input.end_date || undefined;
|
|
550
|
+
if (startDate && endDate && endDate < EFTS_FULLTEXT_FLOOR) {
|
|
551
|
+
const hasFreeText = query.trim().length > 0;
|
|
552
|
+
if (hasFreeText) {
|
|
553
|
+
// No pre-2001 full-text index exists. Reject honestly rather than silently
|
|
554
|
+
// dropping the text terms; bounded local matching is a deferred follow-up.
|
|
555
|
+
if (entityCik) {
|
|
556
|
+
throw ctx.fail('pre2001_full_text_scoped', "Full-text matching of pre-2001 filings is not yet supported. Drop the text terms to browse this entity's pre-2001 filings by form and date.", { ...ctx.recoveryFor('pre2001_full_text_scoped') });
|
|
557
|
+
}
|
|
558
|
+
throw ctx.fail('pre2001_full_text_unscoped', 'Full-text search is unavailable before 2001 (the EFTS index starts at 2001-01-01). For pre-2001 filings, add ticker:/cik: entity scope, or drop the text terms to browse by form and date.', { ...ctx.recoveryFor('pre2001_full_text_unscoped') });
|
|
559
|
+
}
|
|
560
|
+
if (entityCik) {
|
|
561
|
+
return await serveSubmissionsArchive(ctx, {
|
|
562
|
+
entityCik,
|
|
563
|
+
forms: input.forms,
|
|
564
|
+
startDate,
|
|
565
|
+
endDate,
|
|
566
|
+
sort: input.sort,
|
|
567
|
+
limit: input.limit,
|
|
568
|
+
offset: input.offset,
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
// Unscoped + no free text → forms are guaranteed present by the missing_criteria guard.
|
|
572
|
+
return await serveFullIndexBrowse(ctx, {
|
|
573
|
+
forms: input.forms ?? [],
|
|
574
|
+
startDate,
|
|
575
|
+
endDate,
|
|
576
|
+
sort: input.sort,
|
|
577
|
+
limit: input.limit,
|
|
578
|
+
offset: input.offset,
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
if (startDate && endDate && startDate < EFTS_FULLTEXT_FLOOR) {
|
|
582
|
+
// endDate ≥ 2001 (handled above) but startDate < 2001 → straddling. Reject
|
|
583
|
+
// with a split instruction; merged straddling output is a deferred follow-up.
|
|
584
|
+
throw ctx.fail('straddling_date_range', `The date range ${startDate}..${endDate} crosses 2001-01-01. Full-text search covers 2001-present; pre-2001 filings are served from the archives. Search the two eras separately: ${EFTS_FULLTEXT_FLOOR} onward, and before ${EFTS_FULLTEXT_FLOOR}.`, { ...ctx.recoveryFor('straddling_date_range') });
|
|
585
|
+
}
|
|
258
586
|
// EFTS scores by relevance and exposes no sort param. When the caller wants
|
|
259
587
|
// a date sort (the default) or entity filtering, we over-fetch the EFTS
|
|
260
588
|
// window and reorder/slice client-side. Pure relevance mode keeps the
|
|
@@ -262,7 +590,6 @@ export const searchFilingsTool = tool('secedgar_search_filings', {
|
|
|
262
590
|
const wideFetch = entityCik !== undefined || input.sort !== 'relevance';
|
|
263
591
|
const fetchFrom = wideFetch ? 0 : input.offset;
|
|
264
592
|
const fetchSize = wideFetch ? 100 : input.limit;
|
|
265
|
-
const api = getEdgarApiService();
|
|
266
593
|
const response = await api.searchFilings({
|
|
267
594
|
query,
|
|
268
595
|
forms: input.forms,
|
|
@@ -302,6 +629,7 @@ export const searchFilingsTool = tool('secedgar_search_filings', {
|
|
|
302
629
|
file_description: hit._source.file_description ?? undefined,
|
|
303
630
|
sic: hit._source.sics?.[0] ?? undefined,
|
|
304
631
|
location: hit._source.biz_locations?.[0] ?? undefined,
|
|
632
|
+
source: 'efts',
|
|
305
633
|
};
|
|
306
634
|
});
|
|
307
635
|
// Form distribution must reflect the same set as `total`.
|
|
@@ -310,14 +638,7 @@ export const searchFilingsTool = tool('secedgar_search_filings', {
|
|
|
310
638
|
// case, recompute from the post-filter `hits` to keep the counts consistent.
|
|
311
639
|
let formDistribution;
|
|
312
640
|
if (entityCik || input.forms?.length) {
|
|
313
|
-
|
|
314
|
-
for (const hit of hits) {
|
|
315
|
-
const form = hit._source.form;
|
|
316
|
-
if (form)
|
|
317
|
-
dist[form] = (dist[form] ?? 0) + 1;
|
|
318
|
-
}
|
|
319
|
-
if (Object.keys(dist).length > 0)
|
|
320
|
-
formDistribution = dist;
|
|
641
|
+
formDistribution = buildFormDistribution(hits.map((hit) => hit._source.form));
|
|
321
642
|
}
|
|
322
643
|
else if (response.aggregations?.form_filter?.buckets) {
|
|
323
644
|
formDistribution = {};
|
|
@@ -346,6 +667,7 @@ export const searchFilingsTool = tool('secedgar_search_filings', {
|
|
|
346
667
|
start_date: input.start_date,
|
|
347
668
|
end_date: input.end_date,
|
|
348
669
|
entity_cik: entityCik,
|
|
670
|
+
source: 'efts',
|
|
349
671
|
},
|
|
350
672
|
truncated,
|
|
351
673
|
});
|
|
@@ -403,15 +725,21 @@ export const searchFilingsTool = tool('secedgar_search_filings', {
|
|
|
403
725
|
};
|
|
404
726
|
},
|
|
405
727
|
format: (result) => {
|
|
406
|
-
const
|
|
728
|
+
const source = result.results[0]?.source;
|
|
729
|
+
const exactness = result.total_is_exact
|
|
730
|
+
? 'exact'
|
|
731
|
+
: source === 'submissions' || source === 'full-index'
|
|
732
|
+
? 'partial — scan cap reached, more may exist'
|
|
733
|
+
: 'capped at 10,000';
|
|
407
734
|
const lines = [`Found ${result.total} filings (${exactness})`];
|
|
408
735
|
for (const r of result.results) {
|
|
409
736
|
const period = r.period_ending ? ` (period: ${r.period_ending})` : '';
|
|
410
737
|
const desc = r.file_description ? ` — ${r.file_description}` : '';
|
|
411
738
|
const sic = r.sic ? ` | SIC ${r.sic}` : '';
|
|
412
739
|
const loc = r.location ? ` | ${r.location}` : '';
|
|
740
|
+
const src = r.source ? ` | source: ${r.source}` : '';
|
|
413
741
|
const ident = r.ticker ? `${r.ticker}, CIK ${r.cik}` : `CIK ${r.cik}`;
|
|
414
|
-
lines.push(`- ${r.form ?? 'N/A'} ${r.filing_date}${period} — ${r.company_name} (${ident})${sic}${loc}${desc} [${r.accession_number}]`);
|
|
742
|
+
lines.push(`- ${r.form ?? 'N/A'} ${r.filing_date}${period} — ${r.company_name} (${ident})${sic}${loc}${desc}${src} [${r.accession_number}]`);
|
|
415
743
|
}
|
|
416
744
|
if (result.form_distribution) {
|
|
417
745
|
const dist = Object.entries(result.form_distribution)
|
|
@@ -421,7 +749,7 @@ export const searchFilingsTool = tool('secedgar_search_filings', {
|
|
|
421
749
|
}
|
|
422
750
|
if (result.dataset) {
|
|
423
751
|
const truncatedNote = result.dataset.truncated
|
|
424
|
-
? ' (truncated — more matches exist beyond the
|
|
752
|
+
? ' (truncated — more matches exist beyond the materialized set)'
|
|
425
753
|
: '';
|
|
426
754
|
lines.push(`\nDataset: ${result.dataset.name} (${result.dataset.row_count} rows, expires ${result.dataset.expires_at})${truncatedNote} — query with secedgar_dataframe_query.`);
|
|
427
755
|
}
|