From c1653a59c08f80091cce4f1b4cc36e338e1c9629 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=82=96=E6=B3=BD=E6=B6=9B?= Date: Sun, 22 Feb 2026 18:57:30 +0800 Subject: [PATCH] feat(mcp): add stdio env support and env editor component --- scripts/migrate_all.py | 1 + scripts/migrate_business_from_sqlite.py | 1 + server/routers/mcp_router.py | 9 ++ src/services/mcp_service.py | 8 ++ src/storage/db/models.py | 10 ++ src/storage/postgres/manager.py | 1 + src/storage/postgres/models_business.py | 10 ++ web/src/components/McpEnvEditor.vue | 145 ++++++++++++++++++++ web/src/components/McpServerDetailModal.vue | 4 + web/src/components/McpServersComponent.vue | 33 ++++- 10 files changed, 219 insertions(+), 3 deletions(-) create mode 100644 web/src/components/McpEnvEditor.vue diff --git a/scripts/migrate_all.py b/scripts/migrate_all.py index 1dca7acb..d77f0356 100644 --- a/scripts/migrate_all.py +++ b/scripts/migrate_all.py @@ -658,6 +658,7 @@ class MigrationRunner: url=sqlite_server.url, command=sqlite_server.command, args=sqlite_server.args, + env=getattr(sqlite_server, "env", None), headers=sqlite_server.headers, timeout=sqlite_server.timeout, sse_read_timeout=sqlite_server.sse_read_timeout, diff --git a/scripts/migrate_business_from_sqlite.py b/scripts/migrate_business_from_sqlite.py index 252ee2d0..3d8447b8 100644 --- a/scripts/migrate_business_from_sqlite.py +++ b/scripts/migrate_business_from_sqlite.py @@ -532,6 +532,7 @@ async def migrate_mcp_servers(sqlite_reader: SQLiteReader, dry_run: bool, execut url=sqlite_server.url, command=sqlite_server.command, args=sqlite_server.args, + env=getattr(sqlite_server, "env", None), headers=sqlite_server.headers, timeout=sqlite_server.timeout, sse_read_timeout=sqlite_server.sse_read_timeout, diff --git a/server/routers/mcp_router.py b/server/routers/mcp_router.py index 66b31f0d..f1c9828c 100644 --- a/server/routers/mcp_router.py +++ b/server/routers/mcp_router.py @@ -33,6 +33,7 @@ class CreateMcpServerRequest(BaseModel): url: str | None = Field(None, description="服务器 URL(sse/streamable_http)") command: str | None = Field(None, description="命令(stdio)") args: list | None = Field(None, description="命令参数数组(stdio)") + env: dict | None = Field(None, description="环境变量(stdio)") description: str | None = Field(None, description="描述") headers: dict | None = Field(None, description="HTTP 请求头") timeout: int | None = Field(None, description="HTTP 超时时间(秒)") @@ -46,6 +47,7 @@ class UpdateMcpServerRequest(BaseModel): url: str | None = Field(None, description="服务器 URL") command: str | None = Field(None, description="命令(stdio)") args: list | None = Field(None, description="命令参数数组(stdio)") + env: dict | None = Field(None, description="环境变量(stdio)") description: str | None = Field(None, description="描述") headers: dict | None = Field(None, description="HTTP 请求头") timeout: int | None = Field(None, description="HTTP 超时时间(秒)") @@ -112,6 +114,7 @@ async def create_mcp_server_route( url=request.url, command=request.command, args=request.args, + env=request.env, description=request.description, headers=request.headers, timeout=request.timeout, @@ -159,6 +162,11 @@ async def update_mcp_server_route( raise HTTPException(status_code=400, detail=f"传输类型必须是 {', '.join(valid_transports)} 之一") try: + fields_set = getattr(request, "model_fields_set", getattr(request, "__fields_set__", set())) + update_kwargs = {} + if "env" in fields_set: + update_kwargs["env"] = request.env + server = await update_mcp_server( db, name=name, @@ -173,6 +181,7 @@ async def update_mcp_server_route( tags=request.tags, icon=request.icon, updated_by=current_user.username, + **update_kwargs, ) return {"success": True, "data": server.to_dict()} except ValueError as ve: diff --git a/src/services/mcp_service.py b/src/services/mcp_service.py index d3d9da0e..4385820b 100644 --- a/src/services/mcp_service.py +++ b/src/services/mcp_service.py @@ -35,6 +35,7 @@ _mcp_tools_stats: dict[str, dict[str, int]] = {} # MCP Server configurations (Runtime cache, loaded from DB) MCP_SERVERS: dict[str, dict[str, Any]] = {} +_UNSET = object() # Default MCP Server configurations (Imported to DB on first run) _DEFAULT_MCP_SERVERS = { @@ -130,6 +131,7 @@ async def init_mcp_servers() -> None: url=config.get("url"), command=config.get("command"), args=config.get("args"), + env=config.get("env"), headers=config.get("headers"), timeout=config.get("timeout"), sse_read_timeout=config.get("sse_read_timeout"), @@ -155,6 +157,7 @@ async def init_mcp_servers() -> None: url=config.get("url"), command=config.get("command"), args=config.get("args"), + env=config.get("env"), headers=config.get("headers"), timeout=config.get("timeout"), sse_read_timeout=config.get("sse_read_timeout"), @@ -368,6 +371,7 @@ async def create_mcp_server( url: str = None, command: str = None, args: list = None, + env: dict = None, description: str = None, headers: dict = None, timeout: int = None, @@ -389,6 +393,7 @@ async def create_mcp_server( url=url, command=command, args=args, + env=env, headers=headers, timeout=timeout, sse_read_timeout=sse_read_timeout, @@ -417,6 +422,7 @@ async def update_mcp_server( url: str = None, command: str = None, args: list = None, + env: Any = _UNSET, headers: dict = None, timeout: int = None, sse_read_timeout: int = None, @@ -439,6 +445,8 @@ async def update_mcp_server( server.command = command if args is not None: server.args = args + if env is not _UNSET: + server.env = env if headers is not None: server.headers = headers if timeout is not None: diff --git a/src/storage/db/models.py b/src/storage/db/models.py index c1edfc3e..7c464c05 100644 --- a/src/storage/db/models.py +++ b/src/storage/db/models.py @@ -341,6 +341,7 @@ class MCPServer(Base): url = Column(String(500), nullable=True, comment="服务器 URL(sse/streamable_http)") command = Column(String(500), nullable=True, comment="命令(stdio)") args = Column(JSON, nullable=True, comment="命令参数数组(stdio)") + env = Column(JSON, nullable=True, comment="环境变量(stdio)") headers = Column(JSON, nullable=True, comment="HTTP 请求头") timeout = Column(Integer, nullable=True, comment="HTTP 超时时间(秒)") sse_read_timeout = Column(Integer, nullable=True, comment="SSE 读取超时(秒)") @@ -369,6 +370,7 @@ class MCPServer(Base): "url": self.url, "command": self.command, "args": self.args or [], + "env": self.env or {}, "headers": self.headers or {}, "timeout": self.timeout, "sse_read_timeout": self.sse_read_timeout, @@ -402,6 +404,14 @@ class MCPServer(Base): config["args"] = json.loads(self.args) except json.JSONDecodeError: pass + if self.transport == "stdio" and self.env: + if isinstance(self.env, dict): + config["env"] = self.env + elif isinstance(self.env, str): + try: + config["env"] = json.loads(self.env) + except json.JSONDecodeError: + pass # headers 只用于 sse/streamable_http 传输类型 if self.transport in ("sse", "streamable_http") and self.headers: if isinstance(self.headers, dict): diff --git a/src/storage/postgres/manager.py b/src/storage/postgres/manager.py index 390f00f4..b499a568 100644 --- a/src/storage/postgres/manager.py +++ b/src/storage/postgres/manager.py @@ -171,6 +171,7 @@ class PostgresManager(metaclass=SingletonMeta): "ALTER TABLE IF EXISTS skills ADD COLUMN IF NOT EXISTS tool_dependencies JSONB DEFAULT '[]'::jsonb", "ALTER TABLE IF EXISTS skills ADD COLUMN IF NOT EXISTS mcp_dependencies JSONB DEFAULT '[]'::jsonb", "ALTER TABLE IF EXISTS skills ADD COLUMN IF NOT EXISTS skill_dependencies JSONB DEFAULT '[]'::jsonb", + "ALTER TABLE IF EXISTS mcp_servers ADD COLUMN IF NOT EXISTS env JSONB", ] async with self.async_engine.begin() as conn: for stmt in stmts: diff --git a/src/storage/postgres/models_business.py b/src/storage/postgres/models_business.py index a5e7cabd..b7e57478 100644 --- a/src/storage/postgres/models_business.py +++ b/src/storage/postgres/models_business.py @@ -416,6 +416,7 @@ class MCPServer(Base): url = Column(String(500), nullable=True, comment="服务器 URL(sse/streamable_http)") command = Column(String(500), nullable=True, comment="命令(stdio)") args = Column(JSON, nullable=True, comment="命令参数数组(stdio)") + env = Column(JSON, nullable=True, comment="环境变量(stdio)") headers = Column(JSON, nullable=True, comment="HTTP 请求头") timeout = Column(Integer, nullable=True, comment="HTTP 超时时间(秒)") sse_read_timeout = Column(Integer, nullable=True, comment="SSE 读取超时(秒)") @@ -444,6 +445,7 @@ class MCPServer(Base): "url": self.url, "command": self.command, "args": self.args or [], + "env": self.env or {}, "headers": self.headers or {}, "timeout": self.timeout, "sse_read_timeout": self.sse_read_timeout, @@ -475,6 +477,14 @@ class MCPServer(Base): config["args"] = json.loads(self.args) except json.JSONDecodeError: pass + if self.transport == "stdio" and self.env: + if isinstance(self.env, dict): + config["env"] = self.env + elif isinstance(self.env, str): + try: + config["env"] = json.loads(self.env) + except json.JSONDecodeError: + pass # headers 只用于 sse/streamable_http 传输类型 if self.transport in ("sse", "streamable_http") and self.headers: if isinstance(self.headers, dict): diff --git a/web/src/components/McpEnvEditor.vue b/web/src/components/McpEnvEditor.vue new file mode 100644 index 00000000..77600c49 --- /dev/null +++ b/web/src/components/McpEnvEditor.vue @@ -0,0 +1,145 @@ + + + + + diff --git a/web/src/components/McpServerDetailModal.vue b/web/src/components/McpServerDetailModal.vue index 631ca69d..fb9fead6 100644 --- a/web/src/components/McpServerDetailModal.vue +++ b/web/src/components/McpServerDetailModal.vue @@ -78,6 +78,10 @@ +
+ +
{{ JSON.stringify(server.env, null, 2) }}
+
diff --git a/web/src/components/McpServersComponent.vue b/web/src/components/McpServersComponent.vue index 0066a723..ff237394 100644 --- a/web/src/components/McpServersComponent.vue +++ b/web/src/components/McpServersComponent.vue @@ -214,7 +214,7 @@ -