2026-07-03 08:53:56 +08:00
|
|
|
|
from collections.abc import AsyncGenerator
|
|
|
|
|
|
from contextlib import asynccontextmanager
|
|
|
|
|
|
import json
|
|
|
|
|
|
from typing import Any
|
|
|
|
|
|
|
|
|
|
|
|
from anyio import create_memory_object_stream, create_task_group
|
|
|
|
|
|
from mcp.shared.message import SessionMessage
|
|
|
|
|
|
from mcp.types import JSONRPCMessage
|
|
|
|
|
|
|
|
|
|
|
|
from zhenxun.services.ai.sandbox.protocols import SupportsStreamExecution
|
|
|
|
|
|
from zhenxun.services.ai.sandbox.registry import SandboxRegistry
|
2026-07-10 09:14:06 +08:00
|
|
|
|
from zhenxun.services.ai.utils.logger import log_sandbox as logger
|
2026-07-03 08:53:56 +08:00
|
|
|
|
from zhenxun.utils.pydantic_compat import model_dump_json, model_validate
|
|
|
|
|
|
|
2026-07-10 09:14:06 +08:00
|
|
|
|
from .base import BaseMcpProxyExtension
|
|
|
|
|
|
|
2026-07-03 08:53:56 +08:00
|
|
|
|
|
|
|
|
|
|
class UniversalMcpExtension(BaseMcpProxyExtension):
|
2026-07-05 11:47:21 +08:00
|
|
|
|
"""通用 MCP 代理扩展类,用于在沙箱内连接 MCP 服务"""
|
|
|
|
|
|
|
2026-07-03 08:53:56 +08:00
|
|
|
|
@property
|
|
|
|
|
|
def extension_name(self) -> str:
|
2026-07-05 11:47:21 +08:00
|
|
|
|
"""获取 MCP 代理扩展的唯一名称"""
|
2026-07-03 08:53:56 +08:00
|
|
|
|
return "universal_mcp"
|
|
|
|
|
|
|
|
|
|
|
|
@asynccontextmanager
|
|
|
|
|
|
async def connect_mcp(
|
|
|
|
|
|
self, command: str, args: list[str], env: dict[str, str] | None = None
|
|
|
|
|
|
) -> AsyncGenerator[tuple[Any, Any], None]:
|
2026-07-05 11:47:21 +08:00
|
|
|
|
"""启动沙箱内的 MCP 服务器,并建立与之进行 JSON-RPC 通信的双向内存流管道"""
|
2026-07-03 08:53:56 +08:00
|
|
|
|
if not isinstance(self.session, SupportsStreamExecution):
|
|
|
|
|
|
raise RuntimeError(
|
|
|
|
|
|
"当前沙箱驱动不支持流式后台进程执行 (SupportsStreamExecution),"
|
|
|
|
|
|
"无法启动原生 MCP 代理。"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
logger.info(
|
|
|
|
|
|
"[UniversalMcpExtension] 正在沙箱内原生启动 MCP 服务器: "
|
|
|
|
|
|
f"{command} {' '.join(args)}"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
cmd_list = [command, *args]
|
|
|
|
|
|
|
|
|
|
|
|
async with self.session.create_stream_process(
|
|
|
|
|
|
command=cmd_list, cwd=self.session.workspace_path, env=env
|
|
|
|
|
|
) as process_stream:
|
|
|
|
|
|
read_prod, read_cons = create_memory_object_stream(10)
|
|
|
|
|
|
write_prod, write_cons = create_memory_object_stream(10)
|
|
|
|
|
|
|
|
|
|
|
|
async def stream_reader():
|
|
|
|
|
|
buffer = b""
|
|
|
|
|
|
try:
|
|
|
|
|
|
while True:
|
|
|
|
|
|
msg = await process_stream.read()
|
|
|
|
|
|
if msg is None:
|
|
|
|
|
|
break
|
|
|
|
|
|
if msg.stream_type == 1:
|
|
|
|
|
|
buffer += msg.data
|
|
|
|
|
|
while b"\n" in buffer:
|
|
|
|
|
|
line, buffer = buffer.split(b"\n", 1)
|
|
|
|
|
|
if not line.strip():
|
|
|
|
|
|
continue
|
|
|
|
|
|
try:
|
|
|
|
|
|
msg = model_validate(
|
|
|
|
|
|
JSONRPCMessage, json.loads(line)
|
|
|
|
|
|
)
|
|
|
|
|
|
await read_prod.send(SessionMessage(message=msg))
|
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
|
await read_prod.send(exc)
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
finally:
|
|
|
|
|
|
await read_prod.aclose()
|
|
|
|
|
|
|
|
|
|
|
|
async def stream_writer():
|
|
|
|
|
|
try:
|
|
|
|
|
|
async for msg in write_cons:
|
|
|
|
|
|
data = (
|
|
|
|
|
|
model_dump_json(
|
|
|
|
|
|
msg.message, by_alias=True, exclude_none=True
|
|
|
|
|
|
).encode("utf-8")
|
|
|
|
|
|
+ b"\n"
|
|
|
|
|
|
)
|
|
|
|
|
|
await process_stream.write(data)
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
async with create_task_group() as tg:
|
|
|
|
|
|
tg.start_soon(stream_reader)
|
|
|
|
|
|
tg.start_soon(stream_writer)
|
|
|
|
|
|
yield read_cons, write_prod
|
|
|
|
|
|
tg.cancel_scope.cancel()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
SandboxRegistry.register_extension(UniversalMcpExtension)
|
|
|
|
|
|
|
|
|
|
|
|
__all__ = [
|
|
|
|
|
|
"UniversalMcpExtension",
|
|
|
|
|
|
]
|