@etsquare/mcp-server-sec 0.5.1 → 0.6.1
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/dist/etsquare-client.d.ts +27 -0
- package/dist/etsquare-client.js +46 -0
- package/dist/index.js +319 -16
- package/package.json +1 -1
|
@@ -53,6 +53,31 @@ interface KpiExtractionsApiInput {
|
|
|
53
53
|
fiscal_year?: number;
|
|
54
54
|
limit?: number;
|
|
55
55
|
}
|
|
56
|
+
/** Company research packet request. */
|
|
57
|
+
interface CompanyResearchApiInput {
|
|
58
|
+
ticker: string;
|
|
59
|
+
sections?: Array<'snapshot' | 'financials' | 'earnings' | 'narrative' | 'ownership' | 'kpis'>;
|
|
60
|
+
detail_level?: 'summary' | 'standard' | 'full';
|
|
61
|
+
peers?: string[];
|
|
62
|
+
n_periods?: number;
|
|
63
|
+
}
|
|
64
|
+
/** Server-side KPI query with metric filtering. */
|
|
65
|
+
interface QueryKpisApiInput {
|
|
66
|
+
select?: string[];
|
|
67
|
+
filter?: Array<{
|
|
68
|
+
metric: string;
|
|
69
|
+
op: string;
|
|
70
|
+
value: number;
|
|
71
|
+
}>;
|
|
72
|
+
sector?: string;
|
|
73
|
+
tickers?: string[];
|
|
74
|
+
period?: string;
|
|
75
|
+
fiscal_year?: number;
|
|
76
|
+
sort_by?: string;
|
|
77
|
+
sort_order?: string;
|
|
78
|
+
limit?: number;
|
|
79
|
+
action?: string;
|
|
80
|
+
}
|
|
56
81
|
/** Subset of DiscoverMetricsInput that the API v1 backend accepts. */
|
|
57
82
|
interface DiscoverMetricsApiInput {
|
|
58
83
|
question: string;
|
|
@@ -79,6 +104,8 @@ export declare class ETSquareClient {
|
|
|
79
104
|
getInsiderTransactions(input: InsiderTransactionsApiInput): Promise<Record<string, unknown>>;
|
|
80
105
|
getEarningsActuals(input: EarningsActualsApiInput): Promise<Record<string, unknown>>;
|
|
81
106
|
getKpiExtractions(input: KpiExtractionsApiInput): Promise<Record<string, unknown>>;
|
|
107
|
+
getCompanyResearch(input: CompanyResearchApiInput): Promise<Record<string, unknown>>;
|
|
108
|
+
queryKpis(input: QueryKpisApiInput): Promise<Record<string, unknown>>;
|
|
82
109
|
discoverMetrics(input: DiscoverMetricsApiInput): Promise<Record<string, unknown>>;
|
|
83
110
|
getChunk(input: GetChunkInput): Promise<Record<string, unknown>>;
|
|
84
111
|
getChunkContext(input: GetChunkContextInput): Promise<Record<string, unknown>>;
|
package/dist/etsquare-client.js
CHANGED
|
@@ -158,6 +158,52 @@ export class ETSquareClient {
|
|
|
158
158
|
body: JSON.stringify(body),
|
|
159
159
|
});
|
|
160
160
|
}
|
|
161
|
+
async getCompanyResearch(input) {
|
|
162
|
+
const body = {
|
|
163
|
+
ticker: input.ticker,
|
|
164
|
+
};
|
|
165
|
+
if (input.sections && input.sections.length > 0)
|
|
166
|
+
body.sections = input.sections;
|
|
167
|
+
if (input.detail_level)
|
|
168
|
+
body.detail_level = input.detail_level;
|
|
169
|
+
if (input.peers && input.peers.length > 0)
|
|
170
|
+
body.peers = input.peers;
|
|
171
|
+
if (input.n_periods !== undefined)
|
|
172
|
+
body.n_periods = input.n_periods;
|
|
173
|
+
return this.request('/api/v1/company/research', {
|
|
174
|
+
method: 'POST',
|
|
175
|
+
headers: this.headers,
|
|
176
|
+
body: JSON.stringify(body),
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
async queryKpis(input) {
|
|
180
|
+
const body = {
|
|
181
|
+
limit: input.limit || 50,
|
|
182
|
+
};
|
|
183
|
+
if (input.select && input.select.length > 0)
|
|
184
|
+
body.select = input.select;
|
|
185
|
+
if (input.filter && input.filter.length > 0)
|
|
186
|
+
body.filter = input.filter;
|
|
187
|
+
if (input.sector)
|
|
188
|
+
body.sector = input.sector;
|
|
189
|
+
if (input.tickers && input.tickers.length > 0)
|
|
190
|
+
body.tickers = input.tickers;
|
|
191
|
+
if (input.period)
|
|
192
|
+
body.period = input.period;
|
|
193
|
+
if (input.fiscal_year !== undefined)
|
|
194
|
+
body.fiscal_year = input.fiscal_year;
|
|
195
|
+
if (input.sort_by)
|
|
196
|
+
body.sort_by = input.sort_by;
|
|
197
|
+
if (input.sort_order)
|
|
198
|
+
body.sort_order = input.sort_order;
|
|
199
|
+
if (input.action)
|
|
200
|
+
body.action = input.action;
|
|
201
|
+
return this.request('/api/v1/kpis/query', {
|
|
202
|
+
method: 'POST',
|
|
203
|
+
headers: this.headers,
|
|
204
|
+
body: JSON.stringify(body),
|
|
205
|
+
});
|
|
206
|
+
}
|
|
161
207
|
async discoverMetrics(input) {
|
|
162
208
|
const body = {
|
|
163
209
|
question: input.question,
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* @etsquare/mcp-server-sec v0.
|
|
3
|
+
* @etsquare/mcp-server-sec v0.6.0
|
|
4
4
|
* MCP server for SEC Intelligence: search SEC filings,
|
|
5
5
|
* resolve company tickers, execute financial metrics templates,
|
|
6
6
|
* compare companies, and access weekly briefs.
|
|
@@ -57,7 +57,7 @@ if (!apiKey) {
|
|
|
57
57
|
process.exit(1);
|
|
58
58
|
}
|
|
59
59
|
const client = new ETSquareClient({ baseUrl, apiKey });
|
|
60
|
-
log('info', `ETSquare MCP Server v0.
|
|
60
|
+
log('info', `ETSquare MCP Server v0.6.0 starting with backend: ${baseUrl}`);
|
|
61
61
|
// ─── Item Code Labels ───────────────────────────────────────────────────────
|
|
62
62
|
const ITEM_CODE_LABELS = {
|
|
63
63
|
// 10-K
|
|
@@ -358,7 +358,7 @@ function buildMarkdownTable(colNames, rows, maxRows = 20) {
|
|
|
358
358
|
// ─── MCP Server ─────────────────────────────────────────────────────────────
|
|
359
359
|
const server = new McpServer({
|
|
360
360
|
name: 'etsquare-mcp-sec',
|
|
361
|
-
version: '0.
|
|
361
|
+
version: '0.6.0',
|
|
362
362
|
});
|
|
363
363
|
// ─── Tool 1: Company Lookup ─────────────────────────────────────────────────
|
|
364
364
|
server.registerTool('etsquare_lookup_company', {
|
|
@@ -406,7 +406,7 @@ server.registerTool('etsquare_lookup_company', {
|
|
|
406
406
|
// ─── Tool 2: SEC Filing Search ──────────────────────────────────────────────
|
|
407
407
|
server.registerTool('etsquare_search', {
|
|
408
408
|
title: 'Search SEC Filings',
|
|
409
|
-
description: 'Search
|
|
409
|
+
description: 'Search 4.0M+ SEC filing sections (10-K, 10-Q, 8-K) with hybrid retrieval. ' +
|
|
410
410
|
'Returns verbatim filing text with citations — best for qualitative research.\n\n' +
|
|
411
411
|
'Execution contract (required):\n' +
|
|
412
412
|
'- mode_lock: NARRATIVE (recommended) | HYBRID\n' +
|
|
@@ -678,6 +678,13 @@ server.registerTool('etsquare_financial_statements', {
|
|
|
678
678
|
table += `| **${labelDisplay(dk)}** | ${values.join(' | ')} |\n`;
|
|
679
679
|
}
|
|
680
680
|
}
|
|
681
|
+
// Filing source URLs per period
|
|
682
|
+
const periodUrls = periods
|
|
683
|
+
.filter((p) => p.sec_url)
|
|
684
|
+
.map((p) => `- FY${p.fiscal_year}${p.fiscal_period !== 'FY' ? ' ' + p.fiscal_period : ''}: ${p.sec_url}`);
|
|
685
|
+
if (periodUrls.length > 0) {
|
|
686
|
+
table += `\n**Filing Sources:**\n${periodUrls.join('\n')}\n`;
|
|
687
|
+
}
|
|
681
688
|
// Provenance note
|
|
682
689
|
const firstPeriod = periods[0];
|
|
683
690
|
const sampleLine = firstPeriod?.lines?.[labels[0]];
|
|
@@ -752,14 +759,15 @@ server.registerTool('etsquare_insider_trades', {
|
|
|
752
759
|
text += `\n\n`;
|
|
753
760
|
// Transaction table — show top 20
|
|
754
761
|
const shown = transactions.slice(0, 20);
|
|
755
|
-
text += `| Date | Insider | Title | Type | Shares | Price | Value |\n`;
|
|
756
|
-
text +=
|
|
762
|
+
text += `| Date | Insider | Title | Type | Shares | Price | Value | Filing |\n`;
|
|
763
|
+
text += `|------|---------|-------|------|-------:|------:|------:|:------:|\n`;
|
|
757
764
|
for (const t of shown) {
|
|
758
765
|
const shares = t.shares != null ? t.shares.toLocaleString() : '—';
|
|
759
766
|
const price = t.price_per_share != null ? `$${t.price_per_share.toFixed(2)}` : '—';
|
|
760
767
|
const value = t.total_value != null ? fmtVal(t.total_value) : '—';
|
|
761
768
|
const typeLabel = t.is_derivative ? `${t.transaction_type} *` : t.transaction_type;
|
|
762
|
-
|
|
769
|
+
const filing = t.sec_url ? `[SEC](${t.sec_url})` : '—';
|
|
770
|
+
text += `| ${t.transaction_date} | ${t.owner_name} | ${t.officer_title || '—'} | ${typeLabel} | ${shares} | ${price} | ${value} | ${filing} |\n`;
|
|
763
771
|
}
|
|
764
772
|
if (transactions.length > 20) {
|
|
765
773
|
text += `\n*Showing 20 of ${transactions.length} transactions.*\n`;
|
|
@@ -845,6 +853,19 @@ server.registerTool('etsquare_institutional_holdings', {
|
|
|
845
853
|
if (holders.length > 20) {
|
|
846
854
|
text += `\n*Showing 20 of ${holders.length} tracked holders.*\n`;
|
|
847
855
|
}
|
|
856
|
+
// Filing source URLs for this quarter (deduplicated by manager)
|
|
857
|
+
const seenMgrs = new Set();
|
|
858
|
+
const quarterLinks = [];
|
|
859
|
+
for (const h of holders) {
|
|
860
|
+
const key = h.manager_cik || h.manager_name;
|
|
861
|
+
if (h.sec_url && key && !seenMgrs.has(key)) {
|
|
862
|
+
seenMgrs.add(key);
|
|
863
|
+
quarterLinks.push(`- ${h.manager_name || 'Unknown'}: [13F Filing](${h.sec_url})`);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
if (quarterLinks.length > 0) {
|
|
867
|
+
text += `\n**13F Filing Sources (${q}):**\n${quarterLinks.slice(0, 10).join('\n')}\n`;
|
|
868
|
+
}
|
|
848
869
|
text += '\n';
|
|
849
870
|
}
|
|
850
871
|
text += `*Source: SEC 13F-HR filings via EDGAR. Tracked managers only — not full institutional ownership.*`;
|
|
@@ -928,6 +949,18 @@ server.registerTool('etsquare_earnings_actuals', {
|
|
|
928
949
|
const conf = a.confidence != null ? `${(a.confidence * 100).toFixed(0)}%` : '—';
|
|
929
950
|
text += `| ${a.filing_date || '—'} | ${a.metric_name} | ${val} | ${gaap} | ${fy} | ${fq} | ${conf} |\n`;
|
|
930
951
|
}
|
|
952
|
+
// Filing source URLs (deduplicated by accession)
|
|
953
|
+
const seenAccessions = new Set();
|
|
954
|
+
const filingLinks = [];
|
|
955
|
+
for (const a of actuals) {
|
|
956
|
+
if (a.sec_url && a.accession_number && !seenAccessions.has(a.accession_number)) {
|
|
957
|
+
seenAccessions.add(a.accession_number);
|
|
958
|
+
filingLinks.push(`- ${a.filing_date || '—'}: ${a.sec_url}`);
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
if (filingLinks.length > 0) {
|
|
962
|
+
text += `\n**Filing Sources:**\n${filingLinks.join('\n')}\n`;
|
|
963
|
+
}
|
|
931
964
|
text += '\n';
|
|
932
965
|
}
|
|
933
966
|
if (guidance.length > 0) {
|
|
@@ -943,6 +976,18 @@ server.registerTool('etsquare_earnings_actuals', {
|
|
|
943
976
|
const fq = g.fiscal_quarter ?? '—';
|
|
944
977
|
text += `| ${g.filing_date || '—'} | ${g.metric_name} | ${low} | ${high} | ${mid} | ${gaap} | ${fy} | ${fq} |\n`;
|
|
945
978
|
}
|
|
979
|
+
// Filing source URLs for guidance (deduplicated)
|
|
980
|
+
const seenGuidanceAcc = new Set();
|
|
981
|
+
const guidanceLinks = [];
|
|
982
|
+
for (const g of guidance) {
|
|
983
|
+
if (g.sec_url && g.accession_number && !seenGuidanceAcc.has(g.accession_number)) {
|
|
984
|
+
seenGuidanceAcc.add(g.accession_number);
|
|
985
|
+
guidanceLinks.push(`- ${g.filing_date || '—'}: ${g.sec_url}`);
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
if (guidanceLinks.length > 0) {
|
|
989
|
+
text += `\n**Guidance Filing Sources:**\n${guidanceLinks.join('\n')}\n`;
|
|
990
|
+
}
|
|
946
991
|
text += '\n';
|
|
947
992
|
}
|
|
948
993
|
text += `*Source: SEC 8-K Item 2.02 press releases — deterministic extraction from issuer filings.*`;
|
|
@@ -959,11 +1004,15 @@ server.registerTool('etsquare_earnings_actuals', {
|
|
|
959
1004
|
}
|
|
960
1005
|
});
|
|
961
1006
|
server.registerTool('etsquare_kpi_extractions', {
|
|
962
|
-
title: 'Get
|
|
963
|
-
description: '
|
|
964
|
-
'
|
|
965
|
-
'
|
|
966
|
-
'
|
|
1007
|
+
title: 'Get KPI Extractions (Full Detail + Source Chunk IDs)',
|
|
1008
|
+
description: 'Returns full KPI extraction detail including source_chunk_ids for provenance.\n\n' +
|
|
1009
|
+
'USE THIS TOOL for the provenance chain:\n' +
|
|
1010
|
+
' 1. etsquare_query_kpis -> find interesting metric + extraction_id\n' +
|
|
1011
|
+
' 2. etsquare_kpi_extractions(ticker, sector) -> get source_chunk_ids array\n' +
|
|
1012
|
+
' 3. etsquare_get_chunk(chunk_id) -> read actual filing paragraph\n\n' +
|
|
1013
|
+
'Also use when you need the complete kpi_json payload for deep analysis of a single ticker.\n\n' +
|
|
1014
|
+
'RETURNS: extraction_id, kpi_json (full), source_chunk_ids (array of chunk IDs), notes, extraction_model.\n\n' +
|
|
1015
|
+
'For compact tabular queries across many tickers, prefer etsquare_query_kpis instead.',
|
|
967
1016
|
inputSchema: {
|
|
968
1017
|
ticker: z.string().min(1).max(10).optional()
|
|
969
1018
|
.describe('Single ticker filter (e.g., "TXRH")'),
|
|
@@ -1051,6 +1100,9 @@ server.registerTool('etsquare_kpi_extractions', {
|
|
|
1051
1100
|
text += `${entries.join('\n')}\n`;
|
|
1052
1101
|
}
|
|
1053
1102
|
}
|
|
1103
|
+
if (extraction.sec_url) {
|
|
1104
|
+
text += `SEC Filing: ${extraction.sec_url}\n`;
|
|
1105
|
+
}
|
|
1054
1106
|
if (extraction.notes) {
|
|
1055
1107
|
text += `Notes: ${extraction.notes}\n`;
|
|
1056
1108
|
}
|
|
@@ -1072,6 +1124,185 @@ server.registerTool('etsquare_kpi_extractions', {
|
|
|
1072
1124
|
};
|
|
1073
1125
|
}
|
|
1074
1126
|
});
|
|
1127
|
+
server.registerTool('etsquare_query_kpis', {
|
|
1128
|
+
title: 'Query KPI Extractions (Compare, Screen, Catalog)',
|
|
1129
|
+
description: 'Server-side filtered KPI query. Returns compact tabular data with values + YoY changes + summary stats.\n\n' +
|
|
1130
|
+
'WORKFLOW: action="list_metrics" -> discover metrics -> action="query" with select/filter\n\n' +
|
|
1131
|
+
'EXAMPLES:\n' +
|
|
1132
|
+
' Peer comp: select="net_interest_margin,cet1_ratio,return_on_tangible_common_equity", sector="banks", period="FY"\n' +
|
|
1133
|
+
' Screen: filter="net_interest_margin>3;cet1_ratio>11", sector="banks", period="FY"\n' +
|
|
1134
|
+
' SaaS: select="arr,net_dollar_retention,free_cash_flow_margin", sector="saas", sort_by="arr"\n' +
|
|
1135
|
+
' Catalog: action="list_metrics", sector="banks"\n' +
|
|
1136
|
+
' Time series: select="net_interest_margin", tickers="JPM", limit=20\n\n' +
|
|
1137
|
+
'OUTPUT: Each row includes ticker, company_name, extraction_id, metric_value, metric_chg (YoY), metric_unit.\n' +
|
|
1138
|
+
' Summary stats (min/max/avg/median) per metric included.\n\n' +
|
|
1139
|
+
'PROVENANCE CHAIN: Results include extraction_id. To read source MD&A text:\n' +
|
|
1140
|
+
' 1. etsquare_query_kpis -> get extraction_id\n' +
|
|
1141
|
+
' 2. etsquare_kpi_extractions(ticker) -> get source_chunk_ids\n' +
|
|
1142
|
+
' 3. etsquare_get_chunk(chunk_id) -> read actual filing paragraph\n' +
|
|
1143
|
+
'Use when a number looks surprising or needs management context.',
|
|
1144
|
+
inputSchema: {
|
|
1145
|
+
select: z.string().optional()
|
|
1146
|
+
.describe('Comma-separated metric keys to return (e.g., "net_interest_margin,cet1_ratio,diluted_eps")'),
|
|
1147
|
+
filter: z.string().optional()
|
|
1148
|
+
.describe('Filter conditions as "metric>value" pairs separated by semicolons (e.g., "net_interest_margin>3;cet1_ratio>11")'),
|
|
1149
|
+
sector: z.string().optional()
|
|
1150
|
+
.describe('Sector filter (e.g., "banks", "restaurants", "saas", "airlines")'),
|
|
1151
|
+
tickers: z.string().optional()
|
|
1152
|
+
.describe('Comma-separated tickers (e.g., "JPM,BAC,WFC")'),
|
|
1153
|
+
period: z.string().optional()
|
|
1154
|
+
.describe('Fiscal period: "FY", "Q1", "Q2", "Q3", "Q4"'),
|
|
1155
|
+
fiscal_year: z.number().optional()
|
|
1156
|
+
.describe('Fiscal year filter (e.g., 2025)'),
|
|
1157
|
+
sort_by: z.string().optional()
|
|
1158
|
+
.describe('Metric key to sort by'),
|
|
1159
|
+
sort_order: z.string().optional()
|
|
1160
|
+
.describe('Sort direction: "ASC" or "DESC" (default: DESC)'),
|
|
1161
|
+
limit: z.number().min(1).max(500).default(50)
|
|
1162
|
+
.describe('Maximum rows (default: 50)'),
|
|
1163
|
+
action: z.string().optional()
|
|
1164
|
+
.describe('"query" returns data (default). "list_metrics" returns available metric names for a sector.'),
|
|
1165
|
+
response_format: z.string().optional()
|
|
1166
|
+
.describe('"text" returns a markdown table (default). "structured" returns raw JSON for programmatic use.'),
|
|
1167
|
+
},
|
|
1168
|
+
}, async (input) => {
|
|
1169
|
+
if (containsGuardrailBypass(input))
|
|
1170
|
+
return guardrailViolationResponse();
|
|
1171
|
+
try {
|
|
1172
|
+
// Parse string inputs into arrays for the API
|
|
1173
|
+
const apiInput = {
|
|
1174
|
+
limit: input.limit || 50,
|
|
1175
|
+
action: input.action || 'query',
|
|
1176
|
+
};
|
|
1177
|
+
if (input.select) {
|
|
1178
|
+
apiInput.select = input.select.split(',').map((s) => s.trim()).filter(Boolean);
|
|
1179
|
+
}
|
|
1180
|
+
if (input.filter) {
|
|
1181
|
+
// Parse "metric>value;metric2<value2" format
|
|
1182
|
+
const filterParts = input.filter.split(';').map((s) => s.trim()).filter(Boolean);
|
|
1183
|
+
const filters = [];
|
|
1184
|
+
for (const part of filterParts) {
|
|
1185
|
+
const match = part.match(/^([a-z][a-z0-9_]*)\s*(>=|<=|!=|>|<|=)\s*(-?[\d.]+)$/);
|
|
1186
|
+
if (match) {
|
|
1187
|
+
filters.push({ metric: match[1], op: match[2], value: parseFloat(match[3]) });
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
if (filters.length > 0)
|
|
1191
|
+
apiInput.filter = filters;
|
|
1192
|
+
}
|
|
1193
|
+
if (input.tickers) {
|
|
1194
|
+
apiInput.tickers = input.tickers.split(',').map((s) => s.trim().toUpperCase()).filter(Boolean);
|
|
1195
|
+
}
|
|
1196
|
+
if (input.sector)
|
|
1197
|
+
apiInput.sector = input.sector;
|
|
1198
|
+
if (input.period)
|
|
1199
|
+
apiInput.period = input.period;
|
|
1200
|
+
if (input.fiscal_year !== undefined)
|
|
1201
|
+
apiInput.fiscal_year = input.fiscal_year;
|
|
1202
|
+
if (input.sort_by)
|
|
1203
|
+
apiInput.sort_by = input.sort_by;
|
|
1204
|
+
if (input.sort_order)
|
|
1205
|
+
apiInput.sort_order = input.sort_order;
|
|
1206
|
+
const result = await client.queryKpis(apiInput);
|
|
1207
|
+
const data = Array.isArray(result.data)
|
|
1208
|
+
? result.data
|
|
1209
|
+
: [];
|
|
1210
|
+
const count = typeof result.count === 'number'
|
|
1211
|
+
? result.count
|
|
1212
|
+
: data.length;
|
|
1213
|
+
// list_metrics action
|
|
1214
|
+
if ((input.action || 'query') === 'list_metrics') {
|
|
1215
|
+
const metrics = result.metrics || [];
|
|
1216
|
+
const otherMetrics = result.other_kpis_metrics || [];
|
|
1217
|
+
let text = `Available KPI metrics`;
|
|
1218
|
+
if (result.sector)
|
|
1219
|
+
text += ` for sector "${result.sector}"`;
|
|
1220
|
+
if (result.ticker)
|
|
1221
|
+
text += ` ticker ${result.ticker}`;
|
|
1222
|
+
text += `:\n\n`;
|
|
1223
|
+
text += `**Top-level metrics (${metrics.length}):**\n`;
|
|
1224
|
+
text += metrics.map(m => `- ${m}`).join('\n');
|
|
1225
|
+
if (otherMetrics.length > 0) {
|
|
1226
|
+
text += `\n\n**other_kpis metrics (${otherMetrics.length}):**\n`;
|
|
1227
|
+
text += otherMetrics.slice(0, 30).map(m => `- ${m}`).join('\n');
|
|
1228
|
+
if (otherMetrics.length > 30)
|
|
1229
|
+
text += `\n... and ${otherMetrics.length - 30} more`;
|
|
1230
|
+
}
|
|
1231
|
+
return { content: [{ type: 'text', text }] };
|
|
1232
|
+
}
|
|
1233
|
+
// query action
|
|
1234
|
+
if (data.length === 0) {
|
|
1235
|
+
return {
|
|
1236
|
+
content: [{
|
|
1237
|
+
type: 'text',
|
|
1238
|
+
text: 'No KPI data found for the specified query. Try broader filters or use action="list_metrics" to see available metrics.',
|
|
1239
|
+
}],
|
|
1240
|
+
};
|
|
1241
|
+
}
|
|
1242
|
+
// Structured format — return raw JSON
|
|
1243
|
+
if ((input.response_format || 'text') === 'structured') {
|
|
1244
|
+
return {
|
|
1245
|
+
content: [{
|
|
1246
|
+
type: 'text',
|
|
1247
|
+
text: JSON.stringify({ count, data }, null, 2),
|
|
1248
|
+
}],
|
|
1249
|
+
};
|
|
1250
|
+
}
|
|
1251
|
+
// Build markdown table — exclude sec_url from columns (too wide for table)
|
|
1252
|
+
const excludeFromTable = new Set(['sec_url']);
|
|
1253
|
+
const columns = Object.keys(data[0]).filter(c => !excludeFromTable.has(c));
|
|
1254
|
+
let text = `Found ${count} results.\n\n`;
|
|
1255
|
+
text += '| ' + columns.join(' | ') + ' |\n';
|
|
1256
|
+
text += '| ' + columns.map(() => '---').join(' | ') + ' |\n';
|
|
1257
|
+
for (const row of data) {
|
|
1258
|
+
const values = columns.map(col => {
|
|
1259
|
+
const v = row[col];
|
|
1260
|
+
if (v === null || v === undefined)
|
|
1261
|
+
return '—';
|
|
1262
|
+
if (typeof v === 'number')
|
|
1263
|
+
return Number.isInteger(v) ? String(v) : v.toFixed(2);
|
|
1264
|
+
return String(v);
|
|
1265
|
+
});
|
|
1266
|
+
text += '| ' + values.join(' | ') + ' |\n';
|
|
1267
|
+
}
|
|
1268
|
+
if (count > data.length) {
|
|
1269
|
+
text += `\nShowing ${data.length} of ${count} results.`;
|
|
1270
|
+
}
|
|
1271
|
+
// Filing source URLs (deduplicated by ticker)
|
|
1272
|
+
const seenTickers = new Set();
|
|
1273
|
+
const filingLinks = [];
|
|
1274
|
+
for (const row of data) {
|
|
1275
|
+
if (row.sec_url && row.ticker && !seenTickers.has(row.ticker)) {
|
|
1276
|
+
seenTickers.add(row.ticker);
|
|
1277
|
+
filingLinks.push(`- ${row.ticker}: ${row.sec_url}`);
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
if (filingLinks.length > 0) {
|
|
1281
|
+
text += `\n\n**Filing Sources:**\n${filingLinks.join('\n')}`;
|
|
1282
|
+
}
|
|
1283
|
+
// Append summary stats if available
|
|
1284
|
+
const summary = result.summary;
|
|
1285
|
+
if (summary && typeof summary === 'object' && Object.keys(summary).length > 0) {
|
|
1286
|
+
text += '\n\n**Summary Stats:**\n';
|
|
1287
|
+
text += '| Metric | Min | Max | Avg | Median | Count |\n';
|
|
1288
|
+
text += '| --- | --- | --- | --- | --- | --- |\n';
|
|
1289
|
+
for (const [metric, stats] of Object.entries(summary)) {
|
|
1290
|
+
const s = stats;
|
|
1291
|
+
text += `| ${metric} | ${s.min} | ${s.max} | ${s.avg} | ${s.median} | ${s.count} |\n`;
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
// Provenance hint
|
|
1295
|
+
text += '\n*Each row includes extraction_id. Filing source URLs are listed above. Use response_format="structured" for per-row sec_url in JSON.*';
|
|
1296
|
+
return { content: [{ type: 'text', text }] };
|
|
1297
|
+
}
|
|
1298
|
+
catch (error) {
|
|
1299
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
1300
|
+
return {
|
|
1301
|
+
content: [{ type: 'text', text: `KPI query failed: ${message}` }],
|
|
1302
|
+
isError: true,
|
|
1303
|
+
};
|
|
1304
|
+
}
|
|
1305
|
+
});
|
|
1075
1306
|
server.registerTool('etsquare_execute_metrics', {
|
|
1076
1307
|
title: 'Execute Financial Metrics Query',
|
|
1077
1308
|
description: 'Execute an XBRL metrics template by template_id to get structured financial data ' +
|
|
@@ -1183,18 +1414,35 @@ server.registerTool('etsquare_execute_metrics', {
|
|
|
1183
1414
|
return { content: [{ type: 'text', text: JSON.stringify(structured, null, 2) }] };
|
|
1184
1415
|
}
|
|
1185
1416
|
// ── Text response path (existing behavior) ──
|
|
1186
|
-
const
|
|
1417
|
+
const allColNames = columns.length > 0
|
|
1187
1418
|
? columns.map((c) => c.name)
|
|
1188
1419
|
: Object.keys(rows[0] || {});
|
|
1420
|
+
// Exclude sec_url from table columns (too wide for markdown)
|
|
1421
|
+
const colNames = allColNames.filter((c) => c !== 'sec_url');
|
|
1189
1422
|
const table = buildMarkdownTable(colNames, rows);
|
|
1190
1423
|
const suffix = truncated || rows.length > 20
|
|
1191
1424
|
? `\n\n(Showing ${Math.min(rows.length, 20)} of ${rowCount} rows${truncated ? ', results truncated' : ''})`
|
|
1192
1425
|
: '';
|
|
1426
|
+
// Filing source URLs (deduplicated)
|
|
1427
|
+
let filingSources = '';
|
|
1428
|
+
const seenUrls = new Set();
|
|
1429
|
+
const urlLinks = [];
|
|
1430
|
+
for (const row of rows) {
|
|
1431
|
+
if (row.sec_url && !seenUrls.has(row.sec_url)) {
|
|
1432
|
+
seenUrls.add(row.sec_url);
|
|
1433
|
+
const label = row.ticker || row.TICKER || '';
|
|
1434
|
+
const year = row.fiscal_year || row.FISCAL_YEAR || row.year || '';
|
|
1435
|
+
urlLinks.push(`- ${label} ${year}: ${row.sec_url}`);
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
if (urlLinks.length > 0) {
|
|
1439
|
+
filingSources = `\n\n**Filing Sources:**\n${urlLinks.slice(0, 20).join('\n')}`;
|
|
1440
|
+
}
|
|
1193
1441
|
log('info', `Metrics query returned ${rowCount} rows`);
|
|
1194
1442
|
return {
|
|
1195
1443
|
content: [{
|
|
1196
1444
|
type: 'text',
|
|
1197
|
-
text: `Metrics data (${rowCount} rows):\n\n${table}${suffix}`,
|
|
1445
|
+
text: `Metrics data (${rowCount} rows):\n\n${table}${suffix}${filingSources}`,
|
|
1198
1446
|
}],
|
|
1199
1447
|
};
|
|
1200
1448
|
}
|
|
@@ -1343,10 +1591,14 @@ server.registerTool('etsquare_get_chunk', {
|
|
|
1343
1591
|
? result.chunk_text
|
|
1344
1592
|
: '';
|
|
1345
1593
|
const chunkLength = result.chunk_text_length;
|
|
1594
|
+
const secUrl = result.sec_url;
|
|
1595
|
+
let header = `Chunk ${input.chunk_id}${typeof chunkLength === 'number' ? ` (${chunkLength} chars)` : ''}`;
|
|
1596
|
+
if (secUrl)
|
|
1597
|
+
header += `\nSEC Filing: ${secUrl}`;
|
|
1346
1598
|
return {
|
|
1347
1599
|
content: [{
|
|
1348
1600
|
type: 'text',
|
|
1349
|
-
text:
|
|
1601
|
+
text: `${header}\n\n${chunkText}`,
|
|
1350
1602
|
}],
|
|
1351
1603
|
};
|
|
1352
1604
|
}
|
|
@@ -1612,7 +1864,58 @@ server.registerTool('etsquare_weekly_brief', {
|
|
|
1612
1864
|
};
|
|
1613
1865
|
}
|
|
1614
1866
|
});
|
|
1867
|
+
// ─── Tool 9: Company Research ──────────────────────────────────────────────
|
|
1868
|
+
server.registerTool('etsquare_company_research', {
|
|
1869
|
+
title: 'Company Research Packet',
|
|
1870
|
+
description: 'Returns a citation-grounded research packet for one company in a single call. ' +
|
|
1871
|
+
'Server-side orchestration assembles financials, earnings, narrative, ownership, and KPIs with SEC provenance.',
|
|
1872
|
+
inputSchema: {
|
|
1873
|
+
ticker: z.string().min(1).max(10)
|
|
1874
|
+
.describe('Company ticker (e.g., "AAPL", "NVDA"). Resolve names first with etsquare_lookup_company.'),
|
|
1875
|
+
sections: z.array(z.enum(['snapshot', 'financials', 'earnings', 'narrative', 'ownership', 'kpis']))
|
|
1876
|
+
.optional()
|
|
1877
|
+
.describe('Sections to include. Returns all if omitted.'),
|
|
1878
|
+
detail_level: z.enum(['summary', 'standard', 'full']).default('standard')
|
|
1879
|
+
.describe('Controls response size: summary (~4K tokens), standard (~8K), full (~15K).'),
|
|
1880
|
+
peers: z.array(z.string().min(1).max(10)).max(3).optional()
|
|
1881
|
+
.describe('Peer tickers for comparison (financial spine + earnings only). Max 3.'),
|
|
1882
|
+
n_periods: z.number().min(1).max(8).default(4)
|
|
1883
|
+
.describe('Financial statement periods (default: 4).'),
|
|
1884
|
+
},
|
|
1885
|
+
}, async (input) => {
|
|
1886
|
+
if (containsGuardrailBypass(input))
|
|
1887
|
+
return guardrailViolationResponse();
|
|
1888
|
+
const ticker = input.ticker.trim().toUpperCase();
|
|
1889
|
+
try {
|
|
1890
|
+
log('info', `Company research: ${ticker}`, {
|
|
1891
|
+
sections: input.sections,
|
|
1892
|
+
detail_level: input.detail_level,
|
|
1893
|
+
peers: input.peers,
|
|
1894
|
+
n_periods: input.n_periods,
|
|
1895
|
+
});
|
|
1896
|
+
const packet = await client.getCompanyResearch({
|
|
1897
|
+
ticker,
|
|
1898
|
+
sections: input.sections,
|
|
1899
|
+
detail_level: input.detail_level,
|
|
1900
|
+
peers: input.peers?.map((peer) => peer.trim().toUpperCase()),
|
|
1901
|
+
n_periods: input.n_periods,
|
|
1902
|
+
});
|
|
1903
|
+
log('info', `Company research: ${ticker} complete`, {
|
|
1904
|
+
elapsed_ms: packet?.meta?.elapsed_ms,
|
|
1905
|
+
sections_returned: packet?.meta?.sections_returned,
|
|
1906
|
+
sections_failed: packet?.meta?.sections_failed,
|
|
1907
|
+
});
|
|
1908
|
+
return { content: [{ type: 'text', text: JSON.stringify(packet, null, 2) }] };
|
|
1909
|
+
}
|
|
1910
|
+
catch (error) {
|
|
1911
|
+
log('error', `Company research failed: ${ticker}`, { error: error instanceof Error ? error.message : error });
|
|
1912
|
+
return {
|
|
1913
|
+
content: [{ type: 'text', text: `Company research failed for ${ticker}: ${error instanceof Error ? error.message : 'Unknown error'}` }],
|
|
1914
|
+
isError: true,
|
|
1915
|
+
};
|
|
1916
|
+
}
|
|
1917
|
+
});
|
|
1615
1918
|
// ─── Start Server ───────────────────────────────────────────────────────────
|
|
1616
1919
|
const transport = new StdioServerTransport();
|
|
1617
1920
|
await server.connect(transport);
|
|
1618
|
-
log('info', 'ETSquare MCP Server v0.
|
|
1921
|
+
log('info', 'ETSquare MCP Server v0.6.0 ready and listening on stdio (15 tools)');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@etsquare/mcp-server-sec",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "MCP server for Claude Desktop: search SEC filing sections, financial statements, insider trades, institutional ownership, earnings actuals, XBRL metrics, and company lookup.",
|
|
6
6
|
"main": "dist/index.js",
|