@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.
@@ -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
  }
@@ -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://www.etsquare.ai';
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.0',
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 1.2M+ SEC filing sections (10-K, 10-Q, 8-K) with hybrid retrieval. ' +
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 verbatim filing text, similarity scores, and SEC source URLs.',
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
- sections.push(`SEC Filing Citations (${resultCount}):\n\n${formatted}`);
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('Business question (e.g., "revenue trend by quarter", "profit margins comparison")'),
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: `No metrics templates found for: "${input.question}"\nTry a different question or remove filters.`,
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.0",
3
+ "version": "0.2.1",
4
4
  "type": "module",
5
- "description": "MCP server for Claude Desktop: search 1.2M+ SEC filing sections with hybrid retrieval, XBRL templates, and company lookup.",
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": {