Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
c949f66
feat: 支持MCP动态鉴权连接与可视化配置
supreme0597 Jun 2, 2026
b1a44c9
fix: 修复MCP个人连接代理越权
supreme0597 Jun 4, 2026
815a570
fix: 修复 MCP 鉴权工具缓存隔离与 Redis 分级缓存
supreme0597 Jun 4, 2026
2bb296f
fix: 修复 MCP 鉴权代理缓存失效问题
supreme0597 Jun 4, 2026
d295102
fix: 统一 MCP 个人连接运行态用户标识
supreme0597 Jun 4, 2026
61bfd75
fix: 修复运行时 MCP 工具执行注册
supreme0597 Jun 4, 2026
f51bae0
refactor: 简化 current_user 工号使用
supreme0597 Jun 4, 2026
5736090
refactor: 同步 skills_middleware 内部用户标识为 work_id
supreme0597 Jun 4, 2026
125c68f
test: 修复 5 个预存测试失败
supreme0597 Jun 4, 2026
41e4371
refactor(mcp): save intermediate progress before pure architecture re…
supreme0597 Jun 4, 2026
7053d3b
feat(mcp): complete mcp gateway proxy streaming refactor and purge mc…
supreme0597 Jun 4, 2026
2ad8f77
feat(ui): update MCP scope id inputs to dropdown select
supreme0597 Jun 4, 2026
a943370
test: 修复重构引发的端到端及单元测试错误并调整路由命名防冲突
supreme0597 Jun 4, 2026
fc77ddc
fix(mcp-auth): 修复 MCP 鉴权与代理链路的三个关键隐患并完善规范
supreme0597 Jun 4, 2026
8065a16
style: 自动格式化 Python 代码以符合 Ruff 规范
supreme0597 Jun 4, 2026
5de697a
fix(mcp): 修复鉴权系统代码审计中发现的内存泄漏与并发缺陷
supreme0597 Jun 4, 2026
17cc449
refactor(mcp_auth): 深度重构 MCP Auth 模块,修复安全隐患与并发瓶颈
supreme0597 Jun 5, 2026
7002976
fix(web): 修复 MCP 连接配置中用户下拉框显示异常及重复的问题
supreme0597 Jun 5, 2026
89d4e8c
feat(web): 连接管理面板支持显示可读的认证方式名称
supreme0597 Jun 5, 2026
0e63bf4
style(web): MCP 信息详情页只读模式下隐藏冗长的认证配置 JSON,改用简略提示
supreme0597 Jun 5, 2026
81f08e9
feat(web): 支持只读模式渲染认证配置 (McpAuthConfigBuilder)
supreme0597 Jun 5, 2026
3ab2b32
fix(web): 修复 McpAuthConfigBuilder 组件中重复的 v-if 属性导致的编译报错
supreme0597 Jun 5, 2026
5063a8b
style(web): 优化只读模式下 McpAuthConfigBuilder 的样式,避免输入框和按钮看起来像禁用的灰色
supreme0597 Jun 5, 2026
7b03335
feat(backend): 支持在 MCP 认证模板中使用 context.work_id 获取工号,并区分 user_id
supreme0597 Jun 5, 2026
9655d4e
feat(web): MCP 认证配置快捷按钮增加对员工工号 (work_id) 的支持
supreme0597 Jun 5, 2026
f5c794e
feat(mcp): 优化无密钥 MCP 服务器测试和运行体验,如果 auth_config 没有配置 ${secret.xxx} 字段,…
supreme0597 Jun 5, 2026
115e85e
fix(backend): 补充 MCPAuthConfig 中的 get_secret_fields 实例方法,修复测试服务无秘钥跳过连…
supreme0597 Jun 5, 2026
9f1ca54
fix(mcp): fix missing work_id in proxy token payload
supreme0597 Jun 5, 2026
2bb97b8
feat(mcp-auth): 优化免密钥连接测试,修复长连接断开失效及 Demo 服务的响应冲突
supreme0597 Jun 5, 2026
bafaaac
docs(roadmap): 更新 0.6.3 开发路线图关于 MCP 多鉴权场景测试与修复的开发日志
supreme0597 Jun 5, 2026
22e2c7c
fix(mcp): 统一个人范围绑定连接 ID 语义,使用用户自增主键映射 scope_id 并通过 work_id 进行工号匹配,修复因…
supreme0597 Jun 5, 2026
35c4221
test(mcp): 增加个人、部门和全局范围连接的 E2E 接口集成测试
supreme0597 Jun 5, 2026
855c611
fix(mcp): 修复 MCP 服务器详情页测试按钮在已绑定连接时仍报错的问题
supreme0597 Jun 5, 2026
fabfabb
fix(mcp): 修复:no-secret 动态 MCP 代理入口允许无 connection;user scope connectio…
supreme0597 Jun 8, 2026
48be3bd
feat(mcp): 优化连接配置与管理体验
supreme0597 Jun 8, 2026
29576d9
Merge branch 'refs/heads/codex/mcp-auth-orchestrator' into release/0-6-3
supreme0597 Jun 8, 2026
73fa656
fix(mcp): 优化运行态加载与离线降噪
supreme0597 Jun 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .env.template
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ YUXI_INSTANCE_ID=
# # Servies
# YUXI_SUPER_ADMIN_NAME=
# YUXI_SUPER_ADMIN_PASSWORD=
# MCP_CREDENTIALS_MASTER_KEY=

# # URL Whitelist (comma-separated domains/IPs, empty to disable URL parsing)
# YUXI_URL_WHITELIST=github.com,docs.example.com,gitlab.example.com,127.0.0.1
Expand Down Expand Up @@ -73,4 +74,4 @@ YUXI_INSTANCE_ID=
# SANDBOX_NODE_HOST=host.docker.internal
# KUBECONFIG_PATH=/root/.kube/config
# THREAD_PVC=yuxi-thread
# SKILLS_PVC=yuxi-skills # 当前代码会读取,但 Pod 挂载实际仍只使用 THREAD_PVC
# SKILLS_PVC=yuxi-skills # 当前代码会读取,但 Pod 挂载实际仍只使用 THREAD_PVC
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -79,4 +79,7 @@ docs/vibe

/models

.taskr/
.taskr/

.workbuddy
.worktrees/
1 change: 1 addition & 0 deletions backend/package/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ dependencies = [
"aiosqlite>=0.20.0",
"argon2-cffi>=25.1.0",
"asyncpg>=0.30.0",
"cachetools>=5.3.0",
"chardet>=5.0.0",
"colorlog>=6.9.0",
"dashscope>=1.23.2",
Expand Down
2 changes: 1 addition & 1 deletion backend/package/yuxi/agents/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from yuxi.agents.toolkits.utils import get_tool_info

# MCP - Agent 层统一入口(自动过滤 disabled_tools)
from yuxi.services.mcp_service import get_enabled_mcp_tools
from yuxi.services.mcp.tool_registry_service import get_enabled_mcp_tools

__all__ = [
# Base classes
Expand Down
9 changes: 6 additions & 3 deletions backend/package/yuxi/agents/buildin/chatbot/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,19 @@
save_attachments_to_fs,
)
from yuxi.agents.middlewares.knowledge_base_middleware import KnowledgeBaseMiddleware
from yuxi.agents.middlewares.skills_middleware import SkillsMiddleware
from yuxi.services.mcp_service import get_tools_from_all_servers
from yuxi.agents.middlewares.skills_middleware import SkillsMiddleware, collect_context_mcp_names_for_preload
from yuxi.services.mcp.tool_registry_service import get_tools_from_all_servers
from yuxi.services.subagent_service import get_subagents_from_names
from yuxi.utils.logging_config import logger

from .prompt import TODO_MID_PROMPT, build_prompt_with_context


async def _build_middlewares(context):
"""构建中间件列表"""
all_mcp_tools = await get_tools_from_all_servers() # 因为异步加载,无法放在 RuntimeConfigMiddleware 的 __init__ 中
preload_mcp_names = await collect_context_mcp_names_for_preload(context)
logger.info(f"ChatbotAgent MCP preload candidates: {preload_mcp_names}")
all_mcp_tools = await get_tools_from_all_servers(preload_mcp_names)

# summary middleware
# 主 Agent 上下文优化:90k tokens 触发压缩(128k context window 的 70%)
Expand Down
8 changes: 5 additions & 3 deletions backend/package/yuxi/agents/buildin/deep_agent/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@
save_attachments_to_fs,
)
from yuxi.agents.middlewares.knowledge_base_middleware import KnowledgeBaseMiddleware
from yuxi.agents.middlewares.skills_middleware import SkillsMiddleware
from yuxi.agents.middlewares.skills_middleware import SkillsMiddleware, collect_context_mcp_names_for_preload
from yuxi.agents.toolkits.buildin.tools import _create_tavily_search
from yuxi.services.mcp_service import get_tools_from_all_servers
from yuxi.services.mcp.tool_registry_service import get_tools_from_all_servers
from yuxi.services.subagent_service import get_subagents_from_names
from yuxi.utils import logger

Expand Down Expand Up @@ -57,7 +57,9 @@ async def get_graph(self, context=None, **kwargs):
model = load_chat_model(context.model)
sub_model = load_chat_model(context.subagents_model)
search_tools = await self.get_tools()
all_mcp_tools = await get_tools_from_all_servers()
preload_mcp_names = await collect_context_mcp_names_for_preload(context)
logger.info(f"DeepAgent MCP preload candidates: {preload_mcp_names}")
all_mcp_tools = await get_tools_from_all_servers(preload_mcp_names)
# 合并搜索工具和 MCP 工具

# 从数据库加载 subagent specs(工具名称已解析)
Expand Down
14 changes: 14 additions & 0 deletions backend/package/yuxi/agents/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,20 @@ def update(self, data: dict):
metadata={"name": "用户ID", "configurable": False, "description": "用来唯一标识一个用户"},
)

work_id: str | None = field(
default=None,
metadata={
"name": "工号",
"configurable": False,
"description": "用来匹配个人 MCP 连接绑定范围的工号",
},
)

department_id: str | None = field(
default=None,
metadata={"name": "部门ID", "configurable": False, "description": "用来标识当前用户所属部门"},
)

system_prompt: Annotated[str, {"__template_metadata__": {"kind": "prompt"}}] = field(
default="You are a helpful assistant.",
metadata={"name": "系统提示词", "description": "用来描述智能体的角色和行为"},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

from langchain.agents.middleware import AgentMiddleware, ModelRequest, ModelResponse

from yuxi.services.mcp_service import get_mcp_tools
from yuxi.services.mcp.tool_registry_service import get_mcp_tools
from yuxi.utils import logger


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,18 @@
from typing import Any

from langchain.agents.middleware import AgentMiddleware, ModelRequest, ModelResponse
from langchain.tools.tool_node import ToolCallRequest
from langchain_core.messages import SystemMessage

from yuxi.agents import load_chat_model
from yuxi.agents.toolkits import get_all_tool_instances
from yuxi.services.mcp_service import get_enabled_mcp_tools
from yuxi.services.mcp.tool_registry_service import get_enabled_mcp_tools
from yuxi.services.mcp_auth.orchestrator import AuthContext
from yuxi.utils.datetime_utils import shanghai_now
from yuxi.utils.logging_config import logger

_RUNTIME_DYNAMIC_TOOLS_ATTR = "_runtime_config_dynamic_tools_by_name"


class RuntimeConfigMiddleware(AgentMiddleware):
"""运行时配置中间件 - 应用模型/工具/MCP/提示词配置
Expand Down Expand Up @@ -89,14 +93,19 @@ async def awrap_model_call(
# 获取上下文配置的工具
enabled_tools = await self.get_tools_from_context(runtime_context)
existing_tools = list(request.tools or [])
enabled_tool_names = {t.name for t in enabled_tools}
managed_tool_names = {t.name for t in self.tools}
merged_tools = []
for t_bind in existing_tools:
# (1) 已启用的工具保留
# (2) 非本中间件管理的工具保留
if t_bind.name in enabled_tool_names or t_bind.name not in managed_tool_names:
# 非本中间件管理的工具保留;本中间件管理的工具统一用本轮实时加载结果覆盖。
if t_bind.name not in managed_tool_names:
merged_tools.append(t_bind)
merged_tool_names = {t.name for t in merged_tools}
for tool in enabled_tools:
if tool.name in merged_tool_names:
continue
merged_tools.append(tool)
merged_tool_names.add(tool.name)
setattr(runtime_context, _RUNTIME_DYNAMIC_TOOLS_ATTR, {tool.name: tool for tool in enabled_tools})
overrides["tools"] = merged_tools
logger.debug(f"RuntimeConfigMiddleware selected tools: {[t.name for t in merged_tools]}")

Expand All @@ -116,6 +125,33 @@ async def awrap_model_call(

return await handler(request)

async def awrap_tool_call(self, request: ToolCallRequest, handler: Callable[[ToolCallRequest], Any]):
"""Allow ToolNode to execute runtime-auth MCP tools loaded during the last model call."""
if request.tool is None:
runtime_context = getattr(request.runtime, "context", None)
dynamic_tools = getattr(runtime_context, _RUNTIME_DYNAMIC_TOOLS_ATTR, {}) or {}
tool = dynamic_tools.get(request.tool_call.get("name")) if isinstance(dynamic_tools, dict) else None
if tool is not None:
request = request.override(tool=tool)

# NOTE: 注入当前的 AuthContext 以便于长连接拦截器 DynamicMCPTokenAuth 随时刷新 token
runtime_context = getattr(request.runtime, "context", None)
if runtime_context is not None:
user_id = getattr(runtime_context, "user_id", None)
work_id = getattr(runtime_context, "work_id", None)
dept_id = getattr(runtime_context, "department_id", None)
auth_context = AuthContext(user_id=user_id, department_id=dept_id, work_id=work_id)

from yuxi.services.mcp_auth.orchestrator import mcp_auth_context_var

token = mcp_auth_context_var.set(auth_context)
try:
return await handler(request)
finally:
mcp_auth_context_var.reset(token)

return await handler(request)

async def get_tools_from_context(self, context) -> list:
"""从上下文配置中获取工具列表"""
selected_tools = []
Expand Down Expand Up @@ -146,16 +182,41 @@ async def get_tools_from_context(self, context) -> list:
all_mcp_names.append(server_name)

selected_mcp_servers: set[str] = set()
selected_mcp_names: list[str] = []
loaded_mcp_tools: dict[str, int] = {}
unavailable_mcp_servers: list[str] = []
failed_mcp_servers: list[str] = []
for server_name in all_mcp_names:
if server_name in selected_mcp_servers:
continue
selected_mcp_servers.add(server_name)
selected_mcp_names.append(server_name)
try:
mcp_tools = await get_enabled_mcp_tools(server_name)
user_id = getattr(context, "user_id", None)
work_id = getattr(context, "work_id", None)
mcp_tools = await get_enabled_mcp_tools(
server_name,
auth_context=AuthContext(
user_id=user_id,
department_id=getattr(context, "department_id", None),
work_id=work_id,
),
)
Comment on lines +195 to +204

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

在运行时,context 可能会因为 runtime_context 未能成功解析而为 None。直接对 context 调用 getattr 会触发 AttributeError 异常。建议在调用前增加 None 安全保护,以提高运行时的稳定性。

Suggested change
user_id = getattr(context, "user_id", None)
work_id = getattr(context, "work_id", None)
mcp_tools = await get_enabled_mcp_tools(
server_name,
auth_context=AuthContext(
user_id=user_id,
department_id=getattr(context, "department_id", None),
work_id=work_id,
),
)
user_id = getattr(context, "user_id", None) if context is not None else None
work_id = getattr(context, "work_id", None) if context is not None else None
mcp_tools = await get_enabled_mcp_tools(
server_name,
auth_context=AuthContext(
user_id=user_id,
department_id=getattr(context, "department_id", None) if context is not None else None,
work_id=work_id,
),
)

if not mcp_tools:
logger.warning(f"RuntimeConfigMiddleware: mcp dependency unavailable, skip: {server_name}")
unavailable_mcp_servers.append(server_name)
logger.debug(f"RuntimeConfigMiddleware: mcp dependency unavailable, skip: {server_name}")
else:
loaded_mcp_tools[server_name] = len(mcp_tools)
selected_tools.extend(mcp_tools)
except Exception as e:
failed_mcp_servers.append(server_name)
logger.warning(f"RuntimeConfigMiddleware: failed to load mcp dependency '{server_name}': {e}")

if selected_mcp_names:
logger.info(
"RuntimeConfigMiddleware MCP runtime selection: "
f"selected={selected_mcp_names}, loaded={loaded_mcp_tools}, "
f"unavailable={unavailable_mcp_servers}, failed={failed_mcp_servers}"
)

return selected_tools
44 changes: 41 additions & 3 deletions backend/package/yuxi/agents/middlewares/skills_middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@

from yuxi.agents.toolkits import get_all_tool_instances
from yuxi.repositories.skill_repository import SkillRepository
from yuxi.services.mcp_service import get_enabled_mcp_tools
from yuxi.services.mcp.tool_registry_service import get_enabled_mcp_tools
from yuxi.services.mcp_auth.orchestrator import AuthContext
from yuxi.services.skill_service import _normalize_string_list, is_valid_skill_slug
from yuxi.storage.postgres.manager import pg_manager
from yuxi.utils.logging_config import logger
Expand Down Expand Up @@ -79,6 +80,21 @@ async def get_dependency_map(db: AsyncSession | None = None) -> dict[str, SkillD
return result


async def collect_context_mcp_names_for_preload(context, *, skills_context_name: str = "skills") -> list[str]:
"""收集图构建阶段需要预注册的 MCP 名称。"""
names: list[str] = []
names.extend(normalize_selected_skills(getattr(context, "mcps", None) or []))

dependency_map = await get_dependency_map()
configured_skills = normalize_selected_skills(getattr(context, skills_context_name, None) or [])
for slug in expand_skill_closure(configured_skills, dependency_map):
node = dependency_map.get(slug)
if node:
names.extend(node.get("mcps", []))

return normalize_selected_skills(names)


def normalize_selected_skills(selected_skills: list[str] | None) -> list[str]:
"""规范化 skills 列表,去重并过滤无效值"""
return _normalize_string_list(selected_skills)
Expand Down Expand Up @@ -338,20 +354,42 @@ async def _get_mcp_tools_from_context(

# 去重
unique_mcp_names = list(dict.fromkeys(all_mcp_names))
loaded_mcp_tools: dict[str, int] = {}
unavailable_mcp_servers: list[str] = []
failed_mcp_servers: list[str] = []

async def load_mcp_tools(server_name: str) -> list:
"""加载单个 MCP 服务器的工具"""
try:
mcp_tools = await get_enabled_mcp_tools(server_name)
user_id = getattr(context, "user_id", None)
work_id = getattr(context, "work_id", None)
mcp_tools = await get_enabled_mcp_tools(
server_name,
auth_context=AuthContext(
user_id=user_id,
department_id=getattr(context, "department_id", None),
work_id=work_id,
),
)
Comment on lines +364 to +373

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

类似于运行时配置中间件,这里的 context 在某些调用上下文中也可能为 None。直接使用 getattr(context, ...) 会导致 AttributeError 崩溃。建议增加 None 安全保护。

Suggested change
user_id = getattr(context, "user_id", None)
work_id = getattr(context, "work_id", None)
mcp_tools = await get_enabled_mcp_tools(
server_name,
auth_context=AuthContext(
user_id=user_id,
department_id=getattr(context, "department_id", None),
work_id=work_id,
),
)
user_id = getattr(context, "user_id", None) if context is not None else None
work_id = getattr(context, "work_id", None) if context is not None else None
mcp_tools = await get_enabled_mcp_tools(
server_name,
auth_context=AuthContext(
user_id=user_id,
department_id=getattr(context, "department_id", None) if context is not None else None,
work_id=work_id,
),
)

if not mcp_tools:
logger.warning(f"SkillsMiddleware: mcp dependency unavailable, skip: {server_name}")
unavailable_mcp_servers.append(server_name)
logger.debug(f"SkillsMiddleware: mcp dependency unavailable, skip: {server_name}")
else:
loaded_mcp_tools[server_name] = len(mcp_tools)
return mcp_tools
except Exception as e:
failed_mcp_servers.append(server_name)
logger.warning(f"SkillsMiddleware: failed to load mcp dependency '{server_name}': {e}")
return []

# 并行加载所有 MCP 工具
results = await asyncio.gather(*[load_mcp_tools(name) for name in unique_mcp_names])
if unique_mcp_names:
logger.info(
"SkillsMiddleware MCP dependency selection: "
f"selected={unique_mcp_names}, loaded={loaded_mcp_tools}, "
f"unavailable={unavailable_mcp_servers}, failed={failed_mcp_servers}"
)
selected_tools = []
for tools in results:
selected_tools.extend(tools)
Expand Down
Loading