JC321 commited on
Commit
ce94df5
·
verified ·
1 Parent(s): 70b4bcc

Upload 3 files

Browse files
Files changed (3) hide show
  1. Dockerfile +6 -4
  2. edgar_client.py +176 -54
  3. mcp_server_sse.py +166 -136
Dockerfile CHANGED
@@ -22,10 +22,12 @@ EXPOSE 7860
22
  # Set environment variables for production
23
  ENV PYTHONUNBUFFERED=1
24
  ENV PYTHONDONTWRITEBYTECODE=1
 
 
25
 
26
- # Health check for container monitoring
27
- HEALTHCHECK --interval=60s --timeout=15s --start-period=60s --retries=5 \
28
  CMD curl -f http://localhost:7860/health || exit 1
29
 
30
- # Run MCP Server with SSE transport - optimized settings for CPU UPGRADE
31
- CMD ["uvicorn", "mcp_server_sse:app", "--host", "0.0.0.0", "--port", "7860", "--timeout-keep-alive", "120", "--limit-concurrency", "50", "--backlog", "100", "--log-level", "info", "--workers", "1"]
 
22
  # Set environment variables for production
23
  ENV PYTHONUNBUFFERED=1
24
  ENV PYTHONDONTWRITEBYTECODE=1
25
+ ENV REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt
26
+ ENV CURL_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt
27
 
28
+ # Health check for container monitoring - relaxed for stability
29
+ HEALTHCHECK --interval=90s --timeout=20s --start-period=90s --retries=5 \
30
  CMD curl -f http://localhost:7860/health || exit 1
31
 
32
+ # Run MCP Server with SSE transport - optimized for stability
33
+ CMD ["uvicorn", "mcp_server_sse:app", "--host", "0.0.0.0", "--port", "7860", "--timeout-keep-alive", "180", "--timeout-graceful-shutdown", "30", "--limit-concurrency", "30", "--backlog", "50", "--log-level", "info", "--workers", "1", "--no-access-log"]
edgar_client.py CHANGED
@@ -1,6 +1,8 @@
1
  """EDGAR API Client Module"""
2
 
3
  import requests
 
 
4
  try:
5
  from sec_edgar_api.EdgarClient import EdgarClient
6
  except ImportError:
@@ -8,6 +10,7 @@ except ImportError:
8
  import json
9
  import time
10
  from functools import wraps
 
11
 
12
 
13
  class EdgarDataClient:
@@ -18,6 +21,30 @@ class EdgarDataClient:
18
  self.min_request_interval = 0.11 # SEC allows 10 requests/second, use 0.11s to be safe
19
  self.request_timeout = 30 # 30 seconds timeout for HTTP requests
20
  self.max_retries = 3 # Maximum retry attempts
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
 
22
  if EdgarClient:
23
  self.edgar = EdgarClient(user_agent=user_agent)
@@ -25,17 +52,36 @@ class EdgarDataClient:
25
  self.edgar = None
26
 
27
  def _rate_limit(self):
28
- """Rate limiting to comply with SEC API limits (10 requests/second)"""
29
- current_time = time.time()
30
- time_since_last_request = current_time - self.last_request_time
31
-
32
- if time_since_last_request < self.min_request_interval:
33
- sleep_time = self.min_request_interval - time_since_last_request
34
- time.sleep(sleep_time)
35
-
36
- self.last_request_time = time.time()
 
37
 
38
- def _make_request_with_retry(self, url, headers=None):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  """Make HTTP request with retry logic and timeout"""
40
  if headers is None:
41
  headers = {"User-Agent": self.user_agent}
@@ -43,7 +89,10 @@ class EdgarDataClient:
43
  for attempt in range(self.max_retries):
44
  try:
45
  self._rate_limit()
46
- response = requests.get(url, headers=headers, timeout=self.request_timeout)
 
 
 
47
  response.raise_for_status()
48
  return response
49
  except requests.exceptions.Timeout:
@@ -117,7 +166,7 @@ class EdgarDataClient:
117
 
118
  def get_company_info(self, cik):
119
  """
120
- Get basic company information
121
 
122
  Args:
123
  cik (str): Company CIK code
@@ -128,25 +177,51 @@ class EdgarDataClient:
128
  if not self.edgar:
129
  print("sec_edgar_api library not installed")
130
  return None
 
 
 
 
 
 
131
 
132
  try:
133
- # Get company submissions
134
- submissions = self.edgar.get_submissions(cik=cik)
 
 
 
135
 
136
- return {
 
 
 
 
 
 
 
 
 
 
137
  "cik": cik,
138
  "name": submissions.get("name", ""),
139
  "tickers": submissions.get("tickers", []),
140
  "sic": submissions.get("sic", ""),
141
  "sic_description": submissions.get("sicDescription", "")
142
  }
 
 
 
 
 
 
 
143
  except Exception as e:
144
  print(f"Error getting company info: {e}")
145
  return None
146
 
147
  def get_company_filings(self, cik, form_types=None):
148
  """
149
- Get all company filing documents
150
 
151
  Args:
152
  cik (str): Company CIK code
@@ -158,50 +233,72 @@ class EdgarDataClient:
158
  if not self.edgar:
159
  print("sec_edgar_api library not installed")
160
  return []
161
-
162
- try:
163
- # Get company submissions
164
- submissions = self.edgar.get_submissions(cik=cik)
165
-
166
- # Extract filing information
167
- filings = []
168
- recent = submissions.get("filings", {}).get("recent", {})
169
-
170
- # Get data from each field
171
- form_types_list = recent.get("form", [])
172
- filing_dates = recent.get("filingDate", [])
173
- accession_numbers = recent.get("accessionNumber", [])
174
- primary_documents = recent.get("primaryDocument", [])
175
-
176
- # Iterate through all filings
177
- for i in range(len(form_types_list)):
178
- form_type = form_types_list[i]
179
 
180
- # Filter by form type if specified
181
- if form_types and form_type not in form_types:
182
- continue
183
 
184
- filing_date = filing_dates[i] if i < len(filing_dates) else ""
185
- accession_number = accession_numbers[i] if i < len(accession_numbers) else ""
186
- primary_document = primary_documents[i] if i < len(primary_documents) else ""
 
 
 
 
 
187
 
188
- filing = {
189
- "form_type": form_type,
190
- "filing_date": filing_date,
191
- "accession_number": accession_number,
192
- "primary_document": primary_document
193
- }
194
 
195
- filings.append(filing)
196
-
197
- return filings
198
- except Exception as e:
199
- print(f"Error getting company filings: {e}")
200
- return []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
201
 
202
  def get_company_facts(self, cik):
203
  """
204
- Get all company financial facts data
205
 
206
  Args:
207
  cik (str): Company CIK code
@@ -212,10 +309,35 @@ class EdgarDataClient:
212
  if not self.edgar:
213
  print("sec_edgar_api library not installed")
214
  return {}
 
 
 
 
 
 
215
 
216
  try:
217
- facts = self.edgar.get_company_facts(cik=cik)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
218
  return facts
 
 
 
219
  except Exception as e:
220
  print(f"Error getting company facts: {e}")
221
  return {}
 
1
  """EDGAR API Client Module"""
2
 
3
  import requests
4
+ from requests.adapters import HTTPAdapter
5
+ from requests.packages.urllib3.util.retry import Retry
6
  try:
7
  from sec_edgar_api.EdgarClient import EdgarClient
8
  except ImportError:
 
10
  import json
11
  import time
12
  from functools import wraps
13
+ import threading
14
 
15
 
16
  class EdgarDataClient:
 
21
  self.min_request_interval = 0.11 # SEC allows 10 requests/second, use 0.11s to be safe
22
  self.request_timeout = 30 # 30 seconds timeout for HTTP requests
23
  self.max_retries = 3 # Maximum retry attempts
24
+ self._lock = threading.Lock() # Thread-safe rate limiting
25
+
26
+ # Configure requests session with connection pooling and retry logic
27
+ self.session = requests.Session()
28
+ retry_strategy = Retry(
29
+ total=3,
30
+ backoff_factor=1,
31
+ status_forcelist=[429, 500, 502, 503, 504],
32
+ allowed_methods=["HEAD", "GET", "OPTIONS"]
33
+ )
34
+ adapter = HTTPAdapter(
35
+ max_retries=retry_strategy,
36
+ pool_connections=10,
37
+ pool_maxsize=20,
38
+ pool_block=False
39
+ )
40
+ self.session.mount("http://", adapter)
41
+ self.session.mount("https://", adapter)
42
+ self.session.headers.update({"User-Agent": user_agent})
43
+
44
+ # Cache for frequently accessed data
45
+ self._company_cache = {} # Cache company info to avoid repeated calls
46
+ self._cache_ttl = 300 # 5 minutes cache TTL
47
+ self._cache_timestamps = {}
48
 
49
  if EdgarClient:
50
  self.edgar = EdgarClient(user_agent=user_agent)
 
52
  self.edgar = None
53
 
54
  def _rate_limit(self):
55
+ """Thread-safe rate limiting to comply with SEC API limits (10 requests/second)"""
56
+ with self._lock:
57
+ current_time = time.time()
58
+ time_since_last_request = current_time - self.last_request_time
59
+
60
+ if time_since_last_request < self.min_request_interval:
61
+ sleep_time = self.min_request_interval - time_since_last_request
62
+ time.sleep(sleep_time)
63
+
64
+ self.last_request_time = time.time()
65
 
66
+ def _is_cache_valid(self, cache_key):
67
+ """Check if cache entry is still valid"""
68
+ if cache_key not in self._cache_timestamps:
69
+ return False
70
+ age = time.time() - self._cache_timestamps[cache_key]
71
+ return age < self._cache_ttl
72
+
73
+ def _get_cached(self, cache_key):
74
+ """Get cached data if valid"""
75
+ if self._is_cache_valid(cache_key):
76
+ return self._company_cache.get(cache_key)
77
+ return None
78
+
79
+ def _set_cache(self, cache_key, data):
80
+ """Set cache data with timestamp"""
81
+ self._company_cache[cache_key] = data
82
+ self._cache_timestamps[cache_key] = time.time()
83
+
84
+ def _make_request_with_retry(self, url, headers=None, use_session=True):
85
  """Make HTTP request with retry logic and timeout"""
86
  if headers is None:
87
  headers = {"User-Agent": self.user_agent}
 
89
  for attempt in range(self.max_retries):
90
  try:
91
  self._rate_limit()
92
+ if use_session:
93
+ response = self.session.get(url, headers=headers, timeout=self.request_timeout)
94
+ else:
95
+ response = requests.get(url, headers=headers, timeout=self.request_timeout)
96
  response.raise_for_status()
97
  return response
98
  except requests.exceptions.Timeout:
 
166
 
167
  def get_company_info(self, cik):
168
  """
169
+ Get basic company information with caching
170
 
171
  Args:
172
  cik (str): Company CIK code
 
177
  if not self.edgar:
178
  print("sec_edgar_api library not installed")
179
  return None
180
+
181
+ # Check cache first
182
+ cache_key = f"info_{cik}"
183
+ cached = self._get_cached(cache_key)
184
+ if cached:
185
+ return cached
186
 
187
  try:
188
+ # Add timeout wrapper for sec-edgar-api calls
189
+ import signal
190
+
191
+ def timeout_handler(signum, frame):
192
+ raise TimeoutError("SEC API call timeout")
193
 
194
+ # Set alarm for 30 seconds (only works on Unix-like systems)
195
+ try:
196
+ signal.signal(signal.SIGALRM, timeout_handler)
197
+ signal.alarm(30)
198
+ submissions = self.edgar.get_submissions(cik=cik)
199
+ signal.alarm(0) # Cancel alarm
200
+ except AttributeError:
201
+ # Windows doesn't support SIGALRM, use direct call
202
+ submissions = self.edgar.get_submissions(cik=cik)
203
+
204
+ result = {
205
  "cik": cik,
206
  "name": submissions.get("name", ""),
207
  "tickers": submissions.get("tickers", []),
208
  "sic": submissions.get("sic", ""),
209
  "sic_description": submissions.get("sicDescription", "")
210
  }
211
+
212
+ # Cache the result
213
+ self._set_cache(cache_key, result)
214
+ return result
215
+ except TimeoutError:
216
+ print(f"Timeout getting company info for CIK: {cik}")
217
+ return None
218
  except Exception as e:
219
  print(f"Error getting company info: {e}")
220
  return None
221
 
222
  def get_company_filings(self, cik, form_types=None):
223
  """
224
+ Get all company filing documents with caching
225
 
226
  Args:
227
  cik (str): Company CIK code
 
233
  if not self.edgar:
234
  print("sec_edgar_api library not installed")
235
  return []
236
+
237
+ # Check cache first (cache all filings, filter later)
238
+ cache_key = f"filings_{cik}"
239
+ cached = self._get_cached(cache_key)
240
+
241
+ if not cached:
242
+ try:
243
+ # Add timeout wrapper
244
+ import signal
 
 
 
 
 
 
 
 
 
245
 
246
+ def timeout_handler(signum, frame):
247
+ raise TimeoutError("SEC API call timeout")
 
248
 
249
+ try:
250
+ signal.signal(signal.SIGALRM, timeout_handler)
251
+ signal.alarm(30)
252
+ submissions = self.edgar.get_submissions(cik=cik)
253
+ signal.alarm(0)
254
+ except AttributeError:
255
+ # Windows fallback
256
+ submissions = self.edgar.get_submissions(cik=cik)
257
 
258
+ # Extract filing information
259
+ filings = []
260
+ recent = submissions.get("filings", {}).get("recent", {})
 
 
 
261
 
262
+ # Get data from each field
263
+ form_types_list = recent.get("form", [])
264
+ filing_dates = recent.get("filingDate", [])
265
+ accession_numbers = recent.get("accessionNumber", [])
266
+ primary_documents = recent.get("primaryDocument", [])
267
+
268
+ # Iterate through all filings
269
+ for i in range(len(form_types_list)):
270
+ filing_date = filing_dates[i] if i < len(filing_dates) else ""
271
+ accession_number = accession_numbers[i] if i < len(accession_numbers) else ""
272
+ primary_document = primary_documents[i] if i < len(primary_documents) else ""
273
+
274
+ filing = {
275
+ "form_type": form_types_list[i],
276
+ "filing_date": filing_date,
277
+ "accession_number": accession_number,
278
+ "primary_document": primary_document
279
+ }
280
+
281
+ filings.append(filing)
282
+
283
+ # Cache all filings
284
+ self._set_cache(cache_key, filings)
285
+ cached = filings
286
+
287
+ except TimeoutError:
288
+ print(f"Timeout getting company filings for CIK: {cik}")
289
+ return []
290
+ except Exception as e:
291
+ print(f"Error getting company filings: {e}")
292
+ return []
293
+
294
+ # Filter by form type if specified
295
+ if form_types:
296
+ return [f for f in cached if f.get("form_type") in form_types]
297
+ return cached
298
 
299
  def get_company_facts(self, cik):
300
  """
301
+ Get all company financial facts data with caching and timeout
302
 
303
  Args:
304
  cik (str): Company CIK code
 
309
  if not self.edgar:
310
  print("sec_edgar_api library not installed")
311
  return {}
312
+
313
+ # Check cache first
314
+ cache_key = f"facts_{cik}"
315
+ cached = self._get_cached(cache_key)
316
+ if cached:
317
+ return cached
318
 
319
  try:
320
+ # Add timeout wrapper
321
+ import signal
322
+
323
+ def timeout_handler(signum, frame):
324
+ raise TimeoutError("SEC API call timeout")
325
+
326
+ try:
327
+ signal.signal(signal.SIGALRM, timeout_handler)
328
+ signal.alarm(45) # 45 seconds for facts (larger dataset)
329
+ facts = self.edgar.get_company_facts(cik=cik)
330
+ signal.alarm(0)
331
+ except AttributeError:
332
+ # Windows fallback
333
+ facts = self.edgar.get_company_facts(cik=cik)
334
+
335
+ # Cache the result
336
+ self._set_cache(cache_key, facts)
337
  return facts
338
+ except TimeoutError:
339
+ print(f"Timeout getting company facts for CIK: {cik}")
340
+ return {}
341
  except Exception as e:
342
  print(f"Error getting company facts: {e}")
343
  return {}
mcp_server_sse.py CHANGED
@@ -17,12 +17,14 @@ from edgar_client import EdgarDataClient
17
  from financial_analyzer import FinancialAnalyzer
18
  import time
19
  import sys
 
 
20
 
21
  # Initialize FastAPI app
22
  app = FastAPI(
23
  title="SEC Financial Report MCP Server",
24
  description="Model Context Protocol Server for SEC EDGAR Financial Data",
25
- version="2.2.0"
26
  )
27
 
28
  # Server startup time for monitoring
@@ -39,19 +41,32 @@ app.add_middleware(
39
  allow_headers=["*"],
40
  )
41
 
42
- # Request tracking middleware
43
  @app.middleware("http")
44
  async def track_requests(request: Request, call_next):
45
  global request_count, error_count
46
  request_count += 1
 
47
 
48
  try:
49
- response = await call_next(request)
 
 
 
 
50
  if response.status_code >= 400:
51
  error_count += 1
52
  return response
 
 
 
 
 
 
 
53
  except Exception as e:
54
  error_count += 1
 
55
  raise
56
 
57
  # Initialize EDGAR clients
@@ -199,149 +214,163 @@ MCP_TOOLS = [
199
  ]
200
 
201
 
202
- def execute_tool(tool_name: str, arguments: Dict[str, Any]) -> Dict[str, Any]:
203
- """Execute MCP tool and return clean JSON result with timeout protection"""
204
- import signal
205
-
206
- class TimeoutError(Exception):
207
- pass
208
-
209
  def timeout_handler(signum, frame):
210
- raise TimeoutError("Tool execution timeout")
211
 
 
212
  try:
213
- if tool_name == "search_company":
214
- result = edgar_client.search_company_by_name(arguments["company_name"])
215
- if result:
216
- return {
217
- "type": "text",
218
- "text": json.dumps(result, ensure_ascii=False)
219
- }
220
- else:
221
- return {
222
- "type": "text",
223
- "text": json.dumps({
224
- "error": f"No company found with name: {arguments['company_name']}"
225
- }, ensure_ascii=False)
226
- }
227
-
228
- elif tool_name == "get_company_info":
229
- result = edgar_client.get_company_info(arguments["cik"])
230
- if result:
231
- return {
232
- "type": "text",
233
- "text": json.dumps(result, ensure_ascii=False)
234
- }
235
- else:
236
- return {
237
- "type": "text",
238
- "text": json.dumps({
239
- "error": f"No company found with CIK: {arguments['cik']}"
240
- }, ensure_ascii=False)
241
- }
242
-
243
- elif tool_name == "get_company_filings":
244
- form_types = arguments.get("form_types")
245
- result = edgar_client.get_company_filings(arguments["cik"], form_types)
246
- if result:
247
- # Limit to 20 results
248
- limited_result = result[:20]
249
- return {
250
- "type": "text",
251
- "text": json.dumps({
252
- "total": len(result),
253
- "returned": len(limited_result),
254
- "filings": limited_result
255
- }, ensure_ascii=False)
256
- }
257
- else:
258
- return {
259
- "type": "text",
260
- "text": json.dumps({
261
- "error": f"No filings found for CIK: {arguments['cik']}"
262
- }, ensure_ascii=False)
263
- }
264
-
265
- elif tool_name == "get_financial_data":
266
- result = edgar_client.get_financial_data_for_period(arguments["cik"], arguments["period"])
267
- if result and "period" in result:
268
- return {
269
- "type": "text",
270
- "text": json.dumps(result, ensure_ascii=False)
271
- }
272
- else:
273
- return {
274
- "type": "text",
275
- "text": json.dumps({
276
- "error": f"No financial data found for CIK: {arguments['cik']}, Period: {arguments['period']}"
277
- }, ensure_ascii=False)
278
- }
279
-
280
- elif tool_name == "extract_financial_metrics":
281
- years = arguments.get("years", 3)
282
- if years < 1 or years > 10:
283
- return {
284
- "type": "text",
285
- "text": json.dumps({
286
- "error": "Years parameter must be between 1 and 10"
287
- }, ensure_ascii=False)
288
- }
289
 
290
- metrics = financial_analyzer.extract_financial_metrics(arguments["cik"], years)
291
- if metrics:
292
- formatted = financial_analyzer.format_financial_data(metrics)
293
- return {
294
- "type": "text",
295
- "text": json.dumps({
296
- "periods": len(formatted),
297
- "data": formatted
298
- }, ensure_ascii=False)
299
- }
300
- else:
301
- return {
302
- "type": "text",
303
- "text": json.dumps({
304
- "error": f"No financial metrics found for CIK: {arguments['cik']}"
305
- }, ensure_ascii=False)
306
- }
307
-
308
- elif tool_name == "get_latest_financial_data":
309
- result = financial_analyzer.get_latest_financial_data(arguments["cik"])
310
- if result and "period" in result:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
311
  return {
312
  "type": "text",
313
  "text": json.dumps(result, ensure_ascii=False)
314
  }
 
315
  else:
316
  return {
317
  "type": "text",
318
  "text": json.dumps({
319
- "error": f"No latest financial data found for CIK: {arguments['cik']}"
320
- }, ensure_ascii=False)
321
- }
322
-
323
- elif tool_name == "advanced_search_company" or tool_name == "advanced_search":
324
- # Support both names for backward compatibility
325
- result = financial_analyzer.search_company(arguments["company_input"])
326
- if result.get("error"):
327
- return {
328
- "type": "text",
329
- "text": json.dumps({
330
- "error": result["error"]
331
  }, ensure_ascii=False)
332
  }
333
- return {
334
- "type": "text",
335
- "text": json.dumps(result, ensure_ascii=False)
336
- }
337
-
338
- else:
339
- return {
340
- "type": "text",
341
- "text": json.dumps({
342
- "error": f"Unknown tool: {tool_name}"
343
- }, ensure_ascii=False)
344
- }
345
 
346
  except Exception as e:
347
  return {
@@ -374,7 +403,7 @@ async def handle_mcp_message(request: MCPRequest):
374
  },
375
  "serverInfo": {
376
  "name": "sec-financial-data",
377
- "version": "2.2.0"
378
  }
379
  }
380
  ).dict()
@@ -438,10 +467,10 @@ async def sse_endpoint(request: Request):
438
  }
439
  yield f"data: {json.dumps(init_message)}\n\n"
440
 
441
- # Keep connection alive with shorter ping interval for better stability
442
  try:
443
  while True:
444
- await asyncio.sleep(15) # Reduced from 30s to 15s for better keepalive
445
  # Send ping to keep connection alive
446
  ping_message = {
447
  "jsonrpc": "2.0",
@@ -1023,7 +1052,7 @@ async def health_check():
1023
  return {
1024
  "status": "healthy",
1025
  "server": "sec-financial-data",
1026
- "version": "2.2.0",
1027
  "protocol": "MCP",
1028
  "transport": "SSE",
1029
  "tools_count": len(MCP_TOOLS),
@@ -1031,6 +1060,7 @@ async def health_check():
1031
  "python_version": sys.version,
1032
  "request_count": request_count,
1033
  "error_count": error_count,
 
1034
  "timestamp": datetime.now().isoformat()
1035
  }
1036
 
 
17
  from financial_analyzer import FinancialAnalyzer
18
  import time
19
  import sys
20
+ import signal
21
+ from contextlib import contextmanager
22
 
23
  # Initialize FastAPI app
24
  app = FastAPI(
25
  title="SEC Financial Report MCP Server",
26
  description="Model Context Protocol Server for SEC EDGAR Financial Data",
27
+ version="2.2.1"
28
  )
29
 
30
  # Server startup time for monitoring
 
41
  allow_headers=["*"],
42
  )
43
 
44
+ # Request tracking middleware with timeout protection
45
  @app.middleware("http")
46
  async def track_requests(request: Request, call_next):
47
  global request_count, error_count
48
  request_count += 1
49
+ start_time = time.time()
50
 
51
  try:
52
+ # Set a timeout for the entire request
53
+ response = await asyncio.wait_for(
54
+ call_next(request),
55
+ timeout=120.0 # 2 minutes total timeout
56
+ )
57
  if response.status_code >= 400:
58
  error_count += 1
59
  return response
60
+ except asyncio.TimeoutError:
61
+ error_count += 1
62
+ print(f"Request timeout after {time.time() - start_time:.2f}s: {request.url.path}")
63
+ return JSONResponse(
64
+ status_code=504,
65
+ content={"error": "Request timeout", "message": "The request took too long to process"}
66
+ )
67
  except Exception as e:
68
  error_count += 1
69
+ print(f"Request error: {e}")
70
  raise
71
 
72
  # Initialize EDGAR clients
 
214
  ]
215
 
216
 
217
+ @contextmanager
218
+ def timeout_context(seconds):
219
+ """Context manager for timeout on Unix-like systems"""
 
 
 
 
220
  def timeout_handler(signum, frame):
221
+ raise TimeoutError(f"Operation timeout after {seconds} seconds")
222
 
223
+ # Only works on Unix-like systems
224
  try:
225
+ old_handler = signal.signal(signal.SIGALRM, timeout_handler)
226
+ signal.alarm(seconds)
227
+ try:
228
+ yield
229
+ finally:
230
+ signal.alarm(0)
231
+ signal.signal(signal.SIGALRM, old_handler)
232
+ except (AttributeError, ValueError):
233
+ # Windows or signal not available
234
+ yield
235
+
236
+
237
+ def execute_tool(tool_name: str, arguments: Dict[str, Any]) -> Dict[str, Any]:
238
+ """Execute MCP tool and return clean JSON result with enhanced timeout protection"""
239
+ try:
240
+ # Use context manager for timeout (60 seconds per tool)
241
+ with timeout_context(60):
242
+ if tool_name == "search_company":
243
+ result = edgar_client.search_company_by_name(arguments["company_name"])
244
+ if result:
245
+ return {
246
+ "type": "text",
247
+ "text": json.dumps(result, ensure_ascii=False)
248
+ }
249
+ else:
250
+ return {
251
+ "type": "text",
252
+ "text": json.dumps({
253
+ "error": f"No company found with name: {arguments['company_name']}"
254
+ }, ensure_ascii=False)
255
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
256
 
257
+ elif tool_name == "get_company_info":
258
+ result = edgar_client.get_company_info(arguments["cik"])
259
+ if result:
260
+ return {
261
+ "type": "text",
262
+ "text": json.dumps(result, ensure_ascii=False)
263
+ }
264
+ else:
265
+ return {
266
+ "type": "text",
267
+ "text": json.dumps({
268
+ "error": f"No company found with CIK: {arguments['cik']}"
269
+ }, ensure_ascii=False)
270
+ }
271
+
272
+ elif tool_name == "get_company_filings":
273
+ form_types = arguments.get("form_types")
274
+ result = edgar_client.get_company_filings(arguments["cik"], form_types)
275
+ if result:
276
+ # Limit to 20 results
277
+ limited_result = result[:20]
278
+ return {
279
+ "type": "text",
280
+ "text": json.dumps({
281
+ "total": len(result),
282
+ "returned": len(limited_result),
283
+ "filings": limited_result
284
+ }, ensure_ascii=False)
285
+ }
286
+ else:
287
+ return {
288
+ "type": "text",
289
+ "text": json.dumps({
290
+ "error": f"No filings found for CIK: {arguments['cik']}"
291
+ }, ensure_ascii=False)
292
+ }
293
+
294
+ elif tool_name == "get_financial_data":
295
+ result = edgar_client.get_financial_data_for_period(arguments["cik"], arguments["period"])
296
+ if result and "period" in result:
297
+ return {
298
+ "type": "text",
299
+ "text": json.dumps(result, ensure_ascii=False)
300
+ }
301
+ else:
302
+ return {
303
+ "type": "text",
304
+ "text": json.dumps({
305
+ "error": f"No financial data found for CIK: {arguments['cik']}, Period: {arguments['period']}"
306
+ }, ensure_ascii=False)
307
+ }
308
+
309
+ elif tool_name == "extract_financial_metrics":
310
+ years = arguments.get("years", 3)
311
+ if years < 1 or years > 10:
312
+ return {
313
+ "type": "text",
314
+ "text": json.dumps({
315
+ "error": "Years parameter must be between 1 and 10"
316
+ }, ensure_ascii=False)
317
+ }
318
+
319
+ metrics = financial_analyzer.extract_financial_metrics(arguments["cik"], years)
320
+ if metrics:
321
+ formatted = financial_analyzer.format_financial_data(metrics)
322
+ return {
323
+ "type": "text",
324
+ "text": json.dumps({
325
+ "periods": len(formatted),
326
+ "data": formatted
327
+ }, ensure_ascii=False)
328
+ }
329
+ else:
330
+ return {
331
+ "type": "text",
332
+ "text": json.dumps({
333
+ "error": f"No financial metrics found for CIK: {arguments['cik']}"
334
+ }, ensure_ascii=False)
335
+ }
336
+
337
+ elif tool_name == "get_latest_financial_data":
338
+ result = financial_analyzer.get_latest_financial_data(arguments["cik"])
339
+ if result and "period" in result:
340
+ return {
341
+ "type": "text",
342
+ "text": json.dumps(result, ensure_ascii=False)
343
+ }
344
+ else:
345
+ return {
346
+ "type": "text",
347
+ "text": json.dumps({
348
+ "error": f"No latest financial data found for CIK: {arguments['cik']}"
349
+ }, ensure_ascii=False)
350
+ }
351
+
352
+ elif tool_name == "advanced_search_company" or tool_name == "advanced_search":
353
+ # Support both names for backward compatibility
354
+ result = financial_analyzer.search_company(arguments["company_input"])
355
+ if result.get("error"):
356
+ return {
357
+ "type": "text",
358
+ "text": json.dumps({
359
+ "error": result["error"]
360
+ }, ensure_ascii=False)
361
+ }
362
  return {
363
  "type": "text",
364
  "text": json.dumps(result, ensure_ascii=False)
365
  }
366
+
367
  else:
368
  return {
369
  "type": "text",
370
  "text": json.dumps({
371
+ "error": f"Unknown tool: {tool_name}"
 
 
 
 
 
 
 
 
 
 
 
372
  }, ensure_ascii=False)
373
  }
 
 
 
 
 
 
 
 
 
 
 
 
374
 
375
  except Exception as e:
376
  return {
 
403
  },
404
  "serverInfo": {
405
  "name": "sec-financial-data",
406
+ "version": "2.2.1"
407
  }
408
  }
409
  ).dict()
 
467
  }
468
  yield f"data: {json.dumps(init_message)}\n\n"
469
 
470
+ # Keep connection alive with optimized ping interval
471
  try:
472
  while True:
473
+ await asyncio.sleep(20) # 20 seconds ping interval for stability
474
  # Send ping to keep connection alive
475
  ping_message = {
476
  "jsonrpc": "2.0",
 
1052
  return {
1053
  "status": "healthy",
1054
  "server": "sec-financial-data",
1055
+ "version": "2.2.1",
1056
  "protocol": "MCP",
1057
  "transport": "SSE",
1058
  "tools_count": len(MCP_TOOLS),
 
1060
  "python_version": sys.version,
1061
  "request_count": request_count,
1062
  "error_count": error_count,
1063
+ "error_rate": round(error_count / max(request_count, 1) * 100, 2),
1064
  "timestamp": datetime.now().isoformat()
1065
  }
1066