@pipeworx/mcp-edgar 0.3.3 → 0.3.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/README.md +2 -2
- package/package.json +3 -3
- package/server.json +1 -1
- package/src/index.ts +191 -6
- package/src/server.ts +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
The Securities and Exchange Commission's filings database. Every public company in the US files here, every filing is timestamped and immutable, and every disclosure (revenue, debt, risk factors, executive compensation, M&A activity, insider trading) is structured for machine retrieval. Free and authoritative.
|
|
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 1683+ live data sources.
|
|
6
6
|
|
|
7
7
|
## Why this matters for AI agents
|
|
8
8
|
|
|
@@ -106,7 +106,7 @@ directly, instead of just this one's:
|
|
|
106
106
|
}
|
|
107
107
|
```
|
|
108
108
|
|
|
109
|
-
Both URLs reach the same gateway and the same
|
|
109
|
+
Both URLs reach the same gateway and the same 1683+ data sources. The
|
|
110
110
|
only difference is which pack's tools are listed **directly**; `ask_pipeworx`
|
|
111
111
|
reaches all of them from either one.
|
|
112
112
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pipeworx/mcp-edgar",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.5",
|
|
4
4
|
"description": "EDGAR MCP — SEC EDGAR public APIs (free, no auth)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"@cloudflare/workers-types": "^4.20260405.1"
|
|
27
27
|
},
|
|
28
28
|
"pipeworx": {
|
|
29
|
-
"sourceHash": "v1-
|
|
30
|
-
"sourceCommit": "
|
|
29
|
+
"sourceHash": "v1-8ebd704e53458e829aea1033aa14ef78911a50cd60211956318c1574f8b1eb4c",
|
|
30
|
+
"sourceCommit": "1064560fd9dcd0d6fc2606298c2ba244b3a1fbeb"
|
|
31
31
|
}
|
|
32
32
|
}
|
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.5",
|
|
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
|
@@ -452,7 +452,42 @@ async function fetchWithTimeout(
|
|
|
452
452
|
),
|
|
453
453
|
);
|
|
454
454
|
}
|
|
455
|
-
|
|
455
|
+
// Fleet #2382. Everything that isn't a timeout/abort here is a genuine
|
|
456
|
+
// NETWORK-LEVEL failure — DNS resolution, connection refused, TLS handshake,
|
|
457
|
+
// Cloudflare's own "Network connection lost." — meaning `fetch()` itself
|
|
458
|
+
// threw and no HTTP response of any kind was ever received. Until this fix
|
|
459
|
+
// that raw exception was rethrown VERBATIM: a bare `TypeError: fetch failed`
|
|
460
|
+
// (or the Workers-runtime equivalent) names no upstream, carries no class
|
|
461
|
+
// token, and reads exactly like a defect in OUR code — because it says
|
|
462
|
+
// nothing about the call at all. It landed in `error`, the tier that means
|
|
463
|
+
// "Pipeworx has a defect", for every one of the (at the time of writing)
|
|
464
|
+
// ~470 packs that call this helper directly with no wrapper of their own.
|
|
465
|
+
//
|
|
466
|
+
// `dexscreener` hit this independently (fleet #1579) and fixed it with a
|
|
467
|
+
// bespoke per-pack try/catch around `fetchWithTimeout`. That fix is correct
|
|
468
|
+
// but only covers one pack; every other caller of this shared helper still
|
|
469
|
+
// leaked the raw exception. Moving the same fix HERE — the one place that
|
|
470
|
+
// already carries the timeout case — covers every pack that uses
|
|
471
|
+
// `fetchWithTimeout` without a wrapper, for free, and without widening
|
|
472
|
+
// `classifyToolError`'s regex list: the fix is giving the message a proper
|
|
473
|
+
// `upstream_down:` token at the point the two facts (no response was ever
|
|
474
|
+
// received, and which host we were trying to reach) are actually in hand,
|
|
475
|
+
// not teaching the classifier to guess from prose after the fact.
|
|
476
|
+
//
|
|
477
|
+
// Safe on the same grounds as the timeout branch above: no argument a
|
|
478
|
+
// caller passes can make `fetch()` itself throw a connection-level error,
|
|
479
|
+
// so this is always an availability failure, never a caller mistake. Same
|
|
480
|
+
// `markInternalOrigin` treatment — an origin we run that never answered is
|
|
481
|
+
// still ours, not a third party's outage.
|
|
482
|
+
const raw = err instanceof Error ? err.message : String(err);
|
|
483
|
+
throw new Error(
|
|
484
|
+
markInternalOrigin(
|
|
485
|
+
`upstream_down: could not reach ${name} at all (${raw.slice(0, 160)}). ` +
|
|
486
|
+
`No request reached ${name}, so this says NOTHING about whether the arguments you passed ` +
|
|
487
|
+
'are valid — do not re-check them on the strength of this error. Retry shortly.',
|
|
488
|
+
url,
|
|
489
|
+
),
|
|
490
|
+
);
|
|
456
491
|
}
|
|
457
492
|
}
|
|
458
493
|
|
|
@@ -1534,7 +1569,7 @@ const tools: McpToolExport['tools'] = [
|
|
|
1534
1569
|
},
|
|
1535
1570
|
form_type: {
|
|
1536
1571
|
type: 'string',
|
|
1537
|
-
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
|
|
1572
|
+
description: 'When accession is omitted, the form type of the latest filing to fetch, e.g. "10-K", "10-Q", "8-K", "DEF 14A" — or a `|`-separated SET ("10-K|10-Q") for "the most recent of either, whichever is newer". Omit both accession and form_type to get the single most recent filing of ANY type at all (routine 8-Ks/Form 4s/Form 144s included, not just annual/quarterly reports) — use the `|` set instead when you specifically want the newest 10-K or 10-Q.',
|
|
1538
1573
|
},
|
|
1539
1574
|
include_primary_text: {
|
|
1540
1575
|
type: 'boolean',
|
|
@@ -1551,7 +1586,7 @@ const tools: McpToolExport['tools'] = [
|
|
|
1551
1586
|
{
|
|
1552
1587
|
name: 'edgar_filing_text',
|
|
1553
1588
|
description:
|
|
1554
|
-
'AUTHORITATIVE full text of a SEC filing\'s primary document (10-K / 10-Q / 8-K body), HTML stripped to clean plaintext — the source for disclosures that live in prose, not XBRL: going-concern language, ATM / at-the-market equity facilities, committed-equity share caps, public-float figures, subsequent events,
|
|
1589
|
+
'AUTHORITATIVE full text of a SEC filing\'s primary document (10-K / 10-Q / 8-K body), HTML stripped to clean plaintext — the source for disclosures that live in prose, not XBRL: going-concern language, ATM / at-the-market equity facilities, committed-equity share caps, public-float figures, subsequent events, the liquidity footnote, and MD&A KPIs XBRL never tags (test volume, units shipped, subscriber counts, same-store sales). Pass an accession (from edgar_search_filings / edgar_company_filings) plus the filer\'s ticker or CIK; OR omit accession and pass ticker + form_type to auto-resolve the latest matching filing. **For a specific fact inside a long filing, pass `search`** (a word or exact phrase, e.g. "tests processed" or "processed approximately") instead of paging blind — it scans the WHOLE document (before any offset/max_chars windowing) and returns every matching passage with surrounding context and its own `offset` in the document, so a KPI ~100k characters in is found in one call instead of paging through `max_chars` windows by hand. A zero-match `search` is a real answer (the filing does not use that exact wording) — retry with a shorter or different phrase rather than assuming the tool failed. Optionally set `section` to return just one part (going_concern | liquidity | capital_resources | subsequent_events); `search` runs within that slice when both are given. Large docs (a 10-Q is ~100k+ chars of text) are PAGED, not spilled, when `search` is not used: the result caps at `max_chars` (default 50000) from `offset`, and returns `truncated` + `next_offset` — pass next_offset back as `offset` to read the next window. An especially large filing (e.g. an S-1 with heavy inline-XBRL tagging can exceed 10MB of raw HTML) is also capped on the READ side — the response sets `raw_truncated:true` when only the first portion of the document was read at all, which bounds how far `offset` can page (and how far `search` can scan) and can make a late section or search term come back not-found even though it exists further in. Use for "does $TICKER disclose substantial doubt / going concern", "what ATM facility does $TICKER have", "read the liquidity section of the latest 10-Q", "how many tests did $TICKER process this quarter". For the list of documents/exhibits in a filing use edgar_filing_documents; for structured financial numbers use edgar_company_concept.',
|
|
1555
1590
|
summary: 'The full text of an SEC filing\'s main document, HTML stripped to clean plaintext.',
|
|
1556
1591
|
inputSchema: {
|
|
1557
1592
|
type: 'object' as const,
|
|
@@ -1570,7 +1605,7 @@ const tools: McpToolExport['tools'] = [
|
|
|
1570
1605
|
},
|
|
1571
1606
|
form_type: {
|
|
1572
1607
|
type: 'string',
|
|
1573
|
-
description: 'When accession is omitted, the form type of the latest filing to fetch — "10-K", "10-Q", "8-K", "DEF 14A", etc.',
|
|
1608
|
+
description: 'When accession is omitted, the form type of the latest filing to fetch — "10-K", "10-Q", "8-K", "DEF 14A", etc. For "the most recent 10-K OR 10-Q" (or any "whichever of these is newer" question), pass a `|`-separated SET, e.g. "10-K|10-Q" — do NOT guess a single type ("10-K" by habit skips a newer 10-Q) and do NOT omit this field to get "any type", since a company files far more 8-Ks/Form 4s/Form 144s between annual or quarterly reports than it files the reports themselves and an empty form_type returns the single most recent filing of ANY kind (verified live 2026-09-25, fleet #2450: NTRA\'s single most recent SEC filing was a Form 144 insider-sale notice, filed weeks after its real 10-Q and completely unrelated to the question asked).',
|
|
1574
1609
|
},
|
|
1575
1610
|
section: {
|
|
1576
1611
|
type: 'string',
|
|
@@ -1585,6 +1620,22 @@ const tools: McpToolExport['tools'] = [
|
|
|
1585
1620
|
type: 'number',
|
|
1586
1621
|
description: 'Character offset to start from (default 0). Pass the prior result\'s next_offset to page forward.',
|
|
1587
1622
|
},
|
|
1623
|
+
search: {
|
|
1624
|
+
type: 'string',
|
|
1625
|
+
description: 'Find a specific fact instead of paging blind. This is a SUBSTRING match, not a relevance search — pass the exact 2-4 word phrase most likely to appear VERBATIM in the prose ("tests processed", "processed approximately", "going concern"), never a compound of every concept in the question. A phrase that ANDs several unrelated question-words together ("oncology Signatera revenue test volume") returns ZERO matches even in the CORRECT filing, because the filing\'s own sentence never contains all of those words together — verified live 2026-09-25 (fleet #2450) on a real Natera 10-Q that DOES report the exact number asked for: "Signatera revenue" and the 4-word compound above both found nothing, while the filing\'s own wording ("processed approximately", "tests processed") is what actually appears. When unsure of the filing\'s exact phrasing, prefer the SHORTEST distinctive 2-3 word fragment of the metric name itself (a unit, a verb+noun like "processed approximately") over restating the question. Case-insensitive substring match over the WHOLE document text (or the whole `section` slice, if both are given), run BEFORE max_chars/offset windowing. Returns every matching passage (surrounding context + its own offset in the document) instead of the normal paged `text` — use the returned offsets with a follow-up call (no `search`, `offset` set to one of them) if you need more surrounding text than the passage gives. Zero matches means try again with a SHORTER, more literal phrase before concluding the filing does not disclose it — this is a real answer about wording, not a tool failure. Accepted aliases: `contains`, `find`, `phrase`.',
|
|
1626
|
+
},
|
|
1627
|
+
contains: {
|
|
1628
|
+
type: 'string',
|
|
1629
|
+
description: 'Alias for `search`.',
|
|
1630
|
+
},
|
|
1631
|
+
find: {
|
|
1632
|
+
type: 'string',
|
|
1633
|
+
description: 'Alias for `search`.',
|
|
1634
|
+
},
|
|
1635
|
+
phrase: {
|
|
1636
|
+
type: 'string',
|
|
1637
|
+
description: 'Alias for `search`.',
|
|
1638
|
+
},
|
|
1588
1639
|
ticker_or_cik: {
|
|
1589
1640
|
type: 'string',
|
|
1590
1641
|
description: 'Alias for `ticker` / `cik` — the single-argument spelling edgar_company_filings and edgar_insider_transactions use. Takes either a ticker or a CIK.',
|
|
@@ -2317,6 +2368,34 @@ function htmlToText(html: string): string {
|
|
|
2317
2368
|
.trim();
|
|
2318
2369
|
}
|
|
2319
2370
|
|
|
2371
|
+
/**
|
|
2372
|
+
* Index into SEC's own recent-first `forms` array of the latest filing
|
|
2373
|
+
* matching `wantFormTypeArg` — a single form ("10-Q") or a `|`-separated SET
|
|
2374
|
+
* ("10-K|10-Q" for "the most recent annual or quarterly report, whichever is
|
|
2375
|
+
* newer"), same pipe convention this catalog already uses for status fields
|
|
2376
|
+
* (grants-gov's "posted|forecasted"). Omitted/empty returns 0 — the single
|
|
2377
|
+
* most recent filing of ANY type.
|
|
2378
|
+
*
|
|
2379
|
+
* Fleet #2450: a question asking for "the most recent 10-K or 10-Q" cannot
|
|
2380
|
+
* be answered by omitting form_type — that resolves to the single most
|
|
2381
|
+
* recent filing of ANY type, and a company files far more 8-Ks, Form 4s and
|
|
2382
|
+
* Form 144s between annual/quarterly reports than it files the reports
|
|
2383
|
+
* themselves. Verified live: NTRA's single most recent SEC filing on
|
|
2384
|
+
* 2026-09-23 was a Form 144 insider-sale notice, not an annual or quarterly
|
|
2385
|
+
* report at all — form_type left empty silently returns THAT instead of the
|
|
2386
|
+
* 10-Q the question meant. Nor does picking one type and hoping work: a
|
|
2387
|
+
* 10-Q is very often newer than the last 10-K. "10-K|10-Q" is the answer to
|
|
2388
|
+
* "I don't care which, just the newest of these".
|
|
2389
|
+
*/
|
|
2390
|
+
function findLatestFormIndex(forms: string[], wantFormTypeArg: string | undefined): number {
|
|
2391
|
+
const wanted = (wantFormTypeArg ?? '')
|
|
2392
|
+
.split('|')
|
|
2393
|
+
.map((f) => f.trim().toUpperCase())
|
|
2394
|
+
.filter(Boolean);
|
|
2395
|
+
if (!wanted.length) return 0;
|
|
2396
|
+
return forms.findIndex((f) => wanted.includes(f.toUpperCase()));
|
|
2397
|
+
}
|
|
2398
|
+
|
|
2320
2399
|
async function filingDocuments(
|
|
2321
2400
|
accession: string | undefined,
|
|
2322
2401
|
tickerOrCik: string,
|
|
@@ -2347,7 +2426,7 @@ async function filingDocuments(
|
|
|
2347
2426
|
const r = sub.filings?.recent;
|
|
2348
2427
|
const forms = r?.form ?? [];
|
|
2349
2428
|
const accs = r?.accessionNumber ?? [];
|
|
2350
|
-
const idx =
|
|
2429
|
+
const idx = findLatestFormIndex(forms, wantFormTypeArg);
|
|
2351
2430
|
if (idx < 0 || !accs[idx]) {
|
|
2352
2431
|
throw new Error(
|
|
2353
2432
|
`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.`,
|
|
@@ -2464,6 +2543,72 @@ const FILING_SECTION_PATTERNS: Record<string, { anchor: RegExp; pick: 'first' |
|
|
|
2464
2543
|
const FILING_TEXT_DEFAULT_MAX = 50000;
|
|
2465
2544
|
const FILING_TEXT_CAP = 100000;
|
|
2466
2545
|
|
|
2546
|
+
// fleet #2446: a KPI question ("how many tests did $TICKER process") routed
|
|
2547
|
+
// correctly to edgar_filing_text but the fact sat ~100k characters into a
|
|
2548
|
+
// 10-Q's MD&A, which meant paging max_chars windows by hand to find it. This
|
|
2549
|
+
// is a plain case-insensitive substring scan of the ALREADY-EXTRACTED plain-
|
|
2550
|
+
// text document (not a second fetch) — cheap, and it runs before the normal
|
|
2551
|
+
// offset/max_chars windowing so it sees the whole doc (or the whole `section`
|
|
2552
|
+
// slice) rather than one page of it.
|
|
2553
|
+
const SEARCH_CONTEXT_CHARS = 400;
|
|
2554
|
+
const SEARCH_MAX_PASSAGES = 10;
|
|
2555
|
+
|
|
2556
|
+
// A KPI figure looks like "2,056,800" or "1,708,200" — thousands-grouped, or
|
|
2557
|
+
// otherwise a bare run of 4+ digits (a raw count with no separator).
|
|
2558
|
+
const NEARBY_NUMBER_RE = /\d{1,3}(?:,\d{3})+|\d{4,}/;
|
|
2559
|
+
|
|
2560
|
+
function findPassages(
|
|
2561
|
+
text: string,
|
|
2562
|
+
term: string,
|
|
2563
|
+
contextChars = SEARCH_CONTEXT_CHARS,
|
|
2564
|
+
maxPassages = SEARCH_MAX_PASSAGES,
|
|
2565
|
+
): { total: number; distinct_locations: number; passages: Array<{ offset: number; text: string }> } {
|
|
2566
|
+
const needle = term.toLowerCase();
|
|
2567
|
+
if (!needle) return { total: 0, distinct_locations: 0, passages: [] };
|
|
2568
|
+
const hay = text.toLowerCase();
|
|
2569
|
+
const offsets: number[] = [];
|
|
2570
|
+
let from = 0;
|
|
2571
|
+
for (;;) {
|
|
2572
|
+
const idx = hay.indexOf(needle, from);
|
|
2573
|
+
if (idx === -1) break;
|
|
2574
|
+
offsets.push(idx);
|
|
2575
|
+
from = idx + needle.length;
|
|
2576
|
+
}
|
|
2577
|
+
|
|
2578
|
+
// Merge occurrences close enough that their context windows would overlap
|
|
2579
|
+
// into ONE passage — a KPI sentence ("we processed approximately X tests,
|
|
2580
|
+
// compared to Y tests processed") can repeat the search term 2-3 times a
|
|
2581
|
+
// few words apart, and without this a common word burns most of the
|
|
2582
|
+
// (capped) passage budget on one sentence instead of covering the document.
|
|
2583
|
+
const groups: Array<{ first: number; last: number }> = [];
|
|
2584
|
+
for (const idx of offsets) {
|
|
2585
|
+
const g = groups[groups.length - 1];
|
|
2586
|
+
if (g && idx - g.last <= contextChars * 2) g.last = idx;
|
|
2587
|
+
else groups.push({ first: idx, last: idx });
|
|
2588
|
+
}
|
|
2589
|
+
|
|
2590
|
+
// Rank groups whose window carries a nearby NUMBER first. A KPI question
|
|
2591
|
+
// ("how many tests/units/subscribers") is answered by a figure next to the
|
|
2592
|
+
// word, not by every mention of the word — measured live (fleet #2446):
|
|
2593
|
+
// searching "tests" in Natera's Q2 2026 10-Q hits 78 times, mostly
|
|
2594
|
+
// forward-looking-statements boilerplate ("expectations... for our
|
|
2595
|
+
// tests"), with the actual "processed approximately 2,056,800 tests" KPI
|
|
2596
|
+
// buried past the first ~80 of them by document order. Ranking by nearby
|
|
2597
|
+
// digits (no second fetch — same text already in hand) reliably surfaces
|
|
2598
|
+
// it inside a small `maxPassages` cap instead of requiring the caller to
|
|
2599
|
+
// already know the more distinctive phrase to search for. Groups without a
|
|
2600
|
+
// nearby number keep DOCUMENT ORDER among themselves (stable sort).
|
|
2601
|
+
const scored = groups.map((g) => {
|
|
2602
|
+
const start = Math.max(0, g.first - contextChars);
|
|
2603
|
+
const end = Math.min(text.length, g.last + needle.length + contextChars);
|
|
2604
|
+
return { start, end, hasNumber: NEARBY_NUMBER_RE.test(text.slice(start, end)) ? 1 : 0 };
|
|
2605
|
+
});
|
|
2606
|
+
scored.sort((a, b) => b.hasNumber - a.hasNumber);
|
|
2607
|
+
|
|
2608
|
+
const passages = scored.slice(0, maxPassages).map((s) => ({ offset: s.start, text: text.slice(s.start, s.end) }));
|
|
2609
|
+
return { total: offsets.length, distinct_locations: groups.length, passages };
|
|
2610
|
+
}
|
|
2611
|
+
|
|
2467
2612
|
// Fetch ONE filing's primary-document text, HTML-stripped, paged. Splitting this
|
|
2468
2613
|
// out (rather than piggybacking edgar_filing_documents' include_primary_text,
|
|
2469
2614
|
// which caps at 40k and can't page) lets the register pull deep disclosures —
|
|
@@ -2475,6 +2620,7 @@ async function filingText(
|
|
|
2475
2620
|
maxChars?: number,
|
|
2476
2621
|
offset?: number,
|
|
2477
2622
|
wantFormTypeArg?: string,
|
|
2623
|
+
searchTerm?: string,
|
|
2478
2624
|
) {
|
|
2479
2625
|
if (typeof tickerOrCik !== 'string' || !tickerOrCik.trim()) {
|
|
2480
2626
|
throw new Error('A company is required: pass `ticker` ("ACTU"), `cik` or their alias `ticker_or_cik` — plus either an accession or a form_type (e.g. edgar_filing_text({ticker:"ACTU", form_type:"10-Q"})).');
|
|
@@ -2501,7 +2647,7 @@ async function filingText(
|
|
|
2501
2647
|
|
|
2502
2648
|
if (!acc) {
|
|
2503
2649
|
const wantForm = wantFormTypeArg?.trim().toUpperCase();
|
|
2504
|
-
const idx =
|
|
2650
|
+
const idx = findLatestFormIndex(forms, wantFormTypeArg);
|
|
2505
2651
|
if (idx < 0 || !accs[idx]) {
|
|
2506
2652
|
throw new Error(`No ${wantForm ?? 'recent'} filing found for ${tickerOrCik}. Use edgar_company_filings({ticker_or_cik:"${tickerOrCik}"${wantForm ? `, form_type:"${wantForm}"` : ''}}) to list filings, then pass an accession.`);
|
|
2507
2653
|
}
|
|
@@ -2559,6 +2705,44 @@ async function filingText(
|
|
|
2559
2705
|
}
|
|
2560
2706
|
}
|
|
2561
2707
|
|
|
2708
|
+
// `search` short-circuits the normal offset/max_chars windowing: it scans
|
|
2709
|
+
// the whole (possibly section-sliced) document and returns matching
|
|
2710
|
+
// passages with their own offsets, rather than one page of raw text the
|
|
2711
|
+
// caller has to eyeball for the fact they wanted (fleet #2446).
|
|
2712
|
+
const search = (searchTerm ?? '').trim();
|
|
2713
|
+
if (search) {
|
|
2714
|
+
const { total: totalMatches, distinct_locations: distinctLocations, passages } = findPassages(fullText, search);
|
|
2715
|
+
return {
|
|
2716
|
+
accession: acc.dashed,
|
|
2717
|
+
cik: cikNoZeros,
|
|
2718
|
+
company: sub.name ?? null,
|
|
2719
|
+
form: formType,
|
|
2720
|
+
filed: filingDate,
|
|
2721
|
+
document: primaryDocument,
|
|
2722
|
+
document_url: docUrl,
|
|
2723
|
+
section: sectionApplied,
|
|
2724
|
+
...(sectionFound !== null ? { section_found: sectionFound } : {}),
|
|
2725
|
+
total_chars: fullText.length,
|
|
2726
|
+
search: {
|
|
2727
|
+
term: search,
|
|
2728
|
+
total_matches: totalMatches,
|
|
2729
|
+
distinct_locations: distinctLocations,
|
|
2730
|
+
returned: passages.length,
|
|
2731
|
+
truncated: distinctLocations > passages.length,
|
|
2732
|
+
passages,
|
|
2733
|
+
note: totalMatches === 0
|
|
2734
|
+
? 'No case-insensitive match for this exact wording in the document (or section, if one was given). That is a real answer, not a failure — retry with a shorter phrase or drop `section` to search the whole document.'
|
|
2735
|
+
: `Passages carrying a nearby number (a likely KPI figure) are ranked first; the rest keep document order. Each passage's offset is a position in the document text — pass it as \`offset\` (without \`search\`) to read more surrounding text via the normal paged text field.`,
|
|
2736
|
+
},
|
|
2737
|
+
...(rawTruncated
|
|
2738
|
+
? {
|
|
2739
|
+
raw_truncated: true,
|
|
2740
|
+
raw_truncated_note: `This document's raw HTML exceeds the ${(FILING_DOC_MAX_BYTES / 1_000_000).toFixed(0)}MB this tool reads in one call, so the search above only scanned the portion read — NOT the whole filing. Treat total_matches:0 as inconclusive, not "absent", for a raw_truncated document.`,
|
|
2741
|
+
}
|
|
2742
|
+
: {}),
|
|
2743
|
+
};
|
|
2744
|
+
}
|
|
2745
|
+
|
|
2562
2746
|
const total = fullText.length;
|
|
2563
2747
|
const off = Math.max(0, Math.floor(Number(offset) || 0));
|
|
2564
2748
|
const cap = Math.min(FILING_TEXT_CAP, Math.max(1000, Math.floor(Number(maxChars) || FILING_TEXT_DEFAULT_MAX)));
|
|
@@ -4439,6 +4623,7 @@ async function callTool(name: string, args: Record<string, unknown>): Promise<un
|
|
|
4439
4623
|
args.max_chars as number | undefined,
|
|
4440
4624
|
args.offset as number | undefined,
|
|
4441
4625
|
args.form_type as string | undefined,
|
|
4626
|
+
(args.search ?? args.contains ?? args.find ?? args.phrase) as string | undefined,
|
|
4442
4627
|
);
|
|
4443
4628
|
case 'edgar_ticker_to_cik':
|
|
4444
4629
|
return tickerToCik((args.ticker ?? args.ticker_or_cik ?? args.company) as string);
|
package/src/server.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprot
|
|
|
9
9
|
import pack from './index.js';
|
|
10
10
|
|
|
11
11
|
const server = new Server(
|
|
12
|
-
{ name: '@pipeworx/mcp-edgar', version: '0.3.
|
|
12
|
+
{ name: '@pipeworx/mcp-edgar', version: '0.3.5' },
|
|
13
13
|
{ capabilities: { tools: {} } },
|
|
14
14
|
);
|
|
15
15
|
|