@etsquare/mcp-server-sec 0.2.0 → 0.2.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 +3 -1
- package/dist/etsquare-client.js +21 -0
- package/dist/index.js +144 -7
- package/dist/types.d.ts +25 -0
- package/dist/types.js +9 -0
- package/package.json +2 -2
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* ETSquare SEC Intelligence API client.
|
|
3
3
|
* Wraps /api/v1/* endpoints with X-API-Key authentication.
|
|
4
4
|
*/
|
|
5
|
-
import type { SearchInput, LookupCompanyInput, ExecuteMetricsInput, DiscoverMetricsInput } from './types.js';
|
|
5
|
+
import type { SearchInput, LookupCompanyInput, ExecuteMetricsInput, DiscoverMetricsInput, GetChunkInput, GetChunkContextInput } from './types.js';
|
|
6
6
|
export interface ETSquareClientOptions {
|
|
7
7
|
baseUrl: string;
|
|
8
8
|
apiKey: string;
|
|
@@ -17,4 +17,6 @@ export declare class ETSquareClient {
|
|
|
17
17
|
lookupCompany(input: LookupCompanyInput): Promise<Record<string, unknown>>;
|
|
18
18
|
executeMetrics(input: ExecuteMetricsInput): Promise<Record<string, unknown>>;
|
|
19
19
|
discoverMetrics(input: DiscoverMetricsInput): Promise<Record<string, unknown>>;
|
|
20
|
+
getChunk(input: GetChunkInput): Promise<Record<string, unknown>>;
|
|
21
|
+
getChunkContext(input: GetChunkContextInput): Promise<Record<string, unknown>>;
|
|
20
22
|
}
|
package/dist/etsquare-client.js
CHANGED
|
@@ -7,6 +7,7 @@ export class ETSquareClient {
|
|
|
7
7
|
return {
|
|
8
8
|
'Content-Type': 'application/json',
|
|
9
9
|
'X-API-Key': this.apiKey,
|
|
10
|
+
'X-Entry-Point': 'mcp',
|
|
10
11
|
};
|
|
11
12
|
}
|
|
12
13
|
async handleResponse(res) {
|
|
@@ -90,4 +91,24 @@ export class ETSquareClient {
|
|
|
90
91
|
});
|
|
91
92
|
return this.handleResponse(res);
|
|
92
93
|
}
|
|
94
|
+
async getChunk(input) {
|
|
95
|
+
const params = new URLSearchParams({ execution_id: input.execution_id });
|
|
96
|
+
const res = await fetch(`${this.baseUrl}/api/v1/chunk/${input.chunk_id}?${params}`, {
|
|
97
|
+
headers: this.headers,
|
|
98
|
+
});
|
|
99
|
+
return this.handleResponse(res);
|
|
100
|
+
}
|
|
101
|
+
async getChunkContext(input) {
|
|
102
|
+
const params = new URLSearchParams();
|
|
103
|
+
if (input.neighbor_span !== undefined)
|
|
104
|
+
params.set('neighbor_span', String(input.neighbor_span));
|
|
105
|
+
if (input.window !== undefined)
|
|
106
|
+
params.set('window', String(input.window));
|
|
107
|
+
const query = params.toString();
|
|
108
|
+
const suffix = query ? `?${query}` : '';
|
|
109
|
+
const res = await fetch(`${this.baseUrl}/api/v1/chunks/${input.chunk_id}/context${suffix}`, {
|
|
110
|
+
headers: this.headers,
|
|
111
|
+
});
|
|
112
|
+
return this.handleResponse(res);
|
|
113
|
+
}
|
|
93
114
|
}
|
package/dist/index.js
CHANGED
|
@@ -41,7 +41,7 @@ function guardrailViolationResponse() {
|
|
|
41
41
|
};
|
|
42
42
|
}
|
|
43
43
|
// Environment configuration
|
|
44
|
-
const baseUrl = process.env.ETSQUARE_BASE_URL || 'https://
|
|
44
|
+
const baseUrl = process.env.ETSQUARE_BASE_URL || 'https://sec-intelligence-api-4mk2on5fga-uc.a.run.app';
|
|
45
45
|
const apiKey = process.env.ETSQUARE_API_KEY;
|
|
46
46
|
const DEBUG = process.env.DEBUG === 'true';
|
|
47
47
|
function log(level, message, data) {
|
|
@@ -62,7 +62,7 @@ log('info', `ETSquare MCP Server starting with backend: ${baseUrl}`);
|
|
|
62
62
|
// Initialize MCP Server
|
|
63
63
|
const server = new McpServer({
|
|
64
64
|
name: 'etsquare-mcp-sec',
|
|
65
|
-
version: '0.2.
|
|
65
|
+
version: '0.2.1',
|
|
66
66
|
});
|
|
67
67
|
// Tool 1: Company Lookup
|
|
68
68
|
server.registerTool('etsquare_lookup_company', {
|
|
@@ -110,14 +110,14 @@ server.registerTool('etsquare_lookup_company', {
|
|
|
110
110
|
// Tool 2: SEC Filing Search
|
|
111
111
|
server.registerTool('etsquare_search', {
|
|
112
112
|
title: 'Search SEC Filings',
|
|
113
|
-
description: 'Search
|
|
113
|
+
description: 'Search 3.4M+ SEC filing sections (10-K, 10-Q, 8-K) with hybrid retrieval. ' +
|
|
114
114
|
'Returns verbatim filing text with citations — best for qualitative research.\n\n' +
|
|
115
115
|
'Execution contract (required):\n' +
|
|
116
116
|
'- mode_lock: NARRATIVE (recommended) | HYBRID\n' +
|
|
117
117
|
'- scope_lock: COMPANY (specific tickers) | INDUSTRY (SIC sector) | MACRO (cross-market)\n\n' +
|
|
118
118
|
'For structured financial data (revenue, margins, ratios), use etsquare_discover_metrics + etsquare_execute_metrics instead.\n\n' +
|
|
119
119
|
'Use tickers to scope to specific companies (resolve names first with etsquare_lookup_company). ' +
|
|
120
|
-
'Results include
|
|
120
|
+
'Results include filing text plus safe citation metadata such as chunk_id, accession number, and SEC URL for follow-up retrieval.',
|
|
121
121
|
inputSchema: {
|
|
122
122
|
query: z.string().min(3).describe('What to search for in SEC filings (e.g., "customer concentration risk", "Show NVDA revenue trend")'),
|
|
123
123
|
mode_lock: z.enum(['NARRATIVE', 'HYBRID']).describe('NARRATIVE for text search, HYBRID for text + any available metrics'),
|
|
@@ -147,6 +147,12 @@ server.registerTool('etsquare_search', {
|
|
|
147
147
|
? result.xbrl_metrics
|
|
148
148
|
: [];
|
|
149
149
|
const resultCount = searchResults.length;
|
|
150
|
+
const executionId = typeof result.execution_id === 'string'
|
|
151
|
+
? result.execution_id
|
|
152
|
+
: null;
|
|
153
|
+
const researchRunId = typeof result.research_run_id === 'string'
|
|
154
|
+
? result.research_run_id
|
|
155
|
+
: null;
|
|
150
156
|
if (resultCount === 0 && xbrlMetrics.length === 0) {
|
|
151
157
|
let text = `No results found for: "${input.query}"`;
|
|
152
158
|
text += '\nTry broadening your query or removing filters.';
|
|
@@ -167,6 +173,16 @@ server.registerTool('etsquare_search', {
|
|
|
167
173
|
if (r.fiscal_year && r.fiscal_period) {
|
|
168
174
|
lines[1] += ` | ${r.fiscal_period} FY${r.fiscal_year}`;
|
|
169
175
|
}
|
|
176
|
+
const metaParts = [];
|
|
177
|
+
if (r.chunk_id)
|
|
178
|
+
metaParts.push(`Chunk ID: ${r.chunk_id}`);
|
|
179
|
+
if (r.accession_number)
|
|
180
|
+
metaParts.push(`Accession: ${r.accession_number}`);
|
|
181
|
+
if (r.chunk_text_truncated)
|
|
182
|
+
metaParts.push('Truncated: yes');
|
|
183
|
+
if (metaParts.length > 0) {
|
|
184
|
+
lines.push(` ${metaParts.join(' | ')}`);
|
|
185
|
+
}
|
|
170
186
|
if (r.chunk_text) {
|
|
171
187
|
const snippet = r.chunk_text.substring(0, 600);
|
|
172
188
|
lines.push(` ${snippet}${r.chunk_text.length > 600 ? '...' : ''}`);
|
|
@@ -178,7 +194,13 @@ server.registerTool('etsquare_search', {
|
|
|
178
194
|
return lines.join('\n');
|
|
179
195
|
})
|
|
180
196
|
.join('\n\n');
|
|
181
|
-
|
|
197
|
+
const headerParts = [`SEC Filing Citations (${resultCount}):`];
|
|
198
|
+
if (executionId)
|
|
199
|
+
headerParts.push(`Execution ID: ${executionId}`);
|
|
200
|
+
if (researchRunId)
|
|
201
|
+
headerParts.push(`Research Run ID: ${researchRunId}`);
|
|
202
|
+
const header = headerParts.join('\n');
|
|
203
|
+
sections.push(`${header}\n\n${formatted}`);
|
|
182
204
|
}
|
|
183
205
|
// Format XBRL metrics if present (handles both wide-format and long-format rows)
|
|
184
206
|
if (xbrlMetrics.length > 0) {
|
|
@@ -283,10 +305,13 @@ server.registerTool('etsquare_discover_metrics', {
|
|
|
283
305
|
title: 'Discover Financial Metrics Templates',
|
|
284
306
|
description: 'Find available XBRL metrics templates by business question. ' +
|
|
285
307
|
'Returns template IDs and metadata — use the template_id with etsquare_execute_metrics.\n\n' +
|
|
308
|
+
'Use this only for structured metrics such as revenue, gross margin, operating margin, EPS, debt, liquidity, or cash flow.\n\n' +
|
|
309
|
+
'Do not use this for narrative KPI questions like same-store sales, comparable sales, traffic, guest count, average check, or AUV. ' +
|
|
310
|
+
'For those, use etsquare_search in NARRATIVE mode instead.\n\n' +
|
|
286
311
|
'Example: "revenue trend by quarter" → returns matching templates with their required bind_params.\n\n' +
|
|
287
312
|
'Workflow: discover_metrics → pick template_id → execute_metrics with bind_params.',
|
|
288
313
|
inputSchema: {
|
|
289
|
-
question: z.string().min(3).describe('
|
|
314
|
+
question: z.string().min(3).describe('Structured metrics question (e.g., "revenue trend by quarter", "gross margin trend", "EPS trend")'),
|
|
290
315
|
scenario: z.enum(['snapshot', 'trends', 'peer_benchmark']).optional().describe('Filter by scenario type'),
|
|
291
316
|
metric_family: z.string().optional().describe('Filter by metric family: REVENUE, EARNINGS, PROFITABILITY_MARGIN, LEVERAGE_DEBT, FREE_CASH_FLOW, LIQUIDITY'),
|
|
292
317
|
max_results: z.number().min(1).max(10).default(5).optional().describe('Max templates to return'),
|
|
@@ -300,11 +325,27 @@ server.registerTool('etsquare_discover_metrics', {
|
|
|
300
325
|
const templates = Array.isArray(result.templates)
|
|
301
326
|
? result.templates
|
|
302
327
|
: [];
|
|
328
|
+
const guidance = result.guidance && typeof result.guidance === 'object'
|
|
329
|
+
? result.guidance
|
|
330
|
+
: null;
|
|
303
331
|
if (templates.length === 0) {
|
|
332
|
+
let text = `No structured metrics templates found for: "${input.question}"`;
|
|
333
|
+
if (guidance?.message) {
|
|
334
|
+
text += `\n${guidance.message}`;
|
|
335
|
+
}
|
|
336
|
+
if (guidance?.suggested_tool === 'etsquare_search') {
|
|
337
|
+
text += '\nRecommended next step: use etsquare_search with NARRATIVE mode.';
|
|
338
|
+
}
|
|
339
|
+
const examples = Array.isArray(guidance?.valid_metrics_examples)
|
|
340
|
+
? guidance.valid_metrics_examples
|
|
341
|
+
: [];
|
|
342
|
+
if (examples.length > 0) {
|
|
343
|
+
text += `\nValid metrics examples: ${examples.join('; ')}`;
|
|
344
|
+
}
|
|
304
345
|
return {
|
|
305
346
|
content: [{
|
|
306
347
|
type: 'text',
|
|
307
|
-
text
|
|
348
|
+
text,
|
|
308
349
|
}],
|
|
309
350
|
};
|
|
310
351
|
}
|
|
@@ -354,6 +395,102 @@ server.registerTool('etsquare_discover_metrics', {
|
|
|
354
395
|
};
|
|
355
396
|
}
|
|
356
397
|
});
|
|
398
|
+
// Tool 5: Expand a single chunk to full text
|
|
399
|
+
server.registerTool('etsquare_get_chunk', {
|
|
400
|
+
title: 'Get Full Chunk Text',
|
|
401
|
+
description: 'Fetch the full text for a specific search result chunk. ' +
|
|
402
|
+
'Use this when etsquare_search returns a truncated snippet and you need the complete chunk text. ' +
|
|
403
|
+
'Requires both chunk_id and execution_id from a prior search result.',
|
|
404
|
+
inputSchema: {
|
|
405
|
+
chunk_id: z.string().length(32).describe('Chunk ID from etsquare_search output'),
|
|
406
|
+
execution_id: z.string().min(32).describe('Execution ID from the etsquare_search response header'),
|
|
407
|
+
},
|
|
408
|
+
}, async (input) => {
|
|
409
|
+
if (containsGuardrailBypass(input))
|
|
410
|
+
return guardrailViolationResponse();
|
|
411
|
+
try {
|
|
412
|
+
log('debug', 'Fetching full chunk text', { chunk_id: input.chunk_id });
|
|
413
|
+
const result = await client.getChunk(input);
|
|
414
|
+
const chunkText = typeof result.chunk_text === 'string'
|
|
415
|
+
? result.chunk_text
|
|
416
|
+
: '';
|
|
417
|
+
const chunkLength = result.chunk_text_length;
|
|
418
|
+
return {
|
|
419
|
+
content: [{
|
|
420
|
+
type: 'text',
|
|
421
|
+
text: `Chunk ${input.chunk_id}${typeof chunkLength === 'number' ? ` (${chunkLength} chars)` : ''}:\n\n${chunkText}`,
|
|
422
|
+
}],
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
catch (error) {
|
|
426
|
+
log('error', 'Chunk fetch failed', { error: error instanceof Error ? error.message : error });
|
|
427
|
+
return {
|
|
428
|
+
content: [{ type: 'text', text: `Chunk fetch failed: ${error instanceof Error ? error.message : 'Unknown error'}` }],
|
|
429
|
+
isError: true,
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
// Tool 6: Expand chunk with surrounding context
|
|
434
|
+
server.registerTool('etsquare_get_chunk_context', {
|
|
435
|
+
title: 'Get Chunk Context',
|
|
436
|
+
description: 'Fetch the highlighted chunk plus surrounding context from the same filing section. ' +
|
|
437
|
+
'Use this when you need the paragraphs immediately before and after a search hit.',
|
|
438
|
+
inputSchema: {
|
|
439
|
+
chunk_id: z.string().length(32).describe('Chunk ID from etsquare_search output'),
|
|
440
|
+
neighbor_span: z.number().min(0).max(5).default(1).optional().describe('How many adjacent chunks to include before and after'),
|
|
441
|
+
window: z.number().min(200).max(4000).default(900).optional().describe('Approximate character budget for surrounding context'),
|
|
442
|
+
},
|
|
443
|
+
}, async (input) => {
|
|
444
|
+
if (containsGuardrailBypass(input))
|
|
445
|
+
return guardrailViolationResponse();
|
|
446
|
+
try {
|
|
447
|
+
log('debug', 'Fetching chunk context', { chunk_id: input.chunk_id });
|
|
448
|
+
const result = await client.getChunkContext(input);
|
|
449
|
+
const filing = result.filing || {};
|
|
450
|
+
const context = result.context || {};
|
|
451
|
+
const navigation = result.navigation || {};
|
|
452
|
+
const lines = [
|
|
453
|
+
`Chunk ID: ${result.chunk_id || input.chunk_id}`,
|
|
454
|
+
];
|
|
455
|
+
if (result.sec_url)
|
|
456
|
+
lines.push(`SEC URL: ${result.sec_url}`);
|
|
457
|
+
if (filing.ticker || filing.company) {
|
|
458
|
+
lines.push(`Filing: ${filing.ticker || 'N/A'} - ${filing.company || 'Unknown'} (${filing.form_type || filing.doc_subtype || 'N/A'})`);
|
|
459
|
+
}
|
|
460
|
+
if (filing.item_code || filing.accession_number) {
|
|
461
|
+
lines.push(`Section: ${filing.item_code || 'N/A'}${filing.accession_number ? ` | Accession: ${filing.accession_number}` : ''}`);
|
|
462
|
+
}
|
|
463
|
+
if (typeof navigation.chunk_position === 'number' || typeof navigation.total_chunks_in_item === 'number') {
|
|
464
|
+
lines.push(`Position: ${navigation.chunk_position ?? 'N/A'} of ${navigation.total_chunks_in_item ?? 'N/A'}`);
|
|
465
|
+
}
|
|
466
|
+
lines.push('');
|
|
467
|
+
if (context.before) {
|
|
468
|
+
lines.push('Before:');
|
|
469
|
+
lines.push(String(context.before));
|
|
470
|
+
lines.push('');
|
|
471
|
+
}
|
|
472
|
+
lines.push('Highlight:');
|
|
473
|
+
lines.push(String(context.highlight || ''));
|
|
474
|
+
if (context.after) {
|
|
475
|
+
lines.push('');
|
|
476
|
+
lines.push('After:');
|
|
477
|
+
lines.push(String(context.after));
|
|
478
|
+
}
|
|
479
|
+
return {
|
|
480
|
+
content: [{
|
|
481
|
+
type: 'text',
|
|
482
|
+
text: lines.join('\n'),
|
|
483
|
+
}],
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
catch (error) {
|
|
487
|
+
log('error', 'Chunk context fetch failed', { error: error instanceof Error ? error.message : error });
|
|
488
|
+
return {
|
|
489
|
+
content: [{ type: 'text', text: `Chunk context fetch failed: ${error instanceof Error ? error.message : 'Unknown error'}` }],
|
|
490
|
+
isError: true,
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
});
|
|
357
494
|
// Start server
|
|
358
495
|
const transport = new StdioServerTransport();
|
|
359
496
|
await server.connect(transport);
|
package/dist/types.d.ts
CHANGED
|
@@ -63,7 +63,32 @@ export declare const discoverMetricsSchema: z.ZodObject<{
|
|
|
63
63
|
metric_family?: string | undefined;
|
|
64
64
|
max_results?: number | undefined;
|
|
65
65
|
}>;
|
|
66
|
+
export declare const getChunkSchema: z.ZodObject<{
|
|
67
|
+
chunk_id: z.ZodString;
|
|
68
|
+
execution_id: z.ZodString;
|
|
69
|
+
}, "strip", z.ZodTypeAny, {
|
|
70
|
+
chunk_id: string;
|
|
71
|
+
execution_id: string;
|
|
72
|
+
}, {
|
|
73
|
+
chunk_id: string;
|
|
74
|
+
execution_id: string;
|
|
75
|
+
}>;
|
|
76
|
+
export declare const getChunkContextSchema: z.ZodObject<{
|
|
77
|
+
chunk_id: z.ZodString;
|
|
78
|
+
neighbor_span: z.ZodOptional<z.ZodNumber>;
|
|
79
|
+
window: z.ZodOptional<z.ZodNumber>;
|
|
80
|
+
}, "strip", z.ZodTypeAny, {
|
|
81
|
+
chunk_id: string;
|
|
82
|
+
neighbor_span?: number | undefined;
|
|
83
|
+
window?: number | undefined;
|
|
84
|
+
}, {
|
|
85
|
+
chunk_id: string;
|
|
86
|
+
neighbor_span?: number | undefined;
|
|
87
|
+
window?: number | undefined;
|
|
88
|
+
}>;
|
|
66
89
|
export type SearchInput = z.infer<typeof searchSchema>;
|
|
67
90
|
export type LookupCompanyInput = z.infer<typeof lookupCompanySchema>;
|
|
68
91
|
export type ExecuteMetricsInput = z.infer<typeof executeMetricsSchema>;
|
|
69
92
|
export type DiscoverMetricsInput = z.infer<typeof discoverMetricsSchema>;
|
|
93
|
+
export type GetChunkInput = z.infer<typeof getChunkSchema>;
|
|
94
|
+
export type GetChunkContextInput = z.infer<typeof getChunkContextSchema>;
|
package/dist/types.js
CHANGED
|
@@ -23,3 +23,12 @@ export const discoverMetricsSchema = z.object({
|
|
|
23
23
|
metric_family: z.string().optional(),
|
|
24
24
|
max_results: z.number().min(1).max(10).optional(),
|
|
25
25
|
});
|
|
26
|
+
export const getChunkSchema = z.object({
|
|
27
|
+
chunk_id: z.string().length(32, 'chunk_id must be a 32-character hex string'),
|
|
28
|
+
execution_id: z.string().min(32, 'execution_id is required'),
|
|
29
|
+
});
|
|
30
|
+
export const getChunkContextSchema = z.object({
|
|
31
|
+
chunk_id: z.string().length(32, 'chunk_id must be a 32-character hex string'),
|
|
32
|
+
neighbor_span: z.number().min(0).max(5).optional(),
|
|
33
|
+
window: z.number().min(200).max(4000).optional(),
|
|
34
|
+
});
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@etsquare/mcp-server-sec",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "MCP server for Claude Desktop: search
|
|
5
|
+
"description": "MCP server for Claude Desktop: search 3.4M+ SEC filing sections with hybrid retrieval, XBRL templates, and company lookup.",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
8
8
|
"bin": {
|