Entz commited on
Commit
22793b0
Β·
verified Β·
1 Parent(s): 0c893a1

Upload 6 files

Browse files
Files changed (6) hide show
  1. app.py +12 -0
  2. arithmetic_server.py +30 -0
  3. backend.py +255 -0
  4. frontend.py +216 -0
  5. requirements.txt +13 -3
  6. stock_server.py +63 -0
app.py ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app.py (for Hugging Face Spaces)
2
+ """
3
+ Main entry point for Hugging Face Spaces
4
+ This file is required by HF Spaces to run the Streamlit app
5
+ """
6
+
7
+ import subprocess
8
+ import sys
9
+
10
+ # Run the frontend Streamlit app
11
+ if __name__ == "__main__":
12
+ subprocess.run([sys.executable, "-m", "streamlit", "run", "frontend.py"])
arithmetic_server.py ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from mcp.server.fastmcp import FastMCP
2
+
3
+ # Creates a server named "Arithmetic"
4
+ mcp = FastMCP("Arithmetic")
5
+
6
+ @mcp.tool()
7
+ def add(a: int, b: int) -> int:
8
+ """Add two numbers"""
9
+ return a + b
10
+
11
+ @mcp.tool()
12
+ def multiply(a: int, b: int) -> int:
13
+ """Multiply two numbers"""
14
+ return a * b
15
+
16
+ @mcp.tool()
17
+ def minus(a: int, b: int) -> int:
18
+ """Subtract two numbers (a - b)"""
19
+ return a - b
20
+
21
+ @mcp.tool()
22
+ def divide(a: int, b: int) -> float:
23
+ """Divide two numbers (a / b). Returns a float. Raises ValueError on division by zero."""
24
+ if b == 0:
25
+ raise ValueError("Division by zero is not allowed.")
26
+ return a / b
27
+
28
+
29
+ if __name__ == "__main__":
30
+ mcp.run(transport="stdio")
backend.py ADDED
@@ -0,0 +1,255 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # backend.py
2
+ """
3
+ Backend module for MCP Agent
4
+ Handles all the MCP server connections, LLM setup, and agent logic
5
+ """
6
+
7
+ import os
8
+ import re
9
+ import asyncio
10
+ from dotenv import load_dotenv
11
+ from typing import Optional, Dict, List, Any
12
+
13
+ ################### --- Auth setup --- ###################
14
+ ##########################################################
15
+ # load the details from .env, then test it and raise as Asserterror if false
16
+ load_dotenv()
17
+ HF = os.getenv("HF_TOKEN") or os.getenv("HUGGINGFACEHUB_API_TOKEN")
18
+ assert HF, "Set HF_TOKEN (or HUGGINGFACEHUB_API_TOKEN) in your environment/.env"
19
+
20
+ # then all env set to the same token to avoid env error.
21
+ os.environ["HF_TOKEN"] = HF
22
+ os.environ["HUGGINGFACEHUB_API_TOKEN"] = HF
23
+
24
+ # Login with HF token
25
+ try:
26
+ from huggingface_hub import login
27
+ login(token=HF)
28
+ except Exception:
29
+ pass
30
+
31
+ # --- LangChain / MCP ---
32
+ from langgraph.prebuilt import create_react_agent
33
+ from langchain_mcp_adapters.client import MultiServerMCPClient
34
+ from langchain_huggingface import HuggingFaceEndpoint, ChatHuggingFace
35
+ from langchain_core.messages import HumanMessage
36
+
37
+ # First choice, then free/tiny fallbacks
38
+ CANDIDATE_MODELS = [
39
+ os.getenv("HF_MODEL_ID", "Qwen/Qwen2.5-7B-Instruct"),
40
+ "HuggingFaceTB/SmolLM3-3B-Instruct",
41
+ "Qwen/Qwen2.5-1.5B-Instruct",
42
+ "microsoft/Phi-3-mini-4k-instruct",
43
+ ]
44
+
45
+ SYSTEM_PROMPT = (
46
+ "You are an AI assistant with tools.\n"
47
+ "- Use the arithmetic tools (`add`, `minus`, `multiply`, `divide`) for arithmetic or multi-step calculations.\n"
48
+ "- Use the stock tools (`get_stock_price`, `get_market_summary`, `get_company_news`) for financial/market queries.\n"
49
+ "- Otherwise, answer directly with your own knowledge.\n"
50
+ "Be concise and accurate. Only call tools when they clearly help."
51
+ )
52
+
53
+ ################### --- ROUTER helpers --- ###################
54
+ ##############################################################
55
+
56
+ # Detect stock/financial intent
57
+ def is_stock_query(q: str) -> bool:
58
+ """Check if query is about stocks, markets, or financial data."""
59
+ stock_patterns = [
60
+ r"\b(stock|share|price|ticker|market|nasdaq|dow|s&p|spy|qqq)\b",
61
+ r"\b(AAPL|GOOGL|MSFT|TSLA|AMZN|META|NVDA|AMD)\b", # Common tickers
62
+ r"\$[A-Z]{1,5}\b", # $SYMBOL format
63
+ r"\b(trading|invest|portfolio|earnings|dividend)\b",
64
+ r"\b(bull|bear|rally|crash|volatility)\b",
65
+ ]
66
+ return any(re.search(pattern, q, re.I) for pattern in stock_patterns)
67
+
68
+ # Extract ticker symbol from query
69
+ def extract_ticker(q: str) -> str:
70
+ """Extract stock ticker from query."""
71
+ # Check for $SYMBOL format first
72
+ dollar_match = re.search(r"\$([A-Z]{1,5})\b", q, re.I)
73
+ if dollar_match:
74
+ return dollar_match.group(1).upper()
75
+
76
+ # Check for common patterns like "price of AAPL" or "AAPL stock"
77
+ patterns = [
78
+ r"(?:price of|stock price of|quote for)\s+([A-Z]{1,5})\b",
79
+ r"\b([A-Z]{1,5})\s+(?:stock|share|price|quote)",
80
+ r"(?:what is|what's|get)\s+([A-Z]{1,5})\b",
81
+ ]
82
+
83
+ for pattern in patterns:
84
+ match = re.search(pattern, q, re.I)
85
+ if match:
86
+ return match.group(1).upper()
87
+
88
+ # Look for standalone uppercase tickers
89
+ words = q.split()
90
+ for word in words:
91
+ clean_word = word.strip(".,!?")
92
+ if 2 <= len(clean_word) <= 5 and clean_word.isupper():
93
+ return clean_word
94
+
95
+ return None
96
+
97
+ # Check if asking for market summary
98
+ def wants_market_summary(q: str) -> bool:
99
+ """Check if user wants overall market summary."""
100
+ patterns = [
101
+ r"\bmarket\s+(?:summary|overview|today|status)\b",
102
+ r"\bhow(?:'s| is) the market\b",
103
+ r"\b(?:dow|nasdaq|s&p)\s+(?:today|now)\b",
104
+ r"\bmarket indices\b",
105
+ ]
106
+ return any(re.search(pattern, q, re.I) for pattern in patterns)
107
+
108
+ # Check if asking for news
109
+ def wants_news(q: str) -> bool:
110
+ """Check if user wants company news."""
111
+ return bool(re.search(r"\b(news|headline|announcement|update)\b", q, re.I))
112
+
113
+ def build_tool_map(tools):
114
+ mp = {t.name: t for t in tools}
115
+ return mp
116
+
117
+ def find_tool(name: str, tool_map: dict):
118
+ name = name.lower()
119
+ for k, t in tool_map.items():
120
+ kl = k.lower()
121
+ if kl == name or kl.endswith("/" + name):
122
+ return t
123
+ return None
124
+
125
+ async def build_chat_llm_with_fallback():
126
+ """
127
+ Try each candidate model. For each:
128
+ - create HuggingFaceEndpoint + ChatHuggingFace
129
+ - do a tiny 'ping' with a proper LC message to trigger routing
130
+ On 402/Payment Required (or other errors), fall through to next.
131
+ """
132
+ last_err = None
133
+ for mid in CANDIDATE_MODELS:
134
+ try:
135
+ llm = HuggingFaceEndpoint(
136
+ repo_id=mid,
137
+ huggingfacehub_api_token=HF,
138
+ temperature=0.1,
139
+ max_new_tokens=256, # Increased for better responses
140
+ )
141
+ model = ChatHuggingFace(llm=llm)
142
+ # PROBE with a valid message type
143
+ _ = await model.ainvoke([HumanMessage(content="ping")])
144
+ print(f"[LLM] Using: {mid}")
145
+ return model
146
+ except Exception as e:
147
+ msg = str(e)
148
+ if "402" in msg or "Payment Required" in msg:
149
+ print(f"[LLM] {mid} requires payment; trying next...")
150
+ last_err = e
151
+ continue
152
+ print(f"[LLM] {mid} error: {e}; trying next...")
153
+ last_err = e
154
+ continue
155
+ raise RuntimeError(f"Could not initialize any candidate model. Last error: {last_err}")
156
+
157
+ # NEW: Class to manage the MCP Agent (moved from main function)
158
+ class MCPAgent:
159
+ def __init__(self):
160
+ self.client = None
161
+ self.agent = None
162
+ self.tool_map = None
163
+ self.tools = None
164
+ self.model = None
165
+ self.initialized = False
166
+
167
+ async def initialize(self):
168
+ """Initialize the MCP client and agent"""
169
+ if self.initialized:
170
+ return
171
+
172
+ # Start the Stock server separately first: `python stockserver.py`
173
+ self.client = MultiServerMCPClient(
174
+ {
175
+ "arithmetic": {
176
+ "command": "python",
177
+ "args": ["arithmetic_server.py"],
178
+ "transport": "stdio",
179
+ },
180
+ "stocks": {
181
+ "url": "http://localhost:8000/mcp",
182
+ "transport": "streamable_http",
183
+ },
184
+ }
185
+ )
186
+
187
+ # 1. MCP client + tools
188
+ self.tools = await self.client.get_tools()
189
+ self.tool_map = build_tool_map(self.tools)
190
+
191
+ # 2. Build LLM with auto-fallback
192
+ self.model = await build_chat_llm_with_fallback()
193
+
194
+ # Build the ReAct agent with MCP tools
195
+ self.agent = create_react_agent(self.model, self.tools)
196
+
197
+ self.initialized = True
198
+ return list(self.tool_map.keys()) # Return available tools
199
+
200
+ async def process_message(self, user_text: str, history: List[Dict]) -> str:
201
+ """Process a single message with the agent"""
202
+ if not self.initialized:
203
+ await self.initialize()
204
+
205
+ # Try direct stock tool routing first
206
+ if is_stock_query(user_text):
207
+ if wants_market_summary(user_text):
208
+ market_tool = find_tool("get_market_summary", self.tool_map)
209
+ if market_tool:
210
+ return await market_tool.ainvoke({})
211
+
212
+ elif wants_news(user_text):
213
+ ticker = extract_ticker(user_text)
214
+ if ticker:
215
+ news_tool = find_tool("get_company_news", self.tool_map)
216
+ if news_tool:
217
+ return await news_tool.ainvoke({"symbol": ticker, "limit": 3})
218
+
219
+ else:
220
+ ticker = extract_ticker(user_text)
221
+ if ticker:
222
+ price_tool = find_tool("get_stock_price", self.tool_map)
223
+ if price_tool:
224
+ return await price_tool.ainvoke({"symbol": ticker})
225
+
226
+ # Fall back to agent for everything else
227
+ messages = [{"role": "system", "content": SYSTEM_PROMPT}] + history + [
228
+ {"role": "user", "content": user_text}
229
+ ]
230
+ result = await self.agent.ainvoke({"messages": messages})
231
+ return result["messages"][-1].content
232
+
233
+ async def cleanup(self):
234
+ """Clean up resources"""
235
+ if self.client:
236
+ close = getattr(self.client, "close", None)
237
+ if callable(close):
238
+ res = close()
239
+ if asyncio.iscoroutine(res):
240
+ await res
241
+
242
+
243
+
244
+
245
+
246
+
247
+ # NEW: Singleton instance for the agent
248
+ _agent_instance = None
249
+
250
+ def get_agent() -> MCPAgent:
251
+ """Get or create the singleton agent instance"""
252
+ global _agent_instance
253
+ if _agent_instance is None:
254
+ _agent_instance = MCPAgent()
255
+ return _agent_instance
frontend.py ADDED
@@ -0,0 +1,216 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # frontend.py
2
+ """
3
+ Streamlit frontend for MCP Agent
4
+ Provides a chat interface for interacting with the MCP backend
5
+ """
6
+
7
+ import streamlit as st
8
+ import asyncio
9
+ from backend import get_agent, MCPAgent
10
+ import time
11
+
12
+ # Page configuration
13
+ st.set_page_config(
14
+ page_title="MCP Agent Chat",
15
+ page_icon="πŸ€–",
16
+ layout="wide",
17
+ initial_sidebar_state="expanded"
18
+ )
19
+
20
+ # Custom CSS for better chat appearance
21
+ st.markdown("""
22
+ <style>
23
+ .stChatMessage {
24
+ padding: 1rem;
25
+ border-radius: 0.5rem;
26
+ margin-bottom: 1rem;
27
+ }
28
+ .user-message {
29
+ background-color: #e3f2fd;
30
+ }
31
+ .assistant-message {
32
+ background-color: #f5f5f5;
33
+ }
34
+ .sidebar-info {
35
+ padding: 1rem;
36
+ background-color: #f0f2f6;
37
+ border-radius: 0.5rem;
38
+ margin-bottom: 1rem;
39
+ }
40
+ </style>
41
+ """, unsafe_allow_html=True)
42
+
43
+ # Initialize session state
44
+ if "messages" not in st.session_state:
45
+ st.session_state.messages = []
46
+ st.session_state.history = [] # For agent history
47
+ st.session_state.agent_initialized = False
48
+ st.session_state.available_tools = []
49
+ st.session_state.pending_query = None # For example queries
50
+
51
+ # Sidebar
52
+ with st.sidebar:
53
+ st.title("πŸ€– MCP Agent")
54
+ st.markdown("---")
55
+
56
+ # Status indicator
57
+ if st.session_state.agent_initialized:
58
+ st.success("βœ… Agent Connected")
59
+
60
+ # Show available tools
61
+ st.markdown("### πŸ› οΈ Available Tools")
62
+ for tool in st.session_state.available_tools:
63
+ st.markdown(f"β€’ `{tool}`")
64
+ else:
65
+ st.info("⏳ Agent Initializing...")
66
+
67
+ st.markdown("---")
68
+
69
+ # Example queries
70
+ st.markdown("### πŸ’‘ Example Queries")
71
+ example_queries = [
72
+ "What's the price of AAPL?",
73
+ "Show me the market summary",
74
+ "Get news about TSLA",
75
+ "Calculate 25 * 4",
76
+ "What's 100 divided by 7?",
77
+ "Add 456 and 789"
78
+ ]
79
+
80
+ for query in example_queries:
81
+ if st.button(query, key=f"example_{query}"):
82
+ st.session_state.pending_query = query
83
+ st.rerun()
84
+
85
+ st.markdown("---")
86
+
87
+ # Clear chat button
88
+ if st.button("πŸ—‘οΈ Clear Chat", type="secondary"):
89
+ st.session_state.messages = []
90
+ st.session_state.history = []
91
+ st.rerun()
92
+
93
+ # Info section
94
+ st.markdown("### ℹ️ About")
95
+ st.markdown("""
96
+ This chat interface connects to:
97
+ - **Math Server**: Basic arithmetic operations
98
+ - **Stock Server**: Real-time market data
99
+
100
+ The agent uses LangChain and MCP to intelligently route your queries to the appropriate tools.
101
+ """)
102
+
103
+ # Main chat interface
104
+ st.title("πŸ’¬ MCP Agent Chat")
105
+ st.markdown("Ask me about stocks, math calculations, or general questions!")
106
+
107
+ # Initialize agent asynchronously
108
+ async def initialize_agent():
109
+ """Initialize the agent if not already done"""
110
+ if not st.session_state.agent_initialized:
111
+ agent = get_agent()
112
+ with st.spinner("πŸ”§ Initializing MCP servers..."):
113
+ try:
114
+ tools = await agent.initialize()
115
+ st.session_state.available_tools = tools
116
+ st.session_state.agent_initialized = True
117
+ return True
118
+ except Exception as e:
119
+ st.error(f"Failed to initialize agent: {str(e)}")
120
+ st.info("Please make sure the stock server is running: `python stockserver.py`")
121
+ return False
122
+ return True
123
+
124
+ # Process user message
125
+ async def process_user_message(user_input: str):
126
+ """Process the user's message and get response from agent"""
127
+ agent = get_agent()
128
+
129
+ # Add user message to history
130
+ st.session_state.history.append({"role": "user", "content": user_input})
131
+
132
+ try:
133
+ # Get response from agent
134
+ response = await agent.process_message(user_input, st.session_state.history)
135
+
136
+ # Add assistant response to history
137
+ st.session_state.history.append({"role": "assistant", "content": response})
138
+
139
+ return response
140
+ except Exception as e:
141
+ return f"Error: {str(e)}"
142
+
143
+ # Display chat messages
144
+ for message in st.session_state.messages:
145
+ with st.chat_message(message["role"]):
146
+ st.markdown(message["content"])
147
+
148
+ # Process pending query from example buttons
149
+ if st.session_state.pending_query:
150
+ query = st.session_state.pending_query
151
+ st.session_state.pending_query = None # Clear it
152
+
153
+ # Add to messages
154
+ st.session_state.messages.append({"role": "user", "content": query})
155
+
156
+ # Process the query
157
+ async def process_example():
158
+ if not st.session_state.agent_initialized:
159
+ if not await initialize_agent():
160
+ return "Failed to initialize agent. Please check the servers."
161
+ return await process_user_message(query)
162
+
163
+ # Get response
164
+ with st.spinner("Processing..."):
165
+ response = asyncio.run(process_example())
166
+
167
+ # Add response to messages
168
+ st.session_state.messages.append({"role": "assistant", "content": response})
169
+
170
+ # Rerun to display the new messages
171
+ st.rerun()
172
+
173
+ # Chat input
174
+ if prompt := st.chat_input("Type your message here..."):
175
+ # Add user message to chat
176
+ st.session_state.messages.append({"role": "user", "content": prompt})
177
+
178
+ # Display user message
179
+ with st.chat_message("user"):
180
+ st.markdown(prompt)
181
+
182
+ # Get and display assistant response
183
+ with st.chat_message("assistant"):
184
+ message_placeholder = st.empty()
185
+
186
+ # Run async function
187
+ async def get_response():
188
+ # Initialize agent if needed
189
+ if not st.session_state.agent_initialized:
190
+ if not await initialize_agent():
191
+ return "Failed to initialize agent. Please check the servers."
192
+
193
+ # Process message
194
+ return await process_user_message(prompt)
195
+
196
+ # Execute async function
197
+ with st.spinner("Thinking..."):
198
+ response = asyncio.run(get_response())
199
+
200
+ message_placeholder.markdown(response)
201
+ st.session_state.messages.append({"role": "assistant", "content": response})
202
+
203
+ # Initialize agent on first load
204
+ if not st.session_state.agent_initialized:
205
+ asyncio.run(initialize_agent())
206
+
207
+ # Footer
208
+ st.markdown("---")
209
+ st.markdown(
210
+ """
211
+ <div style='text-align: center; color: #666;'>
212
+ Powered by LangChain, MCP, and Hugging Face πŸ€—
213
+ </div>
214
+ """,
215
+ unsafe_allow_html=True
216
+ )
requirements.txt CHANGED
@@ -1,3 +1,13 @@
1
- altair
2
- pandas
3
- streamlit
 
 
 
 
 
 
 
 
 
 
 
1
+ langchain-mcp-adapters==0.1.9
2
+ mcp==1.12.3
3
+ langgraph==0.6.3
4
+ langchain-huggingface==0.3.1
5
+ aiohttp=3.12.15
6
+
7
+
8
+ streamlit==1.48.0
9
+ python-dotenv
10
+ huggingface-hub
11
+
12
+ fastmcp
13
+ asyncio
stock_server.py ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from mcp.server.fastmcp import FastMCP
2
+ import aiohttp
3
+ import json
4
+ from typing import Optional
5
+
6
+ mcp = FastMCP("StockMarket")
7
+
8
+ @mcp.tool()
9
+ async def get_stock_price(symbol: str) -> str:
10
+ """
11
+ Get current stock price and info for a given ticker symbol.
12
+ Uses Alpha Vantage free API (or Yahoo Finance fallback).
13
+ """
14
+ # Using a mock response for demo - replace with actual API call
15
+ # For real implementation, use yfinance or Alpha Vantage API
16
+ mock_prices = {
17
+ "AAPL": {"price": 195.89, "change": "+2.34", "percent": "+1.21%"},
18
+ "GOOGL": {"price": 142.57, "change": "-0.89", "percent": "-0.62%"},
19
+ "MSFT": {"price": 378.91, "change": "+5.12", "percent": "+1.37%"},
20
+ "TSLA": {"price": 238.45, "change": "-3.21", "percent": "-1.33%"},
21
+ }
22
+
23
+ symbol = symbol.upper()
24
+ if symbol in mock_prices:
25
+ data = mock_prices[symbol]
26
+ return f"{symbol}: ${data['price']} ({data['change']}, {data['percent']})"
27
+
28
+ # For production, uncomment and use real API:
29
+ # try:
30
+ # import yfinance as yf
31
+ # ticker = yf.Ticker(symbol)
32
+ # info = ticker.info
33
+ # price = info.get('currentPrice', info.get('regularMarketPrice', 'N/A'))
34
+ # return f"{symbol}: ${price}"
35
+ # except:
36
+ # return f"Could not fetch data for {symbol}"
37
+
38
+ return f"Unknown symbol: {symbol}"
39
+
40
+ @mcp.tool()
41
+ async def get_market_summary() -> str:
42
+ """Get a summary of major market indices."""
43
+ # Mock data - replace with real API calls
44
+ return """Market Summary:
45
+ πŸ“Š S&P 500: 4,783.45 (+0.73%)
46
+ πŸ“ˆ NASDAQ: 15,123.68 (+1.18%)
47
+ πŸ“‰ DOW: 37,863.80 (-0.31%)
48
+ πŸ’± USD/EUR: 0.9234
49
+ πŸͺ™ Bitcoin: $43,521.00 (+2.4%)
50
+ πŸ›’οΈ Oil (WTI): $73.41/barrel"""
51
+
52
+ @mcp.tool()
53
+ async def get_company_news(symbol: str, limit: int = 3) -> str:
54
+ """Get latest news headlines for a company."""
55
+ # Mock news - in production, use NewsAPI or similar
56
+ symbol = symbol.upper()
57
+ return f"""Latest news for {symbol}:
58
+ 1. {symbol} announces Q4 earnings beat expectations
59
+ 2. Analysts upgrade {symbol} to 'Buy' rating
60
+ 3. {symbol} unveils new product line for 2025"""
61
+
62
+ if __name__ == "__main__":
63
+ mcp.run(transport="streamable-http")