JC321 commited on
Commit
c83dd38
Β·
verified Β·
1 Parent(s): 9de3e7b

Upload 6 files

Browse files
Files changed (3) hide show
  1. README.md +3 -2
  2. app.py +135 -211
  3. requirements.txt +10 -1
README.md CHANGED
@@ -3,10 +3,11 @@ title: SEC Financial Data MCP Server
3
  emoji: πŸ“Š
4
  colorFrom: blue
5
  colorTo: green
6
- sdk: docker
7
- app_port: 7860
8
  app_file: app.py
9
  pinned: false
 
10
  ---
11
 
12
  # SEC Financial Data MCP Server (FastMCP)
 
3
  emoji: πŸ“Š
4
  colorFrom: blue
5
  colorTo: green
6
+ sdk: gradio
7
+ sdk_version: 4.44.0
8
  app_file: app.py
9
  pinned: false
10
+ license: mit
11
  ---
12
 
13
  # SEC Financial Data MCP Server (FastMCP)
app.py CHANGED
@@ -1,29 +1,16 @@
1
  """
2
- Simple FastAPI web interface for MCP Server usage guide
3
- Integrated with MCP server directly
4
  """
5
- from fastapi import FastAPI, Request
6
- from fastapi.responses import HTMLResponse, JSONResponse, Response, FileResponse
7
- from fastapi.middleware.cors import CORSMiddleware
8
- from pathlib import Path
9
- import uvicorn
10
  import json
11
-
12
- # Import MCP server components
13
  from mcp.server.fastmcp import FastMCP
14
  from edgar_client import EdgarDataClient
15
  from financial_analyzer import FinancialAnalyzer
16
 
17
- app = FastAPI(title="SEC Financial Data MCP Server")
18
-
19
- # Add CORS middleware
20
- app.add_middleware(
21
- CORSMiddleware,
22
- allow_origins=["*"],
23
- allow_credentials=True,
24
- allow_methods=["*"],
25
- allow_headers=["*"],
26
- )
27
 
28
  # Initialize EDGAR clients
29
  edgar_client = EdgarDataClient(
@@ -34,39 +21,23 @@ financial_analyzer = FinancialAnalyzer(
34
  user_agent="Juntao Peng Financial Report Metrics App (jtyxabc@gmail.com)"
35
  )
36
 
37
- # Create FastMCP server with pure JSON response
38
- mcp = FastMCP("sec-financial-data", json_response=True)
39
-
40
-
41
  @mcp.tool()
42
  def search_company(company_name: str) -> dict:
43
  """Search for a company by name in SEC EDGAR database."""
44
  result = edgar_client.search_company_by_name(company_name)
45
- if result:
46
- return result
47
- else:
48
- return {"error": f"No company found with name: {company_name}"}
49
-
50
 
51
  @mcp.tool()
52
  def get_company_info(cik: str) -> dict:
53
  """Get detailed company information."""
54
  result = edgar_client.get_company_info(cik)
55
- if result:
56
- return result
57
- else:
58
- return {"error": f"No company found with CIK: {cik}"}
59
-
60
 
61
  @mcp.tool()
62
  def get_company_filings(cik: str, form_types: list[str] | None = None) -> dict:
63
  """Get list of company SEC filings."""
64
- # Convert list to tuple for caching
65
- if form_types:
66
- form_types_tuple = tuple(form_types)
67
- else:
68
- form_types_tuple = None
69
-
70
  result = edgar_client.get_company_filings(cik, form_types_tuple)
71
  if result:
72
  limited_result = result[:20]
@@ -75,19 +46,13 @@ def get_company_filings(cik: str, form_types: list[str] | None = None) -> dict:
75
  "returned": len(limited_result),
76
  "filings": limited_result
77
  }
78
- else:
79
- return {"error": f"No filings found for CIK: {cik}"}
80
-
81
 
82
  @mcp.tool()
83
  def get_financial_data(cik: str, period: str) -> dict:
84
  """Get financial data for a specific period."""
85
  result = edgar_client.get_financial_data_for_period(cik, period)
86
- if result and "period" in result:
87
- return result
88
- else:
89
- return {"error": f"No financial data found for CIK: {cik}, Period: {period}"}
90
-
91
 
92
  @mcp.tool()
93
  def extract_financial_metrics(cik: str, years: int = 3) -> dict:
@@ -95,186 +60,145 @@ def extract_financial_metrics(cik: str, years: int = 3) -> dict:
95
  if years < 1 or years > 10:
96
  return {"error": "Years parameter must be between 1 and 10"}
97
 
98
- try:
99
- metrics = financial_analyzer.extract_financial_metrics(cik, years)
100
-
101
- if metrics:
102
- formatted = financial_analyzer.format_financial_data(metrics)
103
- return {
104
- "periods": len(formatted),
105
- "data": formatted
106
- }
107
- else:
108
- # Return helpful debug info
109
- filings_10k = edgar_client.get_company_filings(cik, ('10-K',))
110
- filings_20f = edgar_client.get_company_filings(cik, ('20-F',))
111
-
112
- return {
113
- "error": f"No financial metrics extracted for CIK: {cik}",
114
- "debug": {
115
- "cik": cik,
116
- "years_requested": years,
117
- "filings_found": {
118
- "10-K": len(filings_10k),
119
- "20-F": len(filings_20f)
120
- },
121
- "latest_filings": [
122
- {"form": f.get("form_type"), "date": f.get("filing_date")}
123
- for f in (filings_10k + filings_20f)[:5]
124
- ]
125
- },
126
- "suggestion": "Try using get_latest_financial_data or get_financial_data with a specific period"
127
- }
128
- except Exception as e:
129
- return {
130
- "error": f"Exception extracting metrics: {str(e)}",
131
- "cik": cik,
132
- "years": years
133
- }
134
-
135
 
136
  @mcp.tool()
137
  def get_latest_financial_data(cik: str) -> dict:
138
  """Get the most recent financial data available."""
139
  result = financial_analyzer.get_latest_financial_data(cik)
140
- if result and "period" in result:
141
- return result
142
- else:
143
- return {"error": f"No latest financial data found for CIK: {cik}"}
144
-
145
 
146
  @mcp.tool()
147
  def advanced_search_company(company_input: str) -> dict:
148
  """Advanced search supporting both company name and CIK code."""
149
  result = financial_analyzer.search_company(company_input)
150
- if result.get("error"):
151
- return {"error": result["error"]}
152
- return result
153
 
 
 
 
 
 
 
 
 
154
 
155
- # Mount MCP server HTTP endpoint (support both /mcp and /sse for backward compatibility)
156
- @app.post("/mcp")
157
- @app.get("/mcp")
158
- @app.post("/sse")
159
- @app.get("/sse")
160
- async def mcp_http_endpoint(request: Request):
161
- """MCP HTTP endpoint - handle both GET and POST (accessible via /mcp or /sse)"""
162
  try:
163
- # For POST requests, get JSON body
164
- if request.method == "POST":
165
- body = await request.json()
166
-
167
- # Handle JSON-RPC request
168
- if body.get("method") == "tools/list":
169
- # List all tools
170
- tools = []
171
- for tool_name, tool_func in mcp._tool_manager._tools.items():
172
- tools.append({
173
- "name": tool_name,
174
- "description": tool_func.__doc__ or "",
175
- "inputSchema": {
176
- "type": "object",
177
- "properties": {},
178
- "required": []
179
- }
180
- })
181
-
182
- return JSONResponse({
183
- "jsonrpc": "2.0",
184
- "id": body.get("id"),
185
- "result": {"tools": tools}
186
- })
187
-
188
- elif body.get("method") == "tools/call":
189
- # Call a specific tool
190
- tool_name = body.get("params", {}).get("name")
191
- arguments = body.get("params", {}).get("arguments", {})
192
-
193
- # Directly call the tool functions we defined
194
- tool_map = {
195
- "search_company": search_company,
196
- "get_company_info": get_company_info,
197
- "get_company_filings": get_company_filings,
198
- "get_financial_data": get_financial_data,
199
- "extract_financial_metrics": extract_financial_metrics,
200
- "get_latest_financial_data": get_latest_financial_data,
201
- "advanced_search_company": advanced_search_company
202
- }
203
-
204
- if tool_name in tool_map:
205
- tool_func = tool_map[tool_name]
206
- result = tool_func(**arguments)
207
-
208
- return JSONResponse({
209
- "jsonrpc": "2.0",
210
- "id": body.get("id"),
211
- "result": {
212
- "content": [{
213
- "type": "text",
214
- "text": json.dumps(result)
215
- }]
216
- }
217
- })
218
- else:
219
- return JSONResponse({
220
- "jsonrpc": "2.0",
221
- "id": body.get("id"),
222
- "error": {
223
- "code": -32601,
224
- "message": f"Tool not found: {tool_name}"
225
- }
226
- })
227
-
228
- else:
229
- return JSONResponse({
230
- "jsonrpc": "2.0",
231
- "id": body.get("id"),
232
- "error": {
233
- "code": -32601,
234
- "message": f"Method not found: {body.get('method')}"
235
- }
236
- })
237
-
238
- else:
239
- # GET request - return info
240
- return JSONResponse({
241
- "service": "SEC Financial Data MCP Server",
242
- "protocol": "MCP 2024-11-05",
243
- "transport": "HTTP",
244
- "status": "online"
245
- })
246
-
247
  except Exception as e:
248
- return JSONResponse(
249
- content={"error": str(e), "type": "server_error"},
250
- status_code=500
251
- )
252
 
253
- # Get the template directory path
254
- TEMPLATES_DIR = Path(__file__).parent / "templates"
 
 
 
 
 
255
 
256
- @app.get("/", response_class=HTMLResponse)
257
- async def root():
258
- """Serve the main index page (ignores query parameters like ?logs=container)"""
259
- index_path = TEMPLATES_DIR / "index.html"
260
- if index_path.exists():
261
- return FileResponse(index_path, media_type="text/html")
262
- # Return error with debug info
263
- return HTMLResponse(
264
- f"<h1>Error: Template not found</h1>"
265
- f"<p>Looking for: {index_path}</p>"
266
- f"<p>Exists: {index_path.exists()}</p>"
267
- f"<p>Current dir: {Path(__file__).parent}</p>",
268
- status_code=404
269
- )
270
 
271
- @app.get("/test", response_class=HTMLResponse)
272
- async def test_page():
273
- """Serve the test page"""
274
- test_path = TEMPLATES_DIR / "test.html"
275
- if test_path.exists():
276
- return FileResponse(test_path)
277
- return HTMLResponse("<h1>Error: Test page not found</h1>", status_code=404)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
278
 
279
  if __name__ == "__main__":
280
- uvicorn.run(app, host="0.0.0.0", port=7860)
 
1
  """
2
+ SEC Financial Data MCP Server with Gradio UI
3
+ Provides standard MCP service + Web interface for testing
4
  """
5
+ import os
6
+ import gradio as gr
 
 
 
7
  import json
 
 
8
  from mcp.server.fastmcp import FastMCP
9
  from edgar_client import EdgarDataClient
10
  from financial_analyzer import FinancialAnalyzer
11
 
12
+ # Initialize FastMCP server
13
+ mcp = FastMCP("sec-financial-data")
 
 
 
 
 
 
 
 
14
 
15
  # Initialize EDGAR clients
16
  edgar_client = EdgarDataClient(
 
21
  user_agent="Juntao Peng Financial Report Metrics App (jtyxabc@gmail.com)"
22
  )
23
 
24
+ # Define MCP tools
 
 
 
25
  @mcp.tool()
26
  def search_company(company_name: str) -> dict:
27
  """Search for a company by name in SEC EDGAR database."""
28
  result = edgar_client.search_company_by_name(company_name)
29
+ return result if result else {"error": f"No company found with name: {company_name}"}
 
 
 
 
30
 
31
  @mcp.tool()
32
  def get_company_info(cik: str) -> dict:
33
  """Get detailed company information."""
34
  result = edgar_client.get_company_info(cik)
35
+ return result if result else {"error": f"No company found with CIK: {cik}"}
 
 
 
 
36
 
37
  @mcp.tool()
38
  def get_company_filings(cik: str, form_types: list[str] | None = None) -> dict:
39
  """Get list of company SEC filings."""
40
+ form_types_tuple = tuple(form_types) if form_types else None
 
 
 
 
 
41
  result = edgar_client.get_company_filings(cik, form_types_tuple)
42
  if result:
43
  limited_result = result[:20]
 
46
  "returned": len(limited_result),
47
  "filings": limited_result
48
  }
49
+ return {"error": f"No filings found for CIK: {cik}"}
 
 
50
 
51
  @mcp.tool()
52
  def get_financial_data(cik: str, period: str) -> dict:
53
  """Get financial data for a specific period."""
54
  result = edgar_client.get_financial_data_for_period(cik, period)
55
+ return result if result and "period" in result else {"error": f"No financial data found for CIK: {cik}, Period: {period}"}
 
 
 
 
56
 
57
  @mcp.tool()
58
  def extract_financial_metrics(cik: str, years: int = 3) -> dict:
 
60
  if years < 1 or years > 10:
61
  return {"error": "Years parameter must be between 1 and 10"}
62
 
63
+ metrics = financial_analyzer.extract_financial_metrics(cik, years)
64
+ if metrics:
65
+ formatted = financial_analyzer.format_financial_data(metrics)
66
+ return {"periods": len(formatted), "data": formatted}
67
+ return {"error": f"No financial metrics extracted for CIK: {cik}"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
 
69
  @mcp.tool()
70
  def get_latest_financial_data(cik: str) -> dict:
71
  """Get the most recent financial data available."""
72
  result = financial_analyzer.get_latest_financial_data(cik)
73
+ return result if result and "period" in result else {"error": f"No latest financial data found for CIK: {cik}"}
 
 
 
 
74
 
75
  @mcp.tool()
76
  def advanced_search_company(company_input: str) -> dict:
77
  """Advanced search supporting both company name and CIK code."""
78
  result = financial_analyzer.search_company(company_input)
79
+ return result if not result.get("error") else {"error": result["error"]}
 
 
80
 
81
+ # Gradio wrapper functions
82
+ def gradio_search_company(company_name: str):
83
+ """Gradio wrapper for search_company"""
84
+ try:
85
+ result = search_company(company_name)
86
+ return json.dumps(result, indent=2)
87
+ except Exception as e:
88
+ return json.dumps({"error": str(e)}, indent=2)
89
 
90
+ def gradio_get_company_info(cik: str):
91
+ """Gradio wrapper for get_company_info"""
 
 
 
 
 
92
  try:
93
+ result = get_company_info(cik)
94
+ return json.dumps(result, indent=2)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
  except Exception as e:
96
+ return json.dumps({"error": str(e)}, indent=2)
 
 
 
97
 
98
+ def gradio_extract_metrics(cik: str, years: int):
99
+ """Gradio wrapper for extract_financial_metrics"""
100
+ try:
101
+ result = extract_financial_metrics(cik, years)
102
+ return json.dumps(result, indent=2)
103
+ except Exception as e:
104
+ return json.dumps({"error": str(e)}, indent=2)
105
 
106
+ def gradio_get_latest(cik: str):
107
+ """Gradio wrapper for get_latest_financial_data"""
108
+ try:
109
+ result = get_latest_financial_data(cik)
110
+ return json.dumps(result, indent=2)
111
+ except Exception as e:
112
+ return json.dumps({"error": str(e)}, indent=2)
 
 
 
 
 
 
 
113
 
114
+ # Create Gradio interface
115
+ with gr.Blocks(title="SEC Financial Data MCP Server", theme=gr.themes.Soft()) as demo:
116
+ gr.Markdown("""
117
+ # πŸ“Š SEC Financial Data MCP Server
118
+
119
+ Access real-time SEC EDGAR financial data via Model Context Protocol
120
+
121
+ **MCP Endpoint:** Use `mcp_server_fastmcp.py` for standard MCP client connections
122
+ """)
123
+
124
+ with gr.Tab("πŸ” Search Company"):
125
+ gr.Markdown("### Search for a company by name")
126
+ with gr.Row():
127
+ with gr.Column():
128
+ company_input = gr.Textbox(label="Company Name", placeholder="Tesla", value="Tesla")
129
+ search_btn = gr.Button("Search", variant="primary")
130
+ with gr.Column():
131
+ search_output = gr.Code(label="Result", language="json", lines=15)
132
+ search_btn.click(gradio_search_company, inputs=company_input, outputs=search_output)
133
+
134
+ with gr.Tab("ℹ️ Company Info"):
135
+ gr.Markdown("### Get detailed company information")
136
+ with gr.Row():
137
+ with gr.Column():
138
+ cik_input = gr.Textbox(label="Company CIK", placeholder="0001318605", value="0001318605")
139
+ info_btn = gr.Button("Get Info", variant="primary")
140
+ with gr.Column():
141
+ info_output = gr.Code(label="Result", language="json", lines=15)
142
+ info_btn.click(gradio_get_company_info, inputs=cik_input, outputs=info_output)
143
+
144
+ with gr.Tab("πŸ“ˆ Financial Metrics"):
145
+ gr.Markdown("### Extract multi-year financial metrics ⭐")
146
+ with gr.Row():
147
+ with gr.Column():
148
+ metrics_cik = gr.Textbox(label="Company CIK", placeholder="0001318605", value="0001318605")
149
+ metrics_years = gr.Slider(minimum=1, maximum=10, value=3, step=1, label="Years")
150
+ metrics_btn = gr.Button("Extract Metrics", variant="primary")
151
+ with gr.Column():
152
+ metrics_output = gr.Code(label="Result", language="json", lines=20)
153
+ metrics_btn.click(gradio_extract_metrics, inputs=[metrics_cik, metrics_years], outputs=metrics_output)
154
+
155
+ with gr.Tab("πŸ†• Latest Data"):
156
+ gr.Markdown("### Get latest financial data")
157
+ with gr.Row():
158
+ with gr.Column():
159
+ latest_cik = gr.Textbox(label="Company CIK", placeholder="0001318605", value="0001318605")
160
+ latest_btn = gr.Button("Get Latest", variant="primary")
161
+ with gr.Column():
162
+ latest_output = gr.Code(label="Result", language="json", lines=15)
163
+ latest_btn.click(gradio_get_latest, inputs=latest_cik, outputs=latest_output)
164
+
165
+ with gr.Tab("πŸ“– Documentation"):
166
+ gr.Markdown("""
167
+ ## πŸ› οΈ Available Tools (7)
168
+
169
+ 1. **search_company** - Search by company name
170
+ 2. **get_company_info** - Get company details by CIK
171
+ 3. **get_company_filings** - List SEC filings
172
+ 4. **get_financial_data** - Get specific period data
173
+ 5. **extract_financial_metrics** ⭐ - Multi-year trends
174
+ 6. **get_latest_financial_data** - Latest snapshot
175
+ 7. **advanced_search_company** - Flexible search
176
+
177
+ ## πŸ”— MCP Integration
178
+
179
+ For MCP client integration, run:
180
+ ```bash
181
+ python mcp_server_fastmcp.py
182
+ ```
183
+
184
+ Then configure your MCP client (e.g., Claude Desktop):
185
+ ```json
186
+ {
187
+ "mcpServers": {
188
+ "sec-financial-data": {
189
+ "command": "python",
190
+ "args": ["path/to/mcp_server_fastmcp.py"]
191
+ }
192
+ }
193
+ }
194
+ ```
195
+
196
+ ## πŸ“Š Data Source
197
+
198
+ - **SEC EDGAR API** - Official SEC data
199
+ - **Financial Statements** - 10-K, 10-Q, 20-F forms
200
+ - **XBRL Data** - Structured metrics
201
+ """)
202
 
203
  if __name__ == "__main__":
204
+ demo.launch(server_name="0.0.0.0", server_port=7860)
requirements.txt CHANGED
@@ -1,6 +1,15 @@
1
- mcp[cli]==1.2.0
 
 
 
 
 
 
 
2
  fastapi==0.109.0
3
  uvicorn[standard]>=0.30
4
  pydantic>=2.10.1
 
 
5
  sec-edgar-api==1.1.0
6
  requests==2.31.0
 
1
+ # MCP Server dependencies
2
+ mcp>=1.0.0
3
+ fastmcp>=0.1.0
4
+
5
+ # Web UI
6
+ gradio>=4.44.0,<5.0.0
7
+
8
+ # Web framework (for MCP endpoints)
9
  fastapi==0.109.0
10
  uvicorn[standard]>=0.30
11
  pydantic>=2.10.1
12
+
13
+ # SEC EDGAR data access
14
  sec-edgar-api==1.1.0
15
  requests==2.31.0