A LangGraph agent system that determines the weather forecast at a data center location using the Model Context Protocol (MCP).
Architecture: Custom StateGraph with explicit state management, modular nodes, and conditional routing following LangGraph 2026 best practices.
This system answers the question: "What is the weather forecast of the data center?"
It autonomously: 0. Check the Intent Classification avoind answering questions that it is not supposed too answer or giving the same answer for any question
- Discovers the data center's public IP address
- Resolves the IP to geographic coordinates
- Fetches the current weather forecast for that location
- Synthesizes a natural language response
This implementation uses LangGraph's custom StateGraph approach rather than prebuilt agents because:
- Full Transparency: Every decision point is explicit and debuggable
- Production-Ready: Modular design supports testing, monitoring, and extension
- Best Practices: Follows LangGraph 2026 recommendations for stateful agents acquired in the Langgraph documentation: https://docs.langchain.com/oss/python/langgraph/workflows-agents
- Maintainable: Clear separation of concerns with typed state schemas
┌─────────────────────────────────────────────────────────────────┐
│ Data Center Weather Agent │
│ │
│ ┌─────────────┐ ┌───────────────────────────────────┐ │
│ │ Agent │ HTTP │ MCP Server │ │
│ │ (LangGraph) │◄───────►│ ┌────────┐ ┌─────────┐ ┌───────┐ │ │
│ │ │ SSE │ │ ipify │ │ip_to_geo│ │weather│ │ |
│ │ 5 Nodes │ │ └───┬────┘ └────┬────┘ └──┬────┘ │ │
│ │ 3 Edges │ │ │ │ │ │ │
│ │ 9 State │ │ ▼ ▼ ▼ │ │
│ │ Fields │ │ ipify.org ip-api.com meteo │ │
│ └─────────────┘ └───────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
- Technology: Python, FastMCP, uvicorn
- Port: 8000 (SSE transport)
- Tools:
ipify: Get public IP addressip_to_geo: Convert IP to lat/lon (ip-api.com)weather_forecast: Fetch weather data (Open-Meteo)
- Features: Input validation, error handling, structured logging
- Technology: Python, LangGraph, LangChain, Google Gemini
- Architecture: Custom StateGraph
- State: 9 typed fields tracking workflow progress
- Nodes: 5 modular computation units
- Edges: 3 conditional routing functions
- Features: LLM fallback, detailed trace, error recovery
All data flows through a strongly typed AgentState:
class AgentState(TypedDict):
question: str # User's input query
public_ip: str | None # From ipify tool
latitude: float | None # From ip_to_geo tool
longitude: float | None # From ip_to_geo tool
weather_data: str | None # From weather_forecast tool
answer: str | None # Final LLM response
messages: list # Conversation history
error: str | None # Error tracking
current_step: str # Progress indicatorSTART
↓
┌───────────────────────┐
│ intent classification│ → [custom logic]
└──────┬────────────────┘
↓
┌─────────────┐
│ get_ip │ → [ipify tool]
└──────┬──────┘
│ [conditional: success/error]
↓
┌──────────────────┐
│ resolve_location │ → [ip_to_geo tool]
└──────┬───────────┘
│ [conditional: success/error]
↓
┌──────────────┐
│fetch_weather │ → [weather_forecast tool]
└──────┬───────┘
│ [conditional: success/error]
↓
┌────────────────┐
│generate_answer │ → [LLM synthesis]
└───────┬────────┘
↓
END
[error] ← Any failure routes here
- get_ip_node: Calls ipify tool, updates state with public_ip
- resolve_location_node: Calls ip_to_geo, extracts lat/lon
- fetch_weather_node: Calls weather_forecast with coordinates
- generate_answer_node: LLM creates natural language response
- error_node: Centralized error handling and user messaging
Each edge validates the previous operation before proceeding:
def route_after_ip(state: AgentState) -> Literal["resolve_location", "error"]:
"""Route to next node or error based on IP fetch result"""
if state.get("error") or not state.get("public_ip"):
return "error"
return "resolve_location"- Python 3.10 or higher
- Fully tested on the version 3.14.2
- Google Gemini API key (free tier available)
- Optional: LongCat API key (fallback)
-
Clone or Download this project
-
Install Dependencies:
pip install -r requirements.txt
-
Configure API Keys:
Copy the example environment file:
cp .env.example .env
Edit
.envand add your API keys:GOOGLE_API_KEY=your_gemini_api_key_here LONGCAT_API_KEY=your_longcat_api_key_here # OptionalGet API keys:
- Gemini: https://ai.google.dev/
- LongCat: https://longcat.chat/ (optional fallback)
You need two terminal windows:
Terminal 1 - Start MCP Server:
python3 -m server.mainExpected output:
INFO: Started server process
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: Uvicorn running on http://0.0.0.0:8000
Terminal 2 - Run Agent:
python3 -m agent.main============================================================
Data Center Weather Agent (Custom StateGraph)
============================================================
Connected to MCP Server
LLM configured with fallback: Gemini -> LongCat
Graph structure built successfully
Agent Ready! (Type 'quit' to exit)
------------------------------------------------------------
Enter your question: What is the weather forecast of the data center?
============================================================
EXECUTION TRACE
============================================================
[Step 1: IP Discovery]
Tool: ipify
Result: 174.162.142.78
[Step 2: Location Resolution]
Tool: ip_to_geo
Input: 174.162.142.78
Result: 40.3495, -111.8998
[Step 3: Weather Retrieval]
Tool: weather_forecast
Input: lat=40.3495, lon=-111.8998
Result: Temperature: 0.2 C, Windspeed: 2.2 km/h
[Step 4: Answer Generation]
Generated answer successfully
============================================================
FINAL ANSWER
============================================================
The data center, located at 40.3495°N, 111.8998°W, currently has a
temperature of 0.2°C and a wind speed of 2.2 km/h.
Enter your question: Where is the data center located?
[... processes IP and location lookup ...]
FINAL ANSWER
The data center is located at coordinates 40.3495°N, 111.8998°W
(approximately in Utah, United States).
Enter your question: quit
datacenter_weather_agent/
├── server/
│ ├── main.py # MCP server with FastMCP
│ ├── tools.py # Tool implementations
│ └── __init__.py
├── agent/
│ ├── main.py # Custom StateGraph agent (~570 lines)
│ ├── client.py # MCP client wrapper
│ └── __init__.py
├── README.md # This file
├── ARCHITECTURE.md # Deep technical documentation
├── .env.example # API key template
├── .env # Your API keys (create this)
└── requirements.txt # Python dependencies
Server-Side:
- IP address format validation (IPv4/IPv6)
- IPv4 octet range checking (0-255)
- Coordinate range validation (lat: -90 to 90, lon: -180 to 180)
- Type checking on all parameters
Client-Side:
- Response structure validation
- Non-empty content verification
- Runtime error detection
Per-Node Error Handling:
async def get_ip_node(state, tools):
try:
# ... tool execution ...
return success_state
except Exception as e:
logger.error(f"Error in get_ip_node: {e}")
return error_stateConditional Routing to Error Node:
- Any node failure routes to centralized error handler
- User-friendly error messages
- Agent remains responsive (doesn't crash)
Automatic failover on rate limits:
gemini_llm = ChatGoogleGenerativeAI(model="gemini-3-pro-preview")
longcat_llm = ChatOpenAI(model="LongCat-Flash-Chat", ...)
llm = gemini_llm.with_fallbacks([longcat_llm])Every operation is logged:
- Tool calls with inputs and outputs
- State transitions
- Error conditions
- LLM reasoning
Good for debugging and monitoring.
The agent produces clean, focused output by:
- Suppressing verbose library logs (httpx, google_genai)
- Showing only essential step information
- Automatically handling LLM fallbacks silently
User-facing trace shows only what matters:
[Step 1: IP Discovery]
Tool: ipify
Result: 174.162.142.78
Backend logging still captures details for debugging.
The agent enforces a strict sequential workflow:
-
IP Discovery (ipify)
- No prerequisites
- Must succeed before proceeding
-
Location Resolution (ip_to_geo)
- Requires:
public_ipfrom step 1 - Validates IP format before calling
- Parses lat/lon from response
- Requires:
-
Weather Retrieval (weather_forecast)
- Requires:
latitudeandlongitudefrom step 2 - Validates coordinate ranges
- Returns formatted weather string
- Requires:
-
Answer Generation (LLM)
- Requires: All collected data
- Synthesizes natural language response
- Maintains conversation context
Validation: Each conditional edge verifies prerequisites exist before routing to the next node.
from langgraph.checkpoint.sqlite import SqliteSaver
# In build_graph():
async with SqliteSaver.from_conn_string("./checkpoints.db") as memory:
graph = workflow.compile(checkpointer=memory)
# In main():
config = {"configurable": {"thread_id": "user-123"}}
result = await graph.ainvoke(state, config=config)from langgraph.checkpoint.sqlite import SqliteSaver
# Compile with interrupt
graph = workflow.compile(
checkpointer=memory,
interrupt_before=["generate_answer"] # Pause before final answer
)
# Resume after approval
graph.invoke(None, config=config) # Continue from interruptCreate specialized sub-agents:
weather_specialist = build_weather_subgraph()
location_specialist = build_location_subgraph()
workflow.add_node("weather", weather_specialist)
workflow.add_node("location", location_specialist)
workflow.add_conditional_edges("start", route_to_specialist, {...})Symptom: connection refused or server not found
Solution:
- Verify server is running:
curl http://localhost:8000/sse - Check no other process is using port 8000
- Review server logs for startup errors
Symptom: 429 RESOURCE_EXHAUSTED
Solution:
- Fallback to LongCat triggers automatically if configured
- Wait 60 seconds and retry
- Consider upgrading to Gemini paid tier
Symptom: ModuleNotFoundError
Solution:
# Always run from project root:
cd /path/to/datacenter_weather_agent
python3 -m agent.main Symptom: Invalid IP address format or Invalid latitude
Solution:
- Check server logs for detailed error
- Verify external APIs are accessible
- Test manually:
curl https://api.ipify.org?format=json
Typical Metrics:
- Cold start: 1-2 seconds (library loading)
- Tool execution: 3-5 seconds (3× HTTP requests)
- LLM synthesis: 0.5-1 second
- Total: ~5-8 seconds per query
- Memory: ~50MB runtime
Bottlenecks:
- External API latency (ipify, ip-api, open-meteo)
- LLM generation time - free tier api keys have a significant slower performance, please be patient
- Network conditions
- Sequential Execution: Tools run in sequence (could be parallelized in some cases)
- Public IP Only: Cannot determine location of internal/private IPs
- VPN Aware: Location reflects VPN exit node if active
- No Retry Logic: Failed tool calls immediately error (can add exponential backoff)
- Single Query: Processes one question at a time (can add batch support)
- No Persistence: State cleared between sessions (unless checkpointing enabled)
-
Implement in
server/tools.py:async def get_timezone(latitude: float, longitude: float) -> str: # Implementation return timezone_data
-
Register in
server/main.py:@mcp.tool() async def timezone(latitude: float, longitude: float) -> str: return await get_timezone(latitude, longitude)
-
Add Node in
agent/main.py:async def get_timezone_node(state, tools): tool = next(t for t in tools if t.name == "timezone") result = await tool.ainvoke({ "latitude": state["latitude"], "longitude": state["longitude"] }) return {**state, "timezone": result} workflow.add_node("get_timezone", get_timezone_node) workflow.add_edge("fetch_weather", "get_timezone") workflow.add_edge("get_timezone", "generate_answer")
Change conditional edges to support alternative flows:
def route_after_ip(state: AgentState):
if state.get("error"):
return "error"
# New: Check if IP is internal/private
ip = state.get("public_ip", "")
if ip.startswith("192.168.") or ip.startswith("10."):
return "handle_private_ip" # New node
return "resolve_location"Test individual nodes:
import pytest
from agent.main import get_ip_node
@pytest.mark.asyncio
async def test_get_ip_node():
mock_tools = [MockIPifyTool()]
state = {"question": "test", "messages": []}
result = await get_ip_node(state, mock_tools)
assert result["public_ip"] is not None
assert result["error"] is NoneTest full graph execution:
@pytest.mark.asyncio
async def test_full_workflow(mcp_server_running):
graph = await build_graph(client, llm)
initial_state = {...}
final_state = await graph.ainvoke(initial_state)
assert final_state["answer"] is not None
assert "temperature" in final_state["answer"].lower()Enable tracing for production debugging:
export LANGCHAIN_TRACING_V2=true
export LANGCHAIN_API_KEY=your_key
export LANGCHAIN_PROJECT="datacenter-weather"Every execution will be traced in LangSmith dashboard.
- API Keys: Never commit
.envto version control - Server Exposure: MCP server has no authentication (local use only)
- Rate Limiting: External APIs may ban abusive usage
- Input Validation: Server validates all tool inputs
- Error Messages: Avoid leaking sensitive data in logs
- httpx: Async HTTP client
- mcp: Model Context Protocol SDK
- langgraph: Graph-based agent framework
- langchain: LLM abstraction layer
- langchain-google-genai: Gemini integration
- langchain-openai: OpenAI-compatible APIs (LongCat)
- python-dotenv: Environment variable management
- uvicorn: ASGI server
See requirements.txt for specific versions.
This is a demonstration project for personal purposes. No warranty provided.
Built with: LangGraph Custom StateGraph (Production Best Practices, January 2026)