9
9
import asyncio
10
10
import base64
11
11
import json
12
- import logging
13
12
import mimetypes
14
13
import os
15
14
import shutil
26
25
from fastapi .exceptions import RequestValidationError
27
26
from fastapi .responses import FileResponse , JSONResponse
28
27
from fastapi .security import APIKeyHeader
29
- from mcpm import MCPRouter , RouterConfig
30
- from mcpm .router .router import logger as mcp_router_logger
31
28
from openhands_aci .editor .editor import OHEditor
32
29
from openhands_aci .editor .exceptions import ToolError
33
30
from openhands_aci .editor .results import ToolResult
37
34
from starlette .exceptions import HTTPException as StarletteHTTPException
38
35
from uvicorn import run
39
36
37
+ from openhands .core .config .mcp_config import MCPStdioServerConfig
40
38
from openhands .core .exceptions import BrowserUnavailableException
41
39
from openhands .core .logger import openhands_logger as logger
42
40
from openhands .events .action import (
63
61
from openhands .runtime .browser import browse
64
62
from openhands .runtime .browser .browser_env import BrowserEnv
65
63
from openhands .runtime .file_viewer_server import start_file_viewer_server
64
+
65
+ # Import our custom MCP Proxy Manager
66
+ from openhands .runtime .mcp .proxy import MCPProxyManager
66
67
from openhands .runtime .plugins import ALL_PLUGINS , JupyterPlugin , Plugin , VSCodePlugin
67
68
from openhands .runtime .utils import find_available_tcp_port
68
69
from openhands .runtime .utils .bash import BashSession
69
70
from openhands .runtime .utils .files import insert_lines , read_lines
70
- from openhands .runtime .utils .log_capture import capture_logs
71
71
from openhands .runtime .utils .memory_monitor import MemoryMonitor
72
72
from openhands .runtime .utils .runtime_init import init_user_and_working_directory
73
73
from openhands .runtime .utils .system_stats import get_system_stats
74
74
from openhands .utils .async_utils import call_sync_from_async , wait_all
75
75
76
- # Set MCP router logger to the same level as the main logger
77
- mcp_router_logger .setLevel (logger .getEffectiveLevel ())
78
-
79
-
80
76
if sys .platform == 'win32' :
81
77
from openhands .runtime .utils .windows_bash import WindowsPowershellSession
82
78
@@ -471,7 +467,7 @@ async def read(self, action: FileReadAction) -> Observation:
471
467
filepath = self ._resolve_path (action .path , working_dir )
472
468
try :
473
469
if filepath .lower ().endswith (('.png' , '.jpg' , '.jpeg' , '.bmp' , '.gif' )):
474
- with open (filepath , 'rb' ) as file : # noqa: ASYNC101
470
+ with open (filepath , 'rb' ) as file :
475
471
image_data = file .read ()
476
472
encoded_image = base64 .b64encode (image_data ).decode ('utf-8' )
477
473
mime_type , _ = mimetypes .guess_type (filepath )
@@ -481,13 +477,13 @@ async def read(self, action: FileReadAction) -> Observation:
481
477
482
478
return FileReadObservation (path = filepath , content = encoded_image )
483
479
elif filepath .lower ().endswith ('.pdf' ):
484
- with open (filepath , 'rb' ) as file : # noqa: ASYNC101
480
+ with open (filepath , 'rb' ) as file :
485
481
pdf_data = file .read ()
486
482
encoded_pdf = base64 .b64encode (pdf_data ).decode ('utf-8' )
487
483
encoded_pdf = f'data:application/pdf;base64,{ encoded_pdf } '
488
484
return FileReadObservation (path = filepath , content = encoded_pdf )
489
485
elif filepath .lower ().endswith (('.mp4' , '.webm' , '.ogg' )):
490
- with open (filepath , 'rb' ) as file : # noqa: ASYNC101
486
+ with open (filepath , 'rb' ) as file :
491
487
video_data = file .read ()
492
488
encoded_video = base64 .b64encode (video_data ).decode ('utf-8' )
493
489
mime_type , _ = mimetypes .guess_type (filepath )
@@ -497,7 +493,7 @@ async def read(self, action: FileReadAction) -> Observation:
497
493
498
494
return FileReadObservation (path = filepath , content = encoded_video )
499
495
500
- with open (filepath , 'r' , encoding = 'utf-8' ) as file : # noqa: ASYNC101
496
+ with open (filepath , 'r' , encoding = 'utf-8' ) as file :
501
497
lines = read_lines (file .readlines (), action .start , action .end )
502
498
except FileNotFoundError :
503
499
return ErrorObservation (
@@ -530,7 +526,7 @@ async def write(self, action: FileWriteAction) -> Observation:
530
526
531
527
mode = 'w' if not file_exists else 'r+'
532
528
try :
533
- with open (filepath , mode , encoding = 'utf-8' ) as file : # noqa: ASYNC101
529
+ with open (filepath , mode , encoding = 'utf-8' ) as file :
534
530
if mode != 'w' :
535
531
all_lines = file .readlines ()
536
532
new_file = insert_lines (insert , all_lines , action .start , action .end )
@@ -654,14 +650,11 @@ def close(self):
654
650
plugins_to_load .append (ALL_PLUGINS [plugin ]()) # type: ignore
655
651
656
652
client : ActionExecutor | None = None
657
- mcp_router : MCPRouter | None = None
658
- MCP_ROUTER_PROFILE_PATH = os .path .join (
659
- os .path .dirname (__file__ ), 'mcp' , 'config.json'
660
- )
653
+ mcp_proxy_manager : MCPProxyManager | None = None
661
654
662
655
@asynccontextmanager
663
656
async def lifespan (app : FastAPI ):
664
- global client , mcp_router
657
+ global client , mcp_proxy_manager
665
658
logger .info ('Initializing ActionExecutor...' )
666
659
client = ActionExecutor (
667
660
plugins_to_load ,
@@ -676,63 +669,36 @@ async def lifespan(app: FastAPI):
676
669
# Check if we're on Windows
677
670
is_windows = sys .platform == 'win32'
678
671
679
- # Initialize and mount MCP Router (skip on Windows)
672
+ # Initialize and mount MCP Proxy Manager (skip on Windows)
680
673
if is_windows :
681
- logger .info ('Skipping MCP Router initialization on Windows' )
682
- mcp_router = None
674
+ logger .info ('Skipping MCP Proxy initialization on Windows' )
675
+ mcp_proxy_manager = None
683
676
else :
684
- logger .info ('Initializing MCP Router...' )
685
- mcp_router = MCPRouter (
686
- profile_path = MCP_ROUTER_PROFILE_PATH ,
687
- router_config = RouterConfig (
688
- api_key = SESSION_API_KEY ,
689
- auth_enabled = bool (SESSION_API_KEY ),
690
- ),
677
+ logger .info ('Initializing MCP Proxy Manager...' )
678
+ # Create a MCP Proxy Manager
679
+ mcp_proxy_manager = MCPProxyManager (
680
+ auth_enabled = bool (SESSION_API_KEY ),
681
+ api_key = SESSION_API_KEY ,
682
+ logger_level = logger .getEffectiveLevel (),
691
683
)
684
+ mcp_proxy_manager .initialize ()
685
+ # Mount the proxy to the app
692
686
allowed_origins = ['*' ]
693
- sse_app = await mcp_router .get_sse_server_app (
694
- allow_origins = allowed_origins , include_lifespan = False
695
- )
696
-
697
- # Only mount SSE app if MCP Router is initialized (not on Windows)
698
- if mcp_router is not None :
699
- # Check for route conflicts before mounting
700
- main_app_routes = {route .path for route in app .routes }
701
- sse_app_routes = {route .path for route in sse_app .routes }
702
- conflicting_routes = main_app_routes .intersection (sse_app_routes )
703
-
704
- if conflicting_routes :
705
- logger .error (f'Route conflicts detected: { conflicting_routes } ' )
706
- raise RuntimeError (
707
- f'Cannot mount SSE app - conflicting routes found: { conflicting_routes } '
708
- )
709
-
710
- app .mount ('/' , sse_app )
711
- logger .info (
712
- f'Mounted MCP Router SSE app at root path with allowed origins: { allowed_origins } '
713
- )
714
-
715
- # Additional debug logging
716
- if logger .isEnabledFor (logging .DEBUG ):
717
- logger .debug ('Main app routes:' )
718
- for route in main_app_routes :
719
- logger .debug (f' { route } ' )
720
- logger .debug ('MCP SSE server app routes:' )
721
- for route in sse_app_routes :
722
- logger .debug (f' { route } ' )
687
+ try :
688
+ await mcp_proxy_manager .mount_to_app (app , allowed_origins )
689
+ except Exception as e :
690
+ logger .error (f'Error mounting MCP Proxy: { e } ' , exc_info = True )
691
+ raise RuntimeError (f'Cannot mount MCP Proxy: { e } ' )
723
692
724
693
yield
725
694
726
695
# Clean up & release the resources
727
- logger .info ('Shutting down MCP Router...' )
728
- if mcp_router :
729
- try :
730
- await mcp_router .shutdown ()
731
- logger .info ('MCP Router shutdown successfully.' )
732
- except Exception as e :
733
- logger .error (f'Error shutting down MCP Router: { e } ' , exc_info = True )
696
+ logger .info ('Shutting down MCP Proxy Manager...' )
697
+ if mcp_proxy_manager :
698
+ del mcp_proxy_manager
699
+ mcp_proxy_manager = None
734
700
else :
735
- logger .info ('MCP Router instance not found for shutdown.' )
701
+ logger .info ('MCP Proxy Manager instance not found for shutdown.' )
736
702
737
703
logger .info ('Closing ActionExecutor...' )
738
704
if client :
@@ -824,6 +790,9 @@ async def update_mcp_server(request: Request):
824
790
# Check if we're on Windows
825
791
is_windows = sys .platform == 'win32'
826
792
793
+ # Access the global mcp_proxy_manager variable
794
+ global mcp_proxy_manager
795
+
827
796
if is_windows :
828
797
# On Windows, just return a success response without doing anything
829
798
logger .info (
@@ -838,49 +807,28 @@ async def update_mcp_server(request: Request):
838
807
)
839
808
840
809
# Non-Windows implementation
841
- assert mcp_router is not None
842
- assert os .path .exists (MCP_ROUTER_PROFILE_PATH )
843
-
844
- # Use synchronous file operations outside of async function
845
- def read_profile ():
846
- with open (MCP_ROUTER_PROFILE_PATH , 'r' ) as f :
847
- return json .load (f )
848
-
849
- current_profile = read_profile ()
850
- assert 'default' in current_profile
851
- assert isinstance (current_profile ['default' ], list )
810
+ if mcp_proxy_manager is None :
811
+ raise HTTPException (
812
+ status_code = 500 , detail = 'MCP Proxy Manager is not initialized'
813
+ )
852
814
853
815
# Get the request body
854
816
mcp_tools_to_sync = await request .json ()
855
817
if not isinstance (mcp_tools_to_sync , list ):
856
818
raise HTTPException (
857
819
status_code = 400 , detail = 'Request must be a list of MCP tools to sync'
858
820
)
859
-
860
821
logger .info (
861
- f'Updating MCP server to: { json . dumps ( mcp_tools_to_sync , indent = 2 ) } . \n Previous profile : { json .dumps (current_profile , indent = 2 )} '
822
+ f'Updating MCP server with tools : { json .dumps (mcp_tools_to_sync , indent = 2 )} '
862
823
)
863
- current_profile ['default' ] = mcp_tools_to_sync
864
-
865
- # Use synchronous file operations outside of async function
866
- def write_profile (profile ):
867
- with open (MCP_ROUTER_PROFILE_PATH , 'w' ) as f :
868
- json .dump (profile , f )
869
-
870
- write_profile (current_profile )
871
-
872
- # Manually reload the profile and update the servers
873
- mcp_router .profile_manager .reload ()
874
- servers_wait_for_update = mcp_router .get_unique_servers ()
875
- async with capture_logs ('mcpm.router.router' ) as log_capture :
876
- await mcp_router .update_servers (servers_wait_for_update )
877
- router_error_log = log_capture .getvalue ()
878
-
879
- logger .info (
880
- f'MCP router updated successfully with unique servers: { servers_wait_for_update } '
881
- )
882
- if router_error_log :
883
- logger .warning (f'Some MCP servers failed to be added: { router_error_log } ' )
824
+ mcp_tools_to_sync = [MCPStdioServerConfig (** tool ) for tool in mcp_tools_to_sync ]
825
+ try :
826
+ await mcp_proxy_manager .update_and_remount (app , mcp_tools_to_sync , ['*' ])
827
+ logger .info ('MCP Proxy Manager updated and remounted successfully' )
828
+ router_error_log = ''
829
+ except Exception as e :
830
+ logger .error (f'Error updating MCP Proxy Manager: { e } ' , exc_info = True )
831
+ router_error_log = str (e )
884
832
885
833
return JSONResponse (
886
834
status_code = 200 ,
@@ -915,7 +863,7 @@ async def upload_file(
915
863
)
916
864
917
865
zip_path = os .path .join (full_dest_path , file .filename )
918
- with open (zip_path , 'wb' ) as buffer : # noqa: ASYNC101
866
+ with open (zip_path , 'wb' ) as buffer :
919
867
shutil .copyfileobj (file .file , buffer )
920
868
921
869
# Extract the zip file
@@ -928,7 +876,7 @@ async def upload_file(
928
876
else :
929
877
# For single file uploads
930
878
file_path = os .path .join (full_dest_path , file .filename )
931
- with open (file_path , 'wb' ) as buffer : # noqa: ASYNC101
879
+ with open (file_path , 'wb' ) as buffer :
932
880
shutil .copyfileobj (file .file , buffer )
933
881
logger .debug (f'Uploaded file { file .filename } to { destination } ' )
934
882
0 commit comments