File size: 16,229 Bytes
b5a5403
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
05f9632
b5a5403
 
b450591
05f9632
b450591
05f9632
 
b450591
b5a5403
 
 
 
 
05f9632
b5a5403
 
 
 
 
05f9632
b5a5403
 
 
 
 
 
 
 
 
 
 
 
 
05f9632
b5a5403
 
 
 
 
b450591
05f9632
b450591
05f9632
 
b450591
 
05f9632
 
b450591
05f9632
 
b450591
b5a5403
 
 
 
 
 
 
 
 
 
 
b450591
05f9632
b450591
05f9632
 
 
b450591
b5a5403
 
 
 
 
b450591
05f9632
b450591
05f9632
 
b450591
b5a5403
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
edc6df9
b5a5403
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
"""

SEC Financial Data MCP Server with Gradio UI

Provides standard MCP service + Web interface for testing

"""
import os
import gradio as gr
import json
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from mcp.server.fastmcp import FastMCP
from edgar_client import EdgarDataClient
from financial_analyzer import FinancialAnalyzer

# Initialize FastMCP server
mcp = FastMCP("sec-financial-data")

# Initialize EDGAR clients
edgar_client = EdgarDataClient(
    user_agent="Juntao Peng Financial Report Metrics App (jtyxabc@gmail.com)"
)

financial_analyzer = FinancialAnalyzer(
    user_agent="Juntao Peng Financial Report Metrics App (jtyxabc@gmail.com)"
)

# Define MCP tools with optimized descriptions for LLM
@mcp.tool()
def search_company(company_name: str) -> dict:
    """

    [STEP 1] Find company CIK by name/ticker. Always call this FIRST before other tools.

    

    Args: company_name - Company name or ticker (e.g. "Tesla", "TSLA", "google")

    Returns: {cik, name, ticker} - Use cik for subsequent tool calls

    """
    result = edgar_client.search_company_by_name(company_name)
    return result if result else {"error": f"No company found with name: {company_name}"}

@mcp.tool()
def get_company_info(cik: str) -> dict:
    """Get company details (name, SIC, industry). Requires CIK from search_company."""
    result = edgar_client.get_company_info(cik)
    return result if result else {"error": f"No company found with CIK: {cik}"}

@mcp.tool()
def get_company_filings(cik: str, form_types: list[str] | None = None) -> dict:
    """List SEC filings (10-K, 10-Q, etc). Requires CIK. Use form_types to filter."""
    form_types_tuple = tuple(form_types) if form_types else None
    result = edgar_client.get_company_filings(cik, form_types_tuple)
    if result:
        limited_result = result[:20]
        return {
            "total": len(result),
            "returned": len(limited_result),
            "filings": limited_result
        }
    return {"error": f"No filings found for CIK: {cik}"}

@mcp.tool()
def get_financial_data(cik: str, period: str) -> dict:
    """Get financial data for specific period (e.g. '2024', '2024Q3'). Requires CIK."""
    result = edgar_client.get_financial_data_for_period(cik, period)
    return result if result and "period" in result else {"error": f"No financial data found for CIK: {cik}, Period: {period}"}

@mcp.tool()
def extract_financial_metrics(cik: str, years: int = 3) -> dict:
    """

    [PRIMARY TOOL] Get multi-year financials (annual+quarterly). Includes latest data.

    

    Use for: Any financial query (trends, growth, comparisons, or latest data)

    Workflow: search_company(name) β†’ extract_financial_metrics(cik, years)

    

    Args:

        cik: 10-digit CIK from search_company

        years: Years to retrieve (1-10, default 3)

    

    Returns: {periods, data: [{period, revenue, net_income, eps, ...}]}

    Note: Data includes latest year, so no need to call get_latest_financial_data separately.

    """
    if years < 1 or years > 10:
        return {"error": "Years parameter must be between 1 and 10"}
    
    metrics = financial_analyzer.extract_financial_metrics(cik, years)
    if metrics:
        formatted = financial_analyzer.format_financial_data(metrics)
        return {"periods": len(formatted), "data": formatted}
    return {"error": f"No financial metrics extracted for CIK: {cik}"}

@mcp.tool()
def get_latest_financial_data(cik: str) -> dict:
    """

    Get ONLY most recent annual report. Use extract_financial_metrics instead (it includes latest).

    

    Only use when: User explicitly wants ONLY the latest snapshot (rare case).

    Args: cik - 10-digit CIK from search_company

    Returns: {period, revenue, net_income, eps, ...}

    """
    result = financial_analyzer.get_latest_financial_data(cik)
    return result if result and "period" in result else {"error": f"No latest financial data found for CIK: {cik}"}

@mcp.tool()
def advanced_search_company(company_input: str) -> dict:
    """

    Alternative to search_company. Accepts company name OR CIK. Same output format.

    

    Args: company_input - Name, ticker, or CIK

    Returns: {cik, name, ticker}

    """
    result = financial_analyzer.search_company(company_input)
    return result if not result.get("error") else {"error": result["error"]}

# Gradio wrapper functions (ζ·»εŠ θ°ƒθ―•ε’ŒθΆ…ζ—Άε€„η†)
def gradio_search_company(company_name: str):
    """Gradio wrapper for search_company"""
    if not company_name or not company_name.strip():
        return json.dumps({"error": "Company name cannot be empty"}, indent=2)
    try:
        import sys
        print(f"[DEBUG] Searching company: {company_name.strip()}", file=sys.stderr)
        result = search_company(company_name.strip())
        print(f"[DEBUG] Search result type: {type(result)}", file=sys.stderr)
        print(f"[DEBUG] Search result: {result}", file=sys.stderr)
        # Ensure result is a dict
        if not isinstance(result, dict):
            result = {"error": f"Unexpected result type: {type(result)}"}
        return json.dumps(result, indent=2)
    except TimeoutError as e:
        return json.dumps({"error": f"Request timeout: {str(e)}"}, indent=2)
    except Exception as e:
        import traceback
        traceback.print_exc()
        return json.dumps({"error": f"Exception: {str(e)}", "type": str(type(e))}, indent=2)

def gradio_get_company_info(cik: str):
    """Gradio wrapper for get_company_info"""
    if not cik or not cik.strip():
        return json.dumps({"error": "CIK cannot be empty"}, indent=2)
    try:
        import sys
        print(f"[DEBUG] Getting company info for CIK: {cik.strip()}", file=sys.stderr)
        result = get_company_info(cik.strip())
        print(f"[DEBUG] Company info result: {result}", file=sys.stderr)
        if not isinstance(result, dict):
            result = {"error": f"Unexpected result type: {type(result)}"}
        return json.dumps(result, indent=2)
    except TimeoutError as e:
        return json.dumps({"error": f"Request timeout: {str(e)}"}, indent=2)
    except Exception as e:
        import traceback
        traceback.print_exc()
        return json.dumps({"error": f"Exception: {str(e)}", "type": str(type(e))}, indent=2)

def gradio_extract_metrics(cik: str, years: float):
    """Gradio wrapper for extract_financial_metrics"""
    if not cik or not cik.strip():
        return json.dumps({"error": "CIK cannot be empty"}, indent=2)
    try:
        import sys
        years_int = int(years)
        print(f"[DEBUG] Extracting metrics for CIK: {cik.strip()}, Years: {years_int}", file=sys.stderr)
        result = extract_financial_metrics(cik.strip(), years_int)
        print(f"[DEBUG] Extract metrics result: {result}", file=sys.stderr)
        if not isinstance(result, dict):
            result = {"error": f"Unexpected result type: {type(result)}"}
        return json.dumps(result, indent=2)
    except TimeoutError as e:
        return json.dumps({"error": f"Request timeout: {str(e)}"}, indent=2)
    except Exception as e:
        import traceback
        traceback.print_exc()
        return json.dumps({"error": f"Exception: {str(e)}", "type": str(type(e))}, indent=2)

def gradio_get_latest(cik: str):
    """Gradio wrapper for get_latest_financial_data"""
    if not cik or not cik.strip():
        return json.dumps({"error": "CIK cannot be empty"}, indent=2)
    try:
        import sys
        print(f"[DEBUG] Getting latest data for CIK: {cik.strip()}", file=sys.stderr)
        result = get_latest_financial_data(cik.strip())
        print(f"[DEBUG] Latest data result: {result}", file=sys.stderr)
        if not isinstance(result, dict):
            result = {"error": f"Unexpected result type: {type(result)}"}
        return json.dumps(result, indent=2)
    except TimeoutError as e:
        return json.dumps({"error": f"Request timeout: {str(e)}"}, indent=2)
    except Exception as e:
        import traceback
        traceback.print_exc()
        return json.dumps({"error": f"Exception: {str(e)}", "type": str(type(e))}, indent=2)

# Mount MCP HTTP endpoints for client integration
app = FastAPI()

@app.post("/mcp")
@app.get("/mcp")
@app.post("/sse")
@app.get("/sse")
async def mcp_http_endpoint(request: Request):
    """MCP HTTP endpoint - handle both GET and POST (accessible via /mcp or /sse)"""
    try:
        # For POST requests, get JSON body
        if request.method == "POST":
            body = await request.json()
            
            # Handle JSON-RPC request
            if body.get("method") == "tools/list":
                # List all tools
                tools = []
                for tool_name, tool_func in mcp._tool_manager._tools.items():
                    tools.append({
                        "name": tool_name,
                        "description": tool_func.__doc__ or "",
                        "inputSchema": {
                            "type": "object",
                            "properties": {},
                            "required": []
                        }
                    })
                
                return JSONResponse({
                    "jsonrpc": "2.0",
                    "id": body.get("id"),
                    "result": {"tools": tools}
                })
            
            elif body.get("method") == "tools/call":
                # Call a specific tool
                tool_name = body.get("params", {}).get("name")
                arguments = body.get("params", {}).get("arguments", {})
                
                # Directly call the tool functions we defined
                tool_map = {
                    "search_company": search_company,
                    "get_company_info": get_company_info,
                    "get_company_filings": get_company_filings,
                    "get_financial_data": get_financial_data,
                    "extract_financial_metrics": extract_financial_metrics,
                    "get_latest_financial_data": get_latest_financial_data,
                    "advanced_search_company": advanced_search_company
                }
                
                if tool_name in tool_map:
                    tool_func = tool_map[tool_name]
                    result = tool_func(**arguments)
                    
                    return JSONResponse({
                        "jsonrpc": "2.0",
                        "id": body.get("id"),
                        "result": {
                            "content": [{
                                "type": "text",
                                "text": json.dumps(result)
                            }]
                        }
                    })
                else:
                    return JSONResponse({
                        "jsonrpc": "2.0",
                        "id": body.get("id"),
                        "error": {
                            "code": -32601,
                            "message": f"Tool not found: {tool_name}"
                        }
                    })
            
            else:
                return JSONResponse({
                    "jsonrpc": "2.0",
                    "id": body.get("id"),
                    "error": {
                        "code": -32601,
                        "message": f"Method not found: {body.get('method')}"
                    }
                })
        
        else:
            # GET request - return info
            return JSONResponse({
                "service": "SEC Financial Data MCP Server",
                "protocol": "MCP 2024-11-05",
                "transport": "HTTP",
                "status": "online"
            })
    
    except Exception as e:
        return JSONResponse(
            content={"error": str(e), "type": "server_error"},
            status_code=500
        )

# Create Gradio interface
with gr.Blocks(title="SEC Financial Data MCP Server") as demo:
    gr.Markdown("""

    # πŸ“Š SEC Financial Data MCP Server

    

    Access real-time SEC EDGAR financial data via Model Context Protocol

    

    **MCP Endpoint:** Use `mcp_server_fastmcp.py` for standard MCP client connections

    """)
    
    with gr.Tab("πŸ” Search Company"):
        gr.Markdown("### Search for a company by name")
        with gr.Row():
            with gr.Column():
                company_input = gr.Textbox(label="Company Name", placeholder="Tesla", value="Tesla")
                search_btn = gr.Button("Search", variant="primary")
            with gr.Column():
                search_output = gr.Code(label="Result", language="json", lines=15)
        search_btn.click(gradio_search_company, inputs=company_input, outputs=search_output)
    
    with gr.Tab("ℹ️ Company Info"):
        gr.Markdown("### Get detailed company information")
        with gr.Row():
            with gr.Column():
                cik_input = gr.Textbox(label="Company CIK", placeholder="0001318605", value="0001318605")
                info_btn = gr.Button("Get Info", variant="primary")
            with gr.Column():
                info_output = gr.Code(label="Result", language="json", lines=15)
        info_btn.click(gradio_get_company_info, inputs=cik_input, outputs=info_output)
    
    with gr.Tab("πŸ“ˆ Financial Metrics"):
        gr.Markdown("### Extract multi-year financial metrics ⭐")
        with gr.Row():
            with gr.Column():
                metrics_cik = gr.Textbox(label="Company CIK", placeholder="0001318605", value="0001318605")
                metrics_years = gr.Slider(minimum=1, maximum=10, value=3, step=1, label="Years")
                metrics_btn = gr.Button("Extract Metrics", variant="primary")
            with gr.Column():
                metrics_output = gr.Code(label="Result", language="json", lines=20)
        metrics_btn.click(gradio_extract_metrics, inputs=[metrics_cik, metrics_years], outputs=metrics_output)
    
    with gr.Tab("πŸ†• Latest Data"):
        gr.Markdown("### Get latest financial data")
        with gr.Row():
            with gr.Column():
                latest_cik = gr.Textbox(label="Company CIK", placeholder="0001318605", value="0001318605")
                latest_btn = gr.Button("Get Latest", variant="primary")
            with gr.Column():
                latest_output = gr.Code(label="Result", language="json", lines=15)
        latest_btn.click(gradio_get_latest, inputs=latest_cik, outputs=latest_output)
    
    with gr.Tab("πŸ“– Documentation"):
        gr.Markdown("""

        ## πŸ› οΈ Available Tools (7)

        

        1. **search_company** - Search by company name

        2. **get_company_info** - Get company details by CIK

        3. **get_company_filings** - List SEC filings

        4. **get_financial_data** - Get specific period data

        5. **extract_financial_metrics** ⭐ - Multi-year trends

        6. **get_latest_financial_data** - Latest snapshot

        7. **advanced_search_company** - Flexible search

        

        ## πŸ”— MCP Integration

        

        For MCP client integration, run:

        ```bash

        python mcp_server_fastmcp.py

        ```

        

        Then configure your MCP client (e.g., Claude Desktop):

        ```json

        {

          "mcpServers": {

            "sec-financial-data": {

              "command": "python",

              "args": ["path/to/mcp_server_fastmcp.py"]

            }

          }

        }

        ```

        

        ## πŸ“Š Data Source

        

        - **SEC EDGAR API** - Official SEC data

        - **Financial Statements** - 10-K, 10-Q, 20-F forms

        - **XBRL Data** - Structured metrics

        """)

# Mount Gradio app to FastAPI
app = gr.mount_gradio_app(app, demo, path="/")

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=7860)