@etsquare/mcp-server-sec 0.2.0 → 0.4.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/dist/etsquare-client.d.ts +62 -4
- package/dist/etsquare-client.js +93 -8
- package/dist/index.js +1173 -68
- package/dist/types.d.ts +71 -0
- package/dist/types.js +37 -0
- package/package.json +8 -3
package/dist/index.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* @etsquare/mcp-server-sec
|
|
3
|
+
* @etsquare/mcp-server-sec v0.3.0
|
|
4
4
|
* MCP server for SEC Intelligence: search SEC filings,
|
|
5
|
-
* resolve company tickers,
|
|
5
|
+
* resolve company tickers, execute financial metrics templates,
|
|
6
|
+
* compare companies, and access weekly briefs.
|
|
6
7
|
*/
|
|
7
8
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
8
9
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
9
10
|
import { z } from 'zod';
|
|
10
11
|
import { ETSquareClient } from './etsquare-client.js';
|
|
11
|
-
//
|
|
12
|
+
// ─── Guardrails ─────────────────────────────────────────────────────────────
|
|
12
13
|
const GUARDRAIL_KEYWORDS = [
|
|
13
14
|
'reset guardrails',
|
|
14
15
|
'disable guardrails',
|
|
@@ -40,8 +41,8 @@ function guardrailViolationResponse() {
|
|
|
40
41
|
isError: true,
|
|
41
42
|
};
|
|
42
43
|
}
|
|
43
|
-
// Environment
|
|
44
|
-
const baseUrl = process.env.ETSQUARE_BASE_URL || 'https://
|
|
44
|
+
// ─── Environment ────────────────────────────────────────────────────────────
|
|
45
|
+
const baseUrl = process.env.ETSQUARE_BASE_URL || 'https://sec-intelligence-api-4mk2on5fga-uc.a.run.app';
|
|
45
46
|
const apiKey = process.env.ETSQUARE_API_KEY;
|
|
46
47
|
const DEBUG = process.env.DEBUG === 'true';
|
|
47
48
|
function log(level, message, data) {
|
|
@@ -51,20 +52,277 @@ function log(level, message, data) {
|
|
|
51
52
|
const logData = data ? ` ${JSON.stringify(data)}` : '';
|
|
52
53
|
console.error(`[${timestamp}] [${level.toUpperCase()}] ${message}${logData}`);
|
|
53
54
|
}
|
|
54
|
-
// Validate API key
|
|
55
55
|
if (!apiKey) {
|
|
56
56
|
console.error('ERROR: ETSQUARE_API_KEY environment variable is required');
|
|
57
57
|
process.exit(1);
|
|
58
58
|
}
|
|
59
|
-
// Initialize client
|
|
60
59
|
const client = new ETSquareClient({ baseUrl, apiKey });
|
|
61
|
-
log('info', `ETSquare MCP Server starting with backend: ${baseUrl}`);
|
|
62
|
-
//
|
|
60
|
+
log('info', `ETSquare MCP Server v0.3.0 starting with backend: ${baseUrl}`);
|
|
61
|
+
// ─── Item Code Labels ───────────────────────────────────────────────────────
|
|
62
|
+
const ITEM_CODE_LABELS = {
|
|
63
|
+
// 10-K
|
|
64
|
+
'10k_item_1': 'Business Description',
|
|
65
|
+
'10k_item_1a': 'Risk Factors',
|
|
66
|
+
'10k_item_1_a': 'Risk Factors',
|
|
67
|
+
'10k_item_2': 'Properties',
|
|
68
|
+
'10k_item_3': 'Legal Proceedings',
|
|
69
|
+
'10k_item_5': 'Market for Equity',
|
|
70
|
+
'10k_item_6': 'Selected Financial Data',
|
|
71
|
+
'10k_item_7': 'MD&A',
|
|
72
|
+
'10k_item_7a': 'Market Risk Disclosures',
|
|
73
|
+
'10k_item_7_a': 'Market Risk Disclosures',
|
|
74
|
+
'10k_item_8': 'Financial Statements',
|
|
75
|
+
'10k_item_9a': 'Controls & Procedures',
|
|
76
|
+
// 10-Q
|
|
77
|
+
'10q_item_1': 'Financial Statements (Quarterly)',
|
|
78
|
+
'10q_item_1a': 'Risk Factors (Quarterly)',
|
|
79
|
+
'10q_item_1_a': 'Risk Factors (Quarterly)',
|
|
80
|
+
'10q_item_2': 'MD&A (Quarterly)',
|
|
81
|
+
'10q_item_3': 'Market Risk (Quarterly)',
|
|
82
|
+
// 8-K
|
|
83
|
+
'8k_item_1_01': 'Entry into Material Agreement',
|
|
84
|
+
'8k_item_1_02': 'Termination of Material Agreement',
|
|
85
|
+
'8k_item_1_05': 'Material Cybersecurity Incident',
|
|
86
|
+
'8k_item_2_01': 'Completion of Acquisition/Disposition',
|
|
87
|
+
'8k_item_2_02': 'Results of Operations (Earnings)',
|
|
88
|
+
'8k_item_2_03': 'Creation of Direct Financial Obligation',
|
|
89
|
+
'8k_item_2_04': 'Triggering Events (Acceleration/Default)',
|
|
90
|
+
'8k_item_2_05': 'Costs Associated with Exit/Disposal',
|
|
91
|
+
'8k_item_2_06': 'Material Impairments',
|
|
92
|
+
'8k_item_4_01': 'Change in Accountant',
|
|
93
|
+
'8k_item_4_02': 'Non-Reliance on Financial Statements',
|
|
94
|
+
'8k_item_5_01': 'Change in Control',
|
|
95
|
+
'8k_item_5_02': 'Director/Officer Changes',
|
|
96
|
+
'8k_item_5_03': 'Amendments to Articles/Bylaws',
|
|
97
|
+
'8k_item_7_01': 'Regulation FD Disclosure',
|
|
98
|
+
'8k_item_8_01': 'Other Events',
|
|
99
|
+
'8k_item_9_01': 'Financial Statements and Exhibits',
|
|
100
|
+
};
|
|
101
|
+
function mapItemCodeToLabel(itemCode) {
|
|
102
|
+
if (!itemCode)
|
|
103
|
+
return 'Unknown';
|
|
104
|
+
return ITEM_CODE_LABELS[itemCode] || itemCode;
|
|
105
|
+
}
|
|
106
|
+
// Visualization hints are stored in each template's tags_json.visualization_hint
|
|
107
|
+
// in the database and flow through the API. No static registry needed.
|
|
108
|
+
// ─── Column Type / Role Inference Helpers ───────────────────────────────────
|
|
109
|
+
const TIME_FIELDS = new Set([
|
|
110
|
+
'fiscal_year', 'fiscal_period', 'period', 'period_end',
|
|
111
|
+
'filing_date', 'quarter', 'year', 'data_as_of',
|
|
112
|
+
]);
|
|
113
|
+
const GROUP_FIELDS = new Set(['ticker', 'company_name', 'sic_code', 'sic', 'sector', 'industry', 'name']);
|
|
114
|
+
const SIGNAL_FIELDS = new Set(['trend_direction', 'signal', 'flag']);
|
|
115
|
+
function inferColumnType(col, rows) {
|
|
116
|
+
if (rows.length === 0)
|
|
117
|
+
return 'string';
|
|
118
|
+
const sample = rows[0][col];
|
|
119
|
+
if (typeof sample === 'number')
|
|
120
|
+
return Number.isInteger(sample) ? 'int' : 'float';
|
|
121
|
+
return 'string';
|
|
122
|
+
}
|
|
123
|
+
function inferColumnRole(col) {
|
|
124
|
+
if (TIME_FIELDS.has(col))
|
|
125
|
+
return 'time';
|
|
126
|
+
if (GROUP_FIELDS.has(col))
|
|
127
|
+
return 'group';
|
|
128
|
+
if (SIGNAL_FIELDS.has(col))
|
|
129
|
+
return 'signal';
|
|
130
|
+
return 'measure';
|
|
131
|
+
}
|
|
132
|
+
function inferColumnUnit(col) {
|
|
133
|
+
if (col.endsWith('_pct') || col.includes('margin') || col.includes('growth'))
|
|
134
|
+
return 'pct';
|
|
135
|
+
if (col.endsWith('_b') || col.includes('revenue') || col.includes('income')
|
|
136
|
+
|| col.includes('profit') || col.includes('expense'))
|
|
137
|
+
return 'USD_B';
|
|
138
|
+
if (col.includes('ratio') || col.includes('multiple'))
|
|
139
|
+
return 'ratio';
|
|
140
|
+
if (col.includes('eps') || col.includes('per_share'))
|
|
141
|
+
return 'USD';
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
function humanizeColumnName(col) {
|
|
145
|
+
return col
|
|
146
|
+
.replace(/_/g, ' ')
|
|
147
|
+
.replace(/\b\w/g, c => c.toUpperCase())
|
|
148
|
+
.replace('Pct', '%')
|
|
149
|
+
.replace('Yoy', 'YoY')
|
|
150
|
+
.replace('Qoq', 'QoQ');
|
|
151
|
+
}
|
|
152
|
+
// ─── Structured Response Builders ───────────────────────────────────────────
|
|
153
|
+
function buildSearchContext(apiResponse, originalQuery) {
|
|
154
|
+
const results = apiResponse.narrative_results || [];
|
|
155
|
+
const ec = apiResponse.execution_contract || {};
|
|
156
|
+
return {
|
|
157
|
+
query_received: originalQuery,
|
|
158
|
+
query_type: apiResponse.query_type || null,
|
|
159
|
+
scope_confidence: apiResponse.scope_confidence || null,
|
|
160
|
+
tickers_resolved: [...new Set(results.map((r) => r.ticker).filter(Boolean))],
|
|
161
|
+
scope_effective: ec.scope_effective || null,
|
|
162
|
+
mode_effective: ec.mode_effective || null,
|
|
163
|
+
mode_source: ec.mode_source || null,
|
|
164
|
+
scope_source: ec.scope_source || null,
|
|
165
|
+
intent_matched: apiResponse.intent_matched || null,
|
|
166
|
+
results_returned: results.length,
|
|
167
|
+
unique_tickers: new Set(results.map((r) => r.ticker)).size,
|
|
168
|
+
unique_forms: [...new Set(results.map((r) => r.form_type).filter(Boolean))],
|
|
169
|
+
template_match: ec.template_match || null,
|
|
170
|
+
relaxations_applied: ec.relaxations_applied || [],
|
|
171
|
+
execution_id: apiResponse.execution_id || null,
|
|
172
|
+
processing_time_ms: apiResponse.processing_time_ms || null,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
function deriveLayoutHint(apiResponse) {
|
|
176
|
+
const results = apiResponse.narrative_results || [];
|
|
177
|
+
const tickers = new Set(results.map((r) => r.ticker));
|
|
178
|
+
const ec = apiResponse.execution_contract || {};
|
|
179
|
+
const scopeEffective = ec.scope_effective || '';
|
|
180
|
+
const hasMetrics = Array.isArray(apiResponse.xbrl_metrics) && apiResponse.xbrl_metrics.length > 0;
|
|
181
|
+
if (tickers.size >= 2 && hasMetrics) {
|
|
182
|
+
return {
|
|
183
|
+
suggested: 'comparison_dashboard',
|
|
184
|
+
reason: 'multi_ticker_with_metrics',
|
|
185
|
+
panels: ['metrics_chart', 'narrative_side_by_side'],
|
|
186
|
+
tone: 'comparative',
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
if (tickers.size >= 2) {
|
|
190
|
+
return {
|
|
191
|
+
suggested: 'comparison_grid',
|
|
192
|
+
reason: 'multi_ticker_narrative',
|
|
193
|
+
panels: ['narrative_by_ticker'],
|
|
194
|
+
tone: 'comparative',
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
if (tickers.size === 1 && hasMetrics) {
|
|
198
|
+
return {
|
|
199
|
+
suggested: 'company_profile',
|
|
200
|
+
reason: 'single_ticker_hybrid',
|
|
201
|
+
panels: ['metrics_chart', 'evidence_cards'],
|
|
202
|
+
tone: 'analytical',
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
if (scopeEffective === 'MACRO') {
|
|
206
|
+
return {
|
|
207
|
+
suggested: 'cross_company_theme',
|
|
208
|
+
reason: 'macro_scope',
|
|
209
|
+
panels: ['theme_summary', 'evidence_by_ticker'],
|
|
210
|
+
tone: 'analytical',
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
if (scopeEffective === 'INDUSTRY') {
|
|
214
|
+
return {
|
|
215
|
+
suggested: 'industry_overview',
|
|
216
|
+
reason: 'industry_scope',
|
|
217
|
+
panels: ['sector_summary', 'evidence_by_ticker'],
|
|
218
|
+
tone: 'analytical',
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
return {
|
|
222
|
+
suggested: 'evidence_cards',
|
|
223
|
+
reason: 'single_company_narrative',
|
|
224
|
+
alternatives: ['evidence_timeline', 'risk_factor_summary'],
|
|
225
|
+
tone: 'analytical',
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
function deriveMetricsVizHint(rows, schema) {
|
|
229
|
+
const columns = schema?.columns || [];
|
|
230
|
+
if (columns.length === 0)
|
|
231
|
+
return { chart_type: 'table' };
|
|
232
|
+
const timeCol = columns.find((c) => c.role === 'time');
|
|
233
|
+
const measureCols = columns.filter((c) => c.role === 'measure');
|
|
234
|
+
const groupCol = columns.find((c) => c.role === 'group');
|
|
235
|
+
if (!timeCol || measureCols.length === 0)
|
|
236
|
+
return { chart_type: 'table' };
|
|
237
|
+
const tickerCol = groupCol?.name || 'ticker';
|
|
238
|
+
const tickers = new Set(rows.map((r) => r[tickerCol]).filter(Boolean));
|
|
239
|
+
const primaryMeasure = measureCols[0];
|
|
240
|
+
const secondaryMeasure = measureCols.find((c) => c.unit === 'pct' && c.name !== primaryMeasure.name);
|
|
241
|
+
return {
|
|
242
|
+
chart_type: tickers.size > 1 ? 'grouped_bar' : 'line',
|
|
243
|
+
x: { field: timeCol.name, order: 'chronological', label: timeCol.label },
|
|
244
|
+
y_primary: {
|
|
245
|
+
field: primaryMeasure.name,
|
|
246
|
+
label: primaryMeasure.label,
|
|
247
|
+
format: primaryMeasure.unit === 'USD_B' ? 'currency_b'
|
|
248
|
+
: primaryMeasure.unit === 'pct' ? 'pct' : 'number',
|
|
249
|
+
},
|
|
250
|
+
y_secondary: secondaryMeasure ? {
|
|
251
|
+
field: secondaryMeasure.name,
|
|
252
|
+
label: secondaryMeasure.label,
|
|
253
|
+
chart_type: 'bar',
|
|
254
|
+
opacity: 0.3,
|
|
255
|
+
format: 'pct',
|
|
256
|
+
} : undefined,
|
|
257
|
+
group_by: tickers.size > 1 ? tickerCol : undefined,
|
|
258
|
+
color_palette: tickers.size > 1 ? 'comparison_diverging' : 'single_accent',
|
|
259
|
+
formats: {
|
|
260
|
+
currency_b: { prefix: '$', suffix: 'B', decimals: 1 },
|
|
261
|
+
pct: { suffix: '%', decimals: 1, sign: true },
|
|
262
|
+
number: { decimals: 0, comma_separated: true },
|
|
263
|
+
},
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
function computeSummaryStats(columnNames, rows) {
|
|
267
|
+
if (rows.length === 0)
|
|
268
|
+
return null;
|
|
269
|
+
const numericCols = columnNames.filter(c => typeof rows[0]?.[c] === 'number'
|
|
270
|
+
&& !['fiscal_year', 'year'].includes(c));
|
|
271
|
+
if (numericCols.length === 0)
|
|
272
|
+
return null;
|
|
273
|
+
const primary = numericCols[0];
|
|
274
|
+
const values = rows.map(r => r[primary]).filter((v) => v != null);
|
|
275
|
+
if (values.length === 0)
|
|
276
|
+
return null;
|
|
277
|
+
const min = Math.min(...values);
|
|
278
|
+
const max = Math.max(...values);
|
|
279
|
+
return {
|
|
280
|
+
row_count: rows.length,
|
|
281
|
+
primary_measure: primary,
|
|
282
|
+
min,
|
|
283
|
+
max,
|
|
284
|
+
range_pct: min !== 0
|
|
285
|
+
? Math.round(((max - min) / Math.abs(min)) * 1000) / 10
|
|
286
|
+
: null,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
function buildColumnMeta(apiColumns, rows) {
|
|
290
|
+
const columnNames = apiColumns.length > 0
|
|
291
|
+
? apiColumns.map((c) => typeof c === 'string' ? c : c.name)
|
|
292
|
+
: (rows.length > 0 ? Object.keys(rows[0]) : []);
|
|
293
|
+
return columnNames.map((col) => ({
|
|
294
|
+
name: col,
|
|
295
|
+
type: inferColumnType(col, rows),
|
|
296
|
+
role: inferColumnRole(col),
|
|
297
|
+
unit: inferColumnUnit(col),
|
|
298
|
+
label: humanizeColumnName(col),
|
|
299
|
+
}));
|
|
300
|
+
}
|
|
301
|
+
// ─── Text Formatting Helpers (existing behavior) ────────────────────────────
|
|
302
|
+
function formatNumberForTable(val) {
|
|
303
|
+
if (val === null || val === undefined)
|
|
304
|
+
return 'N/A';
|
|
305
|
+
if (typeof val === 'number')
|
|
306
|
+
return Number.isInteger(val) ? String(val) : val.toFixed(2);
|
|
307
|
+
return String(val);
|
|
308
|
+
}
|
|
309
|
+
function buildMarkdownTable(colNames, rows, maxRows = 20) {
|
|
310
|
+
const header = colNames.join(' | ');
|
|
311
|
+
const separator = colNames.map(() => '---').join(' | ');
|
|
312
|
+
const displayRows = rows.slice(0, maxRows);
|
|
313
|
+
const tableRows = displayRows.map((row) => colNames.map((col) => formatNumberForTable(row[col])).join(' | '));
|
|
314
|
+
const table = [header, separator, ...tableRows].join('\n');
|
|
315
|
+
const suffix = rows.length > maxRows
|
|
316
|
+
? `\n(Showing ${maxRows} of ${rows.length} rows)`
|
|
317
|
+
: '';
|
|
318
|
+
return `${table}${suffix}`;
|
|
319
|
+
}
|
|
320
|
+
// ─── MCP Server ─────────────────────────────────────────────────────────────
|
|
63
321
|
const server = new McpServer({
|
|
64
322
|
name: 'etsquare-mcp-sec',
|
|
65
|
-
version: '0.
|
|
323
|
+
version: '0.3.0',
|
|
66
324
|
});
|
|
67
|
-
// Tool 1: Company Lookup
|
|
325
|
+
// ─── Tool 1: Company Lookup ─────────────────────────────────────────────────
|
|
68
326
|
server.registerTool('etsquare_lookup_company', {
|
|
69
327
|
title: 'Look Up Company Ticker',
|
|
70
328
|
description: 'Resolve a company name or partial ticker to its official SEC ticker symbol and CIK number. ' +
|
|
@@ -107,17 +365,20 @@ server.registerTool('etsquare_lookup_company', {
|
|
|
107
365
|
};
|
|
108
366
|
}
|
|
109
367
|
});
|
|
110
|
-
// Tool 2: SEC Filing Search
|
|
368
|
+
// ─── Tool 2: SEC Filing Search ──────────────────────────────────────────────
|
|
111
369
|
server.registerTool('etsquare_search', {
|
|
112
370
|
title: 'Search SEC Filings',
|
|
113
|
-
description: 'Search
|
|
371
|
+
description: 'Search 3.4M+ SEC filing sections (10-K, 10-Q, 8-K) with hybrid retrieval. ' +
|
|
114
372
|
'Returns verbatim filing text with citations — best for qualitative research.\n\n' +
|
|
115
373
|
'Execution contract (required):\n' +
|
|
116
374
|
'- mode_lock: NARRATIVE (recommended) | HYBRID\n' +
|
|
117
375
|
'- scope_lock: COMPANY (specific tickers) | INDUSTRY (SIC sector) | MACRO (cross-market)\n\n' +
|
|
118
|
-
'For
|
|
376
|
+
'For canonical financial statements (income statement, balance sheet, cash flow), use etsquare_financial_statements.\n' +
|
|
377
|
+
'For custom financial queries or peer comparisons, use etsquare_discover_metrics + etsquare_execute_metrics.\n\n' +
|
|
378
|
+
'For INDUSTRY scope, use sector to target a specific industry (e.g., sector="semiconductors"). ' +
|
|
119
379
|
'Use tickers to scope to specific companies (resolve names first with etsquare_lookup_company). ' +
|
|
120
|
-
'Results include
|
|
380
|
+
'Results include filing text plus safe citation metadata such as chunk_id, accession number, and SEC URL for follow-up retrieval.\n\n' +
|
|
381
|
+
'Set response_format="structured" for typed JSON with search_context, layout hints, and chart-ready data.',
|
|
121
382
|
inputSchema: {
|
|
122
383
|
query: z.string().min(3).describe('What to search for in SEC filings (e.g., "customer concentration risk", "Show NVDA revenue trend")'),
|
|
123
384
|
mode_lock: z.enum(['NARRATIVE', 'HYBRID']).describe('NARRATIVE for text search, HYBRID for text + any available metrics'),
|
|
@@ -125,7 +386,15 @@ server.registerTool('etsquare_search', {
|
|
|
125
386
|
top_k: z.number().min(1).max(50).default(5).describe('Number of results to return (default: 5)'),
|
|
126
387
|
tickers: z.array(z.string()).optional().describe('Limit to specific companies by ticker (e.g., ["AAPL", "MSFT"])'),
|
|
127
388
|
doc_types: z.array(z.string()).optional().describe('Limit by SEC form type: "10-k", "10-q", "8-k"'),
|
|
128
|
-
|
|
389
|
+
sector: z.string().optional().describe('Industry/sector hint for INDUSTRY-scope queries. Resolved internally to SIC codes. ' +
|
|
390
|
+
'Examples: "software", "fintech", "energy", "semiconductors", "biotech", "REITs", "banks", ' +
|
|
391
|
+
'"defense", "utilities", "oil and gas", "healthcare", "cloud", "cybersecurity". ' +
|
|
392
|
+
'If sic_codes is also provided, sic_codes takes precedence.'),
|
|
393
|
+
sic_codes: z.array(z.string()).optional().describe('Expert override: limit by SIC industry code (e.g., "7372", "3674"). Takes precedence over sector.'),
|
|
394
|
+
response_format: z.enum(['text', 'structured']).default('text')
|
|
395
|
+
.describe('"text" returns numbered citations (default). "structured" returns JSON with search_context, typed results, and layout hints.'),
|
|
396
|
+
include_context: z.boolean().default(false)
|
|
397
|
+
.describe('Auto-expand top 2 truncated results with surrounding context'),
|
|
129
398
|
},
|
|
130
399
|
}, async (input) => {
|
|
131
400
|
if (containsGuardrailBypass(input))
|
|
@@ -136,6 +405,7 @@ server.registerTool('etsquare_search', {
|
|
|
136
405
|
mode_lock: input.mode_lock,
|
|
137
406
|
scope_lock: input.scope_lock,
|
|
138
407
|
top_k: input.top_k,
|
|
408
|
+
response_format: input.response_format,
|
|
139
409
|
});
|
|
140
410
|
const result = await client.search(input);
|
|
141
411
|
const searchResults = Array.isArray(result.narrative_results)
|
|
@@ -147,12 +417,78 @@ server.registerTool('etsquare_search', {
|
|
|
147
417
|
? result.xbrl_metrics
|
|
148
418
|
: [];
|
|
149
419
|
const resultCount = searchResults.length;
|
|
420
|
+
const executionId = typeof result.execution_id === 'string'
|
|
421
|
+
? result.execution_id
|
|
422
|
+
: null;
|
|
423
|
+
const researchRunId = typeof result.research_run_id === 'string'
|
|
424
|
+
? result.research_run_id
|
|
425
|
+
: null;
|
|
150
426
|
if (resultCount === 0 && xbrlMetrics.length === 0) {
|
|
151
427
|
let text = `No results found for: "${input.query}"`;
|
|
152
428
|
text += '\nTry broadening your query or removing filters.';
|
|
153
429
|
return { content: [{ type: 'text', text }] };
|
|
154
430
|
}
|
|
155
|
-
//
|
|
431
|
+
// Auto-expand truncated chunks with surrounding context
|
|
432
|
+
if (input.include_context) {
|
|
433
|
+
const truncatedChunks = searchResults
|
|
434
|
+
.filter((r) => r.chunk_text_truncated)
|
|
435
|
+
.slice(0, 2);
|
|
436
|
+
for (const chunk of truncatedChunks) {
|
|
437
|
+
try {
|
|
438
|
+
const ctx = await client.getChunkContext({
|
|
439
|
+
chunk_id: chunk.chunk_id,
|
|
440
|
+
neighbor_span: 1,
|
|
441
|
+
window: 1200,
|
|
442
|
+
});
|
|
443
|
+
const ctxData = ctx.context || {};
|
|
444
|
+
chunk._expanded_context = {
|
|
445
|
+
before: ctxData.before || null,
|
|
446
|
+
highlight: ctxData.highlight || null,
|
|
447
|
+
after: ctxData.after || null,
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
catch {
|
|
451
|
+
// Non-fatal — skip context expansion on failure
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
// ── Structured response path ──
|
|
456
|
+
if (input.response_format === 'structured') {
|
|
457
|
+
const metricsColumnMeta = xbrlMetrics.length > 0
|
|
458
|
+
? buildColumnMeta(result.xbrl_output_schema?.columns || [], xbrlMetrics)
|
|
459
|
+
: null;
|
|
460
|
+
const structured = {
|
|
461
|
+
search_context: buildSearchContext(result, input.query),
|
|
462
|
+
results: searchResults
|
|
463
|
+
.slice(0, input.top_k || 5)
|
|
464
|
+
.map((r, i) => ({
|
|
465
|
+
index: i + 1,
|
|
466
|
+
chunk_id: r.chunk_id,
|
|
467
|
+
ticker: r.ticker,
|
|
468
|
+
company_name: r.company_name,
|
|
469
|
+
form_type: r.form_type,
|
|
470
|
+
filing_date: r.filing_date,
|
|
471
|
+
item_code: r.item_code,
|
|
472
|
+
section_label: mapItemCodeToLabel(r.item_code),
|
|
473
|
+
score: r.similarity_score,
|
|
474
|
+
chunk_text: r.chunk_text,
|
|
475
|
+
truncated: r.chunk_text_truncated || false,
|
|
476
|
+
chunk_text_length: r.chunk_text_length || null,
|
|
477
|
+
accession_number: r.accession_number,
|
|
478
|
+
sec_url: r.sec_url,
|
|
479
|
+
expanded_context: r._expanded_context || undefined,
|
|
480
|
+
})),
|
|
481
|
+
metrics: xbrlMetrics.length > 0 ? {
|
|
482
|
+
rows: xbrlMetrics,
|
|
483
|
+
columns: metricsColumnMeta,
|
|
484
|
+
visualization_hint: deriveMetricsVizHint(xbrlMetrics, { columns: metricsColumnMeta }),
|
|
485
|
+
} : null,
|
|
486
|
+
layout_hint: deriveLayoutHint(result),
|
|
487
|
+
};
|
|
488
|
+
log('info', `Search returned ${resultCount} citations, ${xbrlMetrics.length} metrics [structured]`);
|
|
489
|
+
return { content: [{ type: 'text', text: JSON.stringify(structured, null, 2) }] };
|
|
490
|
+
}
|
|
491
|
+
// ── Text response path (existing behavior) ──
|
|
156
492
|
const sections = [];
|
|
157
493
|
if (resultCount > 0) {
|
|
158
494
|
const formatted = searchResults
|
|
@@ -167,10 +503,29 @@ server.registerTool('etsquare_search', {
|
|
|
167
503
|
if (r.fiscal_year && r.fiscal_period) {
|
|
168
504
|
lines[1] += ` | ${r.fiscal_period} FY${r.fiscal_year}`;
|
|
169
505
|
}
|
|
506
|
+
const metaParts = [];
|
|
507
|
+
if (r.chunk_id)
|
|
508
|
+
metaParts.push(`Chunk ID: ${r.chunk_id}`);
|
|
509
|
+
if (r.accession_number)
|
|
510
|
+
metaParts.push(`Accession: ${r.accession_number}`);
|
|
511
|
+
if (r.chunk_text_truncated)
|
|
512
|
+
metaParts.push('Truncated: yes');
|
|
513
|
+
if (metaParts.length > 0) {
|
|
514
|
+
lines.push(` ${metaParts.join(' | ')}`);
|
|
515
|
+
}
|
|
170
516
|
if (r.chunk_text) {
|
|
171
517
|
const snippet = r.chunk_text.substring(0, 600);
|
|
172
518
|
lines.push(` ${snippet}${r.chunk_text.length > 600 ? '...' : ''}`);
|
|
173
519
|
}
|
|
520
|
+
// Include expanded context if available
|
|
521
|
+
if (r._expanded_context) {
|
|
522
|
+
if (r._expanded_context.before) {
|
|
523
|
+
lines.push(` [Context before]: ${r._expanded_context.before.substring(0, 300)}...`);
|
|
524
|
+
}
|
|
525
|
+
if (r._expanded_context.after) {
|
|
526
|
+
lines.push(` [Context after]: ${r._expanded_context.after.substring(0, 300)}...`);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
174
529
|
const url = r.sec_url || r.source_url;
|
|
175
530
|
if (url) {
|
|
176
531
|
lines.push(` SEC URL: ${url}`);
|
|
@@ -178,27 +533,18 @@ server.registerTool('etsquare_search', {
|
|
|
178
533
|
return lines.join('\n');
|
|
179
534
|
})
|
|
180
535
|
.join('\n\n');
|
|
181
|
-
|
|
536
|
+
const headerParts = [`SEC Filing Citations (${resultCount}):`];
|
|
537
|
+
if (executionId)
|
|
538
|
+
headerParts.push(`Execution ID: ${executionId}`);
|
|
539
|
+
if (researchRunId)
|
|
540
|
+
headerParts.push(`Research Run ID: ${researchRunId}`);
|
|
541
|
+
const header = headerParts.join('\n');
|
|
542
|
+
sections.push(`${header}\n\n${formatted}`);
|
|
182
543
|
}
|
|
183
|
-
// Format XBRL metrics if present (handles both wide-format and long-format rows)
|
|
184
544
|
if (xbrlMetrics.length > 0) {
|
|
185
545
|
const colNames = Object.keys(xbrlMetrics[0] || {}).filter((k) => !k.startsWith('_'));
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
const displayRows = xbrlMetrics.slice(0, 20);
|
|
189
|
-
const tableRows = displayRows.map((row) => colNames.map((col) => {
|
|
190
|
-
const val = row[col];
|
|
191
|
-
if (val === null || val === undefined)
|
|
192
|
-
return 'N/A';
|
|
193
|
-
if (typeof val === 'number')
|
|
194
|
-
return Number.isInteger(val) ? String(val) : val.toFixed(2);
|
|
195
|
-
return String(val);
|
|
196
|
-
}).join(' | '));
|
|
197
|
-
const table = [header, separator, ...tableRows].join('\n');
|
|
198
|
-
const suffix = xbrlMetrics.length > 20
|
|
199
|
-
? `\n(Showing 20 of ${xbrlMetrics.length} rows)`
|
|
200
|
-
: '';
|
|
201
|
-
sections.push(`XBRL Metrics (${xbrlMetrics.length}):\n\n${table}${suffix}`);
|
|
546
|
+
const table = buildMarkdownTable(colNames, xbrlMetrics);
|
|
547
|
+
sections.push(`XBRL Metrics (${xbrlMetrics.length}):\n\n${table}`);
|
|
202
548
|
}
|
|
203
549
|
log('info', `Search returned ${resultCount} citations, ${xbrlMetrics.length} metrics`);
|
|
204
550
|
return {
|
|
@@ -216,28 +562,448 @@ server.registerTool('etsquare_search', {
|
|
|
216
562
|
};
|
|
217
563
|
}
|
|
218
564
|
});
|
|
219
|
-
// Tool 3: Execute Metrics
|
|
565
|
+
// ─── Tool 3: Execute Metrics ────────────────────────────────────────────────
|
|
566
|
+
server.registerTool('etsquare_financial_statements', {
|
|
567
|
+
title: 'Get Financial Statements',
|
|
568
|
+
description: 'Get canonical income statement, balance sheet, or cash flow statement for a company. ' +
|
|
569
|
+
'Assembles structured line items from XBRL facts with concept precedence and provenance.\n\n' +
|
|
570
|
+
'Each line item includes the selected XBRL concept, priority rank, and unit for auditability.\n\n' +
|
|
571
|
+
'Coverage: ~5,100 tickers for income statement and cash flow. ' +
|
|
572
|
+
'Balance sheet coverage is limited for some companies pending XBRL pipeline enhancement.\n\n' +
|
|
573
|
+
'For custom financial queries or peer comparisons, use etsquare_discover_metrics + etsquare_execute_metrics instead.',
|
|
574
|
+
inputSchema: {
|
|
575
|
+
ticker: z.string().min(1).max(10).describe('Company ticker (e.g., "AAPL", "NVDA")'),
|
|
576
|
+
statement_type: z.enum(['income_statement', 'balance_sheet', 'cash_flow'])
|
|
577
|
+
.describe('Which financial statement to return'),
|
|
578
|
+
period_mode: z.enum(['latest_annual', 'latest_quarterly', 'last_n_annual', 'last_n_quarterly'])
|
|
579
|
+
.default('last_n_annual')
|
|
580
|
+
.describe('Period selection: latest single period or last N periods'),
|
|
581
|
+
n_periods: z.number().min(1).max(20).default(5)
|
|
582
|
+
.describe('Number of periods for last_n modes (default: 5)'),
|
|
583
|
+
},
|
|
584
|
+
}, async (input) => {
|
|
585
|
+
if (containsGuardrailBypass(input))
|
|
586
|
+
return guardrailViolationResponse();
|
|
587
|
+
try {
|
|
588
|
+
const result = await client.getFinancialStatement(input);
|
|
589
|
+
const periods = Array.isArray(result.periods) ? result.periods : [];
|
|
590
|
+
const lineOrder = Array.isArray(result.line_order) ? result.line_order : [];
|
|
591
|
+
const ticker = result.ticker || input.ticker.toUpperCase();
|
|
592
|
+
if (periods.length === 0) {
|
|
593
|
+
return {
|
|
594
|
+
content: [{
|
|
595
|
+
type: 'text',
|
|
596
|
+
text: `No ${input.statement_type.replace(/_/g, ' ')} data found for ${ticker}. ` +
|
|
597
|
+
`The company may not have XBRL data available for the requested period.`,
|
|
598
|
+
}],
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
// Format as markdown table
|
|
602
|
+
const labels = lineOrder.filter(label => {
|
|
603
|
+
return periods.some((p) => p.lines?.[label]?.value != null);
|
|
604
|
+
});
|
|
605
|
+
// Build header row
|
|
606
|
+
const periodHeaders = periods.map((p) => `FY${p.fiscal_year}${p.fiscal_period !== 'FY' ? ' ' + p.fiscal_period : ''}`);
|
|
607
|
+
const formatValue = (val, label) => {
|
|
608
|
+
if (val == null)
|
|
609
|
+
return '—';
|
|
610
|
+
if (label.startsWith('eps_'))
|
|
611
|
+
return val.toFixed(2);
|
|
612
|
+
if (label.startsWith('shares_'))
|
|
613
|
+
return (val / 1e6).toFixed(0) + 'M';
|
|
614
|
+
if (Math.abs(val) >= 1e9)
|
|
615
|
+
return (val / 1e9).toFixed(2) + 'B';
|
|
616
|
+
if (Math.abs(val) >= 1e6)
|
|
617
|
+
return (val / 1e6).toFixed(1) + 'M';
|
|
618
|
+
return val.toLocaleString();
|
|
619
|
+
};
|
|
620
|
+
const labelDisplay = (label) => label.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
|
|
621
|
+
let table = `## ${ticker} — ${input.statement_type.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase())}\n\n`;
|
|
622
|
+
table += `| Line Item | ${periodHeaders.join(' | ')} |\n`;
|
|
623
|
+
table += `|---|${periodHeaders.map(() => '---:').join('|')}|\n`;
|
|
624
|
+
for (const label of labels) {
|
|
625
|
+
const values = periods.map((p) => formatValue(p.lines?.[label]?.value, label));
|
|
626
|
+
table += `| ${labelDisplay(label)} | ${values.join(' | ')} |\n`;
|
|
627
|
+
}
|
|
628
|
+
// Add derived metrics (e.g., FCF)
|
|
629
|
+
const derivedKeys = new Set();
|
|
630
|
+
for (const p of periods) {
|
|
631
|
+
if (p.derived) {
|
|
632
|
+
for (const k of Object.keys(p.derived))
|
|
633
|
+
derivedKeys.add(k);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
if (derivedKeys.size > 0) {
|
|
637
|
+
table += `|---|${periodHeaders.map(() => '---:').join('|')}|\n`;
|
|
638
|
+
for (const dk of derivedKeys) {
|
|
639
|
+
const values = periods.map((p) => formatValue(p.derived?.[dk]?.value, dk));
|
|
640
|
+
table += `| **${labelDisplay(dk)}** | ${values.join(' | ')} |\n`;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
// Provenance note
|
|
644
|
+
const firstPeriod = periods[0];
|
|
645
|
+
const sampleLine = firstPeriod?.lines?.[labels[0]];
|
|
646
|
+
if (sampleLine) {
|
|
647
|
+
table += `\n*Source: SEC XBRL filings. Concept precedence via VT_CF_XBRL_CONCEPT_FAMILIES.*`;
|
|
648
|
+
}
|
|
649
|
+
return {
|
|
650
|
+
content: [{ type: 'text', text: table }],
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
catch (error) {
|
|
654
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
655
|
+
return {
|
|
656
|
+
content: [{ type: 'text', text: `Financial statement request failed: ${message}` }],
|
|
657
|
+
isError: true,
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
});
|
|
661
|
+
server.registerTool('etsquare_insider_trades', {
|
|
662
|
+
title: 'Insider Trading Activity',
|
|
663
|
+
description: 'Get insider buying and selling activity for a company from SEC Form 3/4/5 filings. ' +
|
|
664
|
+
'Shows officer/director stock purchases, sales, option exercises, and tax withholdings.\n\n' +
|
|
665
|
+
'Data is sourced directly from SEC EDGAR ownership filings, filed within 2 business days of each transaction.\n\n' +
|
|
666
|
+
'Summary includes open-market buy/sell counts, net shares, and total values. ' +
|
|
667
|
+
'Derivative transactions (options, RSUs) are separated from open-market trades.',
|
|
668
|
+
inputSchema: {
|
|
669
|
+
ticker: z.string().min(1).max(10).describe('Company ticker (e.g., "AAPL", "NVDA")'),
|
|
670
|
+
days_back: z.number().min(1).max(730).default(90)
|
|
671
|
+
.describe('Days of history to return (default: 90)'),
|
|
672
|
+
transaction_types: z.array(z.string()).optional()
|
|
673
|
+
.describe('Filter by type: "P" (purchase), "S" (sale), "M" (exercise), "F" (tax withholding), "A" (award)'),
|
|
674
|
+
include_derivatives: z.boolean().default(true)
|
|
675
|
+
.describe('Include derivative transactions like options and RSUs (default: true)'),
|
|
676
|
+
},
|
|
677
|
+
}, async (input) => {
|
|
678
|
+
if (containsGuardrailBypass(input))
|
|
679
|
+
return guardrailViolationResponse();
|
|
680
|
+
try {
|
|
681
|
+
const result = await client.getInsiderTransactions(input);
|
|
682
|
+
const transactions = Array.isArray(result.transactions) ? result.transactions : [];
|
|
683
|
+
const summary = result.summary || {};
|
|
684
|
+
const ticker = result.ticker || input.ticker.toUpperCase();
|
|
685
|
+
if (transactions.length === 0) {
|
|
686
|
+
return {
|
|
687
|
+
content: [{
|
|
688
|
+
type: 'text',
|
|
689
|
+
text: `No insider transactions found for ${ticker} in the last ${input.days_back || 90} days.`,
|
|
690
|
+
}],
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
// Format summary
|
|
694
|
+
const fmtVal = (v) => {
|
|
695
|
+
if (v == null)
|
|
696
|
+
return '$0';
|
|
697
|
+
if (Math.abs(v) >= 1e9)
|
|
698
|
+
return `$${(v / 1e9).toFixed(1)}B`;
|
|
699
|
+
if (Math.abs(v) >= 1e6)
|
|
700
|
+
return `$${(v / 1e6).toFixed(1)}M`;
|
|
701
|
+
if (Math.abs(v) >= 1e3)
|
|
702
|
+
return `$${(v / 1e3).toFixed(0)}K`;
|
|
703
|
+
return `$${v.toFixed(0)}`;
|
|
704
|
+
};
|
|
705
|
+
let text = `## ${ticker} — Insider Trading (Last ${input.days_back || 90} Days)\n\n`;
|
|
706
|
+
// Summary line
|
|
707
|
+
const netLabel = (summary.net_shares_open_market ?? 0) >= 0 ? 'net buying' : 'net selling';
|
|
708
|
+
text += `**Open market**: ${summary.open_market_buys || 0} buys (${fmtVal(summary.total_buy_value)}) / `;
|
|
709
|
+
text += `${summary.open_market_sells || 0} sells (${fmtVal(summary.total_sell_value)})`;
|
|
710
|
+
if (summary.exercises)
|
|
711
|
+
text += ` | ${summary.exercises} exercises`;
|
|
712
|
+
if (summary.tax_withholdings)
|
|
713
|
+
text += ` | ${summary.tax_withholdings} tax withholdings`;
|
|
714
|
+
text += `\n\n`;
|
|
715
|
+
// Transaction table — show top 20
|
|
716
|
+
const shown = transactions.slice(0, 20);
|
|
717
|
+
text += `| Date | Insider | Title | Type | Shares | Price | Value |\n`;
|
|
718
|
+
text += `|------|---------|-------|------|-------:|------:|------:|\n`;
|
|
719
|
+
for (const t of shown) {
|
|
720
|
+
const shares = t.shares != null ? t.shares.toLocaleString() : '—';
|
|
721
|
+
const price = t.price_per_share != null ? `$${t.price_per_share.toFixed(2)}` : '—';
|
|
722
|
+
const value = t.total_value != null ? fmtVal(t.total_value) : '—';
|
|
723
|
+
const typeLabel = t.is_derivative ? `${t.transaction_type} *` : t.transaction_type;
|
|
724
|
+
text += `| ${t.transaction_date} | ${t.owner_name} | ${t.officer_title || '—'} | ${typeLabel} | ${shares} | ${price} | ${value} |\n`;
|
|
725
|
+
}
|
|
726
|
+
if (transactions.length > 20) {
|
|
727
|
+
text += `\n*Showing 20 of ${transactions.length} transactions.*\n`;
|
|
728
|
+
}
|
|
729
|
+
text += `\n*\\* = derivative transaction (options/RSUs). Source: SEC Form 4 filings via EDGAR.*`;
|
|
730
|
+
return {
|
|
731
|
+
content: [{ type: 'text', text }],
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
catch (error) {
|
|
735
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
736
|
+
return {
|
|
737
|
+
content: [{ type: 'text', text: `Insider trades request failed: ${message}` }],
|
|
738
|
+
isError: true,
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
});
|
|
742
|
+
server.registerTool('etsquare_institutional_holdings', {
|
|
743
|
+
title: 'Institutional Ownership (13F)',
|
|
744
|
+
description: 'Get institutional investor holdings for a company from SEC 13F filings. ' +
|
|
745
|
+
'Shows tracked managers holding the stock, their position sizes, and portfolio concentration.\n\n' +
|
|
746
|
+
'Data is from tracked institutional managers (top filers by AUM). ' +
|
|
747
|
+
'Coverage is partial — not all institutional holders are tracked. ' +
|
|
748
|
+
'Multiple rows per manager may appear due to sub-manager reporting. ' +
|
|
749
|
+
'Source: SEC EDGAR 13F-HR filings, filed quarterly.',
|
|
750
|
+
inputSchema: {
|
|
751
|
+
ticker: z.string().min(1).max(10).describe('Company ticker (e.g., "NVDA", "AAPL")'),
|
|
752
|
+
quarters: z.number().min(1).max(8).default(2)
|
|
753
|
+
.describe('Quarters of history (default: 2)'),
|
|
754
|
+
},
|
|
755
|
+
}, async (input) => {
|
|
756
|
+
if (containsGuardrailBypass(input))
|
|
757
|
+
return guardrailViolationResponse();
|
|
758
|
+
try {
|
|
759
|
+
const result = await client.getInstitutionalHoldings(input);
|
|
760
|
+
const summary = result.summary || {};
|
|
761
|
+
const holdersByQuarter = result.holders_by_quarter || {};
|
|
762
|
+
const ticker = result.ticker || input.ticker.toUpperCase();
|
|
763
|
+
const quarters = Object.keys(holdersByQuarter).sort().reverse();
|
|
764
|
+
if (quarters.length === 0) {
|
|
765
|
+
return {
|
|
766
|
+
content: [{
|
|
767
|
+
type: 'text',
|
|
768
|
+
text: `No institutional holdings found for ${ticker} in tracked managers. ` +
|
|
769
|
+
`The stock may not be held by the top institutional filers currently tracked.`,
|
|
770
|
+
}],
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
const fmtVal = (v) => {
|
|
774
|
+
if (v == null)
|
|
775
|
+
return '—';
|
|
776
|
+
if (Math.abs(v) >= 1e12)
|
|
777
|
+
return `$${(v / 1e12).toFixed(1)}T`;
|
|
778
|
+
if (Math.abs(v) >= 1e9)
|
|
779
|
+
return `$${(v / 1e9).toFixed(1)}B`;
|
|
780
|
+
if (Math.abs(v) >= 1e6)
|
|
781
|
+
return `$${(v / 1e6).toFixed(1)}M`;
|
|
782
|
+
if (Math.abs(v) >= 1e3)
|
|
783
|
+
return `$${(v / 1e3).toFixed(0)}K`;
|
|
784
|
+
return `$${v.toFixed(0)}`;
|
|
785
|
+
};
|
|
786
|
+
let text = `## ${ticker} — Institutional Holders (13F)\n\n`;
|
|
787
|
+
text += `**Distinct tracked managers**: ${summary.distinct_tracked_managers || 0} | `;
|
|
788
|
+
text += `**Holder rows**: ${summary.holder_rows || 0} | `;
|
|
789
|
+
text += `**Tracked shares**: ${(summary.tracked_shares_held || 0).toLocaleString()} | `;
|
|
790
|
+
text += `**Tracked value**: ${fmtVal(summary.tracked_value_usd)}\n`;
|
|
791
|
+
text += `*${summary.coverage_note || 'Partial coverage — tracked managers only'}*\n\n`;
|
|
792
|
+
for (const q of quarters) {
|
|
793
|
+
const holders = holdersByQuarter[q] || [];
|
|
794
|
+
if (holders.length === 0)
|
|
795
|
+
continue;
|
|
796
|
+
text += `### ${q}\n\n`;
|
|
797
|
+
text += `| Manager | Shares | Value | Portfolio % | Discretion |\n`;
|
|
798
|
+
text += `|---------|-------:|------:|------------:|:-----------|\n`;
|
|
799
|
+
for (const h of holders.slice(0, 20)) {
|
|
800
|
+
const shares = h.shares != null ? h.shares.toLocaleString() : '—';
|
|
801
|
+
const value = fmtVal(h.value_usd);
|
|
802
|
+
const pct = h.portfolio_pct != null ? `${h.portfolio_pct.toFixed(2)}%` : '—';
|
|
803
|
+
const disc = h.investment_discretion || '—';
|
|
804
|
+
const label = h.manager_label || h.manager_name || '—';
|
|
805
|
+
text += `| ${label} | ${shares} | ${value} | ${pct} | ${disc} |\n`;
|
|
806
|
+
}
|
|
807
|
+
if (holders.length > 20) {
|
|
808
|
+
text += `\n*Showing 20 of ${holders.length} tracked holders.*\n`;
|
|
809
|
+
}
|
|
810
|
+
text += '\n';
|
|
811
|
+
}
|
|
812
|
+
text += `*Source: SEC 13F-HR filings via EDGAR. Tracked managers only — not full institutional ownership.*`;
|
|
813
|
+
return {
|
|
814
|
+
content: [{ type: 'text', text }],
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
catch (error) {
|
|
818
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
819
|
+
return {
|
|
820
|
+
content: [{ type: 'text', text: `Institutional holdings request failed: ${message}` }],
|
|
821
|
+
isError: true,
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
});
|
|
825
|
+
server.registerTool('etsquare_earnings_actuals', {
|
|
826
|
+
title: 'Earnings Actuals & Guidance',
|
|
827
|
+
description: 'Get issuer-reported earnings actuals and forward guidance for a company from SEC 8-K filings. ' +
|
|
828
|
+
'Extracts revenue, EPS (basic/diluted), and net income from Item 2.02 press releases.\n\n' +
|
|
829
|
+
'Values are deterministically extracted from exhibit press releases (EX-99.1) attached to 8-K filings. ' +
|
|
830
|
+
'Only explicit issuer-reported values are included — no analyst estimates or consensus.\n\n' +
|
|
831
|
+
'Guidance shows forward-looking ranges (low/high) when the issuer provides them.\n\n' +
|
|
832
|
+
'Coverage: ~1,900 tickers, most recent 1-2 quarters (Dec 2025 onward). ' +
|
|
833
|
+
'Historical depth is expanding — some tickers may have fewer quarters than requested.\n\n' +
|
|
834
|
+
'Source: SEC EDGAR 8-K Item 2.02 filings with attached press releases.',
|
|
835
|
+
inputSchema: {
|
|
836
|
+
ticker: z.string().min(1).max(10).describe('Company ticker (e.g., "AAPL", "NVDA")'),
|
|
837
|
+
quarters: z.number().min(1).max(20).default(4)
|
|
838
|
+
.describe('Quarters of history (default: 4)'),
|
|
839
|
+
metrics: z.array(z.string()).optional()
|
|
840
|
+
.describe('Filter metrics: "revenue", "diluted_eps", "basic_eps", "net_income"'),
|
|
841
|
+
},
|
|
842
|
+
}, async (input) => {
|
|
843
|
+
if (containsGuardrailBypass(input))
|
|
844
|
+
return guardrailViolationResponse();
|
|
845
|
+
try {
|
|
846
|
+
const result = await client.getEarningsActuals(input);
|
|
847
|
+
const summary = result.summary || {};
|
|
848
|
+
const actuals = result.actuals || [];
|
|
849
|
+
const guidance = result.guidance || [];
|
|
850
|
+
const ticker = result.ticker || input.ticker.toUpperCase();
|
|
851
|
+
if (actuals.length === 0 && guidance.length === 0) {
|
|
852
|
+
return {
|
|
853
|
+
content: [{
|
|
854
|
+
type: 'text',
|
|
855
|
+
text: `No earnings actuals found for ${ticker}. ` +
|
|
856
|
+
`Earnings data is extracted from 8-K Item 2.02 press releases — ` +
|
|
857
|
+
`the company may not have earnings filings in the extracted dataset yet.`,
|
|
858
|
+
}],
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
const fmtVal = (v, unit) => {
|
|
862
|
+
if (v == null)
|
|
863
|
+
return '—';
|
|
864
|
+
if (unit === 'USD_PER_SHARE')
|
|
865
|
+
return `$${v.toFixed(2)}`;
|
|
866
|
+
if (Math.abs(v) >= 1e12)
|
|
867
|
+
return `$${(v / 1e12).toFixed(1)}T`;
|
|
868
|
+
if (Math.abs(v) >= 1e9)
|
|
869
|
+
return `$${(v / 1e9).toFixed(2)}B`;
|
|
870
|
+
if (Math.abs(v) >= 1e6)
|
|
871
|
+
return `$${(v / 1e6).toFixed(1)}M`;
|
|
872
|
+
if (Math.abs(v) >= 1e3)
|
|
873
|
+
return `$${(v / 1e3).toFixed(0)}K`;
|
|
874
|
+
return `$${v.toFixed(0)}`;
|
|
875
|
+
};
|
|
876
|
+
let text = `## ${ticker} — Earnings Actuals\n\n`;
|
|
877
|
+
text += `**Metrics found**: ${summary.total_actuals || 0} actuals, ${summary.total_guidance || 0} guidance\n`;
|
|
878
|
+
if (summary.date_range?.earliest && summary.date_range?.latest) {
|
|
879
|
+
text += `**Period**: ${summary.date_range.earliest} to ${summary.date_range.latest}\n`;
|
|
880
|
+
}
|
|
881
|
+
text += '\n';
|
|
882
|
+
if (actuals.length > 0) {
|
|
883
|
+
text += `| Filing Date | Metric | Value | GAAP | FY | FQ | Confidence |\n`;
|
|
884
|
+
text += `|-------------|--------|------:|:----:|---:|---:|-----------:|\n`;
|
|
885
|
+
for (const a of actuals) {
|
|
886
|
+
const val = fmtVal(a.value, a.unit);
|
|
887
|
+
const gaap = a.gaap_flag || '—';
|
|
888
|
+
const fy = a.fiscal_year ?? '—';
|
|
889
|
+
const fq = a.fiscal_quarter ?? '—';
|
|
890
|
+
const conf = a.confidence != null ? `${(a.confidence * 100).toFixed(0)}%` : '—';
|
|
891
|
+
text += `| ${a.filing_date || '—'} | ${a.metric_name} | ${val} | ${gaap} | ${fy} | ${fq} | ${conf} |\n`;
|
|
892
|
+
}
|
|
893
|
+
text += '\n';
|
|
894
|
+
}
|
|
895
|
+
if (guidance.length > 0) {
|
|
896
|
+
text += `### Forward Guidance\n\n`;
|
|
897
|
+
text += `| Filing Date | Metric | Low | High | Midpoint | GAAP | Target FY | Target FQ |\n`;
|
|
898
|
+
text += `|-------------|--------|----:|-----:|---------:|:----:|----------:|----------:|\n`;
|
|
899
|
+
for (const g of guidance) {
|
|
900
|
+
const low = fmtVal(g.low_value, g.unit);
|
|
901
|
+
const high = fmtVal(g.high_value, g.unit);
|
|
902
|
+
const mid = g.midpoint != null ? fmtVal(g.midpoint, g.unit) : '—';
|
|
903
|
+
const gaap = g.gaap_flag || '—';
|
|
904
|
+
const fy = g.fiscal_year ?? '—';
|
|
905
|
+
const fq = g.fiscal_quarter ?? '—';
|
|
906
|
+
text += `| ${g.filing_date || '—'} | ${g.metric_name} | ${low} | ${high} | ${mid} | ${gaap} | ${fy} | ${fq} |\n`;
|
|
907
|
+
}
|
|
908
|
+
text += '\n';
|
|
909
|
+
}
|
|
910
|
+
text += `*Source: SEC 8-K Item 2.02 press releases — deterministic extraction from issuer filings.*`;
|
|
911
|
+
return {
|
|
912
|
+
content: [{ type: 'text', text }],
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
catch (error) {
|
|
916
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
917
|
+
return {
|
|
918
|
+
content: [{ type: 'text', text: `Earnings actuals request failed: ${message}` }],
|
|
919
|
+
isError: true,
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
});
|
|
220
923
|
server.registerTool('etsquare_execute_metrics', {
|
|
221
924
|
title: 'Execute Financial Metrics Query',
|
|
222
925
|
description: 'Execute an XBRL metrics template by template_id to get structured financial data ' +
|
|
223
926
|
'(numbers, ratios, time series). Returns tabular rows with columns like revenue, margins, EPS, etc.\n\n' +
|
|
224
|
-
'Common bind_params: p_ticker (
|
|
225
|
-
'p_start_year / p_end_year (time range, e.g., 2023-2025)
|
|
927
|
+
'Common bind_params: p_tickers (comma-separated tickers), p_ticker (single ticker), p_sic_code (industry), ' +
|
|
928
|
+
'p_start_year / p_end_year (time range, e.g., 2023-2025).\n\n' +
|
|
929
|
+
'Use the tickers array to pass multiple companies — automatically joins as p_tickers comma-separated string.\n\n' +
|
|
930
|
+
'Set response_format="structured" for typed JSON with column metadata, visualization hints, and summary stats.',
|
|
226
931
|
inputSchema: {
|
|
227
932
|
template_id: z.string().min(10).describe('Template ID for metrics execution'),
|
|
228
933
|
bind_params: z.record(z.unknown()).optional().describe('Template parameters as key-value pairs'),
|
|
229
934
|
row_limit: z.number().min(1).max(500).optional().describe('Max rows to return (default: 100)'),
|
|
935
|
+
tickers: z.array(z.string()).max(5).optional()
|
|
936
|
+
.describe('Execute same template for multiple tickers and merge results. Overrides p_ticker in bind_params.'),
|
|
937
|
+
response_format: z.enum(['text', 'structured']).default('text')
|
|
938
|
+
.describe('"text" returns markdown table (default). "structured" returns JSON with column metadata and viz hints.'),
|
|
230
939
|
},
|
|
231
940
|
}, async (input) => {
|
|
232
941
|
if (containsGuardrailBypass(input))
|
|
233
942
|
return guardrailViolationResponse();
|
|
234
943
|
try {
|
|
235
|
-
log('debug', 'Executing metrics template', {
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
944
|
+
log('debug', 'Executing metrics template', {
|
|
945
|
+
template_id: input.template_id,
|
|
946
|
+
tickers: input.tickers,
|
|
947
|
+
});
|
|
948
|
+
let rows = [];
|
|
949
|
+
let columns = [];
|
|
950
|
+
let rowCount = 0;
|
|
951
|
+
let truncated = false;
|
|
952
|
+
let vizHintFromApi = null;
|
|
953
|
+
// Multi-ticker: join as comma-separated p_tickers (single call)
|
|
954
|
+
// Templates use p_tickers (plural, comma-separated) for multi-company queries.
|
|
955
|
+
// Fallback: if p_tickers fails, try N parallel calls with p_ticker (singular).
|
|
956
|
+
if (input.tickers && input.tickers.length > 0) {
|
|
957
|
+
const joinedTickers = input.tickers.join(',');
|
|
958
|
+
const baseParams = { ...(input.bind_params || {}) };
|
|
959
|
+
// Remove any existing p_ticker — p_tickers takes precedence
|
|
960
|
+
delete baseParams.p_ticker;
|
|
961
|
+
try {
|
|
962
|
+
// Primary: single call with p_tickers (comma-separated)
|
|
963
|
+
const result = await client.executeMetrics({
|
|
964
|
+
template_id: input.template_id,
|
|
965
|
+
bind_params: { ...baseParams, p_tickers: joinedTickers },
|
|
966
|
+
row_limit: input.row_limit,
|
|
967
|
+
});
|
|
968
|
+
rows = result.rows || [];
|
|
969
|
+
rowCount = result.row_count || rows.length;
|
|
970
|
+
truncated = result.truncated || false;
|
|
971
|
+
columns = result.columns || [];
|
|
972
|
+
vizHintFromApi = result.visualization_hint || null;
|
|
973
|
+
}
|
|
974
|
+
catch {
|
|
975
|
+
// Fallback: parallel calls with p_ticker (singular) per ticker
|
|
976
|
+
log('debug', 'p_tickers failed, falling back to parallel p_ticker calls');
|
|
977
|
+
const results = await Promise.allSettled(input.tickers.map(ticker => client.executeMetrics({
|
|
978
|
+
template_id: input.template_id,
|
|
979
|
+
bind_params: { ...baseParams, p_ticker: ticker },
|
|
980
|
+
row_limit: input.row_limit,
|
|
981
|
+
})));
|
|
982
|
+
for (const res of results) {
|
|
983
|
+
if (res.status === 'fulfilled' && res.value) {
|
|
984
|
+
const val = res.value;
|
|
985
|
+
if (Array.isArray(val.rows))
|
|
986
|
+
rows.push(...val.rows);
|
|
987
|
+
if (!columns.length && Array.isArray(val.columns))
|
|
988
|
+
columns = val.columns;
|
|
989
|
+
if (val.truncated)
|
|
990
|
+
truncated = true;
|
|
991
|
+
if (!vizHintFromApi)
|
|
992
|
+
vizHintFromApi = val.visualization_hint || null;
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
rowCount = rows.length;
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
else {
|
|
999
|
+
// Single execution
|
|
1000
|
+
const result = await client.executeMetrics(input);
|
|
1001
|
+
rows = result.rows || [];
|
|
1002
|
+
rowCount = result.row_count || rows.length;
|
|
1003
|
+
truncated = result.truncated || false;
|
|
1004
|
+
columns = result.columns || [];
|
|
1005
|
+
vizHintFromApi = result.visualization_hint || null;
|
|
1006
|
+
}
|
|
241
1007
|
if (rowCount === 0) {
|
|
242
1008
|
return {
|
|
243
1009
|
content: [{
|
|
@@ -246,21 +1012,31 @@ server.registerTool('etsquare_execute_metrics', {
|
|
|
246
1012
|
}],
|
|
247
1013
|
};
|
|
248
1014
|
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
const
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
1015
|
+
// ── Structured response path ──
|
|
1016
|
+
if (input.response_format === 'structured') {
|
|
1017
|
+
const columnMeta = buildColumnMeta(columns, rows);
|
|
1018
|
+
// API-returned hint takes precedence over auto-derived
|
|
1019
|
+
const autoHint = deriveMetricsVizHint(rows, { columns: columnMeta });
|
|
1020
|
+
const colNames = columnMeta.map((c) => c.name);
|
|
1021
|
+
const structured = {
|
|
1022
|
+
template_id: input.template_id,
|
|
1023
|
+
columns: columnMeta,
|
|
1024
|
+
rows,
|
|
1025
|
+
row_count: rowCount,
|
|
1026
|
+
truncated,
|
|
1027
|
+
visualization_hint: vizHintFromApi || autoHint,
|
|
1028
|
+
summary_stats: computeSummaryStats(colNames, rows),
|
|
1029
|
+
};
|
|
1030
|
+
log('info', `Metrics query returned ${rowCount} rows [structured]`);
|
|
1031
|
+
return { content: [{ type: 'text', text: JSON.stringify(structured, null, 2) }] };
|
|
1032
|
+
}
|
|
1033
|
+
// ── Text response path (existing behavior) ──
|
|
1034
|
+
const colNames = columns.length > 0
|
|
1035
|
+
? columns.map((c) => c.name)
|
|
1036
|
+
: Object.keys(rows[0] || {});
|
|
1037
|
+
const table = buildMarkdownTable(colNames, rows);
|
|
262
1038
|
const suffix = truncated || rows.length > 20
|
|
263
|
-
? `\n\n(Showing ${
|
|
1039
|
+
? `\n\n(Showing ${Math.min(rows.length, 20)} of ${rowCount} rows${truncated ? ', results truncated' : ''})`
|
|
264
1040
|
: '';
|
|
265
1041
|
log('info', `Metrics query returned ${rowCount} rows`);
|
|
266
1042
|
return {
|
|
@@ -278,18 +1054,24 @@ server.registerTool('etsquare_execute_metrics', {
|
|
|
278
1054
|
};
|
|
279
1055
|
}
|
|
280
1056
|
});
|
|
281
|
-
// Tool 4: Discover Metrics Templates
|
|
1057
|
+
// ─── Tool 4: Discover Metrics Templates ─────────────────────────────────────
|
|
282
1058
|
server.registerTool('etsquare_discover_metrics', {
|
|
283
1059
|
title: 'Discover Financial Metrics Templates',
|
|
284
1060
|
description: 'Find available XBRL metrics templates by business question. ' +
|
|
285
1061
|
'Returns template IDs and metadata — use the template_id with etsquare_execute_metrics.\n\n' +
|
|
1062
|
+
'Use this only for structured metrics such as revenue, gross margin, operating margin, EPS, debt, liquidity, or cash flow.\n\n' +
|
|
1063
|
+
'Do not use this for narrative KPI questions like same-store sales, comparable sales, traffic, guest count, average check, or AUV. ' +
|
|
1064
|
+
'For those, use etsquare_search in NARRATIVE mode instead.\n\n' +
|
|
286
1065
|
'Example: "revenue trend by quarter" → returns matching templates with their required bind_params.\n\n' +
|
|
287
|
-
'Workflow: discover_metrics → pick template_id → execute_metrics with bind_params
|
|
1066
|
+
'Workflow: discover_metrics → pick template_id → execute_metrics with bind_params.\n\n' +
|
|
1067
|
+
'Set response_format="structured" for JSON with similarity scores and visualization hints.',
|
|
288
1068
|
inputSchema: {
|
|
289
|
-
question: z.string().min(3).describe('
|
|
1069
|
+
question: z.string().min(3).describe('Structured metrics question (e.g., "revenue trend by quarter", "gross margin trend", "EPS trend")'),
|
|
290
1070
|
scenario: z.enum(['snapshot', 'trends', 'peer_benchmark']).optional().describe('Filter by scenario type'),
|
|
291
1071
|
metric_family: z.string().optional().describe('Filter by metric family: REVENUE, EARNINGS, PROFITABILITY_MARGIN, LEVERAGE_DEBT, FREE_CASH_FLOW, LIQUIDITY'),
|
|
292
1072
|
max_results: z.number().min(1).max(10).default(5).optional().describe('Max templates to return'),
|
|
1073
|
+
response_format: z.enum(['text', 'structured']).default('text')
|
|
1074
|
+
.describe('"text" returns formatted list (default). "structured" returns JSON with scores and viz hints.'),
|
|
293
1075
|
},
|
|
294
1076
|
}, async (input) => {
|
|
295
1077
|
if (containsGuardrailBypass(input))
|
|
@@ -300,14 +1082,49 @@ server.registerTool('etsquare_discover_metrics', {
|
|
|
300
1082
|
const templates = Array.isArray(result.templates)
|
|
301
1083
|
? result.templates
|
|
302
1084
|
: [];
|
|
1085
|
+
const guidance = result.guidance && typeof result.guidance === 'object'
|
|
1086
|
+
? result.guidance
|
|
1087
|
+
: null;
|
|
303
1088
|
if (templates.length === 0) {
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
1089
|
+
let text = `No structured metrics templates found for: "${input.question}"`;
|
|
1090
|
+
if (guidance?.message) {
|
|
1091
|
+
text += `\n${guidance.message}`;
|
|
1092
|
+
}
|
|
1093
|
+
if (guidance?.suggested_tool === 'etsquare_search') {
|
|
1094
|
+
text += '\nRecommended next step: use etsquare_search with NARRATIVE mode.';
|
|
1095
|
+
}
|
|
1096
|
+
const examples = Array.isArray(guidance?.valid_metrics_examples)
|
|
1097
|
+
? guidance.valid_metrics_examples
|
|
1098
|
+
: [];
|
|
1099
|
+
if (examples.length > 0) {
|
|
1100
|
+
text += `\nValid metrics examples: ${examples.join('; ')}`;
|
|
1101
|
+
}
|
|
1102
|
+
return { content: [{ type: 'text', text }] };
|
|
1103
|
+
}
|
|
1104
|
+
// ── Structured response path ──
|
|
1105
|
+
if (input.response_format === 'structured') {
|
|
1106
|
+
const structuredTemplates = templates.map((t, i) => ({
|
|
1107
|
+
template_id: t.template_id,
|
|
1108
|
+
name: t.title,
|
|
1109
|
+
description: t.description,
|
|
1110
|
+
recommended: i === 0,
|
|
1111
|
+
similarity_score: t.similarity_score || null,
|
|
1112
|
+
confidence_score: t.confidence_score || null,
|
|
1113
|
+
scenario: t.scenario,
|
|
1114
|
+
category: t.category,
|
|
1115
|
+
required_params: t.required_params || {},
|
|
1116
|
+
columns: t.columns || [],
|
|
1117
|
+
visualization_hint: t.visualization_hint || null,
|
|
1118
|
+
}));
|
|
1119
|
+
const structured = {
|
|
1120
|
+
question: result.question || input.question,
|
|
1121
|
+
templates: structuredTemplates,
|
|
1122
|
+
guidance: guidance || null,
|
|
309
1123
|
};
|
|
1124
|
+
log('info', `Discovered ${templates.length} templates [structured]`);
|
|
1125
|
+
return { content: [{ type: 'text', text: JSON.stringify(structured, null, 2) }] };
|
|
310
1126
|
}
|
|
1127
|
+
// ── Text response path (existing behavior) ──
|
|
311
1128
|
const formatted = templates
|
|
312
1129
|
.map((t, i) => {
|
|
313
1130
|
const lines = [
|
|
@@ -354,7 +1171,295 @@ server.registerTool('etsquare_discover_metrics', {
|
|
|
354
1171
|
};
|
|
355
1172
|
}
|
|
356
1173
|
});
|
|
357
|
-
//
|
|
1174
|
+
// ─── Tool 5: Get Full Chunk Text ────────────────────────────────────────────
|
|
1175
|
+
server.registerTool('etsquare_get_chunk', {
|
|
1176
|
+
title: 'Get Full Chunk Text',
|
|
1177
|
+
description: 'Fetch the full text for a specific search result chunk. ' +
|
|
1178
|
+
'Use this when etsquare_search returns a truncated snippet and you need the complete chunk text. ' +
|
|
1179
|
+
'Requires both chunk_id and execution_id from a prior search result.',
|
|
1180
|
+
inputSchema: {
|
|
1181
|
+
chunk_id: z.string().length(32).describe('Chunk ID from etsquare_search output'),
|
|
1182
|
+
execution_id: z.string().min(32).describe('Execution ID from the etsquare_search response header'),
|
|
1183
|
+
},
|
|
1184
|
+
}, async (input) => {
|
|
1185
|
+
if (containsGuardrailBypass(input))
|
|
1186
|
+
return guardrailViolationResponse();
|
|
1187
|
+
try {
|
|
1188
|
+
log('debug', 'Fetching full chunk text', { chunk_id: input.chunk_id });
|
|
1189
|
+
const result = await client.getChunk(input);
|
|
1190
|
+
const chunkText = typeof result.chunk_text === 'string'
|
|
1191
|
+
? result.chunk_text
|
|
1192
|
+
: '';
|
|
1193
|
+
const chunkLength = result.chunk_text_length;
|
|
1194
|
+
return {
|
|
1195
|
+
content: [{
|
|
1196
|
+
type: 'text',
|
|
1197
|
+
text: `Chunk ${input.chunk_id}${typeof chunkLength === 'number' ? ` (${chunkLength} chars)` : ''}:\n\n${chunkText}`,
|
|
1198
|
+
}],
|
|
1199
|
+
};
|
|
1200
|
+
}
|
|
1201
|
+
catch (error) {
|
|
1202
|
+
log('error', 'Chunk fetch failed', { error: error instanceof Error ? error.message : error });
|
|
1203
|
+
return {
|
|
1204
|
+
content: [{ type: 'text', text: `Chunk fetch failed: ${error instanceof Error ? error.message : 'Unknown error'}` }],
|
|
1205
|
+
isError: true,
|
|
1206
|
+
};
|
|
1207
|
+
}
|
|
1208
|
+
});
|
|
1209
|
+
// ─── Tool 6: Get Chunk Context ──────────────────────────────────────────────
|
|
1210
|
+
server.registerTool('etsquare_get_chunk_context', {
|
|
1211
|
+
title: 'Get Chunk Context',
|
|
1212
|
+
description: 'Fetch the highlighted chunk plus surrounding context from the same filing section. ' +
|
|
1213
|
+
'Use this when you need the paragraphs immediately before and after a search hit.',
|
|
1214
|
+
inputSchema: {
|
|
1215
|
+
chunk_id: z.string().length(32).describe('Chunk ID from etsquare_search output'),
|
|
1216
|
+
neighbor_span: z.number().min(0).max(5).default(1).optional().describe('How many adjacent chunks to include before and after'),
|
|
1217
|
+
window: z.number().min(200).max(4000).default(900).optional().describe('Approximate character budget for surrounding context'),
|
|
1218
|
+
},
|
|
1219
|
+
}, async (input) => {
|
|
1220
|
+
if (containsGuardrailBypass(input))
|
|
1221
|
+
return guardrailViolationResponse();
|
|
1222
|
+
try {
|
|
1223
|
+
log('debug', 'Fetching chunk context', { chunk_id: input.chunk_id });
|
|
1224
|
+
const result = await client.getChunkContext(input);
|
|
1225
|
+
const filing = result.filing || {};
|
|
1226
|
+
const context = result.context || {};
|
|
1227
|
+
const navigation = result.navigation || {};
|
|
1228
|
+
const lines = [
|
|
1229
|
+
`Chunk ID: ${result.chunk_id || input.chunk_id}`,
|
|
1230
|
+
];
|
|
1231
|
+
if (result.sec_url)
|
|
1232
|
+
lines.push(`SEC URL: ${result.sec_url}`);
|
|
1233
|
+
if (filing.ticker || filing.company) {
|
|
1234
|
+
lines.push(`Filing: ${filing.ticker || 'N/A'} - ${filing.company || 'Unknown'} (${filing.form_type || filing.doc_subtype || 'N/A'})`);
|
|
1235
|
+
}
|
|
1236
|
+
if (filing.item_code || filing.accession_number) {
|
|
1237
|
+
lines.push(`Section: ${filing.item_code || 'N/A'}${filing.accession_number ? ` | Accession: ${filing.accession_number}` : ''}`);
|
|
1238
|
+
}
|
|
1239
|
+
if (typeof navigation.chunk_position === 'number' || typeof navigation.total_chunks_in_item === 'number') {
|
|
1240
|
+
lines.push(`Position: ${navigation.chunk_position ?? 'N/A'} of ${navigation.total_chunks_in_item ?? 'N/A'}`);
|
|
1241
|
+
}
|
|
1242
|
+
lines.push('');
|
|
1243
|
+
if (context.before) {
|
|
1244
|
+
lines.push('Before:');
|
|
1245
|
+
lines.push(String(context.before));
|
|
1246
|
+
lines.push('');
|
|
1247
|
+
}
|
|
1248
|
+
lines.push('Highlight:');
|
|
1249
|
+
lines.push(String(context.highlight || ''));
|
|
1250
|
+
if (context.after) {
|
|
1251
|
+
lines.push('');
|
|
1252
|
+
lines.push('After:');
|
|
1253
|
+
lines.push(String(context.after));
|
|
1254
|
+
}
|
|
1255
|
+
return {
|
|
1256
|
+
content: [{
|
|
1257
|
+
type: 'text',
|
|
1258
|
+
text: lines.join('\n'),
|
|
1259
|
+
}],
|
|
1260
|
+
};
|
|
1261
|
+
}
|
|
1262
|
+
catch (error) {
|
|
1263
|
+
log('error', 'Chunk context fetch failed', { error: error instanceof Error ? error.message : error });
|
|
1264
|
+
return {
|
|
1265
|
+
content: [{ type: 'text', text: `Chunk context fetch failed: ${error instanceof Error ? error.message : 'Unknown error'}` }],
|
|
1266
|
+
isError: true,
|
|
1267
|
+
};
|
|
1268
|
+
}
|
|
1269
|
+
});
|
|
1270
|
+
// ─── Tool 7: Compare Companies ──────────────────────────────────────────────
|
|
1271
|
+
server.registerTool('etsquare_compare', {
|
|
1272
|
+
title: 'Compare Companies in SEC Filings',
|
|
1273
|
+
description: 'Compare 2-5 companies across SEC filings in a single call. ' +
|
|
1274
|
+
'Returns per-ticker results grouped for easy side-by-side analysis.\n\n' +
|
|
1275
|
+
'Resolve company names to tickers first with etsquare_lookup_company.\n\n' +
|
|
1276
|
+
'Use mode_lock=HYBRID to include both narrative text and structured metrics.\n\n' +
|
|
1277
|
+
'Always returns structured JSON (comparison results are inherently structured).',
|
|
1278
|
+
inputSchema: {
|
|
1279
|
+
tickers: z.array(z.string()).min(2).max(5)
|
|
1280
|
+
.describe('Company tickers to compare (2-5). Resolve names first with etsquare_lookup_company.'),
|
|
1281
|
+
query: z.string().min(3)
|
|
1282
|
+
.describe('Comparison question (e.g., "revenue growth and risk factors", "tariff risk exposure")'),
|
|
1283
|
+
mode_lock: z.enum(['NARRATIVE', 'HYBRID']).default('NARRATIVE')
|
|
1284
|
+
.describe('HYBRID recommended for comparisons (gets both text and metrics)'),
|
|
1285
|
+
top_k_per_ticker: z.number().int().min(1).max(10).default(3)
|
|
1286
|
+
.describe('Narrative results per company'),
|
|
1287
|
+
doc_types: z.array(z.string()).optional()
|
|
1288
|
+
.describe('Limit by form type (e.g., ["10-k", "8-k"])'),
|
|
1289
|
+
},
|
|
1290
|
+
}, async (input) => {
|
|
1291
|
+
if (containsGuardrailBypass(input))
|
|
1292
|
+
return guardrailViolationResponse();
|
|
1293
|
+
try {
|
|
1294
|
+
log('debug', 'Executing comparison', {
|
|
1295
|
+
tickers: input.tickers,
|
|
1296
|
+
query: input.query,
|
|
1297
|
+
mode_lock: input.mode_lock,
|
|
1298
|
+
});
|
|
1299
|
+
const searchResult = await client.search({
|
|
1300
|
+
query: input.query,
|
|
1301
|
+
mode_lock: input.mode_lock,
|
|
1302
|
+
scope_lock: 'COMPANY',
|
|
1303
|
+
tickers: input.tickers,
|
|
1304
|
+
top_k: input.top_k_per_ticker * input.tickers.length,
|
|
1305
|
+
doc_types: input.doc_types,
|
|
1306
|
+
});
|
|
1307
|
+
const allResults = Array.isArray(searchResult.narrative_results)
|
|
1308
|
+
? searchResult.narrative_results
|
|
1309
|
+
: [];
|
|
1310
|
+
const xbrlMetrics = Array.isArray(searchResult.xbrl_metrics)
|
|
1311
|
+
? searchResult.xbrl_metrics
|
|
1312
|
+
: [];
|
|
1313
|
+
// Group results by ticker
|
|
1314
|
+
const perTicker = {};
|
|
1315
|
+
for (const ticker of input.tickers) {
|
|
1316
|
+
perTicker[ticker] = allResults
|
|
1317
|
+
.filter((r) => r.ticker === ticker)
|
|
1318
|
+
.slice(0, input.top_k_per_ticker)
|
|
1319
|
+
.map((r, i) => ({
|
|
1320
|
+
index: i + 1,
|
|
1321
|
+
chunk_id: r.chunk_id,
|
|
1322
|
+
company_name: r.company_name,
|
|
1323
|
+
form_type: r.form_type,
|
|
1324
|
+
filing_date: r.filing_date,
|
|
1325
|
+
item_code: r.item_code,
|
|
1326
|
+
section_label: mapItemCodeToLabel(r.item_code),
|
|
1327
|
+
score: r.similarity_score,
|
|
1328
|
+
chunk_text: r.chunk_text,
|
|
1329
|
+
truncated: r.chunk_text_truncated || false,
|
|
1330
|
+
accession_number: r.accession_number,
|
|
1331
|
+
sec_url: r.sec_url,
|
|
1332
|
+
}));
|
|
1333
|
+
}
|
|
1334
|
+
const hasMetrics = xbrlMetrics.length > 0;
|
|
1335
|
+
const metricsColumnMeta = hasMetrics
|
|
1336
|
+
? buildColumnMeta(searchResult.xbrl_output_schema?.columns || [], xbrlMetrics)
|
|
1337
|
+
: null;
|
|
1338
|
+
const response = {
|
|
1339
|
+
comparison: {
|
|
1340
|
+
tickers: input.tickers,
|
|
1341
|
+
query: input.query,
|
|
1342
|
+
per_ticker: perTicker,
|
|
1343
|
+
metrics: hasMetrics ? {
|
|
1344
|
+
rows: xbrlMetrics,
|
|
1345
|
+
columns: metricsColumnMeta,
|
|
1346
|
+
visualization_hint: deriveMetricsVizHint(xbrlMetrics, { columns: metricsColumnMeta }),
|
|
1347
|
+
} : null,
|
|
1348
|
+
},
|
|
1349
|
+
layout_hint: {
|
|
1350
|
+
suggested: hasMetrics ? 'comparison_dashboard' : 'comparison_grid',
|
|
1351
|
+
reason: 'multi_ticker_compare',
|
|
1352
|
+
panels: hasMetrics
|
|
1353
|
+
? ['metrics_chart', 'narrative_side_by_side']
|
|
1354
|
+
: ['narrative_side_by_side'],
|
|
1355
|
+
tone: 'comparative',
|
|
1356
|
+
},
|
|
1357
|
+
search_context: buildSearchContext(searchResult, input.query),
|
|
1358
|
+
};
|
|
1359
|
+
const tickerCounts = input.tickers
|
|
1360
|
+
.map(t => `${t}: ${perTicker[t]?.length || 0}`)
|
|
1361
|
+
.join(', ');
|
|
1362
|
+
log('info', `Compare returned results: ${tickerCounts}, ${xbrlMetrics.length} metrics`);
|
|
1363
|
+
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
|
|
1364
|
+
}
|
|
1365
|
+
catch (error) {
|
|
1366
|
+
log('error', 'Compare failed', { error: error instanceof Error ? error.message : error });
|
|
1367
|
+
return {
|
|
1368
|
+
content: [{ type: 'text', text: `Compare failed: ${error instanceof Error ? error.message : 'Unknown error'}` }],
|
|
1369
|
+
isError: true,
|
|
1370
|
+
};
|
|
1371
|
+
}
|
|
1372
|
+
});
|
|
1373
|
+
// ─── Tool 8: Weekly Brief ───────────────────────────────────────────────────
|
|
1374
|
+
server.registerTool('etsquare_weekly_brief', {
|
|
1375
|
+
title: 'Weekly SEC Filing Intelligence Brief',
|
|
1376
|
+
description: 'Get the latest weekly SEC filing intelligence brief. ' +
|
|
1377
|
+
'Returns notable 8-K events, edge signals (unusual filings), and sector heatmap. ' +
|
|
1378
|
+
'Updated weekly. Great for market monitoring and identifying emerging risks.\n\n' +
|
|
1379
|
+
'Default response_format is "structured" (full JSON). Use "text" for readable summary.',
|
|
1380
|
+
inputSchema: {
|
|
1381
|
+
sections: z.array(z.enum(['notable', 'edge_signals', 'heatmap', 'methodology'])).optional()
|
|
1382
|
+
.describe('Filter to specific sections. Returns all if omitted.'),
|
|
1383
|
+
response_format: z.enum(['text', 'structured']).default('structured')
|
|
1384
|
+
.describe('"structured" returns full JSON (default). "text" returns a readable summary.'),
|
|
1385
|
+
},
|
|
1386
|
+
}, async (input) => {
|
|
1387
|
+
if (containsGuardrailBypass(input))
|
|
1388
|
+
return guardrailViolationResponse();
|
|
1389
|
+
try {
|
|
1390
|
+
log('debug', 'Fetching weekly brief', { sections: input.sections });
|
|
1391
|
+
const brief = await client.weeklyBrief();
|
|
1392
|
+
if (!brief || (!brief.notable && !brief.edge_signals)) {
|
|
1393
|
+
return { content: [{ type: 'text', text: 'No weekly brief available for the current period.' }] };
|
|
1394
|
+
}
|
|
1395
|
+
// Client-side section filtering
|
|
1396
|
+
let output = brief;
|
|
1397
|
+
if (input.sections && input.sections.length > 0) {
|
|
1398
|
+
output = {
|
|
1399
|
+
week_start: brief.week_start,
|
|
1400
|
+
week_end: brief.week_end,
|
|
1401
|
+
generated_at: brief.generated_at,
|
|
1402
|
+
};
|
|
1403
|
+
if (input.sections.includes('notable'))
|
|
1404
|
+
output.notable = brief.notable;
|
|
1405
|
+
if (input.sections.includes('edge_signals'))
|
|
1406
|
+
output.edge_signals = brief.edge_signals;
|
|
1407
|
+
if (input.sections.includes('heatmap'))
|
|
1408
|
+
output.heatmap = brief.heatmap;
|
|
1409
|
+
if (input.sections.includes('methodology'))
|
|
1410
|
+
output.methodology = brief.methodology;
|
|
1411
|
+
}
|
|
1412
|
+
// ── Structured response path ──
|
|
1413
|
+
if (input.response_format === 'structured') {
|
|
1414
|
+
log('info', `Weekly brief returned: ${(brief.notable || []).length} notable, ${(brief.edge_signals || []).length} edge signals`);
|
|
1415
|
+
return { content: [{ type: 'text', text: JSON.stringify(output, null, 2) }] };
|
|
1416
|
+
}
|
|
1417
|
+
// ── Text response path ──
|
|
1418
|
+
const lines = [];
|
|
1419
|
+
lines.push(`SEC Weekly Brief: ${brief.week_start || 'N/A'} to ${brief.week_end || 'N/A'}`);
|
|
1420
|
+
lines.push(`Generated: ${brief.generated_at || 'N/A'}`);
|
|
1421
|
+
lines.push('');
|
|
1422
|
+
if (output.notable?.length) {
|
|
1423
|
+
lines.push(`Notable Filings (${output.notable.length}):`);
|
|
1424
|
+
for (const n of output.notable) {
|
|
1425
|
+
lines.push(` ${n.ticker || 'N/A'} — ${n.company_name || 'Unknown'} (${n.form_type || ''} Item ${n.item_code || ''})`);
|
|
1426
|
+
lines.push(` Filed: ${n.filed || 'N/A'} | Score: ${n.score_notable || 'N/A'}`);
|
|
1427
|
+
if (n.summary)
|
|
1428
|
+
lines.push(` ${n.summary}`);
|
|
1429
|
+
if (n.sec_url)
|
|
1430
|
+
lines.push(` ${n.sec_url}`);
|
|
1431
|
+
lines.push('');
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
if (output.edge_signals?.length) {
|
|
1435
|
+
lines.push(`Edge Signals (${output.edge_signals.length}):`);
|
|
1436
|
+
for (const e of output.edge_signals) {
|
|
1437
|
+
lines.push(` ${e.ticker || 'N/A'} — ${e.company_name || 'Unknown'}`);
|
|
1438
|
+
if (e.signal_reason)
|
|
1439
|
+
lines.push(` ${e.signal_reason}`);
|
|
1440
|
+
if (e.citation)
|
|
1441
|
+
lines.push(` Verbatim: "${e.citation}"`);
|
|
1442
|
+
lines.push('');
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
if (output.heatmap?.length) {
|
|
1446
|
+
lines.push(`Sector Heatmap (${output.heatmap.length} sectors):`);
|
|
1447
|
+
for (const h of output.heatmap) {
|
|
1448
|
+
lines.push(` ${h.sector || h.sic_description || 'N/A'}: ${h.filing_count || h.count || 0} filings`);
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
log('info', `Weekly brief returned: ${(brief.notable || []).length} notable, ${(brief.edge_signals || []).length} edge signals`);
|
|
1452
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
1453
|
+
}
|
|
1454
|
+
catch (error) {
|
|
1455
|
+
log('error', 'Weekly brief fetch failed', { error: error instanceof Error ? error.message : error });
|
|
1456
|
+
return {
|
|
1457
|
+
content: [{ type: 'text', text: `Weekly brief unavailable: ${error instanceof Error ? error.message : 'Unknown error'}` }],
|
|
1458
|
+
isError: true,
|
|
1459
|
+
};
|
|
1460
|
+
}
|
|
1461
|
+
});
|
|
1462
|
+
// ─── Start Server ───────────────────────────────────────────────────────────
|
|
358
1463
|
const transport = new StdioServerTransport();
|
|
359
1464
|
await server.connect(transport);
|
|
360
|
-
log('info', 'ETSquare MCP Server ready and listening on stdio');
|
|
1465
|
+
log('info', 'ETSquare MCP Server v0.3.0 ready and listening on stdio (8 tools)');
|