11import logging
22from typing import Optional
33
4- from fastapi import APIRouter , Header , HTTPException
4+ from fastapi import APIRouter , Header , HTTPException , UploadFile , File , Form
55from fastapi .responses import JSONResponse
66from http import HTTPStatus
77
8- from consts .const import MCP_DOCKER_IMAGE
8+ from consts .const import NEXENT_MCP_DOCKER_IMAGE , ENABLE_UPLOAD_IMAGE
99from consts .exceptions import MCPConnectionError , MCPNameIllegal , MCPContainerError
1010from consts .model import MCPConfigRequest
1111from services .remote_mcp_service import (
1414 get_remote_mcp_server_list ,
1515 check_mcp_health_and_update_db ,
1616 delete_mcp_by_container_id ,
17+ upload_and_start_mcp_image ,
1718)
19+ from database .remote_mcp_db import check_mcp_name_exists
1820from services .tool_configuration_service import get_tool_from_remote_mcp_server
1921from services .mcp_container_service import MCPContainerManager
2022from utils .auth_utils import get_current_user_id
@@ -116,6 +118,7 @@ async def get_remote_proxies(
116118 return JSONResponse (
117119 status_code = HTTPStatus .OK ,
118120 content = {"remote_mcp_server_list" : remote_mcp_server_list ,
121+ "enable_upload_image" : ENABLE_UPLOAD_IMAGE ,
119122 "status" : "success" }
120123 )
121124 except Exception as e :
@@ -153,7 +156,7 @@ async def add_mcp_from_config(
153156 """
154157 Add MCP server by starting a container with command+args config.
155158 Similar to Cursor's MCP server configuration format.
156-
159+
157160 Example request:
158161 {
159162 "mcpServers": {
@@ -167,7 +170,7 @@ async def add_mcp_from_config(
167170 """
168171 try :
169172 user_id , tenant_id = get_current_user_id (authorization )
170-
173+
171174 # Initialize container manager
172175 try :
173176 container_manager = MCPContainerManager ()
@@ -177,25 +180,30 @@ async def add_mcp_from_config(
177180 status_code = HTTPStatus .SERVICE_UNAVAILABLE ,
178181 detail = "Docker service unavailable. Please ensure Docker socket is mounted."
179182 )
180-
183+
181184 results = []
182185 errors = []
183-
186+
184187 for service_name , config in mcp_config .mcpServers .items ():
185188 try :
186189 command = config .command
187190 args = config .args or []
188191 env_vars = config .env or {}
189192 port = config .port
190-
193+
191194 if not command :
192195 errors .append (f"{ service_name } : command is required" )
193196 continue
194-
197+
195198 if port is None :
196199 errors .append (f"{ service_name } : port is required" )
197200 continue
198201
202+ # Check if MCP service name already exists before starting container
203+ if check_mcp_name_exists (mcp_name = service_name , tenant_id = tenant_id ):
204+ errors .append (f"{ service_name } : MCP name already exists" )
205+ continue
206+
199207 # Build full command to run inside nexent/nexent-mcp image
200208 full_command = [
201209 "python" ,
@@ -219,28 +227,19 @@ async def add_mcp_from_config(
219227 user_id = user_id ,
220228 env_vars = env_vars ,
221229 host_port = port ,
222- image = config .image or MCP_DOCKER_IMAGE ,
230+ image = config .image or NEXENT_MCP_DOCKER_IMAGE ,
223231 full_command = full_command ,
224232 )
225-
233+
226234 # Register to remote MCP server list
227- try :
228- await add_remote_mcp_server_list (
229- tenant_id = tenant_id ,
230- user_id = user_id ,
231- remote_mcp_server = container_info ["mcp_url" ],
232- remote_mcp_server_name = service_name ,
233- container_id = container_info ["container_id" ],
234- )
235- except MCPNameIllegal :
236- # If name already exists, try to stop the container we just created
237- try :
238- await container_manager .stop_mcp_container (container_info ["container_id" ])
239- except Exception :
240- pass
241- errors .append (f"{ service_name } : MCP name already exists" )
242- continue
243-
235+ await add_remote_mcp_server_list (
236+ tenant_id = tenant_id ,
237+ user_id = user_id ,
238+ remote_mcp_server = container_info ["mcp_url" ],
239+ remote_mcp_server_name = service_name ,
240+ container_id = container_info ["container_id" ],
241+ )
242+
244243 results .append ({
245244 "service_name" : service_name ,
246245 "status" : "success" ,
@@ -249,20 +248,22 @@ async def add_mcp_from_config(
249248 "container_name" : container_info .get ("container_name" ),
250249 "host_port" : container_info .get ("host_port" )
251250 })
252-
251+
253252 except MCPContainerError as e :
254- logger .error (f"Failed to start MCP container { service_name } : { e } " )
253+ logger .error (
254+ f"Failed to start MCP container { service_name } : { e } " )
255255 errors .append (f"{ service_name } : { str (e )} " )
256256 except Exception as e :
257- logger .error (f"Unexpected error adding MCP { service_name } : { e } " )
257+ logger .error (
258+ f"Unexpected error adding MCP { service_name } : { e } " )
258259 errors .append (f"{ service_name } : { str (e )} " )
259-
260+
260261 if errors and not results :
261262 raise HTTPException (
262263 status_code = HTTPStatus .BAD_REQUEST ,
263264 detail = f"All MCP servers failed: { errors } "
264265 )
265-
266+
266267 return JSONResponse (
267268 status_code = HTTPStatus .OK ,
268269 content = {
@@ -272,7 +273,7 @@ async def add_mcp_from_config(
272273 "status" : "success"
273274 }
274275 )
275-
276+
276277 except HTTPException :
277278 raise
278279 except Exception as e :
@@ -291,7 +292,7 @@ async def stop_mcp_container(
291292 """ Stop and remove MCP container """
292293 try :
293294 user_id , tenant_id = get_current_user_id (authorization )
294-
295+
295296 try :
296297 container_manager = MCPContainerManager ()
297298 except MCPContainerError as e :
@@ -339,7 +340,7 @@ async def list_mcp_containers(
339340 """ List all MCP containers for the current tenant """
340341 try :
341342 user_id , tenant_id = get_current_user_id (authorization )
342-
343+
343344 try :
344345 container_manager = MCPContainerManager ()
345346 except MCPContainerError as e :
@@ -348,9 +349,9 @@ async def list_mcp_containers(
348349 status_code = HTTPStatus .SERVICE_UNAVAILABLE ,
349350 detail = "Docker service unavailable"
350351 )
351-
352+
352353 containers = container_manager .list_mcp_containers (tenant_id = tenant_id )
353-
354+
354355 return JSONResponse (
355356 status_code = HTTPStatus .OK ,
356357 content = {
@@ -377,7 +378,7 @@ async def get_container_logs(
377378 """ Get logs from MCP container """
378379 try :
379380 user_id , tenant_id = get_current_user_id (authorization )
380-
381+
381382 try :
382383 container_manager = MCPContainerManager ()
383384 except MCPContainerError as e :
@@ -386,9 +387,9 @@ async def get_container_logs(
386387 status_code = HTTPStatus .SERVICE_UNAVAILABLE ,
387388 detail = "Docker service unavailable"
388389 )
389-
390+
390391 logs = container_manager .get_container_logs (container_id , tail = tail )
391-
392+
392393 return JSONResponse (
393394 status_code = HTTPStatus .OK ,
394395 content = {
@@ -404,3 +405,62 @@ async def get_container_logs(
404405 status_code = HTTPStatus .INTERNAL_SERVER_ERROR ,
405406 detail = f"Failed to get container logs: { str (e )} "
406407 )
408+
409+
410+ # Conditionally add upload-image route based on ENABLE_UPLOAD_IMAGE setting
411+ if ENABLE_UPLOAD_IMAGE :
412+ @router .post ("/upload-image" )
413+ async def upload_mcp_image (
414+ file : UploadFile = File (..., description = "Docker image tar file" ),
415+ port : int = Form (..., ge = 1 , le = 65535 ,
416+ description = "Host port to expose the MCP server on (1-65535)" ),
417+ service_name : Optional [str ] = Form (
418+ None , description = "Name for the MCP service (auto-generated if not provided)" ),
419+ env_vars : Optional [str ] = Form (
420+ None , description = "Environment variables as JSON string" ),
421+ authorization : Optional [str ] = Header (None )
422+ ):
423+ """
424+ Upload Docker image tar file and start MCP container.
425+
426+ Container naming: {filename-without-extension}-{tenant-id[:8]}-{user-id[:8]}
427+ """
428+ try :
429+ user_id , tenant_id = get_current_user_id (authorization )
430+
431+ # Read file content
432+ content = await file .read ()
433+
434+ # Call service layer to handle the business logic
435+ result = await upload_and_start_mcp_image (
436+ tenant_id = tenant_id ,
437+ user_id = user_id ,
438+ file_content = content ,
439+ filename = file .filename ,
440+ port = port ,
441+ service_name = service_name ,
442+ env_vars = env_vars ,
443+ )
444+
445+ return JSONResponse (status_code = HTTPStatus .OK , content = result )
446+
447+ except ValueError as e :
448+ logger .error (f"Validation error: { e } " )
449+ raise HTTPException (
450+ status_code = HTTPStatus .BAD_REQUEST , detail = str (e ))
451+ except MCPNameIllegal as e :
452+ logger .error (f"MCP name conflict: { e } " )
453+ raise HTTPException (status_code = HTTPStatus .CONFLICT , detail = str (e ))
454+ except MCPContainerError as e :
455+ logger .error (f"Container error: { e } " )
456+ raise HTTPException (
457+ status_code = HTTPStatus .SERVICE_UNAVAILABLE , detail = str (e ))
458+ except Exception as e :
459+ logger .error (f"Failed to upload and start MCP container: { e } " )
460+ raise HTTPException (
461+ status_code = HTTPStatus .INTERNAL_SERVER_ERROR ,
462+ detail = f"Failed to upload and start MCP container: { str (e )} "
463+ )
464+ else :
465+ logger .info (
466+ "MCP image upload feature is disabled (ENABLE_UPLOAD_IMAGE=false)" )
0 commit comments