@pipeworx/mcp-edgar 0.3.5 → 0.3.6
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/package.json +3 -3
- package/server.json +1 -1
- package/src/index.ts +94 -12
- package/src/server.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pipeworx/mcp-edgar",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.6",
|
|
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-11e345a6ca7725b75cd7b396ad673e52735a250951201acc76dbfd62907be72f",
|
|
30
|
+
"sourceCommit": "6182c4cfebddb94950e060b5d2fca18f18ad1aca"
|
|
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.6",
|
|
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
|
@@ -1622,7 +1622,7 @@ const tools: McpToolExport['tools'] = [
|
|
|
1622
1622
|
},
|
|
1623
1623
|
search: {
|
|
1624
1624
|
type: 'string',
|
|
1625
|
-
description: 'Find a specific fact instead of paging blind.
|
|
1625
|
+
description: 'Find a specific fact instead of paging blind. Pass a short 2-4 word phrase likely to appear VERBATIM in the prose ("processed approximately", "tests processed", "going concern") rather than restating the question. Case-insensitive substring match over the WHOLE document text (or the whole `section` slice), run BEFORE max_chars/offset windowing; returns matching passages (context + their own offset) instead of the paged `text`, ranking passages with a nearby figure first. If a multi-word phrase has no verbatim match, it falls back to the phrase\'s individual words and returns the passages holding the most of them (search.match_mode "words") — read those passages for the fact rather than treating them as confirmed. Use a returned offset with a follow-up call (no `search`) to read more surrounding text. Accepted aliases: `contains`, `find`, `phrase`.',
|
|
1626
1626
|
},
|
|
1627
1627
|
contains: {
|
|
1628
1628
|
type: 'string',
|
|
@@ -2609,6 +2609,74 @@ function findPassages(
|
|
|
2609
2609
|
return { total: offsets.length, distinct_locations: groups.length, passages };
|
|
2610
2610
|
}
|
|
2611
2611
|
|
|
2612
|
+
// fleet #2450: the router answers in ONE tool call, so "retry with a shorter
|
|
2613
|
+
// phrase" (the old zero-match note) never happens inside ask_pipeworx. Measured
|
|
2614
|
+
// live 2026-09-26: 5 of 5 routed calls for the Natera "revenue and test volume
|
|
2615
|
+
// in its most recent 10-K or 10-Q" question reached the RIGHT filing and passed
|
|
2616
|
+
// a compound phrase ("Signatera revenue", "oncology Signatera revenue test
|
|
2617
|
+
// volume") that matches nothing verbatim — while the word "Signatera" alone
|
|
2618
|
+
// lands on the "processed approximately 2,056,800 tests" sentence. A description
|
|
2619
|
+
// telling the router to pass shorter phrases did not change what it passed.
|
|
2620
|
+
// So when the exact phrase misses, fall back to the phrase's WORDS: anchor on
|
|
2621
|
+
// the rarer words, rank each window by how many distinct query words it holds
|
|
2622
|
+
// (then by a nearby figure), and say plainly that this is a word-level match.
|
|
2623
|
+
const WORD_FALLBACK_STOP = new Set([
|
|
2624
|
+
'the', 'and', 'for', 'its', 'our', 'their', 'with', 'from', 'that', 'this', 'what', 'which',
|
|
2625
|
+
'most', 'recent', 'latest', 'report', 'reported', 'reports', 'filing', 'total', 'any', 'all',
|
|
2626
|
+
]);
|
|
2627
|
+
const WORD_FALLBACK_ANCHOR_MAX = 60;
|
|
2628
|
+
|
|
2629
|
+
export function findWordPassages(
|
|
2630
|
+
text: string,
|
|
2631
|
+
phrase: string,
|
|
2632
|
+
contextChars = SEARCH_CONTEXT_CHARS,
|
|
2633
|
+
maxPassages = SEARCH_MAX_PASSAGES,
|
|
2634
|
+
): { words: Array<{ word: string; occurrences: number }>; distinct_locations: number; passages: Array<{ offset: number; text: string; words_matched: string[] }> } | null {
|
|
2635
|
+
const words = [...new Set(phrase.toLowerCase().split(/[^a-z0-9&-]+/).filter((w) => w.length >= 3 && !WORD_FALLBACK_STOP.has(w)))];
|
|
2636
|
+
if (words.length < 2) return null;
|
|
2637
|
+
const hay = text.toLowerCase();
|
|
2638
|
+
const occ = new Map<string, number[]>();
|
|
2639
|
+
for (const w of words) {
|
|
2640
|
+
const offs: number[] = [];
|
|
2641
|
+
let from = 0;
|
|
2642
|
+
for (;;) {
|
|
2643
|
+
const i = hay.indexOf(w, from);
|
|
2644
|
+
if (i === -1) break;
|
|
2645
|
+
offs.push(i);
|
|
2646
|
+
from = i + w.length;
|
|
2647
|
+
}
|
|
2648
|
+
occ.set(w, offs);
|
|
2649
|
+
}
|
|
2650
|
+
const present = words.filter((w) => (occ.get(w) ?? []).length > 0);
|
|
2651
|
+
if (present.length === 0) return null;
|
|
2652
|
+
// Anchor on every word rare enough to be distinctive; if every word is
|
|
2653
|
+
// common, anchor on the rarest one only (bounded work on a 200k-char doc).
|
|
2654
|
+
const byRarity = [...present].sort((a, b) => occ.get(a)!.length - occ.get(b)!.length);
|
|
2655
|
+
const anchors = byRarity.filter((w) => occ.get(w)!.length <= WORD_FALLBACK_ANCHOR_MAX);
|
|
2656
|
+
const anchorWords = anchors.length ? anchors : [byRarity[0]];
|
|
2657
|
+
const anchorOffs = [...new Set(anchorWords.flatMap((w) => occ.get(w)!))].sort((a, b) => a - b);
|
|
2658
|
+
|
|
2659
|
+
const groups: Array<{ first: number; last: number }> = [];
|
|
2660
|
+
for (const idx of anchorOffs) {
|
|
2661
|
+
const g = groups[groups.length - 1];
|
|
2662
|
+
if (g && idx - g.last <= contextChars * 2) g.last = idx;
|
|
2663
|
+
else groups.push({ first: idx, last: idx });
|
|
2664
|
+
}
|
|
2665
|
+
const scored = groups.map((g) => {
|
|
2666
|
+
const start = Math.max(0, g.first - contextChars);
|
|
2667
|
+
const end = Math.min(text.length, g.last + contextChars);
|
|
2668
|
+
const win = hay.slice(start, end);
|
|
2669
|
+
const matched = present.filter((w) => win.includes(w));
|
|
2670
|
+
return { start, end, matched, hasNumber: NEARBY_NUMBER_RE.test(text.slice(start, end)) ? 1 : 0 };
|
|
2671
|
+
});
|
|
2672
|
+
scored.sort((a, b) => (b.matched.length - a.matched.length) || (b.hasNumber - a.hasNumber));
|
|
2673
|
+
return {
|
|
2674
|
+
words: words.map((w) => ({ word: w, occurrences: (occ.get(w) ?? []).length })),
|
|
2675
|
+
distinct_locations: groups.length,
|
|
2676
|
+
passages: scored.slice(0, maxPassages).map((s) => ({ offset: s.start, text: text.slice(s.start, s.end), words_matched: s.matched })),
|
|
2677
|
+
};
|
|
2678
|
+
}
|
|
2679
|
+
|
|
2612
2680
|
// Fetch ONE filing's primary-document text, HTML-stripped, paged. Splitting this
|
|
2613
2681
|
// out (rather than piggybacking edgar_filing_documents' include_primary_text,
|
|
2614
2682
|
// which caps at 40k and can't page) lets the register pull deep disclosures —
|
|
@@ -2712,6 +2780,7 @@ async function filingText(
|
|
|
2712
2780
|
const search = (searchTerm ?? '').trim();
|
|
2713
2781
|
if (search) {
|
|
2714
2782
|
const { total: totalMatches, distinct_locations: distinctLocations, passages } = findPassages(fullText, search);
|
|
2783
|
+
const wordHit = totalMatches === 0 ? findWordPassages(fullText, search) : null;
|
|
2715
2784
|
return {
|
|
2716
2785
|
accession: acc.dashed,
|
|
2717
2786
|
cik: cikNoZeros,
|
|
@@ -2723,17 +2792,30 @@ async function filingText(
|
|
|
2723
2792
|
section: sectionApplied,
|
|
2724
2793
|
...(sectionFound !== null ? { section_found: sectionFound } : {}),
|
|
2725
2794
|
total_chars: fullText.length,
|
|
2726
|
-
search:
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
:
|
|
2736
|
-
|
|
2795
|
+
search: wordHit
|
|
2796
|
+
? {
|
|
2797
|
+
term: search,
|
|
2798
|
+
match_mode: 'words',
|
|
2799
|
+
exact_matches: 0,
|
|
2800
|
+
words: wordHit.words,
|
|
2801
|
+
distinct_locations: wordHit.distinct_locations,
|
|
2802
|
+
returned: wordHit.passages.length,
|
|
2803
|
+
truncated: wordHit.distinct_locations > wordHit.passages.length,
|
|
2804
|
+
passages: wordHit.passages,
|
|
2805
|
+
note: `The exact phrase "${search}" does not appear in this document, so these are passages holding its individual WORDS, ranked by how many of them each passage contains (then by a nearby figure). Read each passage for the fact asked — a word-level match is a lead, not a confirmation. Each offset can be passed as \`offset\` (without \`search\`) to read more surrounding text.`,
|
|
2806
|
+
}
|
|
2807
|
+
: {
|
|
2808
|
+
term: search,
|
|
2809
|
+
match_mode: 'exact',
|
|
2810
|
+
total_matches: totalMatches,
|
|
2811
|
+
distinct_locations: distinctLocations,
|
|
2812
|
+
returned: passages.length,
|
|
2813
|
+
truncated: distinctLocations > passages.length,
|
|
2814
|
+
passages,
|
|
2815
|
+
note: totalMatches === 0
|
|
2816
|
+
? 'No case-insensitive match for this wording (nor, for a multi-word phrase, for any of its individual words) in the document (or section, if one was given). That is a real answer, not a failure — retry with different wording or drop `section` to search the whole document.'
|
|
2817
|
+
: `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.`,
|
|
2818
|
+
},
|
|
2737
2819
|
...(rawTruncated
|
|
2738
2820
|
? {
|
|
2739
2821
|
raw_truncated: true,
|
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.6' },
|
|
13
13
|
{ capabilities: { tools: {} } },
|
|
14
14
|
);
|
|
15
15
|
|