Jofthomas commited on
Commit
a039815
·
1 Parent(s): 055cb70
Files changed (1) hide show
  1. echo_server.py +130 -4
echo_server.py CHANGED
@@ -1,8 +1,134 @@
1
  from mcp.server.fastmcp import FastMCP
 
 
 
 
 
2
 
3
- mcp = FastMCP(name="EchoServer", stateless_http=True)
 
 
 
 
4
 
 
 
5
 
6
- @mcp.tool(description="A simple echo tool")
7
- def echo(message: str) -> str:
8
- return f"Echo: {message}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  from mcp.server.fastmcp import FastMCP
2
+ from typing import Optional, Literal
3
+ import httpx
4
+ import os
5
+ import re
6
+ from contextvars import ContextVar
7
 
8
+ # Try to import TokenVerifier from fastmcp; fall back gracefully if path differs
9
+ try:
10
+ from fastmcp.server.auth.verifier import TokenVerifier # type: ignore
11
+ except Exception: # pragma: no cover - fallback for alternate installs
12
+ TokenVerifier = object # type: ignore
13
 
14
+ # Context variable to store the verified bearer token per-request
15
+ _current_bearer_token: ContextVar[Optional[str]] = ContextVar("current_bearer_token", default=None)
16
 
17
+
18
+ class OpenWeatherTokenVerifier(TokenVerifier): # type: ignore[misc]
19
+ """Minimal verifier that accepts an OpenWeather API key as the bearer token.
20
+
21
+ For safety, we perform a lightweight format check (32 hex chars typical for OpenWeather keys).
22
+ On success, we stash the token in a contextvar and return claims including it.
23
+ """
24
+
25
+ _hex32 = re.compile(r"^[a-fA-F0-9]{32}$")
26
+
27
+ def verify_token(self, token: str) -> dict: # type: ignore[override]
28
+ if not token:
29
+ raise ValueError("Missing bearer token")
30
+ # Basic format validation (doesn't call OpenWeather)
31
+ if not self._hex32.match(token):
32
+ # Allow override via env to support alternative key formats if needed
33
+ allow_any = os.getenv("OPENWEATHER_ALLOW_ANY_TOKEN", "false").lower() in {"1", "true", "yes"}
34
+ if not allow_any:
35
+ raise ValueError("Invalid OpenWeather token format; expected 32 hex characters")
36
+ _current_bearer_token.set(token)
37
+ return {"auth_type": "openweather", "openweather_token": token}
38
+
39
+
40
+ mcp = FastMCP(name="OpenWeatherServer", stateless_http=True, auth=OpenWeatherTokenVerifier())
41
+
42
+ BASE_URL = "https://api.openweathermap.org"
43
+ DEFAULT_TIMEOUT_SECONDS = 15.0
44
+
45
+
46
+ def _require_token() -> str:
47
+ # Priority: verified bearer token → env var → error
48
+ token = _current_bearer_token.get()
49
+ if token:
50
+ return token
51
+ env_token = os.getenv("OPENWEATHER_API_KEY")
52
+ if env_token:
53
+ return env_token
54
+ raise ValueError(
55
+ "OpenWeather token required. Provide as HTTP Authorization: Bearer <token> or set OPENWEATHER_API_KEY."
56
+ )
57
+
58
+
59
+ def _get(path: str, params: dict) -> dict:
60
+ try:
61
+ with httpx.Client(timeout=httpx.Timeout(DEFAULT_TIMEOUT_SECONDS)) as client:
62
+ resp = client.get(f"{BASE_URL}{path}", params=params)
63
+ resp.raise_for_status()
64
+ return resp.json()
65
+ except httpx.HTTPStatusError as e:
66
+ status = e.response.status_code if e.response is not None else None
67
+ text = e.response.text if e.response is not None else str(e)
68
+ raise ValueError(f"OpenWeather API error (status={status}): {text}")
69
+ except Exception as e:
70
+ raise RuntimeError(f"OpenWeather request failed: {e}")
71
+
72
+
73
+ @mcp.tool(description="Get current weather using One Call 3.0 (current segment). Token is taken from Authorization bearer or OPENWEATHER_API_KEY env.")
74
+ def current_weather(
75
+ lat: float,
76
+ lon: float,
77
+ units: Literal["standard", "metric", "imperial"] = "metric",
78
+ lang: str = "en",
79
+ ) -> dict:
80
+ token = _require_token()
81
+ params = {
82
+ "lat": lat,
83
+ "lon": lon,
84
+ "appid": token,
85
+ "units": units,
86
+ "lang": lang,
87
+ "exclude": "minutely,hourly,daily,alerts",
88
+ }
89
+ data = _get("/data/3.0/onecall", params)
90
+ return {
91
+ "coord": {"lat": lat, "lon": lon},
92
+ "units": units,
93
+ "lang": lang,
94
+ "current": data.get("current", data),
95
+ }
96
+
97
+
98
+ @mcp.tool(description="Get One Call 3.0 data for coordinates. Use 'exclude' to omit segments (comma-separated). Token comes from bearer or env.")
99
+ def onecall(
100
+ lat: float,
101
+ lon: float,
102
+ exclude: Optional[str] = None,
103
+ units: Literal["standard", "metric", "imperial"] = "metric",
104
+ lang: str = "en",
105
+ ) -> dict:
106
+ token = _require_token()
107
+ params = {
108
+ "lat": lat,
109
+ "lon": lon,
110
+ "appid": token,
111
+ "units": units,
112
+ "lang": lang,
113
+ }
114
+ if exclude:
115
+ params["exclude"] = exclude
116
+ return _get("/data/3.0/onecall", params)
117
+
118
+
119
+ @mcp.tool(description="Get 5-day/3-hour forecast by city name. Token comes from bearer or env.")
120
+ def forecast_city(
121
+ city: str,
122
+ units: Literal["standard", "metric", "imperial"] = "metric",
123
+ lang: str = "en",
124
+ ) -> dict:
125
+ token = _require_token()
126
+ params = {"q": city, "appid": token, "units": units, "lang": lang}
127
+ return _get("/data/2.5/forecast", params)
128
+
129
+
130
+ @mcp.tool(description="Get air pollution data for coordinates. Token comes from bearer or env.")
131
+ def air_pollution(lat: float, lon: float) -> dict:
132
+ token = _require_token()
133
+ params = {"lat": lat, "lon": lon, "appid": token}
134
+ return _get("/data/2.5/air_pollution", params)