Entz commited on
Commit
effc62f
Β·
verified Β·
1 Parent(s): f0d48eb

Upload 8 files

Browse files
Files changed (8) hide show
  1. Dockerfile +30 -0
  2. README.md +19 -0
  3. arithmetic_server.py +30 -0
  4. backend.py +257 -0
  5. frontend.py +218 -0
  6. huggingface.yaml +6 -0
  7. requirements.txt +9 -0
  8. stock_server.py +63 -0
Dockerfile ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -------- base
2
+ FROM python:3.10-slim
3
+
4
+ ENV PYTHONUNBUFFERED=1 \
5
+ PIP_DISABLE_PIP_VERSION_CHECK=1 \
6
+ PORT=7860
7
+
8
+ # -------- system deps (curl for healthcheck)
9
+ RUN apt-get update && apt-get install -y --no-install-recommends \
10
+ curl \
11
+ && rm -rf /var/lib/apt/lists/*
12
+
13
+ # -------- workdir
14
+ WORKDIR /app
15
+
16
+ # -------- python deps
17
+ COPY requirements.txt /app/
18
+ RUN pip install --upgrade pip && pip install --no-cache-dir -r requirements.txt
19
+
20
+ # -------- app code
21
+ # (Put ALL your python files in the image)
22
+ COPY . /app
23
+
24
+ # -------- healthcheck & run
25
+ # Hugging Face probes container health; probing "/" is the simplest + most robust for Streamlit
26
+ HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=5 \
27
+ CMD curl --fail http://127.0.0.1:${PORT}/ || exit 1
28
+
29
+ # Streamlit must bind to 0.0.0.0 and HF's provided $PORT
30
+ CMD ["streamlit", "run", "frontend.py", "--server.port=${PORT}", "--server.address=0.0.0.0"]
README.md ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Mcp Stock Math
3
+ emoji: πŸš€
4
+ colorFrom: red
5
+ colorTo: red
6
+ sdk: docker
7
+ app_port: 8501
8
+ tags:
9
+ - streamlit
10
+ pinned: false
11
+ short_description: mcp-agent-chat for live stock prices and math
12
+ ---
13
+
14
+ # Welcome to Streamlit!
15
+
16
+ Edit `/src/streamlit_app.py` to customize this app to your heart's desire. :heart:
17
+
18
+ If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community
19
+ forums](https://discuss.streamlit.io).
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,257 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # backend.py
2
+ """
3
+ Backend module for MCP Agent
4
+ Handles all the MCP server connections, LLM setup, and agent logic
5
+ """
6
+ import sys
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
+ from pathlib import Path # already imported
14
+ here = Path(__file__).parent.resolve()
15
+
16
+
17
+
18
+ ################### --- Auth setup --- ###################
19
+ ##########################################################
20
+ HF = os.getenv("HF_TOKEN") or os.getenv("HUGGINGFACEHUB_API_TOKEN")
21
+ if not HF:
22
+ print("WARNING: HF_TOKEN not set. The app will start, but model calls may fail.")
23
+ else:
24
+ os.environ["HF_TOKEN"] = HF
25
+ os.environ["HUGGINGFACEHUB_API_TOKEN"] = HF
26
+ try:
27
+ from huggingface_hub import login
28
+ login(token=HF)
29
+ except Exception:
30
+ pass
31
+
32
+
33
+ # --- LangChain / MCP ---
34
+ from langgraph.prebuilt import create_react_agent
35
+ from langchain_mcp_adapters.client import MultiServerMCPClient
36
+ from langchain_huggingface import HuggingFaceEndpoint, ChatHuggingFace
37
+ from langchain_core.messages import HumanMessage
38
+
39
+ # First choice, then free/tiny fallbacks
40
+ CANDIDATE_MODELS = [
41
+ os.getenv("HF_MODEL_ID", "Qwen/Qwen2.5-7B-Instruct"),
42
+ "HuggingFaceTB/SmolLM3-3B-Instruct",
43
+ "Qwen/Qwen2.5-1.5B-Instruct",
44
+ "microsoft/Phi-3-mini-4k-instruct",
45
+ ]
46
+
47
+ SYSTEM_PROMPT = (
48
+ "You are an AI assistant with tools.\n"
49
+ "- Use the arithmetic tools (`add`, `minus`, `multiply`, `divide`) for arithmetic or multi-step calculations.\n"
50
+ "- Use the stock tools (`get_stock_price`, `get_market_summary`, `get_company_news`) for financial/market queries.\n"
51
+ "- Otherwise, answer directly with your own knowledge.\n"
52
+ "Be concise and accurate. Only call tools when they clearly help."
53
+ )
54
+
55
+ ################### --- ROUTER helpers --- ###################
56
+ ##############################################################
57
+
58
+ # Detect stock/financial intent
59
+ def is_stock_query(q: str) -> bool:
60
+ """Check if query is about stocks, markets, or financial data."""
61
+ stock_patterns = [
62
+ r"\b(stock|share|price|ticker|market|nasdaq|dow|s&p|spy|qqq)\b",
63
+ r"\b(AAPL|GOOGL|MSFT|TSLA|AMZN|META|NVDA|AMD)\b", # Common tickers
64
+ r"\$[A-Z]{1,5}\b", # $SYMBOL format
65
+ r"\b(trading|invest|portfolio|earnings|dividend)\b",
66
+ r"\b(bull|bear|rally|crash|volatility)\b",
67
+ ]
68
+ return any(re.search(pattern, q, re.I) for pattern in stock_patterns)
69
+
70
+ # Extract ticker symbol from query
71
+ def extract_ticker(q: str) -> str:
72
+ """Extract stock ticker from query."""
73
+ # Check for $SYMBOL format first
74
+ dollar_match = re.search(r"\$([A-Z]{1,5})\b", q, re.I)
75
+ if dollar_match:
76
+ return dollar_match.group(1).upper()
77
+
78
+ # Check for common patterns like "price of AAPL" or "AAPL stock"
79
+ patterns = [
80
+ r"(?:price of|stock price of|quote for)\s+([A-Z]{1,5})\b",
81
+ r"\b([A-Z]{1,5})\s+(?:stock|share|price|quote)",
82
+ r"(?:what is|what's|get)\s+([A-Z]{1,5})\b",
83
+ ]
84
+
85
+ for pattern in patterns:
86
+ match = re.search(pattern, q, re.I)
87
+ if match:
88
+ return match.group(1).upper()
89
+
90
+ # Look for standalone uppercase tickers
91
+ words = q.split()
92
+ for word in words:
93
+ clean_word = word.strip(".,!?")
94
+ if 2 <= len(clean_word) <= 5 and clean_word.isupper():
95
+ return clean_word
96
+
97
+ return None
98
+
99
+ # Check if asking for market summary
100
+ def wants_market_summary(q: str) -> bool:
101
+ """Check if user wants overall market summary."""
102
+ patterns = [
103
+ r"\bmarket\s+(?:summary|overview|today|status)\b",
104
+ r"\bhow(?:'s| is) the market\b",
105
+ r"\b(?:dow|nasdaq|s&p)\s+(?:today|now)\b",
106
+ r"\bmarket indices\b",
107
+ ]
108
+ return any(re.search(pattern, q, re.I) for pattern in patterns)
109
+
110
+ # Check if asking for news
111
+ def wants_news(q: str) -> bool:
112
+ """Check if user wants company news."""
113
+ return bool(re.search(r"\b(news|headline|announcement|update)\b", q, re.I))
114
+
115
+ def build_tool_map(tools):
116
+ mp = {t.name: t for t in tools}
117
+ return mp
118
+
119
+ def find_tool(name: str, tool_map: dict):
120
+ name = name.lower()
121
+ for k, t in tool_map.items():
122
+ kl = k.lower()
123
+ if kl == name or kl.endswith("/" + name):
124
+ return t
125
+ return None
126
+
127
+ async def build_chat_llm_with_fallback():
128
+ """
129
+ Try each candidate model. For each:
130
+ - create HuggingFaceEndpoint + ChatHuggingFace
131
+ - do a tiny 'ping' with a proper LC message to trigger routing
132
+ On 402/Payment Required (or other errors), fall through to next.
133
+ """
134
+ last_err = None
135
+ for mid in CANDIDATE_MODELS:
136
+ try:
137
+ llm = HuggingFaceEndpoint(
138
+ repo_id=mid,
139
+ huggingfacehub_api_token=HF,
140
+ temperature=0.1,
141
+ max_new_tokens=256, # Increased for better responses
142
+ )
143
+ model = ChatHuggingFace(llm=llm)
144
+ # PROBE with a valid message type
145
+ _ = await model.ainvoke([HumanMessage(content="ping")])
146
+ print(f"[LLM] Using: {mid}")
147
+ return model
148
+ except Exception as e:
149
+ msg = str(e)
150
+ if "402" in msg or "Payment Required" in msg:
151
+ print(f"[LLM] {mid} requires payment; trying next...")
152
+ last_err = e
153
+ continue
154
+ print(f"[LLM] {mid} error: {e}; trying next...")
155
+ last_err = e
156
+ continue
157
+ raise RuntimeError(f"Could not initialize any candidate model. Last error: {last_err}")
158
+
159
+ # NEW: Class to manage the MCP Agent (moved from main function)
160
+ class MCPAgent:
161
+ def __init__(self):
162
+ self.client = None
163
+ self.agent = None
164
+ self.tool_map = None
165
+ self.tools = None
166
+ self.model = None
167
+ self.initialized = False
168
+
169
+ async def initialize(self):
170
+ """Initialize the MCP client and agent"""
171
+ if self.initialized:
172
+ return
173
+
174
+ # Start the Stock server separately first: `python stockserver.py`
175
+ self.client = MultiServerMCPClient({
176
+ "arithmetic": {
177
+ "command": sys.executable,
178
+ "args": [str(here / "arithmetic_server.py")],
179
+ "transport": "stdio",
180
+ },
181
+ "stocks": {
182
+ "command": sys.executable,
183
+ "args": [str(here / "stock_server.py")],
184
+ "transport": "stdio",
185
+ },
186
+ }
187
+ )
188
+
189
+ # 1. MCP client + tools
190
+ self.tools = await self.client.get_tools()
191
+ self.tool_map = build_tool_map(self.tools)
192
+
193
+ # 2. Build LLM with auto-fallback
194
+ self.model = await build_chat_llm_with_fallback()
195
+
196
+ # Build the ReAct agent with MCP tools
197
+ self.agent = create_react_agent(self.model, self.tools)
198
+
199
+ self.initialized = True
200
+ return list(self.tool_map.keys()) # Return available tools
201
+
202
+ async def process_message(self, user_text: str, history: List[Dict]) -> str:
203
+ """Process a single message with the agent"""
204
+ if not self.initialized:
205
+ await self.initialize()
206
+
207
+ # Try direct stock tool routing first
208
+ if is_stock_query(user_text):
209
+ if wants_market_summary(user_text):
210
+ market_tool = find_tool("get_market_summary", self.tool_map)
211
+ if market_tool:
212
+ return await market_tool.ainvoke({})
213
+
214
+ elif wants_news(user_text):
215
+ ticker = extract_ticker(user_text)
216
+ if ticker:
217
+ news_tool = find_tool("get_company_news", self.tool_map)
218
+ if news_tool:
219
+ return await news_tool.ainvoke({"symbol": ticker, "limit": 3})
220
+
221
+ else:
222
+ ticker = extract_ticker(user_text)
223
+ if ticker:
224
+ price_tool = find_tool("get_stock_price", self.tool_map)
225
+ if price_tool:
226
+ return await price_tool.ainvoke({"symbol": ticker})
227
+
228
+ # Fall back to agent for everything else
229
+ messages = [{"role": "system", "content": SYSTEM_PROMPT}] + history + [
230
+ {"role": "user", "content": user_text}
231
+ ]
232
+ result = await self.agent.ainvoke({"messages": messages})
233
+ return result["messages"][-1].content
234
+
235
+ async def cleanup(self):
236
+ """Clean up resources"""
237
+ if self.client:
238
+ close = getattr(self.client, "close", None)
239
+ if callable(close):
240
+ res = close()
241
+ if asyncio.iscoroutine(res):
242
+ await res
243
+
244
+
245
+
246
+
247
+
248
+
249
+ # NEW: Singleton instance for the agent
250
+ _agent_instance = None
251
+
252
+ def get_agent() -> MCPAgent:
253
+ """Get or create the singleton agent instance"""
254
+ global _agent_instance
255
+ if _agent_instance is None:
256
+ _agent_instance = MCPAgent()
257
+ return _agent_instance
frontend.py ADDED
@@ -0,0 +1,218 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ # Optional: manual connect so page renders fast
205
+ if not st.session_state.agent_initialized:
206
+ if st.sidebar.button("πŸ”Œ Connect agent"):
207
+ asyncio.run(initialize_agent())
208
+
209
+ # Footer
210
+ st.markdown("---")
211
+ st.markdown(
212
+ """
213
+ <div style='text-align: center; color: #666;'>
214
+ Powered by LangChain, MCP, and Hugging Face πŸ€—
215
+ </div>
216
+ """,
217
+ unsafe_allow_html=True
218
+ )
huggingface.yaml ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ # huggingface.yaml
2
+
3
+ title: mcp-stock-math
4
+ sdk: streamlit
5
+ app_file: frontend.py
6
+ python_version: "3.10"
requirements.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
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
+ streamlit==1.48.0
7
+ python-dotenv
8
+ huggingface-hub
9
+ fastmcp
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="stdio")