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()