import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
const DATA_URL = "https://data.sec.gov";
const WWW_URL = "https://www.sec.gov";
const USER_AGENT = "MCP-SEC-EDGAR/1.0 (contact@example.com)";
// Cache for company tickers lookup
let tickerCache: Map<string, string> | null = null;
let tickerData: any = null;
async function secRequest(path: string, useWww = false): Promise<any> {
const baseUrl = useWww ? WWW_URL : DATA_URL;
const url = `${baseUrl}${path}`;
const response = await fetch(url, {
headers: {
"User-Agent": USER_AGENT,
Accept: "application/json",
},
});
if (!response.ok) {
throw new Error(`SEC API error: ${response.status} - ${response.statusText}`);
}
return response.json();
}
function padCik(cik: string): string {
// CIK must be 10 digits with leading zeros
return cik.replace(/\D/g, "").padStart(10, "0");
}
async function loadTickerMap(): Promise<Map<string, string>> {
if (tickerCache) return tickerCache;
try {
// Tickers file is on www.sec.gov, not data.sec.gov
tickerData = await secRequest("/files/company_tickers.json", true);
tickerCache = new Map();
// Format: { "0": { "cik_str": 320193, "ticker": "AAPL", "title": "Apple Inc." }, ... }
for (const key of Object.keys(tickerData)) {
const entry = tickerData[key];
const cik = padCik(String(entry.cik_str));
tickerCache.set(entry.ticker.toUpperCase(), cik);
// Also map by company name (lowercase for search)
tickerCache.set(entry.title.toLowerCase(), cik);
}
return tickerCache;
} catch (error) {
tickerCache = new Map();
return tickerCache;
}
}
async function resolveToCik(identifier: string): Promise<string | null> {
// If it's already a CIK (all digits), pad and return
if (/^\d+$/.test(identifier)) {
return padCik(identifier);
}
// Try to resolve ticker or company name
const tickerMap = await loadTickerMap();
// Try exact ticker match
const upperIdentifier = identifier.toUpperCase();
if (tickerMap.has(upperIdentifier)) {
return tickerMap.get(upperIdentifier) || null;
}
// Try company name match
const lowerIdentifier = identifier.toLowerCase();
if (tickerMap.has(lowerIdentifier)) {
return tickerMap.get(lowerIdentifier) || null;
}
return null;
}
const server = new Server(
{ name: "sec-edgar", version: "1.0.0" },
{ capabilities: { tools: {} } }
);
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: "search_companies",
description: "Search for companies by name or ticker to find their CIK (Central Index Key). Returns matching companies with CIK, ticker, and name.",
inputSchema: {
type: "object",
properties: {
query: {
type: "string",
description: "Company name or ticker symbol to search (e.g., 'Apple', 'AAPL', 'Microsoft')"
},
limit: {
type: "number",
description: "Max results to return (default 20)"
},
},
required: ["query"],
},
},
{
name: "get_company_filings",
description: "Get recent SEC filings for a company. Returns 10-K, 10-Q, 8-K, and other filing types with dates and links.",
inputSchema: {
type: "object",
properties: {
company: {
type: "string",
description: "Company identifier: ticker (AAPL), CIK (320193), or name (Apple Inc.)"
},
formType: {
type: "string",
description: "Filter by form type: '10-K' (annual), '10-Q' (quarterly), '8-K' (current events), '4' (insider trades), etc."
},
limit: {
type: "number",
description: "Max filings to return (default 20)"
},
},
required: ["company"],
},
},
{
name: "get_company_facts",
description: "Get all XBRL financial facts for a company. Returns revenues, assets, liabilities, and other financial metrics from filings.",
inputSchema: {
type: "object",
properties: {
company: {
type: "string",
description: "Company identifier: ticker (AAPL), CIK (320193), or name"
},
taxonomy: {
type: "string",
description: "Taxonomy to use: 'us-gaap' (default), 'dei', 'ifrs-full'"
},
},
required: ["company"],
},
},
{
name: "get_financial_metric",
description: "Get a specific financial metric's history for a company (e.g., Revenue, Assets, NetIncome over multiple years).",
inputSchema: {
type: "object",
properties: {
company: {
type: "string",
description: "Company identifier: ticker, CIK, or name"
},
metric: {
type: "string",
description: "XBRL metric tag (e.g., 'Revenues', 'Assets', 'NetIncomeLoss', 'StockholdersEquity', 'CashAndCashEquivalentsAtCarryingValue')"
},
taxonomy: {
type: "string",
description: "Taxonomy: 'us-gaap' (default), 'dei', 'ifrs-full'"
},
},
required: ["company", "metric"],
},
},
{
name: "get_industry_metric",
description: "Get a financial metric aggregated across all companies for a specific period. Useful for industry comparisons.",
inputSchema: {
type: "object",
properties: {
metric: {
type: "string",
description: "XBRL metric tag (e.g., 'Revenues', 'Assets', 'NetIncomeLoss')"
},
year: {
type: "number",
description: "Calendar year (e.g., 2023)"
},
quarter: {
type: "string",
description: "Quarter: 'Q1', 'Q2', 'Q3', 'Q4', or omit for annual"
},
unit: {
type: "string",
description: "Unit of measurement: 'USD' (default), 'shares', 'pure'"
},
limit: {
type: "number",
description: "Max companies to return (default 50)"
},
},
required: ["metric", "year"],
},
},
{
name: "list_common_metrics",
description: "List commonly used XBRL financial metrics with their tags. Helpful for finding the right metric name.",
inputSchema: {
type: "object",
properties: {
category: {
type: "string",
description: "Filter by category: 'income', 'balance', 'cash', 'shares', or 'all' (default)"
},
},
required: [],
},
},
],
}));
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case "search_companies": {
const { query, limit = 20 } = args as any;
const searchTerm = query.toLowerCase();
// Load tickers (uses cache after first load)
await loadTickerMap();
const results: any[] = [];
for (const key of Object.keys(tickerData)) {
const entry = tickerData[key];
const matchesTicker = entry.ticker.toLowerCase().includes(searchTerm);
const matchesName = entry.title.toLowerCase().includes(searchTerm);
if (matchesTicker || matchesName) {
results.push({
cik: padCik(String(entry.cik_str)),
ticker: entry.ticker,
name: entry.title,
});
}
if (results.length >= limit) break;
}
return {
content: [{
type: "text",
text: JSON.stringify({
count: results.length,
companies: results,
}, null, 2),
}],
};
}
case "get_company_filings": {
const { company, formType, limit = 20 } = args as any;
const cik = await resolveToCik(company);
if (!cik) {
return {
content: [{
type: "text",
text: JSON.stringify({
error: `Could not resolve "${company}" to a CIK. Try using search_companies first.`,
}, null, 2),
}],
};
}
const data = await secRequest(`/submissions/CIK${cik}.json`);
const filings: any[] = [];
const recentFilings = data.filings?.recent || {};
const forms = recentFilings.form || [];
const dates = recentFilings.filingDate || [];
const accessions = recentFilings.accessionNumber || [];
const primaryDocs = recentFilings.primaryDocument || [];
for (let i = 0; i < forms.length && filings.length < limit; i++) {
if (formType && forms[i] !== formType) continue;
filings.push({
form: forms[i],
filingDate: dates[i],
accessionNumber: accessions[i],
document: primaryDocs[i],
url: `https://www.sec.gov/Archives/edgar/data/${parseInt(cik)}/${accessions[i].replace(/-/g, "")}/${primaryDocs[i]}`,
});
}
return {
content: [{
type: "text",
text: JSON.stringify({
cik,
name: data.name,
ticker: data.tickers?.[0],
sic: data.sic,
sicDescription: data.sicDescription,
filings,
}, null, 2),
}],
};
}
case "get_company_facts": {
const { company, taxonomy = "us-gaap" } = args as any;
const cik = await resolveToCik(company);
if (!cik) {
return {
content: [{
type: "text",
text: JSON.stringify({
error: `Could not resolve "${company}" to a CIK. Try using search_companies first.`,
}, null, 2),
}],
};
}
const data = await secRequest(`/api/xbrl/companyfacts/CIK${cik}.json`);
const facts = data.facts?.[taxonomy] || {};
const metrics = Object.keys(facts).slice(0, 50).map((tag) => {
const tagData = facts[tag];
const units = Object.keys(tagData.units || {});
const latestUnit = units[0];
const values = tagData.units?.[latestUnit] || [];
const latest = values[values.length - 1];
return {
metric: tag,
label: tagData.label,
description: tagData.description?.substring(0, 100),
unit: latestUnit,
latestValue: latest?.val,
latestPeriod: latest?.fy ? `FY${latest.fy} ${latest.fp}` : latest?.end,
dataPoints: values.length,
};
});
return {
content: [{
type: "text",
text: JSON.stringify({
cik,
entityName: data.entityName,
taxonomy,
metricsAvailable: Object.keys(facts).length,
note: "Showing first 50 metrics. Use get_financial_metric for detailed history of a specific metric.",
metrics,
}, null, 2),
}],
};
}
case "get_financial_metric": {
const { company, metric, taxonomy = "us-gaap" } = args as any;
const cik = await resolveToCik(company);
if (!cik) {
return {
content: [{
type: "text",
text: JSON.stringify({
error: `Could not resolve "${company}" to a CIK. Try using search_companies first.`,
}, null, 2),
}],
};
}
const data = await secRequest(`/api/xbrl/companyconcept/CIK${cik}/${taxonomy}/${metric}.json`);
const units = data.units || {};
const primaryUnit = Object.keys(units)[0];
const values = units[primaryUnit] || [];
// Group by fiscal year for cleaner output
const byYear: Record<string, any[]> = {};
values.forEach((v: any) => {
const key = `FY${v.fy}`;
if (!byYear[key]) byYear[key] = [];
byYear[key].push({
period: v.fp,
value: v.val,
filed: v.filed,
form: v.form,
});
});
return {
content: [{
type: "text",
text: JSON.stringify({
cik,
entityName: data.entityName,
metric: data.tag,
label: data.label,
description: data.description,
unit: primaryUnit,
dataPoints: values.length,
byFiscalYear: byYear,
}, null, 2),
}],
};
}
case "get_industry_metric": {
const { metric, year, quarter, unit = "USD", limit = 50 } = args as any;
// Build the frame identifier
let frame = `CY${year}`;
if (quarter) {
frame += quarter.toUpperCase();
}
// Add 'I' for instantaneous (balance sheet items) vs duration
// We'll try without first, then with 'I' if that fails
const taxonomy = "us-gaap";
let data;
try {
data = await secRequest(`/api/xbrl/frames/${taxonomy}/${metric}/${unit}/${frame}.json`);
} catch {
// Try instantaneous version
try {
data = await secRequest(`/api/xbrl/frames/${taxonomy}/${metric}/${unit}/${frame}I.json`);
} catch {
return {
content: [{
type: "text",
text: JSON.stringify({
error: `Could not find data for ${metric} in ${frame}. Try a different metric, year, or quarter.`,
suggestion: "Use list_common_metrics to find valid metric names.",
}, null, 2),
}],
};
}
}
const companies = (data.data || []).slice(0, limit).map((d: any) => ({
cik: padCik(String(d.cik)),
entityName: d.entityName,
value: d.val,
filed: d.filed,
}));
// Sort by value descending
companies.sort((a: any, b: any) => (b.value || 0) - (a.value || 0));
return {
content: [{
type: "text",
text: JSON.stringify({
metric,
label: data.label,
description: data.description,
unit,
frame,
totalCompanies: data.data?.length || 0,
showing: companies.length,
companies,
}, null, 2),
}],
};
}
case "list_common_metrics": {
const { category = "all" } = args as any;
const metrics: Record<string, any[]> = {
income: [
{ tag: "Revenues", description: "Total revenues" },
{ tag: "RevenueFromContractWithCustomerExcludingAssessedTax", description: "Revenue from contracts (ASC 606)" },
{ tag: "CostOfRevenue", description: "Cost of goods sold" },
{ tag: "GrossProfit", description: "Gross profit" },
{ tag: "OperatingIncomeLoss", description: "Operating income/loss" },
{ tag: "NetIncomeLoss", description: "Net income/loss" },
{ tag: "EarningsPerShareBasic", description: "Basic EPS" },
{ tag: "EarningsPerShareDiluted", description: "Diluted EPS" },
],
balance: [
{ tag: "Assets", description: "Total assets" },
{ tag: "AssetsCurrent", description: "Current assets" },
{ tag: "Liabilities", description: "Total liabilities" },
{ tag: "LiabilitiesCurrent", description: "Current liabilities" },
{ tag: "StockholdersEquity", description: "Total stockholders equity" },
{ tag: "RetainedEarningsAccumulatedDeficit", description: "Retained earnings" },
{ tag: "CashAndCashEquivalentsAtCarryingValue", description: "Cash and equivalents" },
{ tag: "AccountsReceivableNetCurrent", description: "Accounts receivable" },
{ tag: "InventoryNet", description: "Inventory" },
{ tag: "LongTermDebt", description: "Long-term debt" },
],
cash: [
{ tag: "NetCashProvidedByUsedInOperatingActivities", description: "Operating cash flow" },
{ tag: "NetCashProvidedByUsedInInvestingActivities", description: "Investing cash flow" },
{ tag: "NetCashProvidedByUsedInFinancingActivities", description: "Financing cash flow" },
{ tag: "PaymentsOfDividends", description: "Dividend payments" },
{ tag: "PaymentsToAcquirePropertyPlantAndEquipment", description: "CapEx" },
],
shares: [
{ tag: "CommonStockSharesOutstanding", description: "Shares outstanding" },
{ tag: "WeightedAverageNumberOfSharesOutstandingBasic", description: "Weighted avg shares (basic)" },
{ tag: "WeightedAverageNumberOfDilutedSharesOutstanding", description: "Weighted avg shares (diluted)" },
],
};
let result: any[];
if (category === "all") {
result = Object.entries(metrics).flatMap(([cat, items]) =>
items.map((item) => ({ ...item, category: cat }))
);
} else {
result = (metrics[category] || []).map((item) => ({ ...item, category }));
}
return {
content: [{
type: "text",
text: JSON.stringify({
note: "Use these tags with get_financial_metric or get_industry_metric",
metrics: result,
}, null, 2),
}],
};
}
default:
throw new Error(`Unknown tool: ${name}`);
}
} catch (error: any) {
return {
content: [{ type: "text", text: `Error: ${error.message}` }],
isError: true,
};
}
});
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("SEC EDGAR MCP server running");
}
main().catch(console.error);