Spaces:
Paused
Paused
frdel commited on
Commit ·
21051cf
1
Parent(s): 15728b4
mcp server auth token
Browse files- python/helpers/settings.py +30 -0
- run_ui.py +37 -10
- webui/components/settings/mcp/server/example.html +3 -2
python/helpers/settings.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
|
|
|
|
|
| 1 |
import json
|
| 2 |
import os
|
| 3 |
import re
|
|
@@ -67,6 +69,7 @@ class Settings(TypedDict):
|
|
| 67 |
mcp_client_init_timeout: int
|
| 68 |
mcp_client_tool_timeout: int
|
| 69 |
mcp_server_enabled: bool
|
|
|
|
| 70 |
|
| 71 |
|
| 72 |
class PartialSettings(Settings, total=False):
|
|
@@ -748,6 +751,17 @@ def convert_out(settings: Settings) -> SettingsOutput:
|
|
| 748 |
}
|
| 749 |
)
|
| 750 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 751 |
mcp_server_section: SettingsSection = {
|
| 752 |
"id": "mcp_server",
|
| 753 |
"title": "A0 MCP Server",
|
|
@@ -845,6 +859,9 @@ def normalize_settings(settings: Settings) -> Settings:
|
|
| 845 |
except (ValueError, TypeError):
|
| 846 |
copy[key] = value # make default instead
|
| 847 |
|
|
|
|
|
|
|
|
|
|
| 848 |
return copy
|
| 849 |
|
| 850 |
|
|
@@ -939,6 +956,7 @@ def get_default_settings() -> Settings:
|
|
| 939 |
mcp_client_init_timeout=5,
|
| 940 |
mcp_client_tool_timeout=120,
|
| 941 |
mcp_server_enabled=False,
|
|
|
|
| 942 |
)
|
| 943 |
|
| 944 |
|
|
@@ -1075,3 +1093,15 @@ def get_runtime_config(set: Settings):
|
|
| 1075 |
"code_exec_http_port": set["rfc_port_http"],
|
| 1076 |
"code_exec_ssh_user": "root",
|
| 1077 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import base64
|
| 2 |
+
import hashlib
|
| 3 |
import json
|
| 4 |
import os
|
| 5 |
import re
|
|
|
|
| 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):
|
|
|
|
| 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",
|
|
|
|
| 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 |
|
|
|
|
| 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 |
|
|
|
|
| 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
CHANGED
|
@@ -240,19 +240,46 @@ def run():
|
|
| 240 |
|
| 241 |
|
| 242 |
# define a Starlette-compatible middleware handler
|
|
|
|
|
|
|
| 243 |
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
PrintStyle.error("[MCP] Access denied: MCP server is disabled in settings.")
|
| 250 |
-
raise StarletteHTTPException(status_code=403,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 251 |
return await call_next(request)
|
| 252 |
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
]
|
| 256 |
|
| 257 |
mcp_app = create_sse_app(
|
| 258 |
server=mcp_server_instance,
|
|
@@ -269,7 +296,7 @@ def run():
|
|
| 269 |
app = DispatcherMiddleware(webapp, {
|
| 270 |
"/mcp": ASGIMiddleware(app=mcp_app), # type: ignore
|
| 271 |
}) # type: ignore
|
| 272 |
-
PrintStyle().debug("Registered middleware for MCP")
|
| 273 |
|
| 274 |
try:
|
| 275 |
PrintStyle().debug(f"Starting server at {host}:{port}...")
|
|
|
|
| 240 |
|
| 241 |
|
| 242 |
# define a Starlette-compatible middleware handler
|
| 243 |
+
from starlette.requests import Request
|
| 244 |
+
import re
|
| 245 |
|
| 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.")
|
| 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 |
|
| 281 |
+
|
| 282 |
+
mcp_middlewares = [Middleware(BaseHTTPMiddleware, dispatch=mcp_middleware)]
|
|
|
|
| 283 |
|
| 284 |
mcp_app = create_sse_app(
|
| 285 |
server=mcp_server_instance,
|
|
|
|
| 296 |
app = DispatcherMiddleware(webapp, {
|
| 297 |
"/mcp": ASGIMiddleware(app=mcp_app), # type: ignore
|
| 298 |
}) # type: ignore
|
| 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
CHANGED
|
@@ -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 |
-
|
| 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 jsonExample = JSON.stringify({
|
| 20 |
"mcpServers":
|
| 21 |
{
|
| 22 |
"agent-zero": {
|
| 23 |
"type": "sse",
|
| 24 |
-
"serverUrl": `${url}/mcp/sse`
|
| 25 |
}
|
| 26 |
}
|
| 27 |
}, null, 2);
|
|
|
|
| 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 |
+
|
| 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",
|
| 25 |
+
"serverUrl": `${url}/mcp/t-${token}/sse`
|
| 26 |
}
|
| 27 |
}
|
| 28 |
}, null, 2);
|