dbenham2 Claude Sonnet 4.6 commited on
Commit
7f46631
Β·
1 Parent(s): c6872da

Add beginner-friendly documentation to api.py and weather_mcp_server.py

Browse files

Explains FastAPI, MCP, async/await, HuggingFace, and the overall architecture
for readers unfamiliar with these technologies.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Files changed (2) hide show
  1. api.py +137 -26
  2. weather_mcp_server.py +101 -14
api.py CHANGED
@@ -1,14 +1,47 @@
1
  """
2
- api.py β€” FastAPI server (port 7860, required by HuggingFace Spaces)
3
-
4
- Exposes:
5
- POST /ask { "question": "What's the temp in Tokyo?" }
6
- β†’ { "answer": "..." }
7
-
8
- GET /health β†’ { "status": "ok" }
9
-
10
- The MCP server (weather_mcp_server.py) runs as a separate process on port 8000
11
- managed by supervisord. This app connects to it via SSE URL.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  """
13
 
14
  import asyncio
@@ -24,13 +57,23 @@ from mcp.client.sse import sse_client
24
  from huggingface_hub import InferenceClient
25
 
26
  # ---------------------------------------------------------------------------
27
- # Config
28
  # ---------------------------------------------------------------------------
29
 
 
 
30
  MODEL = "Qwen/Qwen2.5-7B-Instruct"
 
 
 
31
  HF_TOKEN = os.environ.get("HF_TOKEN")
 
 
 
32
  MCP_SERVER_URL = os.environ.get("MCP_SERVER_URL", "http://localhost:8000/sse")
33
 
 
 
34
  SYSTEM_PROMPT = (
35
  "You are a helpful assistant with access to real-time weather data. "
36
  "When the user asks about temperature or weather, always use the "
@@ -39,12 +82,18 @@ SYSTEM_PROMPT = (
39
  )
40
 
41
  # ---------------------------------------------------------------------------
42
- # Startup: wait for MCP server to be ready
43
  # ---------------------------------------------------------------------------
44
 
 
 
45
  @asynccontextmanager
46
  async def lifespan(app: FastAPI):
47
  print("Waiting for MCP server to be ready...", flush=True)
 
 
 
 
48
  for _ in range(20):
49
  try:
50
  async with sse_client(MCP_SERVER_URL) as (read, write):
@@ -54,23 +103,32 @@ async def lifespan(app: FastAPI):
54
  print(f"MCP ready β€” {len(tools.tools)} tool(s) available", flush=True)
55
  break
56
  except Exception:
57
- await asyncio.sleep(1)
58
  else:
59
  print("WARNING: MCP server did not become ready in time", flush=True)
60
- yield
61
 
 
62
 
 
 
63
  app = FastAPI(title="Weather via MCP + Qwen2.5", lifespan=lifespan)
64
 
65
  # ---------------------------------------------------------------------------
66
- # MCP helpers (SSE transport)
67
  # ---------------------------------------------------------------------------
68
 
69
  async def fetch_mcp_tools() -> list[dict]:
 
 
 
 
 
 
70
  async with sse_client(MCP_SERVER_URL) as (read, write):
71
  async with ClientSession(read, write) as session:
72
  await session.initialize()
73
  tools_result = await session.list_tools()
 
74
  return [
75
  {
76
  "type": "function",
@@ -85,39 +143,70 @@ async def fetch_mcp_tools() -> list[dict]:
85
 
86
 
87
  async def call_mcp_tool(tool_name: str, tool_args: dict) -> str:
 
 
 
 
 
 
 
88
  async with sse_client(MCP_SERVER_URL) as (read, write):
89
  async with ClientSession(read, write) as session:
90
  await session.initialize()
91
  result = await session.call_tool(tool_name, tool_args)
 
92
  return "\n".join(
93
  block.text for block in result.content if hasattr(block, "text")
94
  )
95
 
96
  # ---------------------------------------------------------------------------
97
- # Agentic loop
98
  # ---------------------------------------------------------------------------
99
 
100
  async def run(user_message: str) -> str:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
  client = InferenceClient(model=MODEL, token=HF_TOKEN)
 
 
102
  tools = await fetch_mcp_tools()
103
 
 
 
104
  messages = [
105
  {"role": "system", "content": SYSTEM_PROMPT},
106
  {"role": "user", "content": user_message},
107
  ]
108
 
 
109
  response = client.chat.completions.create(
110
  messages=messages,
111
- tools=tools,
112
- tool_choice="auto",
113
- max_tokens=512,
114
- temperature=0.2,
115
  )
116
 
117
  choice = response.choices[0]
118
  assistant_msg = choice.message
119
 
 
120
  while choice.finish_reason == "tool_calls" and assistant_msg.tool_calls:
 
 
121
  messages.append({
122
  "role": "assistant",
123
  "content": assistant_msg.content or "",
@@ -134,16 +223,18 @@ async def run(user_message: str) -> str:
134
  ],
135
  })
136
 
 
137
  for tc in assistant_msg.tool_calls:
138
  fn_name = tc.function.name
139
- fn_args = json.loads(tc.function.arguments)
140
  tool_result = await call_mcp_tool(fn_name, fn_args)
141
  messages.append({
142
  "role": "tool",
143
- "tool_call_id": tc.id,
144
  "content": tool_result,
145
  })
146
 
 
147
  response = client.chat.completions.create(
148
  messages=messages,
149
  tools=tools,
@@ -154,26 +245,44 @@ async def run(user_message: str) -> str:
154
  choice = response.choices[0]
155
  assistant_msg = choice.message
156
 
 
157
  return assistant_msg.content or "(no response)"
158
 
159
  # ---------------------------------------------------------------------------
160
- # Routes
161
  # ---------------------------------------------------------------------------
162
 
 
 
 
163
  class AskRequest(BaseModel):
164
- question: str
165
 
166
  class AskResponse(BaseModel):
167
- answer: str
168
 
 
 
 
169
 
170
  @app.get("/health")
171
  async def health():
 
172
  return {"status": "ok"}
173
 
174
 
175
  @app.post("/ask", response_model=AskResponse)
176
  async def ask(req: AskRequest):
 
 
 
 
 
 
 
 
 
 
177
  if not req.question.strip():
178
  raise HTTPException(status_code=400, detail="question must not be empty")
179
  answer = await run(req.question)
@@ -181,9 +290,11 @@ async def ask(req: AskRequest):
181
 
182
 
183
  # ---------------------------------------------------------------------------
184
- # Entry point
185
  # ---------------------------------------------------------------------------
186
 
 
 
187
  if __name__ == "__main__":
188
  import uvicorn
189
  uvicorn.run(app, host="0.0.0.0", port=7860)
 
1
  """
2
+ api.py β€” The main web server for this weather assistant app.
3
+
4
+ HOW THIS APP WORKS:
5
+ When someone sends a question like "What's the weather in Tokyo?", this file
6
+ handles it end-to-end:
7
+
8
+ 1. Receives the question via HTTP (like a web form submission)
9
+ 2. Passes it to an AI language model (Qwen2.5) running on HuggingFace
10
+ 3. The AI decides it needs real weather data, so it asks for the
11
+ get_current_temperature tool to be called
12
+ 4. This server calls that tool (defined in weather_mcp_server.py)
13
+ 5. The tool fetches live weather from the internet
14
+ 6. The result is sent back to the AI, which writes a natural response
15
+ 7. That response is returned to whoever asked the question
16
+
17
+ WHAT IS FastAPI?
18
+ FastAPI is a Python library for building web servers. It lets you define
19
+ "endpoints" β€” URLs that accept requests and return responses. Think of it
20
+ like building a simple API that other apps or curl commands can talk to.
21
+
22
+ WHAT IS MCP (Model Context Protocol)?
23
+ MCP is a standard way for AI models to use external tools. Instead of the
24
+ AI just generating text, it can say "I need to call this tool with these
25
+ arguments." This server listens for those requests and executes the tools.
26
+ weather_mcp_server.py defines the tools; this file calls them.
27
+
28
+ WHAT IS HuggingFace?
29
+ HuggingFace is a platform that hosts AI models and lets you run them via
30
+ their API. This app uses their "Inference API" to run the Qwen2.5 language
31
+ model without needing to host it ourselves.
32
+
33
+ ENDPOINTS (URLs this server responds to):
34
+ POST /ask Send a question, get an answer
35
+ Request: { "question": "What's the temp in Tokyo?" }
36
+ Response: { "answer": "It's currently 72Β°F and sunny in Tokyo..." }
37
+
38
+ GET /health Just checks that the server is running
39
+ Response: { "status": "ok" }
40
+
41
+ ENVIRONMENT VARIABLES (settings loaded from the environment, not hardcoded):
42
+ HF_TOKEN Your HuggingFace API token β€” needed to use the AI model
43
+ MCP_SERVER_URL Where the weather tool server is running
44
+ (defaults to http://localhost:8000/sse)
45
  """
46
 
47
  import asyncio
 
57
  from huggingface_hub import InferenceClient
58
 
59
  # ---------------------------------------------------------------------------
60
+ # Configuration
61
  # ---------------------------------------------------------------------------
62
 
63
+ # The AI model we're using. Qwen2.5 is an open-source language model made by
64
+ # Alibaba, hosted for free on HuggingFace.
65
  MODEL = "Qwen/Qwen2.5-7B-Instruct"
66
+
67
+ # Your HuggingFace token, loaded from an environment variable (not hardcoded
68
+ # here for security reasons).
69
  HF_TOKEN = os.environ.get("HF_TOKEN")
70
+
71
+ # The URL of the weather tool server (weather_mcp_server.py). SSE (Server-Sent
72
+ # Events) is the communication protocol they use to talk to each other.
73
  MCP_SERVER_URL = os.environ.get("MCP_SERVER_URL", "http://localhost:8000/sse")
74
 
75
+ # This is the instruction we give the AI at the start of every conversation.
76
+ # It tells the AI what its job is and how to behave.
77
  SYSTEM_PROMPT = (
78
  "You are a helpful assistant with access to real-time weather data. "
79
  "When the user asks about temperature or weather, always use the "
 
82
  )
83
 
84
  # ---------------------------------------------------------------------------
85
+ # Startup: wait for the weather tool server to be ready
86
  # ---------------------------------------------------------------------------
87
 
88
+ # This function runs when the web server first starts up. FastAPI calls it
89
+ # automatically before accepting any requests.
90
  @asynccontextmanager
91
  async def lifespan(app: FastAPI):
92
  print("Waiting for MCP server to be ready...", flush=True)
93
+
94
+ # Try up to 20 times (once per second) to connect to the weather tool
95
+ # server. Both servers start at the same time, so this gives the tool
96
+ # server time to finish booting before we start accepting requests.
97
  for _ in range(20):
98
  try:
99
  async with sse_client(MCP_SERVER_URL) as (read, write):
 
103
  print(f"MCP ready β€” {len(tools.tools)} tool(s) available", flush=True)
104
  break
105
  except Exception:
106
+ await asyncio.sleep(1) # Wait 1 second before trying again
107
  else:
108
  print("WARNING: MCP server did not become ready in time", flush=True)
 
109
 
110
+ yield # Hand control back to FastAPI β€” start accepting requests
111
 
112
+
113
+ # Create the FastAPI web server instance.
114
  app = FastAPI(title="Weather via MCP + Qwen2.5", lifespan=lifespan)
115
 
116
  # ---------------------------------------------------------------------------
117
+ # Functions for talking to the weather tool server (MCP)
118
  # ---------------------------------------------------------------------------
119
 
120
  async def fetch_mcp_tools() -> list[dict]:
121
+ """
122
+ Ask the weather tool server what tools it has available.
123
+
124
+ Returns a list of tool definitions formatted the way the AI model expects
125
+ them β€” including the tool's name, what it does, and what arguments it takes.
126
+ """
127
  async with sse_client(MCP_SERVER_URL) as (read, write):
128
  async with ClientSession(read, write) as session:
129
  await session.initialize()
130
  tools_result = await session.list_tools()
131
+ # Reformat each tool into the structure the AI model expects
132
  return [
133
  {
134
  "type": "function",
 
143
 
144
 
145
  async def call_mcp_tool(tool_name: str, tool_args: dict) -> str:
146
+ """
147
+ Call a specific tool on the weather tool server and return its result.
148
+
149
+ For example: call_mcp_tool("get_current_temperature", {"location": "Tokyo"})
150
+ connects to weather_mcp_server.py, runs that function, and returns the
151
+ weather data as a string.
152
+ """
153
  async with sse_client(MCP_SERVER_URL) as (read, write):
154
  async with ClientSession(read, write) as session:
155
  await session.initialize()
156
  result = await session.call_tool(tool_name, tool_args)
157
+ # The result may contain multiple content blocks; join them into one string
158
  return "\n".join(
159
  block.text for block in result.content if hasattr(block, "text")
160
  )
161
 
162
  # ---------------------------------------------------------------------------
163
+ # The agentic loop β€” the core logic of this app
164
  # ---------------------------------------------------------------------------
165
 
166
  async def run(user_message: str) -> str:
167
+ """
168
+ Send a user's question to the AI and return its final answer.
169
+
170
+ This is called an "agentic loop" because the AI can go back and forth
171
+ multiple times β€” asking for tool results, getting them, then deciding
172
+ whether to ask for more or give a final answer.
173
+
174
+ Typical flow:
175
+ 1. We send: [system prompt] + [user question] + [available tools]
176
+ 2. AI responds: "I need to call get_current_temperature for Tokyo"
177
+ 3. We call that tool and get the weather data
178
+ 4. We send the weather data back to the AI
179
+ 5. AI responds with a natural language answer β€” we return that
180
+ """
181
+ # Create a client that can talk to HuggingFace's AI inference API
182
  client = InferenceClient(model=MODEL, token=HF_TOKEN)
183
+
184
+ # Get the list of available tools from the weather server
185
  tools = await fetch_mcp_tools()
186
 
187
+ # Build the conversation history. The AI sees this as a back-and-forth chat.
188
+ # "system" sets the AI's behavior; "user" is the human's message.
189
  messages = [
190
  {"role": "system", "content": SYSTEM_PROMPT},
191
  {"role": "user", "content": user_message},
192
  ]
193
 
194
+ # Send the conversation to the AI model and get its first response
195
  response = client.chat.completions.create(
196
  messages=messages,
197
+ tools=tools, # Tell the AI what tools it can use
198
+ tool_choice="auto", # Let the AI decide whether to use a tool
199
+ max_tokens=512, # Maximum length of the AI's response
200
+ temperature=0.2, # Low temperature = more focused, less random responses
201
  )
202
 
203
  choice = response.choices[0]
204
  assistant_msg = choice.message
205
 
206
+ # If the AI wants to call a tool, handle it and loop back
207
  while choice.finish_reason == "tool_calls" and assistant_msg.tool_calls:
208
+
209
+ # Add the AI's tool request to the conversation history
210
  messages.append({
211
  "role": "assistant",
212
  "content": assistant_msg.content or "",
 
223
  ],
224
  })
225
 
226
+ # Execute each tool the AI requested and add results to the conversation
227
  for tc in assistant_msg.tool_calls:
228
  fn_name = tc.function.name
229
+ fn_args = json.loads(tc.function.arguments) # Parse the JSON arguments string
230
  tool_result = await call_mcp_tool(fn_name, fn_args)
231
  messages.append({
232
  "role": "tool",
233
+ "tool_call_id": tc.id, # Links this result to the specific tool call
234
  "content": tool_result,
235
  })
236
 
237
+ # Send the updated conversation (now including tool results) back to the AI
238
  response = client.chat.completions.create(
239
  messages=messages,
240
  tools=tools,
 
245
  choice = response.choices[0]
246
  assistant_msg = choice.message
247
 
248
+ # The AI has finished β€” return its final text response
249
  return assistant_msg.content or "(no response)"
250
 
251
  # ---------------------------------------------------------------------------
252
+ # Request/response shapes
253
  # ---------------------------------------------------------------------------
254
 
255
+ # These classes define the exact structure of data that goes in and out of
256
+ # the /ask endpoint. FastAPI uses them to validate requests and format responses.
257
+
258
  class AskRequest(BaseModel):
259
+ question: str # The user's weather question
260
 
261
  class AskResponse(BaseModel):
262
+ answer: str # The AI's response
263
 
264
+ # ---------------------------------------------------------------------------
265
+ # Endpoints (URLs the server responds to)
266
+ # ---------------------------------------------------------------------------
267
 
268
  @app.get("/health")
269
  async def health():
270
+ """Simple health check β€” returns OK if the server is running."""
271
  return {"status": "ok"}
272
 
273
 
274
  @app.post("/ask", response_model=AskResponse)
275
  async def ask(req: AskRequest):
276
+ """
277
+ Main endpoint β€” accepts a weather question and returns an AI-generated answer.
278
+
279
+ Example request:
280
+ POST /ask
281
+ { "question": "What's the temperature in Paris?" }
282
+
283
+ Example response:
284
+ { "answer": "It's currently 58Β°F and partly cloudy in Paris, France." }
285
+ """
286
  if not req.question.strip():
287
  raise HTTPException(status_code=400, detail="question must not be empty")
288
  answer = await run(req.question)
 
290
 
291
 
292
  # ---------------------------------------------------------------------------
293
+ # Entry point (only used when running locally, not in Docker)
294
  # ---------------------------------------------------------------------------
295
 
296
+ # This block only runs if you start the file directly: `python api.py`
297
+ # In Docker/HuggingFace Spaces, supervisord starts uvicorn directly instead.
298
  if __name__ == "__main__":
299
  import uvicorn
300
  uvicorn.run(app, host="0.0.0.0", port=7860)
weather_mcp_server.py CHANGED
@@ -1,37 +1,95 @@
1
  """
2
- weather_mcp_server.py
3
-
4
- A simple MCP server exposing a get_current_temperature tool.
5
- Uses Open-Meteo (free, no API key required).
6
- Run with: python weather_mcp_server.py
7
- Transport: stdio (default for local use) or SSE for remote.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  """
9
 
10
  import httpx
11
  from mcp.server.fastmcp import FastMCP
12
 
 
 
 
13
  mcp = FastMCP("weather-server", host="0.0.0.0", port=8000)
14
 
15
 
16
  def _geocode(location: str) -> tuple[float, float, str]:
17
- """Resolve a place name to (lat, lon, resolved_name) via Open-Meteo geocoding."""
 
 
 
 
 
 
 
 
 
 
 
18
  resp = httpx.get(
19
  "https://geocoding-api.open-meteo.com/v1/search",
20
  params={"name": location, "count": 1, "language": "en", "format": "json"},
21
  timeout=10,
22
  )
23
- resp.raise_for_status()
24
  data = resp.json()
25
  results = data.get("results")
26
  if not results:
27
  raise ValueError(f"Location not found: {location!r}")
28
  r = results[0]
 
29
  name = f"{r['name']}, {r.get('admin1', '')}, {r.get('country', '')}".strip(", ")
30
  return r["latitude"], r["longitude"], name
31
 
32
 
33
  def _fetch_temperature(lat: float, lon: float, unit: str) -> dict:
34
- """Fetch current temperature from Open-Meteo."""
 
 
 
 
 
 
 
 
 
 
35
  unit_param = "fahrenheit" if unit == "fahrenheit" else "celsius"
36
  resp = httpx.get(
37
  "https://api.open-meteo.com/v1/forecast",
@@ -41,7 +99,7 @@ def _fetch_temperature(lat: float, lon: float, unit: str) -> dict:
41
  "current": "temperature_2m,weathercode,wind_speed_10m",
42
  "temperature_unit": unit_param,
43
  "wind_speed_unit": "mph",
44
- "timezone": "auto",
45
  },
46
  timeout=10,
47
  )
@@ -49,7 +107,10 @@ def _fetch_temperature(lat: float, lon: float, unit: str) -> dict:
49
  return resp.json()
50
 
51
 
52
- # WMO weather code descriptions (subset)
 
 
 
53
  WMO_CODES = {
54
  0: "Clear sky", 1: "Mainly clear", 2: "Partly cloudy", 3: "Overcast",
55
  45: "Fog", 48: "Icy fog",
@@ -61,22 +122,39 @@ WMO_CODES = {
61
  }
62
 
63
 
 
 
 
 
 
 
 
 
64
  @mcp.tool()
65
  def get_current_temperature(location: str, unit: str = "fahrenheit") -> str:
66
  """
67
  Get the current temperature for any city or location.
68
 
 
 
 
 
69
  Args:
70
  location: City name or place (e.g. "Chicago", "Paris", "Tokyo").
71
  unit: Temperature unit β€” "fahrenheit" or "celsius". Defaults to fahrenheit.
72
 
73
  Returns:
74
- A plain-text summary of current conditions.
 
 
 
 
75
  """
76
  unit = unit.lower()
77
  if unit not in ("fahrenheit", "celsius"):
78
  return f"Invalid unit {unit!r}. Use 'fahrenheit' or 'celsius'."
79
 
 
80
  try:
81
  lat, lon, resolved = _geocode(location)
82
  except ValueError as e:
@@ -84,16 +162,18 @@ def get_current_temperature(location: str, unit: str = "fahrenheit") -> str:
84
  except httpx.HTTPError as e:
85
  return f"Geocoding request failed: {e}"
86
 
 
87
  try:
88
  data = _fetch_temperature(lat, lon, unit)
89
  except httpx.HTTPError as e:
90
  return f"Weather request failed: {e}"
91
 
 
92
  current = data["current"]
93
  temp = current["temperature_2m"]
94
  wind = current["wind_speed_10m"]
95
  code = current.get("weathercode", 0)
96
- condition = WMO_CODES.get(code, f"Weather code {code}")
97
  symbol = "Β°F" if unit == "fahrenheit" else "Β°C"
98
 
99
  return (
@@ -104,11 +184,18 @@ def get_current_temperature(location: str, unit: str = "fahrenheit") -> str:
104
  )
105
 
106
 
 
 
 
 
107
  if __name__ == "__main__":
108
  import os
 
 
 
109
  transport = os.environ.get("MCP_TRANSPORT", "sse")
110
  if transport == "stdio":
111
  mcp.run(transport="stdio")
112
  else:
113
- # SSE β€” persistent server, used in Docker
114
  mcp.run(transport="sse")
 
1
  """
2
+ weather_mcp_server.py β€” The weather tool server for this app.
3
+
4
+ WHAT THIS FILE DOES:
5
+ This file is a "tool server" β€” it doesn't talk to users directly. Instead,
6
+ it waits for api.py to ask it to do things, like "fetch the current
7
+ temperature in Tokyo." It does the work and sends the result back.
8
+
9
+ WHAT IS MCP (Model Context Protocol)?
10
+ MCP is a standard developed by Anthropic that defines how AI models can use
11
+ external tools. The problem it solves: AI models only know what they were
12
+ trained on β€” they can't look things up in real time. MCP lets you plug in
13
+ tools that the AI can call to get live data.
14
+
15
+ There are two sides to MCP:
16
+ - MCP Server (this file): defines and runs the tools
17
+ - MCP Client (api.py): connects to this server and calls the tools
18
+ on behalf of the AI
19
+
20
+ Think of it like a power outlet and a plug β€” MCP is the standard that
21
+ makes them compatible.
22
+
23
+ WHAT IS FastMCP?
24
+ FastMCP is a Python library that makes it easy to build MCP servers. Without
25
+ it, you'd have to implement the MCP protocol by hand. With it, you just write
26
+ normal Python functions and add a @mcp.tool() decorator, and FastMCP handles
27
+ all the communication details automatically.
28
+
29
+ HOW THE WEATHER DATA WORKS:
30
+ This app uses Open-Meteo (open-meteo.com), a free weather API that doesn't
31
+ require an account or API key. Getting weather for a city takes two steps:
32
+ 1. Geocoding: convert the city name ("Tokyo") into coordinates (lat/lon)
33
+ 2. Weather fetch: use those coordinates to get current conditions
34
+
35
+ HOW THIS SERVER COMMUNICATES:
36
+ This server uses SSE (Server-Sent Events) β€” a way for one server to talk to
37
+ another over HTTP. It runs on port 8000 and api.py connects to it there.
38
+ SSE is used instead of stdio (standard input/output) because both servers
39
+ run as separate processes inside Docker and need to communicate over the
40
+ network, not through a shared terminal.
41
  """
42
 
43
  import httpx
44
  from mcp.server.fastmcp import FastMCP
45
 
46
+ # Create the MCP server instance.
47
+ # "weather-server" is just a name that identifies this server.
48
+ # host/port tell it to listen on all network interfaces on port 8000.
49
  mcp = FastMCP("weather-server", host="0.0.0.0", port=8000)
50
 
51
 
52
  def _geocode(location: str) -> tuple[float, float, str]:
53
+ """
54
+ Convert a city name into GPS coordinates (latitude and longitude).
55
+
56
+ AI models work with place names like "Tokyo" or "Paris", but weather APIs
57
+ need exact coordinates. This function bridges that gap by calling Open-Meteo's
58
+ free geocoding API.
59
+
60
+ Returns a tuple of (latitude, longitude, full_place_name).
61
+ For example: "Tokyo" β†’ (35.6895, 139.6917, "Tokyo, Tokyo, Japan")
62
+
63
+ Raises ValueError if the location isn't found.
64
+ """
65
  resp = httpx.get(
66
  "https://geocoding-api.open-meteo.com/v1/search",
67
  params={"name": location, "count": 1, "language": "en", "format": "json"},
68
  timeout=10,
69
  )
70
+ resp.raise_for_status() # Raises an error if the HTTP request failed
71
  data = resp.json()
72
  results = data.get("results")
73
  if not results:
74
  raise ValueError(f"Location not found: {location!r}")
75
  r = results[0]
76
+ # Build a full place name like "Chicago, Illinois, United States"
77
  name = f"{r['name']}, {r.get('admin1', '')}, {r.get('country', '')}".strip(", ")
78
  return r["latitude"], r["longitude"], name
79
 
80
 
81
  def _fetch_temperature(lat: float, lon: float, unit: str) -> dict:
82
+ """
83
+ Fetch current weather conditions from Open-Meteo using GPS coordinates.
84
+
85
+ Requests three pieces of data:
86
+ - temperature_2m: air temperature measured 2 meters above ground
87
+ - weathercode: a WMO standard code representing current conditions
88
+ (e.g. 0 = clear sky, 61 = slight rain)
89
+ - wind_speed_10m: wind speed measured 10 meters above ground
90
+
91
+ Returns the raw JSON response from Open-Meteo.
92
+ """
93
  unit_param = "fahrenheit" if unit == "fahrenheit" else "celsius"
94
  resp = httpx.get(
95
  "https://api.open-meteo.com/v1/forecast",
 
99
  "current": "temperature_2m,weathercode,wind_speed_10m",
100
  "temperature_unit": unit_param,
101
  "wind_speed_unit": "mph",
102
+ "timezone": "auto", # Automatically detect the timezone for this location
103
  },
104
  timeout=10,
105
  )
 
107
  return resp.json()
108
 
109
 
110
+ # WMO (World Meteorological Organization) weather codes.
111
+ # Open-Meteo returns a numeric code for current conditions β€” this table
112
+ # translates those numbers into human-readable descriptions.
113
+ # This is a subset of the full WMO code table (codes go up to 99).
114
  WMO_CODES = {
115
  0: "Clear sky", 1: "Mainly clear", 2: "Partly cloudy", 3: "Overcast",
116
  45: "Fog", 48: "Icy fog",
 
122
  }
123
 
124
 
125
+ # ---------------------------------------------------------------------------
126
+ # The MCP tool β€” this is what the AI model can call
127
+ # ---------------------------------------------------------------------------
128
+
129
+ # @mcp.tool() is a "decorator" β€” it registers this function with the MCP server
130
+ # so that api.py can discover it and offer it to the AI model. Without this
131
+ # decorator, the function would just be a regular Python function that nothing
132
+ # outside this file knows about.
133
  @mcp.tool()
134
  def get_current_temperature(location: str, unit: str = "fahrenheit") -> str:
135
  """
136
  Get the current temperature for any city or location.
137
 
138
+ This is the function the AI model calls when it needs weather data.
139
+ The AI passes in the location name and preferred unit, and gets back
140
+ a plain-text summary it can include in its response to the user.
141
+
142
  Args:
143
  location: City name or place (e.g. "Chicago", "Paris", "Tokyo").
144
  unit: Temperature unit β€” "fahrenheit" or "celsius". Defaults to fahrenheit.
145
 
146
  Returns:
147
+ A plain-text summary of current conditions, for example:
148
+ Current conditions in Tokyo, Tokyo, Japan:
149
+ Temperature: 68Β°F
150
+ Conditions: Partly cloudy
151
+ Wind speed: 12 mph
152
  """
153
  unit = unit.lower()
154
  if unit not in ("fahrenheit", "celsius"):
155
  return f"Invalid unit {unit!r}. Use 'fahrenheit' or 'celsius'."
156
 
157
+ # Step 1: Convert the city name to GPS coordinates
158
  try:
159
  lat, lon, resolved = _geocode(location)
160
  except ValueError as e:
 
162
  except httpx.HTTPError as e:
163
  return f"Geocoding request failed: {e}"
164
 
165
+ # Step 2: Fetch the current weather for those coordinates
166
  try:
167
  data = _fetch_temperature(lat, lon, unit)
168
  except httpx.HTTPError as e:
169
  return f"Weather request failed: {e}"
170
 
171
+ # Step 3: Extract the relevant fields from the response and format them
172
  current = data["current"]
173
  temp = current["temperature_2m"]
174
  wind = current["wind_speed_10m"]
175
  code = current.get("weathercode", 0)
176
+ condition = WMO_CODES.get(code, f"Weather code {code}") # Fall back to the raw code if unknown
177
  symbol = "Β°F" if unit == "fahrenheit" else "Β°C"
178
 
179
  return (
 
184
  )
185
 
186
 
187
+ # ---------------------------------------------------------------------------
188
+ # Entry point β€” how this server starts up
189
+ # ---------------------------------------------------------------------------
190
+
191
  if __name__ == "__main__":
192
  import os
193
+ # MCP_TRANSPORT can be set to "stdio" for local testing (communicates via
194
+ # terminal input/output). In Docker/HuggingFace Spaces it defaults to "sse"
195
+ # so api.py can connect to it over the network.
196
  transport = os.environ.get("MCP_TRANSPORT", "sse")
197
  if transport == "stdio":
198
  mcp.run(transport="stdio")
199
  else:
200
+ # SSE (Server-Sent Events) β€” runs as a persistent HTTP server on port 8000
201
  mcp.run(transport="sse")