Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions 6_mcp/community_contributions/must_mcp_week6/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Real Estate City Matchmaker (MCP Implementation)

Welcome to the **City Matchmaker**, a production-grade demonstration of the Model Context Protocol (MCP) using the official `agents` SDK and Streamlit.

This agent acts as a personalized real estate and lifestyle AI assistant. It helps you decide which city to move to by dynamically querying an MCP server to fetch live (simulated) data about apartment availability, cost of living, and the local cultural vibe.

## Architecture

This module follows a clean, decoupled architecture:
1. **`matchmaker_server.py`**: A robust `FastMCP` server exposing three distinct tools over the `stdio` transport layer. It comes fully equipped with type checking and error boundaries.
2. **`backend.py`**: The agent controller layer. It securely handles `dotenv` secrets, overrides OpenAI routing vectors to leverage OpenRouter, and orchestrates the async lifecycle connecting the MCP schema to the LLM Runner.
3. **`matchmaker_ui.py`**: A synchronized Streamlit GUI that wraps the background MCP agent process and provides a chat-based user experience.

## Getting Started

1. Ensure your OpenRouter or OpenAI keys are set in the `.env` file at the root of the repository.
```bash
OPENROUTER_API_KEY=sk-or-v1-...
```
2. Make sure you have navigated to this directory.
```bash
cd 6_mcp/community_contributions/must_mcp_week6
```
3. Run the Streamlit application.
```bash
uv run streamlit run matchmaker_ui.py
```

Enjoy exploring the possibilities of MCP natively within Streamlit!
53 changes: 53 additions & 0 deletions 6_mcp/community_contributions/must_mcp_week6/backend.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import os
from dotenv import load_dotenv
from agents import Agent, Runner
from agents.mcp import MCPServerStdio

# Securely load environment variables
dotenv_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..', '.env'))
load_dotenv(dotenv_path, override=True)

# Important logic to trick OpenAI SDK to hit OpenRouter
if os.getenv("OPENROUTER_API_KEY"):
os.environ["OPENAI_API_KEY"] = os.getenv("OPENROUTER_API_KEY")
os.environ["OPENAI_BASE_URL"] = "https://openrouter.ai/api/v1"
# Suppress AGENT tracing if OpenRouter is being used to prevent 401 warnings
if "AGENTS_TRACING_ENABLED" not in os.environ:
os.environ["AGENTS_TRACING_ENABLED"] = "false"

def get_system_instructions() -> str:
"""Returns the core personality and instructions for the agent."""
return """
You are the City Matchmaker, a knowledgeable real estate and lifestyle AI assistant.
You take user criteria (like budget and city preferences) and use your available tools
to find cost of living, available apartments, and neighborhood vibes.
Present your findings clearly using Markdown with nice bullet points. Provide a final
recommendation on which city fits best based on all the simulated data you find.
"""

async def run_agent_query(user_request: str) -> str:
"""
Spawns the MCP Server and executes the AI logic via the Agents SDK.

Args:
user_request: The user's chat prompt.

Returns:
The text response generated by the AI agent.
"""
# Set up our MCP Server Subprocess Configuration
matchmaker_params = {"command": "uv", "args": ["run", "matchmaker_server.py"]}

# Context manager to initialize and cleanup the MCP server connection securely
async with MCPServerStdio(params=matchmaker_params, client_session_timeout_seconds=60) as server:

agent = Agent(
name="matchmaker_agent",
instructions=get_system_instructions(),
model="openai/gpt-4o-mini",
mcp_servers=[server]
)

# Dispatch the Runner and wait for tools to resolve
result = await Runner.run(agent, user_request)
return result.final_output
95 changes: 95 additions & 0 deletions 6_mcp/community_contributions/must_mcp_week6/matchmaker_server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import json
import logging
import random
from typing import Dict, Any
from mcp.server.fastmcp import FastMCP

# Configure robust logging for production
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("matchmaker_server")

mcp = FastMCP('matchmaker_server')

@mcp.tool()
def get_cost_of_living(city: str) -> str:
"""Get the standard cost of living index and average rent for a given city.

Args:
city: The name of the city.
"""
try:
base_rent = 1200
city_lower = city.lower().strip()

if city_lower in ['new york', 'san francisco', 'seattle']:
base_rent += 1500
elif city_lower in ['austin', 'denver', 'miami']:
base_rent += 800

data: Dict[str, Any] = {
"city": city,
"average_rent_usd": base_rent + random.randint(-200, 200),
"groceries_monthly_usd": random.randint(300, 600),
"transit_monthly_usd": random.randint(50, 150)
}
return json.dumps(data)
except Exception as e:
logger.error(f"Error calculating cost of living for {city}: {e}")
return json.dumps({"error": "Unable to fetch cost of living data."})

@mcp.tool()
def find_apartments(city: str, budget: int) -> str:
"""Find available apartment listings in a city under a specific budget.

Args:
city: The name of the city.
budget: Maximum monthly rent budget in USD.
"""
try:
names = ["The Vue", "City Center Lofts", "Sunset Apartments", "Riverside Park", "Greenwood Hub"]
results = []

# Ensure budget is an int and reasonable
budget = int(budget)

for _ in range(3):
price = random.randint(800, budget) if budget >= 800 else budget
results.append({
"name": random.choice(names),
"price_usd": price,
"bedrooms": random.choice([1, 2, 3]),
"amenities": random.sample(["Gym", "Pool", "In-unit Washer", "Doorman"], k=2)
})

return json.dumps({"apartments": results})
except ValueError:
logger.warning(f"Invalid budget format provided: {budget}")
return json.dumps({"error": "Budget must be a valid number."})
except Exception as e:
logger.error(f"Error fetching apartments: {e}")
return json.dumps({"error": "Unable to fetch apartments."})

@mcp.tool()
def get_neighborhood_vibe(city: str) -> str:
"""Get the general cultural or lifestyle vibe of a city.

Args:
city: The name of the city.
"""
try:
vibes = {
"austin": "Very vibrant, known for live music scenes, tech startups, and great BBQ. Has a growing but distinct artistic vibe.",
"seattle": "Rainy and cozy. Incredible coffee culture, very tech-heavy, surrounded by gorgeous mountains and nature.",
"new york": "Fast-paced and bustling. You can find anything 24/7. Diverse neighborhoods with intense energy and high walkability.",
}

city_lower = city.lower().strip()
description = vibes.get(city_lower, "A classic metropolitan area with a mix of residential quiet zones and a busy downtown core.")
return json.dumps({"city": city, "vibe": description})
except Exception as e:
logger.error(f"Error fetching vibe for {city}: {e}")
return json.dumps({"error": "Unable to fetch neighborhood data."})

if __name__ == "__main__":
logger.info("Starting Matchmaker MCP Server on stdio transport...")
mcp.run(transport='stdio')
38 changes: 38 additions & 0 deletions 6_mcp/community_contributions/must_mcp_week6/matchmaker_ui.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import streamlit as st
import asyncio
from backend import run_agent_query # type: ignore

# Streamlit UI Configuration
st.set_page_config(page_title="City Matchmaker", page_icon="🏙️", layout="centered")

st.title("🏙️ The Real Estate City Matchmaker")
st.markdown("I am your personalized AI relocation assistant. Ask me to compare cities, find apartment prices, and rate neighborhood vibes! (Powered by OpenRouter & MCP)")

# Initialize session state for chat history
if "messages" not in st.session_state:
st.session_state.messages = []

# Display existing chat messages
for message in st.session_state.messages:
with st.chat_message(message["role"]):
st.markdown(message["content"])

# Capture user input
if prompt := st.chat_input("E.g., Compare moving to Seattle vs Austin with a $2000 budget..."):
# Render user prompt
st.session_state.messages.append({"role": "user", "content": prompt})
with st.chat_message("user"):
st.markdown(prompt)

# We use st.spinner so the user knows MCP is booting up and querying in background
with st.chat_message("assistant"):
with st.spinner("Connecting to Property MCP Servers & Synthesizing Recommendations..."):
try:
# Wrap the asynchronous call inside asyncio.run
response_text = asyncio.run(run_agent_query(prompt))
st.markdown(response_text)
st.session_state.messages.append({"role": "assistant", "content": response_text})
except Exception as e:
error_msg = f"Uh oh! An error occurred: {str(e)}"
st.error(error_msg)
st.session_state.messages.append({"role": "assistant", "content": error_msg})