mcp urls fixed

frdel committed Jun 5, 2025 at 23:06 UTC 8ecd46cb207e6554e6818cd7b404f78dbd7585a4
3 files changed +103 -72
python/helpers/mcp_server.py
+70 -3
@@ -4,15 +4,20 @@ from typing import Annotated, Literal, Union
4 from urllib.parse import urlparse
5 from openai import BaseModel
6 from pydantic import Field
7 -import uuid
8 -import asyncio
7 from fastmcp import FastMCP
8
9 from agent import AgentContext, AgentContextType, UserMessage
10 from python.helpers.persist_chat import save_tmp_chat, remove_chat
11 from initialize import initialize
12 from python.helpers.print_style import PrintStyle
15 -from python.helpers.task_scheduler import DeferredTask
13 +from python.helpers import settings
14 +from starlette.middleware import Middleware
15 +from starlette.middleware.base import BaseHTTPMiddleware
16 +from starlette.exceptions import HTTPException as StarletteHTTPException
17 +from starlette.types import ASGIApp, Receive, Scope, Send
18 +from fastmcp.server.http import create_sse_app
19 +from starlette.requests import Request
20 +import threading
21
22 _PRINTER = PrintStyle(italic=True, font_color="green", padding=False)
23
@@ -260,3 +265,65 @@ async def _run_chat(
265 _PRINTER.print(f"MCP Chat message failed: {e}")
266
267 raise RuntimeError(f"MCP Chat message failed: {e}") from e
268 +
269 +
270 +class DynamicMcpProxy:
271 + _instance: "DynamicMcpProxy | None" = None
272 +
273 + """A dynamic proxy that allows swapping the underlying MCP application on the fly."""
274 +
275 + def __init__(self):
276 + cfg = settings.get_settings()
277 + self.app: ASGIApp | None = None
278 + self._lock = threading.RLock() # Use RLock to avoid deadlocks
279 + self.reconfigure(cfg["mcp_server_token"])
280 +
281 + @staticmethod
282 + def get_instance():
283 + if DynamicMcpProxy._instance is None:
284 + DynamicMcpProxy._instance = DynamicMcpProxy()
285 + return DynamicMcpProxy._instance
286 +
287 + def reconfigure(self, token: str):
288 + self.token = token
289 + sse_path = f"/t-{self.token}/sse"
290 + message_path = f"/t-{self.token}/messages/"
291 +
292 + # Update settings in the MCP server instance if provided
293 + mcp_server.settings.message_path = message_path
294 + mcp_server.settings.sse_path = sse_path
295 +
296 + # Create a new MCP app with updated settings
297 + with self._lock:
298 + self.app = create_sse_app(
299 + server=mcp_server,
300 + message_path=mcp_server.settings.message_path,
301 + sse_path=mcp_server.settings.sse_path,
302 + auth_server_provider=mcp_server._auth_server_provider,
303 + auth_settings=mcp_server.settings.auth,
304 + debug=mcp_server.settings.debug,
305 + routes=mcp_server._additional_http_routes,
306 + middleware=[Middleware(BaseHTTPMiddleware, dispatch=mcp_middleware)],
307 + )
308 +
309 + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
310 + """Forward the ASGI calls to the current app"""
311 + with self._lock:
312 + app = self.app
313 + if app:
314 + await app(scope, receive, send)
315 + else:
316 + raise RuntimeError("MCP app not initialized")
317 +
318 +
319 +async def mcp_middleware(request: Request, call_next):
320 +
321 + # check if MCP server is enabled
322 + cfg = settings.get_settings()
323 + if not cfg["mcp_server_enabled"]:
324 + PrintStyle.error("[MCP] Access denied: MCP server is disabled in settings.")
325 + raise StarletteHTTPException(
326 + status_code=403, detail="MCP server is disabled in settings."
327 + )
328 +
329 + return await call_next(request)
\ No newline at end of file
python/helpers/settings.py
+20 -4
@@ -860,7 +860,7 @@ def normalize_settings(settings: Settings) -> Settings:
860 copy[key] = value # make default instead
861
862 # mcp server token is set automatically
863 - copy["mcp_server_token"] = create_token()
863 + copy["mcp_server_token"] = create_auth_token()
864
865 return copy
866
@@ -887,6 +887,7 @@ def _remove_sensitive_settings(settings: Settings):
887 settings["auth_password"] = ""
888 settings["rfc_password"] = ""
889 settings["root_password"] = ""
890 + settings["mcp_server_token"] = ""
891
892
893 def _write_sensitive_settings(settings: Settings):
@@ -956,7 +957,7 @@ def get_default_settings() -> Settings:
957 mcp_client_init_timeout=5,
958 mcp_client_tool_timeout=120,
959 mcp_server_enabled=False,
959 - mcp_server_token=create_token(),
960 + mcp_server_token=create_auth_token(),
961 )
962
963
@@ -1039,6 +1040,21 @@ def _apply_settings(previous: Settings | None):
1040 update_mcp_settings, config.mcp_servers
1041 ) # TODO overkill, replace with background task
1042
1043 + # update token in mcp server
1044 + current_token = create_auth_token() #TODO - ugly, token in settings is generated from dotenv and does not always correspond
1045 + if (
1046 + not previous
1047 + or current_token != previous["mcp_server_token"]
1048 + ):
1049 +
1050 + async def update_mcp_token(token: str):
1051 + from python.helpers.mcp_server import DynamicMcpProxy
1052 + DynamicMcpProxy.get_instance().reconfigure(token=token)
1053 +
1054 + task3 = defer.DeferredTask().start_task(
1055 + update_mcp_token, current_token
1056 + ) # TODO overkill, replace with background task
1057 +
1058
1059 def _env_to_dict(data: str):
1060 env_dict = {}
@@ -1095,7 +1111,7 @@ def get_runtime_config(set: Settings):
1111 }
1112
1113
1098 -def create_token() -> str:
1114 +def create_auth_token() -> str:
1115 username = dotenv.get_dotenv_value(dotenv.KEY_AUTH_LOGIN) or ""
1116 password = dotenv.get_dotenv_value(dotenv.KEY_AUTH_PASSWORD) or ""
1117 if not username or not password:
@@ -1103,5 +1119,5 @@ def create_token() -> str:
1119 # use base64 encoding for a more compact token with alphanumeric chars
1120 hash_bytes = hashlib.sha256(f"{username}:{password}".encode()).digest()
1121 # encode as base64 and remove any non-alphanumeric chars (like +, /, =)
1106 - b64_token = base64.urlsafe_b64encode(hash_bytes).decode().replace('=', '')
1122 + b64_token = base64.urlsafe_b64encode(hash_bytes).decode().replace("=", "")
1123 return b64_token[:16]
run_ui.py
+13 -65
@@ -3,13 +3,12 @@ import sys
3 import time
4 import socket
5 import struct
6 -import asyncio
6 from functools import wraps
7 import threading
8 import signal
9 from flask import Flask, request, Response
10 from flask_basicauth import BasicAuth
12 -from python.helpers import errors, files, git, settings
11 +from python.helpers import errors, files, git, mcp_server
12 from python.helpers.files import get_abs_path
13 from python.helpers import persist_chat, runtime, dotenv, process
14 from python.helpers.cloudflare_tunnel import CloudflareTunnel
@@ -17,11 +16,7 @@ from python.helpers.extract_tools import load_classes_from_folder
16 from python.helpers.api import ApiHandler
17 from python.helpers.job_loop import run_loop
18 from python.helpers.print_style import PrintStyle
20 -from python.helpers.task_scheduler import TaskScheduler
19 from python.helpers.defer import DeferredTask
22 -from starlette.middleware import Middleware
23 -from starlette.middleware.base import BaseHTTPMiddleware
24 -from starlette.exceptions import HTTPException as StarletteHTTPException
20
21
22 # Set the new timezone to 'UTC'
@@ -160,6 +155,7 @@ def run():
155 job_loop = DeferredTask().start_task(run_loop)
156
157 PrintStyle().print("Starting server...")
158 +
159 class NoRequestLoggingWSGIRequestHandler(WSGIRequestHandler):
160 def log_request(self, code="-", size="-"):
161 pass # Override to suppress request logging
@@ -238,64 +234,13 @@ def run():
234 for handler in handlers:
235 register_api_handler(webapp, handler)
236
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,
286 - message_path=mcp_server_instance.settings.message_path,
287 - sse_path=mcp_server_instance.settings.sse_path,
288 - auth_server_provider=mcp_server_instance._auth_server_provider,
289 - auth_settings=mcp_server_instance.settings.auth,
290 - debug=mcp_server_instance.settings.debug,
291 - routes=mcp_server_instance._additional_http_routes,
292 - middleware=mcp_middlewares
293 - )
294 -
237 # add the webapp and mcp to the app
296 - app = DispatcherMiddleware(webapp, {
297 - "/mcp": ASGIMiddleware(app=mcp_app), # type: ignore
298 - }) # type: ignore
238 + app = DispatcherMiddleware(
239 + webapp,
240 + {
241 + "/mcp": ASGIMiddleware(app=mcp_server.DynamicMcpProxy.get_instance()), # type: ignore
242 + },
243 + )
244 PrintStyle().debug("Registered middleware for MCP and MCP token")
245
246 try:
@@ -329,21 +274,24 @@ def run():
274
275 process.set_server(server)
276 server.log_startup()
332 -
277 +
278 # Start init_a0 in a background thread when server starts
279 import threading
280 +
281 threading.Thread(target=init_a0, daemon=True).start()
336 -
282 +
283 server.serve_forever()
284 finally:
285 # Clean up tunnel if it was started
286 if tunnel:
287 tunnel.stop()
288
289 +
290 def init_a0():
291 # initialize contexts from persisted chats
292 persist_chat.load_tmp_chats()
293
294 +
295 # run the internal server
296 if __name__ == "__main__":
297 runtime.initialize()