@pipeworx/mcp-edgar 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Pipeworx
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,34 @@
1
+ # mcp-edgar
2
+
3
+ EDGAR MCP — SEC EDGAR public APIs (free, no auth)
4
+
5
+ Part of the [Pipeworx](https://pipeworx.io) open MCP gateway.
6
+
7
+ ## Tools
8
+
9
+ | Tool | Description |
10
+ |------|-------------|
11
+
12
+ ## Quick Start
13
+
14
+ Add to your MCP client config:
15
+
16
+ ```json
17
+ {
18
+ "mcpServers": {
19
+ "edgar": {
20
+ "url": "https://gateway.pipeworx.io/edgar/mcp"
21
+ }
22
+ }
23
+ }
24
+ ```
25
+
26
+ Or use the CLI:
27
+
28
+ ```bash
29
+ npx pipeworx use edgar
30
+ ```
31
+
32
+ ## License
33
+
34
+ MIT
package/package.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "@pipeworx/mcp-edgar",
3
+ "version": "0.1.0",
4
+ "description": "EDGAR MCP — SEC EDGAR public APIs (free, no auth)",
5
+ "type": "module",
6
+ "main": "src/index.ts",
7
+ "types": "src/index.ts",
8
+ "keywords": ["mcp", "mcp-server", "model-context-protocol", "pipeworx", "edgar"],
9
+ "license": "MIT",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "https://github.com/pipeworx-io/mcp-edgar"
13
+ },
14
+ "scripts": {
15
+ "typecheck": "tsc --noEmit"
16
+ },
17
+ "devDependencies": {
18
+ "typescript": "^5.7.0"
19
+ }
20
+ }
package/server.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
+ "name": "io.github.pipeworx-io/edgar",
4
+ "title": "edgar",
5
+ "description": "EDGAR MCP — SEC EDGAR public APIs (free, no auth)",
6
+ "version": "0.1.0",
7
+ "websiteUrl": "https://pipeworx.io/packs/edgar",
8
+ "repository": {
9
+ "url": "https://github.com/pipeworx-io/mcp-edgar",
10
+ "source": "github"
11
+ },
12
+ "remotes": [
13
+ {
14
+ "type": "streamable-http",
15
+ "url": "https://gateway.pipeworx.io/edgar/mcp"
16
+ }
17
+ ]
18
+ }
package/src/index.ts ADDED
@@ -0,0 +1,447 @@
1
+ interface McpToolDefinition {
2
+ name: string;
3
+ description: string;
4
+ inputSchema: {
5
+ type: 'object';
6
+ properties: Record<string, unknown>;
7
+ required?: string[];
8
+ };
9
+ }
10
+
11
+ interface McpToolExport {
12
+ tools: McpToolDefinition[];
13
+ callTool: (name: string, args: Record<string, unknown>) => Promise<unknown>;
14
+ }
15
+
16
+ /**
17
+ * EDGAR MCP — SEC EDGAR public APIs (free, no auth)
18
+ *
19
+ * Tools:
20
+ * - edgar_search_filings: full-text search across all SEC filings
21
+ * - edgar_company_filings: get filings for a specific company by CIK or ticker
22
+ * - edgar_company_facts: get structured XBRL financial data for a company
23
+ * - edgar_company_concept: get a specific financial metric over time
24
+ * - edgar_ticker_to_cik: look up CIK from ticker symbol
25
+ *
26
+ * Note: SEC requires a descriptive User-Agent header per their guidelines.
27
+ */
28
+
29
+
30
+ const EFTS_BASE = 'https://efts.sec.gov/LATEST';
31
+ const DATA_BASE = 'https://data.sec.gov';
32
+ const SEC_HEADERS: Record<string, string> = {
33
+ 'User-Agent': 'Pipeworx/1.0 (support@pipeworx.io)',
34
+ Accept: 'application/json',
35
+ };
36
+
37
+ const tools: McpToolExport['tools'] = [
38
+ {
39
+ name: 'edgar_search_filings',
40
+ description:
41
+ 'Full-text search across all SEC EDGAR filings. Search by keyword, company name, or topic. Optionally filter by form type and date range.',
42
+ inputSchema: {
43
+ type: 'object' as const,
44
+ properties: {
45
+ query: { type: 'string', description: 'Search query (e.g., "artificial intelligence", "Tesla revenue")' },
46
+ form_type: {
47
+ type: 'string',
48
+ description: 'Filter by SEC form type (e.g., "10-K", "10-Q", "8-K", "DEF 14A"). Omit for all types.',
49
+ },
50
+ start_date: { type: 'string', description: 'Start date in YYYY-MM-DD format (e.g., "2024-01-01")' },
51
+ end_date: { type: 'string', description: 'End date in YYYY-MM-DD format (e.g., "2024-12-31")' },
52
+ limit: { type: 'number', description: 'Number of results to return (1-40, default 10)' },
53
+ },
54
+ required: ['query'],
55
+ },
56
+ },
57
+ {
58
+ name: 'edgar_company_filings',
59
+ description:
60
+ 'Get recent SEC filings for a specific company. Accepts a ticker symbol or CIK number. Optionally filter by form type.',
61
+ inputSchema: {
62
+ type: 'object' as const,
63
+ properties: {
64
+ ticker_or_cik: {
65
+ type: 'string',
66
+ description: 'Ticker symbol (e.g., "AAPL") or CIK number (e.g., "320193")',
67
+ },
68
+ form_type: {
69
+ type: 'string',
70
+ description: 'Filter by SEC form type (e.g., "10-K", "10-Q", "8-K"). Omit for all types.',
71
+ },
72
+ limit: { type: 'number', description: 'Max filings to return (1-40, default 20)' },
73
+ },
74
+ required: ['ticker_or_cik'],
75
+ },
76
+ },
77
+ {
78
+ name: 'edgar_company_facts',
79
+ description:
80
+ 'Get structured XBRL financial data for a company by CIK. Returns key financial metrics like revenue, net income, assets, and more with their most recent annual values.',
81
+ inputSchema: {
82
+ type: 'object' as const,
83
+ properties: {
84
+ cik: {
85
+ type: 'string',
86
+ description: 'Company CIK number (e.g., "320193" for Apple). Use edgar_ticker_to_cik to look up if needed.',
87
+ },
88
+ },
89
+ required: ['cik'],
90
+ },
91
+ },
92
+ {
93
+ name: 'edgar_company_concept',
94
+ description:
95
+ 'Get a specific financial metric over time for a company. Returns all reported values across filings for a given US-GAAP concept.',
96
+ inputSchema: {
97
+ type: 'object' as const,
98
+ properties: {
99
+ cik: {
100
+ type: 'string',
101
+ description: 'Company CIK number (e.g., "320193" for Apple)',
102
+ },
103
+ concept: {
104
+ type: 'string',
105
+ description:
106
+ 'US-GAAP concept name (e.g., "Revenue", "NetIncomeLoss", "Assets", "Liabilities", "StockholdersEquity", "EarningsPerShareDiluted")',
107
+ },
108
+ },
109
+ required: ['cik', 'concept'],
110
+ },
111
+ },
112
+ {
113
+ name: 'edgar_ticker_to_cik',
114
+ description:
115
+ 'Look up a company CIK number from its ticker symbol. The CIK is needed for other EDGAR tools.',
116
+ inputSchema: {
117
+ type: 'object' as const,
118
+ properties: {
119
+ ticker: {
120
+ type: 'string',
121
+ description: 'Stock ticker symbol (e.g., "AAPL", "MSFT", "TSLA")',
122
+ },
123
+ },
124
+ required: ['ticker'],
125
+ },
126
+ },
127
+ ];
128
+
129
+ // ── Helpers ─────────────────────────────────────────────────────────
130
+
131
+ function padCik(cik: string): string {
132
+ return cik.replace(/\D/g, '').padStart(10, '0');
133
+ }
134
+
135
+ function isNumericCik(value: string): boolean {
136
+ return /^\d+$/.test(value.trim());
137
+ }
138
+
139
+ // ── Tool implementations ────────────────────────────────────────────
140
+
141
+ async function searchFilings(
142
+ query: string,
143
+ formType?: string,
144
+ startDate?: string,
145
+ endDate?: string,
146
+ limit?: number,
147
+ ) {
148
+ const count = Math.min(40, Math.max(1, limit ?? 10));
149
+ const params = new URLSearchParams({ q: query, from: '0', size: String(count) });
150
+
151
+ if (formType) params.set('forms', formType);
152
+ if (startDate || endDate) {
153
+ params.set('dateRange', 'custom');
154
+ if (startDate) params.set('startdt', startDate);
155
+ if (endDate) params.set('enddt', endDate);
156
+ }
157
+
158
+ const res = await fetch(`${EFTS_BASE}/search-index?${params}`, { headers: SEC_HEADERS });
159
+ if (!res.ok) throw new Error(`SEC EDGAR search error: ${res.status}`);
160
+
161
+ const data = (await res.json()) as {
162
+ hits: {
163
+ hits: {
164
+ _source: {
165
+ entity_name: string;
166
+ entity_id: string;
167
+ file_num: string;
168
+ form_type: string;
169
+ file_date: string;
170
+ period_of_report: string;
171
+ biz_location: string;
172
+ category: string;
173
+ };
174
+ _id: string;
175
+ }[];
176
+ total: { value: number };
177
+ };
178
+ };
179
+
180
+ const results = (data.hits?.hits ?? []).map((hit) => {
181
+ const s = hit._source;
182
+ return {
183
+ entity_name: s.entity_name,
184
+ cik: s.entity_id,
185
+ form_type: s.form_type,
186
+ filing_date: s.file_date,
187
+ period_of_report: s.period_of_report,
188
+ location: s.biz_location,
189
+ filing_id: hit._id,
190
+ };
191
+ });
192
+
193
+ return {
194
+ query,
195
+ form_type_filter: formType ?? 'all',
196
+ date_range: { start: startDate ?? null, end: endDate ?? null },
197
+ total_hits: data.hits?.total?.value ?? 0,
198
+ results,
199
+ };
200
+ }
201
+
202
+ async function tickerToCik(ticker: string) {
203
+ const res = await fetch('https://www.sec.gov/files/company_tickers.json', { headers: SEC_HEADERS });
204
+ if (!res.ok) throw new Error(`SEC ticker lookup error: ${res.status}`);
205
+
206
+ const data = (await res.json()) as Record<
207
+ string,
208
+ { cik_str: number; ticker: string; title: string }
209
+ >;
210
+
211
+ const upperTicker = ticker.toUpperCase().trim();
212
+ for (const entry of Object.values(data)) {
213
+ if (entry.ticker === upperTicker) {
214
+ return {
215
+ ticker: entry.ticker,
216
+ cik: String(entry.cik_str),
217
+ cik_padded: String(entry.cik_str).padStart(10, '0'),
218
+ company_name: entry.title,
219
+ };
220
+ }
221
+ }
222
+
223
+ throw new Error(`Ticker "${ticker}" not found in SEC company tickers`);
224
+ }
225
+
226
+ async function resolveCik(tickerOrCik: string): Promise<string> {
227
+ if (isNumericCik(tickerOrCik)) return tickerOrCik.trim();
228
+ const result = await tickerToCik(tickerOrCik);
229
+ return result.cik;
230
+ }
231
+
232
+ async function companyFilings(tickerOrCik: string, formType?: string, limit?: number) {
233
+ const cik = await resolveCik(tickerOrCik);
234
+ const paddedCik = padCik(cik);
235
+ const count = Math.min(40, Math.max(1, limit ?? 20));
236
+
237
+ const res = await fetch(`${DATA_BASE}/submissions/CIK${paddedCik}.json`, { headers: SEC_HEADERS });
238
+ if (!res.ok) throw new Error(`SEC EDGAR submissions error: ${res.status}`);
239
+
240
+ const data = (await res.json()) as {
241
+ cik: string;
242
+ name: string;
243
+ sic: string;
244
+ sicDescription: string;
245
+ stateOfIncorporation: string;
246
+ fiscalYearEnd: string;
247
+ tickers: string[];
248
+ filings: {
249
+ recent: {
250
+ accessionNumber: string[];
251
+ filingDate: string[];
252
+ form: string[];
253
+ primaryDocument: string[];
254
+ items: string[];
255
+ size: number[];
256
+ };
257
+ };
258
+ };
259
+
260
+ const recent = data.filings.recent;
261
+ const filings: {
262
+ accession_number: string;
263
+ filing_date: string;
264
+ form: string;
265
+ primary_document: string;
266
+ document_url: string;
267
+ }[] = [];
268
+
269
+ for (let i = 0; i < recent.accessionNumber.length && filings.length < count; i++) {
270
+ const form = recent.form[i];
271
+ if (formType && form !== formType) continue;
272
+
273
+ const accession = recent.accessionNumber[i];
274
+ const accessionPath = accession.replace(/-/g, '');
275
+ filings.push({
276
+ accession_number: accession,
277
+ filing_date: recent.filingDate[i],
278
+ form,
279
+ primary_document: recent.primaryDocument[i],
280
+ document_url: `https://www.sec.gov/Archives/edgar/data/${data.cik}/${accessionPath}/${recent.primaryDocument[i]}`,
281
+ });
282
+ }
283
+
284
+ return {
285
+ cik: data.cik,
286
+ company_name: data.name,
287
+ tickers: data.tickers ?? [],
288
+ sic_description: data.sicDescription,
289
+ state_of_incorporation: data.stateOfIncorporation,
290
+ fiscal_year_end: data.fiscalYearEnd,
291
+ filter_form_type: formType ?? 'all',
292
+ filings,
293
+ };
294
+ }
295
+
296
+ async function companyFacts(cik: string) {
297
+ const paddedCik = padCik(cik);
298
+ const res = await fetch(`${DATA_BASE}/api/xbrl/companyfacts/CIK${paddedCik}.json`, {
299
+ headers: SEC_HEADERS,
300
+ });
301
+ if (!res.ok) throw new Error(`SEC EDGAR company facts error: ${res.status}`);
302
+
303
+ const data = (await res.json()) as {
304
+ cik: number;
305
+ entityName: string;
306
+ facts: {
307
+ 'us-gaap'?: Record<
308
+ string,
309
+ {
310
+ label: string;
311
+ description: string;
312
+ units: Record<
313
+ string,
314
+ { end: string; val: number; accn: string; fy: number; fp: string; form: string; filed: string; frame?: string }[]
315
+ >;
316
+ }
317
+ >;
318
+ };
319
+ };
320
+
321
+ const usGaap = data.facts?.['us-gaap'] ?? {};
322
+ const KEY_METRICS = [
323
+ 'Revenues',
324
+ 'RevenueFromContractWithCustomerExcludingAssessedTax',
325
+ 'NetIncomeLoss',
326
+ 'Assets',
327
+ 'Liabilities',
328
+ 'StockholdersEquity',
329
+ 'CashAndCashEquivalentsAtCarryingValue',
330
+ 'EarningsPerShareBasic',
331
+ 'EarningsPerShareDiluted',
332
+ 'CommonStockSharesOutstanding',
333
+ 'OperatingIncomeLoss',
334
+ 'GrossProfit',
335
+ 'ResearchAndDevelopmentExpense',
336
+ ];
337
+
338
+ const metrics: Record<
339
+ string,
340
+ { label: string; most_recent_annual: { year: number; value: number; filed: string } | null }
341
+ > = {};
342
+
343
+ for (const key of KEY_METRICS) {
344
+ const fact = usGaap[key];
345
+ if (!fact) continue;
346
+
347
+ const usdEntries = fact.units['USD'] ?? fact.units['shares'] ?? [];
348
+ const annual = usdEntries
349
+ .filter((e) => e.form === '10-K' && e.frame !== undefined)
350
+ .sort((a, b) => (b.fy ?? 0) - (a.fy ?? 0));
351
+
352
+ metrics[key] = {
353
+ label: fact.label,
354
+ most_recent_annual: annual[0]
355
+ ? { year: annual[0].fy, value: annual[0].val, filed: annual[0].filed }
356
+ : null,
357
+ };
358
+ }
359
+
360
+ return {
361
+ cik: String(data.cik),
362
+ company_name: data.entityName,
363
+ key_financials: metrics,
364
+ available_concepts: Object.keys(usGaap).length,
365
+ };
366
+ }
367
+
368
+ async function companyConcept(cik: string, concept: string) {
369
+ const paddedCik = padCik(cik);
370
+ const res = await fetch(
371
+ `${DATA_BASE}/api/xbrl/companyconcept/CIK${paddedCik}/us-gaap/${encodeURIComponent(concept)}.json`,
372
+ { headers: SEC_HEADERS },
373
+ );
374
+ if (!res.ok) throw new Error(`SEC EDGAR company concept error: ${res.status} — concept "${concept}" may not exist for CIK ${cik}`);
375
+
376
+ const data = (await res.json()) as {
377
+ cik: number;
378
+ entityName: string;
379
+ tag: string;
380
+ taxonomy: string;
381
+ label: string;
382
+ description: string;
383
+ units: Record<
384
+ string,
385
+ { end: string; val: number; accn: string; fy: number; fp: string; form: string; filed: string; frame?: string }[]
386
+ >;
387
+ };
388
+
389
+ // Return annual (10-K) values sorted by fiscal year descending
390
+ const entries: { fiscal_year: number; period_end: string; value: number; filed: string; unit: string }[] = [];
391
+
392
+ for (const [unit, values] of Object.entries(data.units)) {
393
+ const annuals = values
394
+ .filter((e) => e.form === '10-K' && e.frame !== undefined)
395
+ .sort((a, b) => (b.fy ?? 0) - (a.fy ?? 0));
396
+
397
+ for (const entry of annuals) {
398
+ entries.push({
399
+ fiscal_year: entry.fy,
400
+ period_end: entry.end,
401
+ value: entry.val,
402
+ filed: entry.filed,
403
+ unit,
404
+ });
405
+ }
406
+ }
407
+
408
+ return {
409
+ cik: String(data.cik),
410
+ company_name: data.entityName,
411
+ concept: data.tag,
412
+ label: data.label,
413
+ description: data.description,
414
+ annual_values: entries,
415
+ };
416
+ }
417
+
418
+ // ── callTool router ─────────────────────────────────────────────────
419
+
420
+ async function callTool(name: string, args: Record<string, unknown>): Promise<unknown> {
421
+ switch (name) {
422
+ case 'edgar_search_filings':
423
+ return searchFilings(
424
+ args.query as string,
425
+ args.form_type as string | undefined,
426
+ args.start_date as string | undefined,
427
+ args.end_date as string | undefined,
428
+ args.limit as number | undefined,
429
+ );
430
+ case 'edgar_company_filings':
431
+ return companyFilings(
432
+ args.ticker_or_cik as string,
433
+ args.form_type as string | undefined,
434
+ args.limit as number | undefined,
435
+ );
436
+ case 'edgar_company_facts':
437
+ return companyFacts(args.cik as string);
438
+ case 'edgar_company_concept':
439
+ return companyConcept(args.cik as string, args.concept as string);
440
+ case 'edgar_ticker_to_cik':
441
+ return tickerToCik(args.ticker as string);
442
+ default:
443
+ throw new Error(`Unknown tool: ${name}`);
444
+ }
445
+ }
446
+
447
+ export default { tools, callTool, meter: { credits: 10 } } satisfies McpToolExport;
package/tsconfig.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "skipLibCheck": true,
9
+ "outDir": "dist",
10
+ "rootDir": "src",
11
+ "declaration": true
12
+ },
13
+ "include": ["src"]
14
+ }