mcp server auth token

frdel committed Jun 5, 2025 at 12:16 UTC bb07e56f0004048349c63131f466181ca435e6fa
3 files changed +70 -12
python/helpers/settings.py
+30
@@ -1,3 +1,5 @@
1 +import base64
2 +import hashlib
3 import json
4 import os
5 import re
@@ -67,6 +69,7 @@ class Settings(TypedDict):
69 mcp_client_init_timeout: int
70 mcp_client_tool_timeout: int
71 mcp_server_enabled: bool
72 + mcp_server_token: str
73
74
75 class PartialSettings(Settings, total=False):
@@ -748,6 +751,17 @@ def convert_out(settings: Settings) -> SettingsOutput:
751 }
752 )
753
754 + mcp_server_fields.append(
755 + {
756 + "id": "mcp_server_token",
757 + "title": "MCP Server Token",
758 + "description": "Token for MCP server authentication.",
759 + "type": "text",
760 + "hidden": True,
761 + "value": settings["mcp_server_token"],
762 + }
763 + )
764 +
765 mcp_server_section: SettingsSection = {
766 "id": "mcp_server",
767 "title": "A0 MCP Server",
@@ -845,6 +859,9 @@ def normalize_settings(settings: Settings) -> Settings:
859 except (ValueError, TypeError):
860 copy[key] = value # make default instead
861
862 + # mcp server token is set automatically
863 + copy["mcp_server_token"] = create_token()
864 +
865 return copy
866
867
@@ -939,6 +956,7 @@ def get_default_settings() -> Settings:
956 mcp_client_init_timeout=5,
957 mcp_client_tool_timeout=120,
958 mcp_server_enabled=False,
959 + mcp_server_token=create_token(),
960 )
961
962
@@ -1075,3 +1093,15 @@ def get_runtime_config(set: Settings):
1093 "code_exec_http_port": set["rfc_port_http"],
1094 "code_exec_ssh_user": "root",
1095 }
1096 +
1097 +
1098 +def create_token() -> str:
1099 + username = dotenv.get_dotenv_value(dotenv.KEY_AUTH_LOGIN) or ""
1100 + password = dotenv.get_dotenv_value(dotenv.KEY_AUTH_PASSWORD) or ""
1101 + if not username or not password:
1102 + return "0"
1103 + # use base64 encoding for a more compact token with alphanumeric chars
1104 + hash_bytes = hashlib.sha256(f"{username}:{password}".encode()).digest()
1105 + # encode as base64 and remove any non-alphanumeric chars (like +, /, =)
1106 + b64_token = base64.urlsafe_b64encode(hash_bytes).decode().replace('=', '')
1107 + return b64_token[:16]
run_ui.py
+37 -10
@@ -240,19 +240,46 @@ def run():
240
241
242 # define a Starlette-compatible middleware handler
243 + from starlette.requests import Request
244 + import re
245
244 -
245 - async def mcp_middleware(request, call_next):
246 - set = settings.get_settings()
247 - if not set["mcp_server_enabled"]:
248 - # raise a proper Starlette HTTPException with a clear message
246 + async def mcp_middleware(request: Request, call_next):
247 +
248 + # check if MCP server is enabled
249 + cfg = settings.get_settings()
250 + if not cfg["mcp_server_enabled"]:
251 PrintStyle.error("[MCP] Access denied: MCP server is disabled in settings.")
250 - raise StarletteHTTPException(status_code=403, detail="MCP server is disabled in settings.")
252 + raise StarletteHTTPException(status_code=403,
253 + detail="MCP server is disabled in settings.")
254 +
255 + # get auth token from path
256 + full_path = request.url.path
257 + if not full_path.startswith("/mcp/t-"):
258 + raise StarletteHTTPException(status_code=401,
259 + detail="Missing token.")
260 + pattern = r'^/mcp/t-([^/]+)/(.+)$'
261 + match = re.match(pattern, full_path)
262 + if not match:
263 + raise StarletteHTTPException(status_code=401,
264 + detail="Missing token.")
265 + token = match.group(1)
266 + remainder = match.group(2)
267 +
268 + # validate token
269 + if token != cfg["mcp_server_token"]:
270 + raise StarletteHTTPException(status_code=401,
271 + detail="Invalid token.")
272 +
273 + # rewrite path to standard MCP path and continue
274 + new_path = f"/mcp/{remainder}"
275 + request.scope["path"] = new_path
276 + request.scope["raw_path"] = new_path.encode()
277 + # request.state.token = token
278 +
279 return await call_next(request)
280
253 - mcp_middlewares = [
254 - Middleware(BaseHTTPMiddleware, dispatch=mcp_middleware)
255 - ]
281 +
282 + mcp_middlewares = [Middleware(BaseHTTPMiddleware, dispatch=mcp_middleware)]
283
284 mcp_app = create_sse_app(
285 server=mcp_server_instance,
@@ -269,7 +296,7 @@ def run():
296 app = DispatcherMiddleware(webapp, {
297 "/mcp": ASGIMiddleware(app=mcp_app), # type: ignore
298 }) # type: ignore
272 - PrintStyle().debug("Registered middleware for MCP")
299 + PrintStyle().debug("Registered middleware for MCP and MCP token")
300
301 try:
302 PrintStyle().debug(f"Starting server at {host}:{port}...")
webui/components/settings/mcp/server/example.html
+3 -2
@@ -9,19 +9,20 @@
9 <div x-data>
10 <p>Agent Zero MCP Server is an SSE MCP running on the same URL and port as the Web UI + /mcp/sse path.</p>
11 <p>The same applies if you run A0 on a public URL using a tunnel.</p>
12 -
12 +
13 <h3>Example MCP Server Configuration JSON</h3>
14 <div id="mcp-server-example"></div>
15
16 <script>
17 setTimeout(() => {
18 const url = window.location.origin;
19 + const token = settingsModalProxy.settings.sections.filter(x => x.id == "mcp_server")[0].fields.filter(x => x.id == "mcp_server_token")[0].value;
20 const jsonExample = JSON.stringify({
21 "mcpServers":
22 {
23 "agent-zero": {
24 "type": "sse",
24 - "serverUrl": `${url}/mcp/sse`
25 + "serverUrl": `${url}/mcp/t-${token}/sse`
26 }
27 }
28 }, null, 2);