Upload 2 files
Browse files
EasyReportDataMCP/edgar_client.py
CHANGED
|
@@ -3,6 +3,7 @@
|
|
| 3 |
import requests
|
| 4 |
from requests.adapters import HTTPAdapter
|
| 5 |
from urllib3.util.retry import Retry
|
|
|
|
| 6 |
try:
|
| 7 |
from sec_edgar_api.EdgarClient import EdgarClient
|
| 8 |
except ImportError:
|
|
@@ -15,6 +16,9 @@ from datetime import datetime, timedelta
|
|
| 15 |
import re
|
| 16 |
import difflib
|
| 17 |
|
|
|
|
|
|
|
|
|
|
| 18 |
|
| 19 |
class EdgarDataClient:
|
| 20 |
# Class-level cache for company_tickers.json (shared across instances)
|
|
@@ -65,9 +69,9 @@ class EdgarDataClient:
|
|
| 65 |
# Configure requests session with connection pooling
|
| 66 |
self.session = requests.Session()
|
| 67 |
|
| 68 |
-
# Configure retry strategy
|
| 69 |
retry_strategy = Retry(
|
| 70 |
-
total=3
|
| 71 |
backoff_factor=1,
|
| 72 |
status_forcelist=[429, 500, 502, 503, 504],
|
| 73 |
allowed_methods=["HEAD", "GET", "OPTIONS"]
|
|
@@ -83,8 +87,8 @@ class EdgarDataClient:
|
|
| 83 |
self.session.mount("http://", adapter)
|
| 84 |
self.session.mount("https://", adapter)
|
| 85 |
|
| 86 |
-
# Set default timeout
|
| 87 |
-
self.timeout = 30 #
|
| 88 |
|
| 89 |
# Initialize sec_edgar_api client with timeout wrapper
|
| 90 |
if EdgarClient:
|
|
@@ -393,15 +397,25 @@ class EdgarDataClient:
|
|
| 393 |
print("sec_edgar_api library not installed")
|
| 394 |
return []
|
| 395 |
|
|
|
|
|
|
|
|
|
|
| 396 |
# Convert list to tuple for caching (lists are not hashable)
|
| 397 |
if form_types and isinstance(form_types, list):
|
| 398 |
form_types = tuple(form_types)
|
| 399 |
|
| 400 |
try:
|
| 401 |
self._rate_limit()
|
|
|
|
|
|
|
|
|
|
| 402 |
# Get company submissions (now has timeout protection)
|
| 403 |
submissions = self.edgar.get_submissions(cik=cik)
|
| 404 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 405 |
# Extract filing information
|
| 406 |
filings = []
|
| 407 |
recent = submissions.get("filings", {}).get("recent", {})
|
|
@@ -433,6 +447,11 @@ class EdgarDataClient:
|
|
| 433 |
|
| 434 |
filings.append(filing)
|
| 435 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 436 |
return filings
|
| 437 |
except TimeoutError as e:
|
| 438 |
print(f"Timeout getting company filings for CIK {cik}: {e}")
|
|
@@ -544,16 +563,17 @@ class EdgarDataClient:
|
|
| 544 |
# Extract year from filing_date (format: YYYY-MM-DD)
|
| 545 |
file_year = int(filing_date[:4]) if len(filing_date) >= 4 else 0
|
| 546 |
|
| 547 |
-
#
|
| 548 |
-
|
| 549 |
-
|
| 550 |
-
|
| 551 |
-
|
| 552 |
-
|
| 553 |
-
|
| 554 |
-
|
| 555 |
-
|
| 556 |
-
|
|
|
|
| 557 |
|
| 558 |
# Iterate through each financial metric
|
| 559 |
for metric_key, metric_tags in financial_metrics.items():
|
|
@@ -650,11 +670,36 @@ class EdgarDataClient:
|
|
| 650 |
# Get form and accession info
|
| 651 |
form_type = matched_entry.get("form", "")
|
| 652 |
accn_from_facts = matched_entry.get('accn', '').replace('-', '')
|
|
|
|
| 653 |
|
| 654 |
-
#
|
|
|
|
|
|
|
|
|
|
| 655 |
filing_key = f"{form_type}_{year}"
|
| 656 |
filing_info = filings_map.get(filing_key)
|
| 657 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 658 |
if filing_info:
|
| 659 |
# Use filing info from get_company_filings
|
| 660 |
accession_number = filing_info["accession_number"].replace('-', '')
|
|
|
|
| 3 |
import requests
|
| 4 |
from requests.adapters import HTTPAdapter
|
| 5 |
from urllib3.util.retry import Retry
|
| 6 |
+
import urllib3
|
| 7 |
try:
|
| 8 |
from sec_edgar_api.EdgarClient import EdgarClient
|
| 9 |
except ImportError:
|
|
|
|
| 16 |
import re
|
| 17 |
import difflib
|
| 18 |
|
| 19 |
+
# Disable SSL warnings for better compatibility
|
| 20 |
+
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
| 21 |
+
|
| 22 |
|
| 23 |
class EdgarDataClient:
|
| 24 |
# Class-level cache for company_tickers.json (shared across instances)
|
|
|
|
| 69 |
# Configure requests session with connection pooling
|
| 70 |
self.session = requests.Session()
|
| 71 |
|
| 72 |
+
# Configure retry strategy with enhanced retries for stability
|
| 73 |
retry_strategy = Retry(
|
| 74 |
+
total=5, # Increased from 3 to 5 for better reliability
|
| 75 |
backoff_factor=1,
|
| 76 |
status_forcelist=[429, 500, 502, 503, 504],
|
| 77 |
allowed_methods=["HEAD", "GET", "OPTIONS"]
|
|
|
|
| 87 |
self.session.mount("http://", adapter)
|
| 88 |
self.session.mount("https://", adapter)
|
| 89 |
|
| 90 |
+
# Set default timeout with connection and read timeouts
|
| 91 |
+
self.timeout = (10, 30) # (connect timeout, read timeout)
|
| 92 |
|
| 93 |
# Initialize sec_edgar_api client with timeout wrapper
|
| 94 |
if EdgarClient:
|
|
|
|
| 397 |
print("sec_edgar_api library not installed")
|
| 398 |
return []
|
| 399 |
|
| 400 |
+
# ✅ 添加调试日志
|
| 401 |
+
print(f"[DEBUG] get_company_filings called with CIK: {cik}, form_types: {form_types}")
|
| 402 |
+
|
| 403 |
# Convert list to tuple for caching (lists are not hashable)
|
| 404 |
if form_types and isinstance(form_types, list):
|
| 405 |
form_types = tuple(form_types)
|
| 406 |
|
| 407 |
try:
|
| 408 |
self._rate_limit()
|
| 409 |
+
# ✅ 调试: 打印实际调用SEC API的CIK
|
| 410 |
+
print(f"[DEBUG] Calling SEC API get_submissions with CIK: {cik}")
|
| 411 |
+
|
| 412 |
# Get company submissions (now has timeout protection)
|
| 413 |
submissions = self.edgar.get_submissions(cik=cik)
|
| 414 |
|
| 415 |
+
# ✅ 调试: 打印返回的基本信息
|
| 416 |
+
print(f"[DEBUG] Got submissions for: {submissions.get('name', 'Unknown')}")
|
| 417 |
+
print(f"[DEBUG] Available form types in recent filings: {set(submissions.get('filings', {}).get('recent', {}).get('form', [])[:20])}")
|
| 418 |
+
|
| 419 |
# Extract filing information
|
| 420 |
filings = []
|
| 421 |
recent = submissions.get("filings", {}).get("recent", {})
|
|
|
|
| 447 |
|
| 448 |
filings.append(filing)
|
| 449 |
|
| 450 |
+
# ✅ 调试: 打印过滤后的结果
|
| 451 |
+
print(f"[DEBUG] After filtering, found {len(filings)} filings matching form_types: {form_types}")
|
| 452 |
+
if len(filings) > 0:
|
| 453 |
+
print(f"[DEBUG] First filing: {filings[0]}")
|
| 454 |
+
|
| 455 |
return filings
|
| 456 |
except TimeoutError as e:
|
| 457 |
print(f"Timeout getting company filings for CIK {cik}: {e}")
|
|
|
|
| 563 |
# Extract year from filing_date (format: YYYY-MM-DD)
|
| 564 |
file_year = int(filing_date[:4]) if len(filing_date) >= 4 else 0
|
| 565 |
|
| 566 |
+
# ✅ FIXED: Remove year filter to keep all filings
|
| 567 |
+
# 20-F forms are often filed in the year after the fiscal year
|
| 568 |
+
# We'll match them later using fiscal year (fy) and filed date
|
| 569 |
+
key = f"{form_type}_{file_year}"
|
| 570 |
+
if key not in filings_map:
|
| 571 |
+
filings_map[key] = {
|
| 572 |
+
"accession_number": accession_number,
|
| 573 |
+
"primary_document": primary_document,
|
| 574 |
+
"form_type": form_type,
|
| 575 |
+
"filing_date": filing_date
|
| 576 |
+
}
|
| 577 |
|
| 578 |
# Iterate through each financial metric
|
| 579 |
for metric_key, metric_tags in financial_metrics.items():
|
|
|
|
| 670 |
# Get form and accession info
|
| 671 |
form_type = matched_entry.get("form", "")
|
| 672 |
accn_from_facts = matched_entry.get('accn', '').replace('-', '')
|
| 673 |
+
filed_date = matched_entry.get('filed', '')
|
| 674 |
|
| 675 |
+
# ✅ ENHANCED: Multi-strategy filing lookup for 20-F and cross-year submissions
|
| 676 |
+
filing_info = None
|
| 677 |
+
|
| 678 |
+
# Strategy 1: Try matching by fiscal year (original logic)
|
| 679 |
filing_key = f"{form_type}_{year}"
|
| 680 |
filing_info = filings_map.get(filing_key)
|
| 681 |
|
| 682 |
+
# Strategy 2: Try matching by filed year (for 20-F filed in next year)
|
| 683 |
+
if not filing_info and filed_date:
|
| 684 |
+
filed_year = int(filed_date[:4]) if len(filed_date) >= 4 else 0
|
| 685 |
+
if filed_year > 0:
|
| 686 |
+
filing_key = f"{form_type}_{filed_year}"
|
| 687 |
+
filing_info = filings_map.get(filing_key)
|
| 688 |
+
|
| 689 |
+
# Strategy 3: Try fiscal year + 1 (common for 20-F)
|
| 690 |
+
if not filing_info:
|
| 691 |
+
filing_key = f"{form_type}_{year + 1}"
|
| 692 |
+
filing_info = filings_map.get(filing_key)
|
| 693 |
+
|
| 694 |
+
# Strategy 4: Search all filings with matching form type and accession
|
| 695 |
+
if not filing_info and accn_from_facts:
|
| 696 |
+
for key, finfo in filings_map.items():
|
| 697 |
+
if finfo["form_type"] == form_type:
|
| 698 |
+
filing_accn = finfo["accession_number"].replace('-', '')
|
| 699 |
+
if filing_accn == accn_from_facts:
|
| 700 |
+
filing_info = finfo
|
| 701 |
+
break
|
| 702 |
+
|
| 703 |
if filing_info:
|
| 704 |
# Use filing info from get_company_filings
|
| 705 |
accession_number = filing_info["accession_number"].replace('-', '')
|
EasyReportDataMCP/mcp_server_fastmcp.py
CHANGED
|
@@ -213,15 +213,29 @@ def extract_financial_metrics(cik: str, years: int = 3) -> dict:
|
|
| 213 |
if years < 1 or years > 10:
|
| 214 |
return {"error": "Years parameter must be between 1 and 10"}
|
| 215 |
|
|
|
|
|
|
|
|
|
|
| 216 |
# Check if company has filings (use tuple for caching)
|
| 217 |
filings_10k = edgar_client.get_company_filings(cik, ('10-K',))
|
| 218 |
filings_20f = edgar_client.get_company_filings(cik, ('20-F',))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 219 |
total_filings = len(filings_10k) + len(filings_20f)
|
| 220 |
|
| 221 |
if total_filings == 0:
|
|
|
|
| 222 |
return {
|
| 223 |
-
"error": f"No annual filings found for CIK: {cik}",
|
| 224 |
-
"suggestion": "Please
|
|
|
|
|
|
|
| 225 |
}
|
| 226 |
|
| 227 |
# Extract metrics
|
|
@@ -302,7 +316,13 @@ def get_latest_financial_data(cik: str) -> dict:
|
|
| 302 |
if result and "period" in result:
|
| 303 |
return result
|
| 304 |
else:
|
| 305 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 306 |
|
| 307 |
|
| 308 |
@mcp.tool()
|
|
@@ -350,20 +370,13 @@ def advanced_search_company(company_input: str) -> dict:
|
|
| 350 |
if __name__ == "__main__":
|
| 351 |
import os
|
| 352 |
|
| 353 |
-
# Set port
|
| 354 |
-
port = int(os.getenv("PORT", "
|
| 355 |
host = os.getenv("HOST", "0.0.0.0")
|
| 356 |
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
def patched_init(self, *args, **kwargs):
|
| 362 |
-
kwargs['host'] = host
|
| 363 |
-
kwargs['port'] = port
|
| 364 |
-
return original_config_init(self, *args, **kwargs)
|
| 365 |
-
|
| 366 |
-
uvicorn.Config.__init__ = patched_init
|
| 367 |
|
| 368 |
-
# Run FastMCP with
|
| 369 |
-
mcp.run(transport="
|
|
|
|
| 213 |
if years < 1 or years > 10:
|
| 214 |
return {"error": "Years parameter must be between 1 and 10"}
|
| 215 |
|
| 216 |
+
# ✅ 添加调试日志,查看实际CIK格式和查询结果
|
| 217 |
+
print(f"[DEBUG] extract_financial_metrics called with CIK: {cik}, type: {type(cik)}, years: {years}")
|
| 218 |
+
|
| 219 |
# Check if company has filings (use tuple for caching)
|
| 220 |
filings_10k = edgar_client.get_company_filings(cik, ('10-K',))
|
| 221 |
filings_20f = edgar_client.get_company_filings(cik, ('20-F',))
|
| 222 |
+
|
| 223 |
+
# ✅ 打印查询结果
|
| 224 |
+
print(f"[DEBUG] Found {len(filings_10k)} 10-K filings, {len(filings_20f)} 20-F filings")
|
| 225 |
+
if len(filings_10k) > 0:
|
| 226 |
+
print(f"[DEBUG] Latest 10-K: {filings_10k[0]}")
|
| 227 |
+
if len(filings_20f) > 0:
|
| 228 |
+
print(f"[DEBUG] Latest 20-F: {filings_20f[0]}")
|
| 229 |
+
|
| 230 |
total_filings = len(filings_10k) + len(filings_20f)
|
| 231 |
|
| 232 |
if total_filings == 0:
|
| 233 |
+
# ✅ 提供更详细的错误信息,帮助用户了解问题
|
| 234 |
return {
|
| 235 |
+
"error": f"No 10-K or 20-F annual filings found for CIK: {cik}",
|
| 236 |
+
"suggestion": "This company might not have filed 10-K or 20-F forms yet. Please verify the CIK is correct.",
|
| 237 |
+
"note": "Some companies may use different filing forms or may be newly listed.",
|
| 238 |
+
"cik": cik
|
| 239 |
}
|
| 240 |
|
| 241 |
# Extract metrics
|
|
|
|
| 316 |
if result and "period" in result:
|
| 317 |
return result
|
| 318 |
else:
|
| 319 |
+
# ✅ 提供更有帮助的错误信息
|
| 320 |
+
return {
|
| 321 |
+
"error": f"No latest financial data found for CIK: {cik}",
|
| 322 |
+
"suggestion": "The company may not have recent 10-K or 20-F filings, or the data format may not be supported.",
|
| 323 |
+
"note": "Try using a different CIK or check if the company has filed recent annual reports.",
|
| 324 |
+
"cik": cik
|
| 325 |
+
}
|
| 326 |
|
| 327 |
|
| 328 |
@mcp.tool()
|
|
|
|
| 370 |
if __name__ == "__main__":
|
| 371 |
import os
|
| 372 |
|
| 373 |
+
# ✅ Set port to 7861 for EasyReportDataMCP (MarketandStockMCP uses 7870)
|
| 374 |
+
port = int(os.getenv("PORT", "7861"))
|
| 375 |
host = os.getenv("HOST", "0.0.0.0")
|
| 376 |
|
| 377 |
+
print("▶️ Starting EasyReportDataMCP Server...")
|
| 378 |
+
print(f"📡 MCP server will listen on {host}:{port}")
|
| 379 |
+
print("✅ Available tools: advanced_search_company, get_latest_financial_data, extract_financial_metrics")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 380 |
|
| 381 |
+
# Run FastMCP with SSE transport (Server-Sent Events for chat_direct.py compatibility)
|
| 382 |
+
mcp.run(transport="sse", port=port)
|