JC321 commited on
Commit
b5a5403
·
verified ·
1 Parent(s): 2b5eb7d

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +357 -394
app.py CHANGED
@@ -1,394 +1,357 @@
1
- import gradio as gr
2
- import requests
3
- import json
4
- import os
5
- import warnings
6
- from huggingface_hub import InferenceClient
7
-
8
- # 抑制 asyncio 警告
9
- warnings.filterwarnings('ignore', category=DeprecationWarning)
10
- os.environ['PYTHONWARNINGS'] = 'ignore'
11
-
12
- # 如果在 GPU 环境但不需要 GPU,禁用 CUDA
13
- if 'CUDA_VISIBLE_DEVICES' not in os.environ:
14
- os.environ['CUDA_VISIBLE_DEVICES'] = ''
15
-
16
- # ========== MCP 工具简化定义(符合MCP协议标准) ==========
17
- MCP_TOOLS = [
18
- {"type": "function", "function": {"name": "advanced_search_company", "description": "Search US companies", "parameters": {"type": "object", "properties": {"company_input": {"type": "string"}}, "required": ["company_input"]}}},
19
- {"type": "function", "function": {"name": "get_latest_financial_data", "description": "Get latest financial data", "parameters": {"type": "object", "properties": {"cik": {"type": "string"}}, "required": ["cik"]}}},
20
- {"type": "function", "function": {"name": "extract_financial_metrics", "description": "Get multi-year trends", "parameters": {"type": "object", "properties": {"cik": {"type": "string"}, "years": {"type": "integer"}}, "required": ["cik", "years"]}}},
21
- {"type": "function", "function": {"name": "get_quote", "description": "Get stock quote", "parameters": {"type": "object", "properties": {"symbol": {"type": "string"}}, "required": ["symbol"]}}},
22
- {"type": "function", "function": {"name": "get_market_news", "description": "Get market news", "parameters": {"type": "object", "properties": {"category": {"type": "string"}}, "required": ["category"]}}},
23
- {"type": "function", "function": {"name": "get_company_news", "description": "Get company news", "parameters": {"type": "object", "properties": {"symbol": {"type": "string"}, "from_date": {"type": "string"}, "to_date": {"type": "string"}}, "required": ["symbol"]}}}
24
- ]
25
-
26
- # ========== MCP 服务配置 ==========
27
- MCP_SERVICES = {
28
- "financial": {"url": "https://huggingface.co/spaces/JC321/EasyReportDataMCP", "type": "fastmcp"},
29
- "market": {"url": "https://jc321-marketandstockmcp.hf.space", "type": "gradio"}
30
- }
31
-
32
- TOOL_ROUTING = {
33
- "advanced_search_company": MCP_SERVICES["financial"],
34
- "get_latest_financial_data": MCP_SERVICES["financial"],
35
- "extract_financial_metrics": MCP_SERVICES["financial"],
36
- "get_quote": MCP_SERVICES["market"],
37
- "get_market_news": MCP_SERVICES["market"],
38
- "get_company_news": MCP_SERVICES["market"]
39
- }
40
-
41
- # ========== 初始化 LLM 客户端 ==========
42
- hf_token = os.environ.get("HF_TOKEN") or os.environ.get("HUGGING_FACE_HUB_TOKEN")
43
- client = InferenceClient(api_key=hf_token) if hf_token else InferenceClient()
44
- print(f"✅ LLM initialized: Qwen/Qwen3-32B:groq")
45
- print(f"📊 MCP Services: {len(MCP_SERVICES)} services, {len(MCP_TOOLS)} tools")
46
-
47
- # ========== Token 限制配置 ==========
48
- # HuggingFace Inference API 实际限制约 8000-16000 tokens
49
- # 为了安全,设置更低的限制
50
- MAX_TOTAL_TOKENS = 6000 # 总上下文限制
51
- MAX_TOOL_RESULT_CHARS = 1500 # 工具返回最大字符数 (增加到1500)
52
- MAX_HISTORY_CHARS = 500 # 单条历史消息最大字符数
53
- MAX_HISTORY_TURNS = 2 # 最大历史轮数
54
- MAX_TOOL_ITERATIONS = 6 # 最大工具调用轮数 (增加到6,支持多工具调用)
55
- MAX_OUTPUT_TOKENS = 2000 # 最大输出 tokens (增加到2000)
56
-
57
- def estimate_tokens(text):
58
- """估算文本 token 数量(粗略:1 token ≈ 2 字符)"""
59
- return len(str(text)) // 2
60
-
61
- def truncate_text(text, max_chars, suffix="...[truncated]"):
62
- """截断文本到指定长度"""
63
- text = str(text)
64
- if len(text) <= max_chars:
65
- return text
66
- return text[:max_chars] + suffix
67
-
68
- def get_system_prompt():
69
- """生成包含当前日期的系统提示词(精简版)"""
70
- from datetime import datetime
71
- current_date = datetime.now().strftime("%Y-%m-%d")
72
- return f"""Financial analyst. Today: {current_date}. Use tools for company data, stock prices, news. Be concise."""
73
-
74
- # ============================================================
75
- # MCP 服务调用核心代码区
76
- # 支持 FastMCP (JSON-RPC) 和 Gradio (SSE) 两种协议
77
- # ============================================================
78
-
79
- def call_mcp_tool(tool_name, arguments):
80
- """调用 MCP 工具"""
81
- service_config = TOOL_ROUTING.get(tool_name)
82
- if not service_config:
83
- return {"error": f"Unknown tool: {tool_name}"}
84
-
85
- try:
86
- if service_config["type"] == "fastmcp":
87
- return _call_fastmcp(service_config["url"], tool_name, arguments)
88
- elif service_config["type"] == "gradio":
89
- return _call_gradio_api(service_config["url"], tool_name, arguments)
90
- else:
91
- return {"error": "Unknown service type"}
92
- except Exception as e:
93
- return {"error": str(e)}
94
-
95
-
96
- def _call_fastmcp(service_url, tool_name, arguments):
97
- """FastMCP: 标准 MCP JSON-RPC"""
98
- response = requests.post(
99
- service_url,
100
- json={"jsonrpc": "2.0", "method": "tools/call", "params": {"name": tool_name, "arguments": arguments}, "id": 1},
101
- headers={"Content-Type": "application/json"},
102
- timeout=30
103
- )
104
-
105
- if response.status_code != 200:
106
- return {"error": f"HTTP {response.status_code}"}
107
-
108
- data = response.json()
109
-
110
- # 解包 MCP 协议: jsonrpc -> result -> content[0].text -> JSON
111
- if isinstance(data, dict) and "result" in data:
112
- result = data["result"]
113
- if isinstance(result, dict) and "content" in result:
114
- content = result["content"]
115
- if isinstance(content, list) and len(content) > 0:
116
- first_item = content[0]
117
- if isinstance(first_item, dict) and "text" in first_item:
118
- try:
119
- return json.loads(first_item["text"])
120
- except (json.JSONDecodeError, TypeError):
121
- return {"text": first_item["text"]}
122
- return result
123
- return data
124
-
125
-
126
- def _call_gradio_api(service_url, tool_name, arguments):
127
- """Gradio: SSE 流式协议"""
128
- tool_map = {"get_quote": "test_quote_tool", "get_market_news": "test_market_news_tool", "get_company_news": "test_company_news_tool"}
129
- gradio_fn = tool_map.get(tool_name)
130
- if not gradio_fn:
131
- return {"error": "No mapping"}
132
-
133
- # 构造参数
134
- if tool_name == "get_quote":
135
- params = [arguments.get("symbol", "")]
136
- elif tool_name == "get_market_news":
137
- params = [arguments.get("category", "general")]
138
- elif tool_name == "get_company_news":
139
- params = [arguments.get("symbol", ""), arguments.get("from_date", ""), arguments.get("to_date", "")]
140
- else:
141
- params = []
142
-
143
- # 提交请求
144
- call_url = f"{service_url}/call/{gradio_fn}"
145
- resp = requests.post(call_url, json={"data": params}, timeout=10)
146
- if resp.status_code != 200:
147
- return {"error": f"HTTP {resp.status_code}"}
148
-
149
- event_id = resp.json().get("event_id")
150
- if not event_id:
151
- return {"error": "No event_id"}
152
-
153
- # 获取结果 (SSE)
154
- result_resp = requests.get(f"{call_url}/{event_id}", stream=True, timeout=20)
155
- if result_resp.status_code != 200:
156
- return {"error": f"HTTP {result_resp.status_code}"}
157
-
158
- # 解析 SSE
159
- for line in result_resp.iter_lines():
160
- if line and line.decode('utf-8').startswith('data: '):
161
- try:
162
- result_data = json.loads(line.decode('utf-8')[6:])
163
- if isinstance(result_data, list) and len(result_data) > 0:
164
- return {"text": result_data[0]}
165
- except json.JSONDecodeError:
166
- continue
167
-
168
- return {"error": "No result"}
169
-
170
- # ============================================================
171
- # End of MCP 服务调用代码区
172
- # ============================================================
173
-
174
- def chatbot_response(message, history):
175
- """AI 助手主函数(流式输出,性能优化)"""
176
- try:
177
- messages = [{"role": "system", "content": get_system_prompt()}]
178
-
179
- # 添加历史(最近2轮) - 严格限制上下文长度
180
- if history:
181
- for item in history[-MAX_HISTORY_TURNS:]:
182
- if isinstance(item, (list, tuple)) and len(item) == 2:
183
- # 用户消息(不截断)
184
- messages.append({"role": "user", "content": item[0]})
185
-
186
- # 助手回复(严格截断)
187
- assistant_msg = str(item[1])
188
- if len(assistant_msg) > MAX_HISTORY_CHARS:
189
- assistant_msg = truncate_text(assistant_msg, MAX_HISTORY_CHARS)
190
- messages.append({"role": "assistant", "content": assistant_msg})
191
-
192
- messages.append({"role": "user", "content": message})
193
-
194
- tool_calls_log = []
195
-
196
- # LLM 调用循环(支持多轮工具调用)
197
- final_response_content = None
198
- for iteration in range(MAX_TOOL_ITERATIONS):
199
- response = client.chat.completions.create(
200
- model="Qwen/Qwen3-32B:groq",
201
- messages=messages,
202
- tools=MCP_TOOLS,
203
- max_tokens=MAX_OUTPUT_TOKENS,
204
- temperature=0.5,
205
- tool_choice="auto",
206
- stream=False
207
- )
208
-
209
- choice = response.choices[0]
210
-
211
- if choice.message.tool_calls:
212
- messages.append(choice.message)
213
-
214
- for tool_call in choice.message.tool_calls:
215
- tool_name = tool_call.function.name
216
- try:
217
- tool_args = json.loads(tool_call.function.arguments)
218
- except json.JSONDecodeError:
219
- tool_args = {}
220
-
221
- # 调用 MCP 工具
222
- tool_result = call_mcp_tool(tool_name, tool_args)
223
-
224
- # 检查错误
225
- if isinstance(tool_result, dict) and "error" in tool_result:
226
- # 工具调用失败,记录错误
227
- tool_calls_log.append({"name": tool_name, "arguments": tool_args, "result": tool_result, "error": True})
228
- result_for_llm = json.dumps({"error": tool_result.get("error", "Unknown error")}, ensure_ascii=False)
229
- else:
230
- # 限制返回结果大小
231
- result_str = json.dumps(tool_result, ensure_ascii=False)
232
-
233
- if len(result_str) > MAX_TOOL_RESULT_CHARS:
234
- if isinstance(tool_result, dict) and "text" in tool_result:
235
- truncated_text = truncate_text(tool_result["text"], MAX_TOOL_RESULT_CHARS - 50)
236
- tool_result_truncated = {"text": truncated_text, "_truncated": True}
237
- elif isinstance(tool_result, dict):
238
- truncated = {}
239
- char_count = 0
240
- for k, v in list(tool_result.items())[:8]: # 保留前8个字段
241
- v_str = str(v)[:300] # 每个值最多300字符
242
- truncated[k] = v_str
243
- char_count += len(k) + len(v_str)
244
- if char_count > MAX_TOOL_RESULT_CHARS:
245
- break
246
- tool_result_truncated = {**truncated, "_truncated": True}
247
- else:
248
- tool_result_truncated = {"preview": truncate_text(result_str, MAX_TOOL_RESULT_CHARS), "_truncated": True}
249
- result_for_llm = json.dumps(tool_result_truncated, ensure_ascii=False)
250
- else:
251
- result_for_llm = result_str
252
-
253
- # 记录成功的工具调用
254
- tool_calls_log.append({"name": tool_name, "arguments": tool_args, "result": tool_result})
255
-
256
- messages.append({
257
- "role": "tool",
258
- "name": tool_name,
259
- "content": result_for_llm,
260
- "tool_call_id": tool_call.id
261
- })
262
-
263
- continue
264
- else:
265
- # 没有更多工具调用,保存最终答案
266
- final_response_content = choice.message.content
267
- break
268
-
269
- # 构建响应前缀(简化版)
270
- response_prefix = ""
271
-
272
- # 显示工具调用(使用原生HTML details标签)
273
- if tool_calls_log:
274
- response_prefix += """<div style='margin-bottom: 15px;'>
275
- <div style='background: #f0f0f0; padding: 8px 12px; border-radius: 6px; font-weight: 600; color: #333;'>
276
- 🛠️ Tools Used ({} calls)
277
- </div>
278
- """.format(len(tool_calls_log))
279
-
280
- for idx, tool_call in enumerate(tool_calls_log):
281
- # 预先计算 JSON 字符串,避免重复调用
282
- args_json = json.dumps(tool_call['arguments'], ensure_ascii=False)
283
- result_json = json.dumps(tool_call.get('result', {}), ensure_ascii=False, indent=2)
284
- result_preview = result_json[:1500] + ('...' if len(result_json) > 1500 else '')
285
-
286
- # 显示错误状态
287
- error_indicator = " Error" if tool_call.get('error') else ""
288
-
289
- # 使用原生 HTML5 details/summary 标签(不需要 JavaScript)
290
- response_prefix += f"""<details style='margin: 8px 0; border: 1px solid #ddd; border-radius: 6px; overflow: hidden;'>
291
- <summary style='background: #fff; padding: 10px; cursor: pointer; user-select: none; list-style: none;'>
292
- <div style='display: flex; justify-content: space-between; align-items: center;'>
293
- <div style='flex: 1;'>
294
- <strong style='color: #2c5aa0;'>📌 {idx+1}. {tool_call['name']}{error_indicator}</strong>
295
- <div style='font-size: 0.85em; color: #666; margin-top: 4px;'>📥 Input: <code style='background: #f5f5f5; padding: 2px 6px; border-radius: 3px;'>{args_json}</code></div>
296
- </div>
297
- <span style='font-size: 1.2em; color: #999; margin-left: 10px;'>▶</span>
298
- </div>
299
- </summary>
300
- <div style='background: #f9f9f9; padding: 12px; border-top: 1px solid #eee;'>
301
- <div style='font-size: 0.9em; color: #333;'>
302
- <strong>📤 Output:</strong>
303
- <pre style='background: #fff; padding: 10px; border-radius: 4px; overflow-x: auto; margin-top: 6px; font-size: 0.85em; border: 1px solid #e0e0e0; max-height: 400px; white-space: pre-wrap;'>{result_preview}</pre>
304
- </div>
305
- </div>
306
- </details>
307
- """
308
-
309
- response_prefix += """</div>
310
-
311
- ---
312
-
313
- """
314
- response_prefix += "\n"
315
-
316
- # 流式输出最终答案
317
- yield response_prefix
318
-
319
- # 如果已经有最终答案,直接输出
320
- if final_response_content:
321
- # 已经从循环中获得了最终答案,直接输出
322
- yield response_prefix + final_response_content
323
- else:
324
- # 如果循环结束但没有最终答案(达到最大迭代次数),需要再调用一次让模型总结
325
- try:
326
- stream = client.chat.completions.create(
327
- model="Qwen/Qwen3-32B:groq",
328
- messages=messages,
329
- tools=None, # 不再允许调用工具
330
- max_tokens=MAX_OUTPUT_TOKENS,
331
- temperature=0.5,
332
- stream=True
333
- )
334
-
335
- accumulated_text = ""
336
- for chunk in stream:
337
- if chunk.choices and len(chunk.choices) > 0 and chunk.choices[0].delta.content:
338
- accumulated_text += chunk.choices[0].delta.content
339
- yield response_prefix + accumulated_text
340
- except Exception as stream_error:
341
- # 流式输出失败,尝试非流式
342
- final_resp = client.chat.completions.create(
343
- model="Qwen/Qwen3-32B:groq",
344
- messages=messages,
345
- tools=None,
346
- max_tokens=MAX_OUTPUT_TOKENS,
347
- temperature=0.5,
348
- stream=False
349
- )
350
- yield response_prefix + final_resp.choices[0].message.content
351
-
352
- except Exception as e:
353
- import traceback
354
- error_detail = str(e)
355
- if "500" in error_detail:
356
- yield f"❌ Error: 模型服务器错误。可能是数据太大或请求超时。\n\n详细信息: {error_detail[:200]}"
357
- else:
358
- yield f"❌ Error: {error_detail}\n\n{traceback.format_exc()[:500]}"
359
-
360
- # ========== Gradio 界面(极简版)==========
361
- with gr.Blocks(title="Financial AI Assistant") as demo:
362
- gr.Markdown("# 💬 Financial AI Assistant")
363
-
364
- chat = gr.ChatInterface(
365
- fn=chatbot_response,
366
- examples=[
367
- "What's Apple's latest revenue and profit?",
368
- "Show me NVIDIA's 3-year financial trends",
369
- "How is Tesla's stock performing today?",
370
- "Get the latest market news about crypto",
371
- "Compare Microsoft's latest earnings with its current stock price",
372
- ],
373
- chatbot=gr.Chatbot(height=600),
374
- )
375
-
376
- # 启动应用
377
- if __name__ == "__main__":
378
- import sys
379
-
380
- # 修复 asyncio 事件循环问题
381
- if sys.platform == 'linux':
382
- try:
383
- import asyncio
384
- asyncio.set_event_loop_policy(asyncio.DefaultEventLoopPolicy())
385
- except:
386
- pass
387
-
388
- demo.launch(
389
- server_name="0.0.0.0",
390
- server_port=7860,
391
- show_error=True,
392
- ssr_mode=False,
393
- quiet=False
394
- )
 
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 fastapi import FastAPI, Request
9
+ from fastapi.responses import JSONResponse
10
+ from mcp.server.fastmcp import FastMCP
11
+ from edgar_client import EdgarDataClient
12
+ from financial_analyzer import FinancialAnalyzer
13
+
14
+ # Initialize FastMCP server
15
+ mcp = FastMCP("sec-financial-data")
16
+
17
+ # Initialize EDGAR clients
18
+ edgar_client = EdgarDataClient(
19
+ user_agent="Juntao Peng Financial Report Metrics App (jtyxabc@gmail.com)"
20
+ )
21
+
22
+ financial_analyzer = FinancialAnalyzer(
23
+ user_agent="Juntao Peng Financial Report Metrics App (jtyxabc@gmail.com)"
24
+ )
25
+
26
+ # Define MCP tools
27
+ @mcp.tool()
28
+ def search_company(company_name: str) -> dict:
29
+ """Search for a company by name in SEC EDGAR database."""
30
+ result = edgar_client.search_company_by_name(company_name)
31
+ return result if result else {"error": f"No company found with name: {company_name}"}
32
+
33
+ @mcp.tool()
34
+ def get_company_info(cik: str) -> dict:
35
+ """Get detailed company information."""
36
+ result = edgar_client.get_company_info(cik)
37
+ return result if result else {"error": f"No company found with CIK: {cik}"}
38
+
39
+ @mcp.tool()
40
+ def get_company_filings(cik: str, form_types: list[str] | None = None) -> dict:
41
+ """Get list of company SEC filings."""
42
+ form_types_tuple = tuple(form_types) if form_types else None
43
+ result = edgar_client.get_company_filings(cik, form_types_tuple)
44
+ if result:
45
+ limited_result = result[:20]
46
+ return {
47
+ "total": len(result),
48
+ "returned": len(limited_result),
49
+ "filings": limited_result
50
+ }
51
+ return {"error": f"No filings found for CIK: {cik}"}
52
+
53
+ @mcp.tool()
54
+ def get_financial_data(cik: str, period: str) -> dict:
55
+ """Get financial data for a specific period."""
56
+ result = edgar_client.get_financial_data_for_period(cik, period)
57
+ return result if result and "period" in result else {"error": f"No financial data found for CIK: {cik}, Period: {period}"}
58
+
59
+ @mcp.tool()
60
+ def extract_financial_metrics(cik: str, years: int = 3) -> dict:
61
+ """Extract comprehensive financial metrics for multiple years."""
62
+ if years < 1 or years > 10:
63
+ return {"error": "Years parameter must be between 1 and 10"}
64
+
65
+ metrics = financial_analyzer.extract_financial_metrics(cik, years)
66
+ if metrics:
67
+ formatted = financial_analyzer.format_financial_data(metrics)
68
+ return {"periods": len(formatted), "data": formatted}
69
+ return {"error": f"No financial metrics extracted for CIK: {cik}"}
70
+
71
+ @mcp.tool()
72
+ def get_latest_financial_data(cik: str) -> dict:
73
+ """Get the most recent financial data available."""
74
+ result = financial_analyzer.get_latest_financial_data(cik)
75
+ return result if result and "period" in result else {"error": f"No latest financial data found for CIK: {cik}"}
76
+
77
+ @mcp.tool()
78
+ def advanced_search_company(company_input: str) -> dict:
79
+ """Advanced search supporting both company name and CIK code."""
80
+ result = financial_analyzer.search_company(company_input)
81
+ return result if not result.get("error") else {"error": result["error"]}
82
+
83
+ # Gradio wrapper functions (添加调试和超时处理)
84
+ def gradio_search_company(company_name: str):
85
+ """Gradio wrapper for search_company"""
86
+ if not company_name or not company_name.strip():
87
+ return json.dumps({"error": "Company name cannot be empty"}, indent=2)
88
+ try:
89
+ import sys
90
+ print(f"[DEBUG] Searching company: {company_name.strip()}", file=sys.stderr)
91
+ result = search_company(company_name.strip())
92
+ print(f"[DEBUG] Search result type: {type(result)}", file=sys.stderr)
93
+ print(f"[DEBUG] Search result: {result}", file=sys.stderr)
94
+ # Ensure result is a dict
95
+ if not isinstance(result, dict):
96
+ result = {"error": f"Unexpected result type: {type(result)}"}
97
+ return json.dumps(result, indent=2)
98
+ except TimeoutError as e:
99
+ return json.dumps({"error": f"Request timeout: {str(e)}"}, indent=2)
100
+ except Exception as e:
101
+ import traceback
102
+ traceback.print_exc()
103
+ return json.dumps({"error": f"Exception: {str(e)}", "type": str(type(e))}, indent=2)
104
+
105
+ def gradio_get_company_info(cik: str):
106
+ """Gradio wrapper for get_company_info"""
107
+ if not cik or not cik.strip():
108
+ return json.dumps({"error": "CIK cannot be empty"}, indent=2)
109
+ try:
110
+ import sys
111
+ print(f"[DEBUG] Getting company info for CIK: {cik.strip()}", file=sys.stderr)
112
+ result = get_company_info(cik.strip())
113
+ print(f"[DEBUG] Company info result: {result}", file=sys.stderr)
114
+ if not isinstance(result, dict):
115
+ result = {"error": f"Unexpected result type: {type(result)}"}
116
+ return json.dumps(result, indent=2)
117
+ except TimeoutError as e:
118
+ return json.dumps({"error": f"Request timeout: {str(e)}"}, indent=2)
119
+ except Exception as e:
120
+ import traceback
121
+ traceback.print_exc()
122
+ return json.dumps({"error": f"Exception: {str(e)}", "type": str(type(e))}, indent=2)
123
+
124
+ def gradio_extract_metrics(cik: str, years: float):
125
+ """Gradio wrapper for extract_financial_metrics"""
126
+ if not cik or not cik.strip():
127
+ return json.dumps({"error": "CIK cannot be empty"}, indent=2)
128
+ try:
129
+ import sys
130
+ years_int = int(years)
131
+ print(f"[DEBUG] Extracting metrics for CIK: {cik.strip()}, Years: {years_int}", file=sys.stderr)
132
+ result = extract_financial_metrics(cik.strip(), years_int)
133
+ print(f"[DEBUG] Extract metrics result: {result}", file=sys.stderr)
134
+ if not isinstance(result, dict):
135
+ result = {"error": f"Unexpected result type: {type(result)}"}
136
+ return json.dumps(result, indent=2)
137
+ except TimeoutError as e:
138
+ return json.dumps({"error": f"Request timeout: {str(e)}"}, indent=2)
139
+ except Exception as e:
140
+ import traceback
141
+ traceback.print_exc()
142
+ return json.dumps({"error": f"Exception: {str(e)}", "type": str(type(e))}, indent=2)
143
+
144
+ def gradio_get_latest(cik: str):
145
+ """Gradio wrapper for get_latest_financial_data"""
146
+ if not cik or not cik.strip():
147
+ return json.dumps({"error": "CIK cannot be empty"}, indent=2)
148
+ try:
149
+ import sys
150
+ print(f"[DEBUG] Getting latest data for CIK: {cik.strip()}", file=sys.stderr)
151
+ result = get_latest_financial_data(cik.strip())
152
+ print(f"[DEBUG] Latest data result: {result}", file=sys.stderr)
153
+ if not isinstance(result, dict):
154
+ result = {"error": f"Unexpected result type: {type(result)}"}
155
+ return json.dumps(result, indent=2)
156
+ except TimeoutError as e:
157
+ return json.dumps({"error": f"Request timeout: {str(e)}"}, indent=2)
158
+ except Exception as e:
159
+ import traceback
160
+ traceback.print_exc()
161
+ return json.dumps({"error": f"Exception: {str(e)}", "type": str(type(e))}, indent=2)
162
+
163
+ # Mount MCP HTTP endpoints for client integration
164
+ app = FastAPI()
165
+
166
+ @app.post("/mcp")
167
+ @app.get("/mcp")
168
+ @app.post("/sse")
169
+ @app.get("/sse")
170
+ async def mcp_http_endpoint(request: Request):
171
+ """MCP HTTP endpoint - handle both GET and POST (accessible via /mcp or /sse)"""
172
+ try:
173
+ # For POST requests, get JSON body
174
+ if request.method == "POST":
175
+ body = await request.json()
176
+
177
+ # Handle JSON-RPC request
178
+ if body.get("method") == "tools/list":
179
+ # List all tools
180
+ tools = []
181
+ for tool_name, tool_func in mcp._tool_manager._tools.items():
182
+ tools.append({
183
+ "name": tool_name,
184
+ "description": tool_func.__doc__ or "",
185
+ "inputSchema": {
186
+ "type": "object",
187
+ "properties": {},
188
+ "required": []
189
+ }
190
+ })
191
+
192
+ return JSONResponse({
193
+ "jsonrpc": "2.0",
194
+ "id": body.get("id"),
195
+ "result": {"tools": tools}
196
+ })
197
+
198
+ elif body.get("method") == "tools/call":
199
+ # Call a specific tool
200
+ tool_name = body.get("params", {}).get("name")
201
+ arguments = body.get("params", {}).get("arguments", {})
202
+
203
+ # Directly call the tool functions we defined
204
+ tool_map = {
205
+ "search_company": search_company,
206
+ "get_company_info": get_company_info,
207
+ "get_company_filings": get_company_filings,
208
+ "get_financial_data": get_financial_data,
209
+ "extract_financial_metrics": extract_financial_metrics,
210
+ "get_latest_financial_data": get_latest_financial_data,
211
+ "advanced_search_company": advanced_search_company
212
+ }
213
+
214
+ if tool_name in tool_map:
215
+ tool_func = tool_map[tool_name]
216
+ result = tool_func(**arguments)
217
+
218
+ return JSONResponse({
219
+ "jsonrpc": "2.0",
220
+ "id": body.get("id"),
221
+ "result": {
222
+ "content": [{
223
+ "type": "text",
224
+ "text": json.dumps(result)
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"Tool not found: {tool_name}"
235
+ }
236
+ })
237
+
238
+ else:
239
+ return JSONResponse({
240
+ "jsonrpc": "2.0",
241
+ "id": body.get("id"),
242
+ "error": {
243
+ "code": -32601,
244
+ "message": f"Method not found: {body.get('method')}"
245
+ }
246
+ })
247
+
248
+ else:
249
+ # GET request - return info
250
+ return JSONResponse({
251
+ "service": "SEC Financial Data MCP Server",
252
+ "protocol": "MCP 2024-11-05",
253
+ "transport": "HTTP",
254
+ "status": "online"
255
+ })
256
+
257
+ except Exception as e:
258
+ return JSONResponse(
259
+ content={"error": str(e), "type": "server_error"},
260
+ status_code=500
261
+ )
262
+
263
+ # Create Gradio interface
264
+ with gr.Blocks(title="SEC Financial Data MCP Server", theme=gr.themes.Soft()) as demo:
265
+ gr.Markdown("""
266
+ # 📊 SEC Financial Data MCP Server
267
+
268
+ Access real-time SEC EDGAR financial data via Model Context Protocol
269
+
270
+ **MCP Endpoint:** Use `mcp_server_fastmcp.py` for standard MCP client connections
271
+ """)
272
+
273
+ with gr.Tab("🔍 Search Company"):
274
+ gr.Markdown("### Search for a company by name")
275
+ with gr.Row():
276
+ with gr.Column():
277
+ company_input = gr.Textbox(label="Company Name", placeholder="Tesla", value="Tesla")
278
+ search_btn = gr.Button("Search", variant="primary")
279
+ with gr.Column():
280
+ search_output = gr.Code(label="Result", language="json", lines=15)
281
+ search_btn.click(gradio_search_company, inputs=company_input, outputs=search_output)
282
+
283
+ with gr.Tab("ℹ️ Company Info"):
284
+ gr.Markdown("### Get detailed company information")
285
+ with gr.Row():
286
+ with gr.Column():
287
+ cik_input = gr.Textbox(label="Company CIK", placeholder="0001318605", value="0001318605")
288
+ info_btn = gr.Button("Get Info", variant="primary")
289
+ with gr.Column():
290
+ info_output = gr.Code(label="Result", language="json", lines=15)
291
+ info_btn.click(gradio_get_company_info, inputs=cik_input, outputs=info_output)
292
+
293
+ with gr.Tab("📈 Financial Metrics"):
294
+ gr.Markdown("### Extract multi-year financial metrics ⭐")
295
+ with gr.Row():
296
+ with gr.Column():
297
+ metrics_cik = gr.Textbox(label="Company CIK", placeholder="0001318605", value="0001318605")
298
+ metrics_years = gr.Slider(minimum=1, maximum=10, value=3, step=1, label="Years")
299
+ metrics_btn = gr.Button("Extract Metrics", variant="primary")
300
+ with gr.Column():
301
+ metrics_output = gr.Code(label="Result", language="json", lines=20)
302
+ metrics_btn.click(gradio_extract_metrics, inputs=[metrics_cik, metrics_years], outputs=metrics_output)
303
+
304
+ with gr.Tab("🆕 Latest Data"):
305
+ gr.Markdown("### Get latest financial data")
306
+ with gr.Row():
307
+ with gr.Column():
308
+ latest_cik = gr.Textbox(label="Company CIK", placeholder="0001318605", value="0001318605")
309
+ latest_btn = gr.Button("Get Latest", variant="primary")
310
+ with gr.Column():
311
+ latest_output = gr.Code(label="Result", language="json", lines=15)
312
+ latest_btn.click(gradio_get_latest, inputs=latest_cik, outputs=latest_output)
313
+
314
+ with gr.Tab("📖 Documentation"):
315
+ gr.Markdown("""
316
+ ## 🛠️ Available Tools (7)
317
+
318
+ 1. **search_company** - Search by company name
319
+ 2. **get_company_info** - Get company details by CIK
320
+ 3. **get_company_filings** - List SEC filings
321
+ 4. **get_financial_data** - Get specific period data
322
+ 5. **extract_financial_metrics** - Multi-year trends
323
+ 6. **get_latest_financial_data** - Latest snapshot
324
+ 7. **advanced_search_company** - Flexible search
325
+
326
+ ## 🔗 MCP Integration
327
+
328
+ For MCP client integration, run:
329
+ ```bash
330
+ python mcp_server_fastmcp.py
331
+ ```
332
+
333
+ Then configure your MCP client (e.g., Claude Desktop):
334
+ ```json
335
+ {
336
+ "mcpServers": {
337
+ "sec-financial-data": {
338
+ "command": "python",
339
+ "args": ["path/to/mcp_server_fastmcp.py"]
340
+ }
341
+ }
342
+ }
343
+ ```
344
+
345
+ ## 📊 Data Source
346
+
347
+ - **SEC EDGAR API** - Official SEC data
348
+ - **Financial Statements** - 10-K, 10-Q, 20-F forms
349
+ - **XBRL Data** - Structured metrics
350
+ """)
351
+
352
+ # Mount Gradio app to FastAPI
353
+ app = gr.mount_gradio_app(app, demo, path="/")
354
+
355
+ if __name__ == "__main__":
356
+ import uvicorn
357
+ uvicorn.run(app, host="0.0.0.0", port=7860)