395 lines
14 KiB
Python
395 lines
14 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
EdgarTools MCP Server
|
|
|
|
MCP (Model Context Protocol) server providing AI agents access to SEC filing data.
|
|
This module provides the main entry point for the MCP server.
|
|
|
|
Usage:
|
|
python -m edgar.ai.mcp # Via module
|
|
edgartools-mcp # Via console script
|
|
"""
|
|
|
|
import asyncio
|
|
import logging
|
|
import os
|
|
from typing import Any
|
|
|
|
from mcp import Resource, Tool
|
|
from mcp.server import NotificationOptions, Server
|
|
from mcp.server.models import InitializationOptions
|
|
from mcp.server.stdio import stdio_server
|
|
from mcp.types import TextContent
|
|
|
|
# Set up logging
|
|
logging.basicConfig(
|
|
level=logging.INFO,
|
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
|
)
|
|
logger = logging.getLogger("edgartools-mcp")
|
|
|
|
|
|
def setup_edgar_identity():
|
|
"""Configure SEC identity from environment variable.
|
|
|
|
The SEC requires proper identification for API requests. This function
|
|
checks for the EDGAR_IDENTITY environment variable and configures it.
|
|
If not set, logs a warning but continues (API errors will guide user).
|
|
"""
|
|
try:
|
|
from edgar import set_identity
|
|
|
|
identity = os.environ.get('EDGAR_IDENTITY')
|
|
if not identity:
|
|
logger.warning(
|
|
"EDGAR_IDENTITY environment variable not set. "
|
|
"The SEC requires proper identification for API requests.\n"
|
|
"Add to your MCP client configuration:\n"
|
|
' "env": {"EDGAR_IDENTITY": "Your Name your.email@example.com"}\n'
|
|
"Or set in your shell: export EDGAR_IDENTITY=\"Your Name your.email@example.com\""
|
|
)
|
|
return
|
|
|
|
set_identity(identity)
|
|
logger.info(f"SEC identity configured: {identity}")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error setting up EDGAR identity: {e}")
|
|
|
|
# Create the server
|
|
app = Server("edgartools")
|
|
|
|
|
|
@app.list_tools()
|
|
async def list_tools() -> list[Tool]:
|
|
"""List available tools."""
|
|
return [
|
|
Tool(
|
|
name="edgar_company_research",
|
|
description="Get company overview and background. Returns profile, 3-year financial trends, and recent filing activity. Use this for initial company research or to get a snapshot of recent performance.",
|
|
inputSchema={
|
|
"type": "object",
|
|
"properties": {
|
|
"identifier": {
|
|
"type": "string",
|
|
"description": "Company ticker (AAPL), CIK (0000320193), or name (Apple Inc)"
|
|
},
|
|
"include_financials": {
|
|
"type": "boolean",
|
|
"description": "Include 3-year income statement showing revenue and profit trends",
|
|
"default": True
|
|
},
|
|
"include_filings": {
|
|
"type": "boolean",
|
|
"description": "Include summary of last 5 SEC filings",
|
|
"default": True
|
|
},
|
|
"include_ownership": {
|
|
"type": "boolean",
|
|
"description": "Include insider and institutional ownership data (currently not implemented)",
|
|
"default": False
|
|
},
|
|
"detail_level": {
|
|
"type": "string",
|
|
"enum": ["minimal", "standard", "detailed"],
|
|
"description": "Response detail: 'minimal' (key metrics only), 'standard' (balanced), 'detailed' (comprehensive data)",
|
|
"default": "standard"
|
|
}
|
|
},
|
|
"required": ["identifier"]
|
|
}
|
|
),
|
|
Tool(
|
|
name="edgar_analyze_financials",
|
|
description="Detailed financial statement analysis across multiple periods. Use this for trend analysis, growth calculations, or comparing financial performance over time.",
|
|
inputSchema={
|
|
"type": "object",
|
|
"properties": {
|
|
"company": {
|
|
"type": "string",
|
|
"description": "Company ticker (TSLA), CIK (0001318605), or name (Tesla Inc)"
|
|
},
|
|
"periods": {
|
|
"type": "integer",
|
|
"description": "Number of periods: 4-5 for trends, 8-10 for patterns (max 10)",
|
|
"default": 4
|
|
},
|
|
"annual": {
|
|
"type": "boolean",
|
|
"description": "Use annual periods (true) for long-term trends and year-over-year comparisons, or quarterly periods (false) for recent performance and current earnings. Quarterly provides more recent data but may show seasonal volatility.",
|
|
"default": True
|
|
},
|
|
"statement_types": {
|
|
"type": "array",
|
|
"items": {"type": "string", "enum": ["income", "balance", "cash_flow"]},
|
|
"description": "Statements to include: 'income' (revenue, profit, growth), 'balance' (assets, liabilities, equity), 'cash_flow' (operating, investing, financing cash flows)",
|
|
"default": ["income"]
|
|
}
|
|
},
|
|
"required": ["company"]
|
|
}
|
|
),
|
|
Tool(
|
|
name="edgar_industry_overview",
|
|
description="Get overview of an industry sector including company count, major players, and aggregate metrics. Use this to understand industry landscape before diving into specific companies.",
|
|
inputSchema={
|
|
"type": "object",
|
|
"properties": {
|
|
"industry": {
|
|
"type": "string",
|
|
"enum": [
|
|
"pharmaceuticals", "biotechnology", "software",
|
|
"semiconductors", "banking", "investment",
|
|
"insurance", "real_estate", "oil_gas", "retail"
|
|
],
|
|
"description": "Industry sector to analyze"
|
|
},
|
|
"include_top_companies": {
|
|
"type": "boolean",
|
|
"description": "Include list of major companies in the sector",
|
|
"default": True
|
|
},
|
|
"limit": {
|
|
"type": "integer",
|
|
"description": "Number of top companies to show (by filing activity)",
|
|
"default": 10
|
|
}
|
|
},
|
|
"required": ["industry"]
|
|
}
|
|
),
|
|
Tool(
|
|
name="edgar_compare_industry_companies",
|
|
description="Compare financial performance of companies within an industry sector. Automatically selects top companies or accepts custom company list for side-by-side financial comparison.",
|
|
inputSchema={
|
|
"type": "object",
|
|
"properties": {
|
|
"industry": {
|
|
"type": "string",
|
|
"enum": [
|
|
"pharmaceuticals", "biotechnology", "software",
|
|
"semiconductors", "banking", "investment",
|
|
"insurance", "real_estate", "oil_gas", "retail"
|
|
],
|
|
"description": "Industry sector to analyze"
|
|
},
|
|
"companies": {
|
|
"type": "array",
|
|
"items": {"type": "string"},
|
|
"description": "Optional: Specific tickers to compare (e.g., ['AAPL', 'MSFT', 'GOOGL']). If omitted, uses top companies by market presence.",
|
|
"default": None
|
|
},
|
|
"limit": {
|
|
"type": "integer",
|
|
"description": "Number of companies to compare if not specified (default 5, max 10)",
|
|
"default": 5
|
|
},
|
|
"periods": {
|
|
"type": "integer",
|
|
"description": "Number of periods for comparison (default 3)",
|
|
"default": 3
|
|
},
|
|
"annual": {
|
|
"type": "boolean",
|
|
"description": "Annual (true) or quarterly (false) comparison",
|
|
"default": True
|
|
}
|
|
},
|
|
"required": ["industry"]
|
|
}
|
|
)
|
|
]
|
|
|
|
|
|
@app.call_tool()
|
|
async def call_tool(name: str, arguments: dict[str, Any] | None) -> list[TextContent]:
|
|
"""Handle tool calls."""
|
|
if arguments is None:
|
|
arguments = {}
|
|
|
|
try:
|
|
if name == "edgar_company_research":
|
|
from edgar.ai.mcp.tools.company_research import handle_company_research
|
|
return await handle_company_research(arguments)
|
|
elif name == "edgar_analyze_financials":
|
|
from edgar.ai.mcp.tools.financial_analysis import handle_analyze_financials
|
|
return await handle_analyze_financials(arguments)
|
|
elif name == "edgar_industry_overview":
|
|
from edgar.ai.mcp.tools.industry_analysis import handle_industry_overview
|
|
return await handle_industry_overview(arguments)
|
|
elif name == "edgar_compare_industry_companies":
|
|
from edgar.ai.mcp.tools.industry_analysis import handle_compare_industry_companies
|
|
return await handle_compare_industry_companies(arguments)
|
|
else:
|
|
raise ValueError(f"Unknown tool: {name}")
|
|
|
|
except Exception as e:
|
|
logger.error("Error in tool %s: %s", name, e)
|
|
return [TextContent(
|
|
type="text",
|
|
text=f"Error: {str(e)}"
|
|
)]
|
|
|
|
|
|
@app.list_resources()
|
|
async def list_resources() -> list[Resource]:
|
|
"""List available resources."""
|
|
return [
|
|
Resource(
|
|
uri="edgartools://docs/quickstart",
|
|
name="EdgarTools Quickstart Guide",
|
|
description="Quick start guide for using EdgarTools",
|
|
mimeType="text/markdown"
|
|
)
|
|
]
|
|
|
|
|
|
@app.read_resource()
|
|
async def read_resource(uri: str) -> str:
|
|
"""Read a resource."""
|
|
if uri == "edgartools://docs/quickstart":
|
|
return """# EdgarTools Quickstart
|
|
|
|
## Basic Usage
|
|
|
|
```python
|
|
from edgar import Company, get_current_filings
|
|
|
|
# Get company information
|
|
company = Company("AAPL")
|
|
print(f"{company.name} - CIK: {company.cik}")
|
|
|
|
# Get filings
|
|
filings = company.get_filings(form="10-K", limit=5)
|
|
for filing in filings:
|
|
print(f"{filing.form} - {filing.filing_date}")
|
|
|
|
# Get current filings across all companies
|
|
current = get_current_filings(limit=20)
|
|
for filing in current.data.to_pylist():
|
|
print(f"{filing['company']} - {filing['form']}")
|
|
```
|
|
|
|
## Available Tools
|
|
|
|
- **edgar_get_company**: Get detailed company information
|
|
- **edgar_current_filings**: Get the latest SEC filings
|
|
|
|
## Example Queries
|
|
|
|
- "Get information about Apple Inc including recent financials"
|
|
- "Show me the 20 most recent SEC filings"
|
|
- "Find current 8-K filings"
|
|
"""
|
|
else:
|
|
raise ValueError(f"Unknown resource: {uri}")
|
|
|
|
|
|
def main():
|
|
"""Main entry point for MCP server."""
|
|
try:
|
|
# Get package version for server version
|
|
from edgar.__about__ import __version__
|
|
|
|
# Configure EDGAR identity from environment
|
|
setup_edgar_identity()
|
|
|
|
async def run_server():
|
|
"""Run the async MCP server."""
|
|
logger.info(f"Starting EdgarTools MCP Server v{__version__}")
|
|
|
|
# Use stdio transport
|
|
async with stdio_server() as (read_stream, write_stream):
|
|
await app.run(
|
|
read_stream,
|
|
write_stream,
|
|
InitializationOptions(
|
|
server_name="edgartools",
|
|
server_version=__version__, # Sync with package version
|
|
capabilities=app.get_capabilities(
|
|
notification_options=NotificationOptions(),
|
|
experimental_capabilities={}
|
|
)
|
|
)
|
|
)
|
|
|
|
asyncio.run(run_server())
|
|
|
|
except KeyboardInterrupt:
|
|
logger.info("Server stopped by user")
|
|
except Exception as e:
|
|
logger.error(f"Server error: {e}", exc_info=True)
|
|
raise
|
|
|
|
|
|
def test_server():
|
|
"""Test that MCP server is properly configured and ready to run.
|
|
|
|
Returns:
|
|
bool: True if all checks pass, False otherwise
|
|
"""
|
|
import sys
|
|
|
|
print("Testing EdgarTools MCP Server Configuration...\n")
|
|
|
|
all_passed = True
|
|
|
|
# Test 1: EdgarTools import check
|
|
try:
|
|
from edgar import Company
|
|
from edgar.__about__ import __version__
|
|
print(f"✓ EdgarTools v{__version__} imports successfully")
|
|
except ImportError as e:
|
|
print(f"✗ EdgarTools import error: {e}")
|
|
print(" Install with: pip install edgartools")
|
|
all_passed = False
|
|
|
|
# Test 2: MCP framework check
|
|
try:
|
|
from mcp.server import Server
|
|
print("✓ MCP framework available")
|
|
except ImportError as e:
|
|
print(f"✗ MCP framework not installed: {e}")
|
|
print(" Install with: pip install edgartools[ai]")
|
|
all_passed = False
|
|
|
|
# Test 3: Identity configuration check
|
|
identity = os.environ.get('EDGAR_IDENTITY')
|
|
if identity:
|
|
print(f"✓ EDGAR_IDENTITY configured: {identity}")
|
|
else:
|
|
print("⚠ EDGAR_IDENTITY not set (recommended)")
|
|
print(" Set with: export EDGAR_IDENTITY=\"Your Name your@email.com\"")
|
|
print(" Or configure in MCP client's env settings")
|
|
|
|
# Test 4: Quick functionality test
|
|
try:
|
|
from edgar import get_current_filings
|
|
print("✓ Core EdgarTools functionality available")
|
|
except Exception as e:
|
|
print(f"✗ EdgarTools functionality check failed: {e}")
|
|
all_passed = False
|
|
|
|
# Summary
|
|
print()
|
|
if all_passed:
|
|
print("✓ All checks passed - MCP server is ready to run")
|
|
print("\nTo start the server:")
|
|
print(" python -m edgar.ai")
|
|
print(" or")
|
|
print(" edgartools-mcp")
|
|
return True
|
|
else:
|
|
print("✗ Some checks failed - please fix the issues above")
|
|
return False
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import sys
|
|
|
|
# Check for --test flag
|
|
if "--test" in sys.argv or "-t" in sys.argv:
|
|
sys.exit(0 if test_server() else 1)
|
|
else:
|
|
main()
|