abdurrahman commited on
Commit
67bbf46
·
1 Parent(s): 08c0115

Add HTTP MCP Server implementation with Google OAuth and email restriction

Browse files
Files changed (1) hide show
  1. http_mcp_server.py +186 -0
http_mcp_server.py ADDED
@@ -0,0 +1,186 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ HTTP MCP Server using FastMCP - curl-like HTTP requests with Bearer token support.
4
+ Secured with Google OAuth authentication.
5
+ Only allows specific emails to call tools.
6
+
7
+ Install: uv pip install mcp httpx fastmcp
8
+ Run: uv run http_mcp_server.py
9
+ """
10
+
11
+ import json
12
+ import os
13
+ import httpx
14
+ from fastmcp import FastMCP
15
+ from fastmcp.server.auth.providers.google import GoogleProvider
16
+ from fastmcp.server.dependencies import get_access_token
17
+
18
+ # --- Google OAuth Configuration ---
19
+ auth_provider = GoogleProvider(
20
+ client_id=os.getenv("GOOGLE_CLIENT_ID"),
21
+ client_secret=os.getenv("GOOGLE_CLIENT_SECRET"),
22
+ base_url=os.getenv("APP_BASE_URL", "http://localhost:8000"),
23
+ required_scopes=[
24
+ "openid",
25
+ "https://www.googleapis.com/auth/userinfo.email",
26
+ ],
27
+ )
28
+
29
+ mcp = FastMCP(name="http-client", auth=auth_provider)
30
+
31
+ # --- Allowed Emails ---
32
+ ALLOWED_EMAILS = set()
33
+ allowed_email_text = os.getenv("ALLOWED_EMAILS")
34
+ if allowed_email_text:
35
+ for email in allowed_email_text.split(","):
36
+ ALLOWED_EMAILS.add(email.strip())
37
+
38
+
39
+ def require_allowed_email() -> str:
40
+ """
41
+ Checks the authenticated user's email against the allowlist.
42
+ Raises PermissionError if not allowed.
43
+ Returns the email if allowed.
44
+ """
45
+ token = get_access_token()
46
+ email = token.claims.get("email")
47
+
48
+ if not email:
49
+ raise PermissionError("No email found in token. Ensure 'userinfo.email' scope is granted.")
50
+
51
+ if email not in ALLOWED_EMAILS:
52
+ raise PermissionError(f"Access denied: {email} is not authorized to use this server.")
53
+
54
+ return email
55
+
56
+
57
+ # --- Auth Info Tool ---
58
+ @mcp.tool
59
+ async def get_user_info() -> dict:
60
+ """Returns information about the authenticated Google user."""
61
+ email = require_allowed_email()
62
+
63
+ token = get_access_token()
64
+ return {
65
+ "google_id": token.claims.get("sub"),
66
+ "email": email,
67
+ "name": token.claims.get("name"),
68
+ "picture": token.claims.get("picture"),
69
+ "locale": token.claims.get("locale"),
70
+ }
71
+
72
+
73
+ # --- HTTP Request Tools ---
74
+ @mcp.tool
75
+ async def http_request(
76
+ url: str,
77
+ method: str = "GET",
78
+ bearer_token: str = "",
79
+ body: str = "",
80
+ content_type: str = "application/json",
81
+ extra_headers: str = "",
82
+ timeout: int = 30,
83
+ ) -> str:
84
+ """
85
+ Make an HTTP request (like curl) with optional Bearer token auth.
86
+
87
+ Args:
88
+ url: Full URL including https://
89
+ method: HTTP method - GET, POST, PUT, PATCH, DELETE (default: GET)
90
+ bearer_token: Bearer token for Authorization header (optional)
91
+ body: Request body as JSON string (optional)
92
+ content_type: Content-Type header (default: application/json)
93
+ extra_headers: Extra headers as JSON string e.g. '{"X-Api-Key": "abc"}' (optional)
94
+ timeout: Request timeout in seconds (default: 30)
95
+ """
96
+ require_allowed_email()
97
+
98
+ headers: dict[str, str] = {}
99
+
100
+ if bearer_token:
101
+ headers["Authorization"] = f"Bearer {bearer_token}"
102
+
103
+ if body:
104
+ headers["Content-Type"] = content_type
105
+
106
+ if extra_headers:
107
+ try:
108
+ headers.update(json.loads(extra_headers))
109
+ except json.JSONDecodeError:
110
+ return 'Error: extra_headers must be valid JSON e.g. {"Key": "Value"}'
111
+
112
+ try:
113
+ async with httpx.AsyncClient(follow_redirects=True, timeout=timeout) as client:
114
+ response = await client.request(
115
+ method=method.upper(),
116
+ url=url,
117
+ headers=headers,
118
+ content=body.encode() if body else None,
119
+ )
120
+
121
+ try:
122
+ response_body = json.dumps(response.json(), indent=2)
123
+ except Exception:
124
+ response_body = response.text
125
+
126
+ return (
127
+ f"HTTP {response.status_code} {response.reason_phrase}\n"
128
+ f"URL: {response.url}\n"
129
+ f"Content-Type: {response.headers.get('content-type', '')}\n"
130
+ f"\n--- Response Body ---\n{response_body}"
131
+ )
132
+
133
+ except httpx.ConnectError as e:
134
+ return f"Connection error: {e}"
135
+ except httpx.TimeoutException:
136
+ return f"Request timed out after {timeout}s"
137
+ except Exception as e:
138
+ return f"Error: {type(e).__name__}: {e}"
139
+
140
+
141
+ @mcp.tool
142
+ async def http_get(url: str, bearer_token: str = "") -> str:
143
+ """
144
+ Make a GET request with optional Bearer token.
145
+
146
+ Args:
147
+ url: Full URL including https://
148
+ bearer_token: Bearer token for Authorization header (optional)
149
+ """
150
+ require_allowed_email()
151
+ return await http_request(url=url, method="GET", bearer_token=bearer_token)
152
+
153
+
154
+ @mcp.tool
155
+ async def http_post(
156
+ url: str,
157
+ body: str,
158
+ bearer_token: str = "",
159
+ content_type: str = "application/json",
160
+ ) -> str:
161
+ """
162
+ Make a POST request with a body and optional Bearer token.
163
+
164
+ Args:
165
+ url: Full URL including https://
166
+ body: Request body as a JSON string
167
+ bearer_token: Bearer token for Authorization header (optional)
168
+ content_type: Content-Type header (default: application/json)
169
+ """
170
+ require_allowed_email()
171
+ return await http_request(
172
+ url=url,
173
+ method="POST",
174
+ bearer_token=bearer_token,
175
+ body=body,
176
+ content_type=content_type,
177
+ )
178
+
179
+
180
+ # --- Run as HTTP server ---
181
+ if __name__ == "__main__":
182
+ mcp.run(
183
+ transport="streamable-http",
184
+ host="0.0.0.0",
185
+ port=7860,
186
+ )