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

changes meddleware

Browse files
Files changed (3) hide show
  1. echo_server.py +105 -111
  2. server.py +3 -2
  3. templates/index.html +4 -3
echo_server.py CHANGED
@@ -1,133 +1,127 @@
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)
 
1
  from mcp.server.fastmcp import FastMCP
2
+ from typing import Dict, Any, Optional
 
 
 
3
  from contextvars import ContextVar
4
+ from starlette.middleware.base import BaseHTTPMiddleware
5
+ from starlette.requests import Request
6
+ from fastmcp.server.middleware import Middleware, MiddlewareContext
7
+ import requests
8
+
9
+ mcp = FastMCP(name="OpenWeatherServer", stateless_http=True)
10
+
11
+ OPENWEATHER_BASE_URL = "https://api.openweathermap.org/data/2.5"
12
+
13
+ # Context variable to hold the token for the current request
14
+ _current_token: ContextVar[Optional[str]] = ContextVar("openweather_token", default=None)
15
+
16
+
17
+ class AuthorizationHeaderMiddleware(BaseHTTPMiddleware):
18
+ async def dispatch(self, request: Request, call_next):
19
+ auth_header = request.headers.get("Authorization", "").strip()
20
+ token: Optional[str] = None
21
+ if auth_header:
22
+ lower = auth_header.lower()
23
+ if lower.startswith("bearer "):
24
+ token = auth_header[7:].strip()
25
+ else:
26
+ token = auth_header
27
+ _current_token.set(token)
28
+ response = await call_next(request)
29
+ return response
30
+
31
+
32
+ class RequireAuthMiddleware(Middleware):
33
+ async def on_request(self, context: MiddlewareContext, call_next):
34
+ # Enforce a token is present for any MCP request (tool calls, listings, etc.)
35
+ token = _current_token.get()
36
+ if not token:
37
+ raise ValueError(
38
+ "Missing OpenWeather API key. Provide it as a Bearer token in the Authorization header."
39
+ )
40
+ return await call_next(context)
41
 
42
 
43
+ # Register FastMCP middleware
44
+ mcp.add_middleware(RequireAuthMiddleware())
45
 
 
 
 
46
 
47
+ def _require_api_key_from_header() -> str:
48
+ token = _current_token.get()
49
+ if not token:
50
+ raise ValueError(
51
+ "Missing OpenWeather API key. Provide it as a Bearer token in the Authorization header."
52
+ )
53
+ return token
54
 
 
 
 
 
 
 
 
 
 
 
 
55
 
56
+ def _request(path: str, params: Dict[str, Any]) -> Dict[str, Any]:
57
+ url = f"{OPENWEATHER_BASE_URL}/{path}"
58
+ try:
59
+ response = requests.get(url, params=params, timeout=15)
60
+ except requests.RequestException as exc:
61
+ raise RuntimeError(f"Failed to call OpenWeather: {exc}") from exc
62
 
63
+ if response.status_code != 200:
64
+ try:
65
+ payload = response.json()
66
+ except Exception:
67
+ payload = {"message": response.text}
68
+ message = payload.get("message") or payload
69
+ raise ValueError(f"OpenWeather error {response.status_code}: {message}")
70
 
71
+ return response.json()
 
72
 
73
 
74
+ @mcp.tool(description="Get current weather by city name. Token must be in Authorization: Bearer <token> header.")
75
+ def get_current_weather_city(
76
+ city: str,
77
+ country_code: str = "",
78
+ units: str = "metric",
79
+ lang: str = "en",
80
+ ) -> Dict[str, Any]:
81
+ key = _require_api_key_from_header()
82
+ q = city if not country_code else f"{city},{country_code}"
83
+ params = {"q": q, "appid": key, "units": units, "lang": lang}
84
+ return _request("weather", params)
85
 
86
 
87
+ @mcp.tool(description="Get current weather by geographic coordinates. Token must be in Authorization header.")
88
+ def get_current_weather_coords(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
  lat: float,
90
  lon: float,
91
+ units: str = "metric",
92
  lang: str = "en",
93
+ ) -> Dict[str, Any]:
94
+ key = _require_api_key_from_header()
95
+ params = {"lat": lat, "lon": lon, "appid": key, "units": units, "lang": lang}
96
+ return _request("weather", params)
97
+
98
+
99
+ @mcp.tool(description="Get 5 day / 3 hour forecast by city. Token must be in Authorization header.")
100
+ def get_forecast_city(
101
+ city: str,
102
+ country_code: str = "",
103
+ units: str = "metric",
104
+ lang: str = "en",
105
+ ) -> Dict[str, Any]:
106
+ key = _require_api_key_from_header()
107
+ q = city if not country_code else f"{city},{country_code}"
108
+ params = {"q": q, "appid": key, "units": units, "lang": lang}
109
+ return _request("forecast", params)
110
+
111
+
112
+ @mcp.tool(description="Get 5 day / 3 hour forecast by coordinates. Token must be in Authorization header.")
113
+ def get_forecast_coords(
114
  lat: float,
115
  lon: float,
116
+ units: str = "metric",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
  lang: str = "en",
118
+ ) -> Dict[str, Any]:
119
+ key = _require_api_key_from_header()
120
+ params = {"lat": lat, "lon": lon, "appid": key, "units": units, "lang": lang}
121
+ return _request("forecast", params)
122
 
123
 
124
+ def openweather_http_app():
125
+ app = mcp.streamable_http_app()
126
+ app.add_middleware(AuthorizationHeaderMiddleware)
127
+ return app
 
server.py CHANGED
@@ -4,6 +4,7 @@ from fastapi.responses import HTMLResponse, FileResponse
4
  from fastapi.staticfiles import StaticFiles
5
  from fastapi.templating import Jinja2Templates
6
  from echo_server import mcp as echo_mcp
 
7
  from math_server import mcp as math_mcp
8
  import os
9
 
@@ -39,10 +40,10 @@ async def index(request: Request):
39
  return templates.TemplateResponse("index.html", {"request": request, "base_url": base_url})
40
 
41
 
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
 
4
  from fastapi.staticfiles import StaticFiles
5
  from fastapi.templating import Jinja2Templates
6
  from echo_server import mcp as echo_mcp
7
+ from echo_server import openweather_http_app
8
  from math_server import mcp as math_mcp
9
  import os
10
 
 
40
  return templates.TemplateResponse("index.html", {"request": request, "base_url": base_url})
41
 
42
 
43
+ app.mount("/echo", openweather_http_app())
44
  app.mount("/math", math_mcp.streamable_http_app())
45
 
46
+ PORT = int(os.environ.get("PORT", "10000"))
47
 
48
  if __name__ == "__main__":
49
  import uvicorn
templates/index.html CHANGED
@@ -89,9 +89,10 @@ Math MCP = {{ base_url }}/math/mcp
89
  </ul>
90
  <h3>Use in clients</h3>
91
  <p class="muted">Point your MCP client to the endpoints below:</p>
92
- <pre><code>HTTP streaming endpoints:
93
- Echo: {{ base_url }}/echo/mcp
94
- Math: {{ base_url }}/math/mcp
 
95
  </code></pre>
96
  </div>
97
  </div>
 
89
  </ul>
90
  <h3>Use in clients</h3>
91
  <p class="muted">Point your MCP client to the endpoints below:</p>
92
+ <pre><code>HTTP streaming endpoints
93
+
94
+ Echo: GET {{ base_url }}/echo
95
+ Math: GET {{ base_url }}/math
96
  </code></pre>
97
  </div>
98
  </div>