Skip to content

Commit ddd9394

Browse files
committed
✨ Adapt MCP protocal to different program language #416
[Specification Details] 1. Add a basic image for starting the mcp service 2. Implementation of the app and service for containerized startup of mcp services 3. Test case modification
1 parent f49c3a6 commit ddd9394

File tree

12 files changed

+684
-251
lines changed

12 files changed

+684
-251
lines changed

backend/apps/remote_mcp_app.py

Lines changed: 255 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,17 @@
55
from fastapi.responses import JSONResponse
66
from http import HTTPStatus
77

8-
from consts.exceptions import MCPConnectionError, MCPNameIllegal
8+
from consts.const import MCP_DOCKER_IMAGE
9+
from consts.exceptions import MCPConnectionError, MCPNameIllegal, MCPContainerError
10+
from consts.model import MCPConfigRequest
911
from services.remote_mcp_service import (
1012
add_remote_mcp_server_list,
1113
delete_remote_mcp_server_list,
1214
get_remote_mcp_server_list,
1315
check_mcp_health_and_update_db,
1416
)
1517
from services.tool_configuration_service import get_tool_from_remote_mcp_server
18+
from services.mcp_container_service import MCPContainerManager
1619
from utils.auth_utils import get_current_user_id
1720

1821
router = APIRouter(prefix="/mcp")
@@ -138,3 +141,254 @@ async def check_mcp_health(mcp_url: str, service_name: str, authorization: Optio
138141
logger.error(f"Failed to check the health of the MCP server: {e}")
139142
raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
140143
detail="Failed to check the health of the MCP server")
144+
145+
146+
@router.post("/add-from-config")
147+
async def add_mcp_from_config(
148+
mcp_config: MCPConfigRequest,
149+
authorization: Optional[str] = Header(None)
150+
):
151+
"""
152+
Add MCP server by starting a container with command+args config.
153+
Similar to Cursor's MCP server configuration format.
154+
155+
Example request:
156+
{
157+
"mcpServers": {
158+
"12306-mcp": {
159+
"command": "npx",
160+
"args": ["-y", "12306-mcp"],
161+
"env": {"NODE_ENV": "production"}
162+
}
163+
}
164+
}
165+
"""
166+
try:
167+
user_id, tenant_id = get_current_user_id(authorization)
168+
169+
# Initialize container manager
170+
try:
171+
container_manager = MCPContainerManager()
172+
except MCPContainerError as e:
173+
logger.error(f"Failed to initialize container manager: {e}")
174+
raise HTTPException(
175+
status_code=HTTPStatus.SERVICE_UNAVAILABLE,
176+
detail="Docker service unavailable. Please ensure Docker socket is mounted."
177+
)
178+
179+
results = []
180+
errors = []
181+
182+
for service_name, config in mcp_config.mcpServers.items():
183+
try:
184+
command = config.command
185+
args = config.args or []
186+
env_vars = config.env or {}
187+
port = config.port
188+
189+
if not command:
190+
errors.append(f"{service_name}: command is required")
191+
continue
192+
193+
if port is None:
194+
errors.append(f"{service_name}: port is required")
195+
continue
196+
197+
# Build full command to run inside nexent/nexent-mcp image
198+
full_command = [
199+
"python",
200+
"-m",
201+
"mcp_proxy",
202+
"--host",
203+
"0.0.0.0",
204+
"--port",
205+
str(port),
206+
"--transport",
207+
"streamablehttp",
208+
"--",
209+
command,
210+
*args,
211+
]
212+
213+
# Start container
214+
container_info = await container_manager.start_mcp_container(
215+
service_name=service_name,
216+
tenant_id=tenant_id,
217+
user_id=user_id,
218+
env_vars=env_vars,
219+
host_port=port,
220+
image=config.image or MCP_DOCKER_IMAGE,
221+
full_command=full_command,
222+
)
223+
224+
# Register to remote MCP server list
225+
try:
226+
await add_remote_mcp_server_list(
227+
tenant_id=tenant_id,
228+
user_id=user_id,
229+
remote_mcp_server=container_info["mcp_url"],
230+
remote_mcp_server_name=service_name
231+
)
232+
except MCPNameIllegal:
233+
# If name already exists, try to stop the container we just created
234+
try:
235+
await container_manager.stop_mcp_container(container_info["container_id"])
236+
except Exception:
237+
pass
238+
errors.append(f"{service_name}: MCP name already exists")
239+
continue
240+
241+
results.append({
242+
"service_name": service_name,
243+
"status": "success",
244+
"mcp_url": container_info["mcp_url"],
245+
"container_id": container_info["container_id"],
246+
"container_name": container_info.get("container_name"),
247+
"host_port": container_info.get("host_port")
248+
})
249+
250+
except MCPContainerError as e:
251+
logger.error(f"Failed to start MCP container {service_name}: {e}")
252+
errors.append(f"{service_name}: {str(e)}")
253+
except Exception as e:
254+
logger.error(f"Unexpected error adding MCP {service_name}: {e}")
255+
errors.append(f"{service_name}: {str(e)}")
256+
257+
if errors and not results:
258+
raise HTTPException(
259+
status_code=HTTPStatus.BAD_REQUEST,
260+
detail=f"All MCP servers failed: {errors}"
261+
)
262+
263+
return JSONResponse(
264+
status_code=HTTPStatus.OK,
265+
content={
266+
"message": "MCP servers processed",
267+
"results": results,
268+
"errors": errors if errors else None,
269+
"status": "success"
270+
}
271+
)
272+
273+
except HTTPException:
274+
raise
275+
except Exception as e:
276+
logger.error(f"Failed to add MCP from config: {e}")
277+
raise HTTPException(
278+
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
279+
detail=f"Failed to add MCP servers: {str(e)}"
280+
)
281+
282+
283+
@router.delete("/container/{container_id}")
284+
async def stop_mcp_container(
285+
container_id: str,
286+
authorization: Optional[str] = Header(None)
287+
):
288+
""" Stop and remove MCP container """
289+
try:
290+
user_id, tenant_id = get_current_user_id(authorization)
291+
292+
try:
293+
container_manager = MCPContainerManager()
294+
except MCPContainerError as e:
295+
logger.error(f"Failed to initialize container manager: {e}")
296+
raise HTTPException(
297+
status_code=HTTPStatus.SERVICE_UNAVAILABLE,
298+
detail="Docker service unavailable"
299+
)
300+
301+
success = await container_manager.stop_mcp_container(container_id)
302+
303+
if success:
304+
return JSONResponse(
305+
status_code=HTTPStatus.OK,
306+
content={"message": "Container stopped successfully", "status": "success"}
307+
)
308+
else:
309+
return JSONResponse(
310+
status_code=HTTPStatus.NOT_FOUND,
311+
content={"message": "Container not found", "status": "error"}
312+
)
313+
except HTTPException:
314+
raise
315+
except Exception as e:
316+
logger.error(f"Failed to stop container: {e}")
317+
raise HTTPException(
318+
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
319+
detail=f"Failed to stop container: {str(e)}"
320+
)
321+
322+
323+
@router.get("/containers")
324+
async def list_mcp_containers(
325+
authorization: Optional[str] = Header(None)
326+
):
327+
""" List all MCP containers for the current tenant """
328+
try:
329+
user_id, tenant_id = get_current_user_id(authorization)
330+
331+
try:
332+
container_manager = MCPContainerManager()
333+
except MCPContainerError as e:
334+
logger.error(f"Failed to initialize container manager: {e}")
335+
raise HTTPException(
336+
status_code=HTTPStatus.SERVICE_UNAVAILABLE,
337+
detail="Docker service unavailable"
338+
)
339+
340+
containers = container_manager.list_mcp_containers(tenant_id=tenant_id)
341+
342+
return JSONResponse(
343+
status_code=HTTPStatus.OK,
344+
content={
345+
"containers": containers,
346+
"status": "success"
347+
}
348+
)
349+
except HTTPException:
350+
raise
351+
except Exception as e:
352+
logger.error(f"Failed to list containers: {e}")
353+
raise HTTPException(
354+
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
355+
detail=f"Failed to list containers: {str(e)}"
356+
)
357+
358+
359+
@router.get("/container/{container_id}/logs")
360+
async def get_container_logs(
361+
container_id: str,
362+
tail: int = 100,
363+
authorization: Optional[str] = Header(None)
364+
):
365+
""" Get logs from MCP container """
366+
try:
367+
user_id, tenant_id = get_current_user_id(authorization)
368+
369+
try:
370+
container_manager = MCPContainerManager()
371+
except MCPContainerError as e:
372+
logger.error(f"Failed to initialize container manager: {e}")
373+
raise HTTPException(
374+
status_code=HTTPStatus.SERVICE_UNAVAILABLE,
375+
detail="Docker service unavailable"
376+
)
377+
378+
logs = container_manager.get_container_logs(container_id, tail=tail)
379+
380+
return JSONResponse(
381+
status_code=HTTPStatus.OK,
382+
content={
383+
"logs": logs,
384+
"status": "success"
385+
}
386+
)
387+
except HTTPException:
388+
raise
389+
except Exception as e:
390+
logger.error(f"Failed to get container logs: {e}")
391+
raise HTTPException(
392+
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
393+
detail=f"Failed to get container logs: {str(e)}"
394+
)

backend/consts/const.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ class VectorDatabaseType(str, Enum):
130130
"DISABLE_CELERY_FLOWER", "false").lower() == "true"
131131
DOCKER_ENVIRONMENT = os.getenv("DOCKER_ENVIRONMENT", "false").lower() == "true"
132132
DOCKER_HOST = os.getenv("DOCKER_HOST")
133+
MCP_DOCKER_IMAGE = os.getenv("MCP_DOCKER_IMAGE", "nexent/nexent-mcp:latest")
133134

134135

135136
# Celery Configuration

backend/consts/exceptions.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,4 +90,9 @@ class VoiceConfigException(Exception):
9090

9191
class ToolExecutionException(Exception):
9292
"""Raised when mcp tool execution failed."""
93+
pass
94+
95+
96+
class MCPContainerError(Exception):
97+
"""Raised when MCP container operation fails."""
9398
pass

backend/consts/model.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,3 +429,23 @@ class ToolValidateRequest(BaseModel):
429429
None, description="Tool inputs")
430430
params: Optional[Dict[str, Any]] = Field(
431431
None, description="Tool configuration parameters")
432+
433+
434+
class MCPServerConfig(BaseModel):
435+
"""Configuration for a single MCP server"""
436+
command: str = Field(..., description="Command to run (e.g., 'npx')")
437+
args: List[str] = Field(default_factory=list, description="Command arguments")
438+
env: Optional[Dict[str, str]] = Field(
439+
None, description="Environment variables for the MCP server")
440+
port: Optional[int] = Field(
441+
None, description="Host port to expose the MCP server on (e.g., 5020)")
442+
image: Optional[str] = Field(
443+
None,
444+
description="Docker image for the MCP proxy container (optional, overrides MCP_DOCKER_IMAGE)",
445+
)
446+
447+
448+
class MCPConfigRequest(BaseModel):
449+
"""Request model for adding MCP servers from configuration"""
450+
mcpServers: Dict[str, MCPServerConfig] = Field(
451+
..., description="Dictionary of MCP server configurations")

0 commit comments

Comments
 (0)