Jofthomas commited on
Commit
d3ac497
·
1 Parent(s): 6587561

auth token

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