Description
Disabling an MCP server at the organization level (MCPServers.is_enabled = false) does not stop assistants that already have it attached from using it at chat time. Disabling makes the server disappear from pickers and blocks new attachments, but existing assistant → server associations are never pruned and the runtime never re-checks is_enabled. The disabled server keeps connecting and its tools keep being exposed to the model.
The only ways to actually revoke a server are to delete it (the assistant_mcp_servers rows are removed by FK cascade) or to manually remove it from every assistant.
Steps to reproduce
- As admin, enable an MCP server and attach it (with at least one tool enabled) to an assistant in a shared space.
- Chat with the assistant and confirm the tool runs.
- As admin, disable the server in Organization settings (
is_enabled = false).
- Chat with the same assistant again.
- The tool still executes — the disabled server is still connected and its tools are still offered to the model.
Expected behavior
Disabling a server should revoke it everywhere: assistants should immediately stop being able to call it, regardless of stored associations. (This is already how the governed personal default assistant path behaves — see the asymmetry below — so the two paths are inconsistent.)
Screenshots / logs
No error is produced — that is the problem; the disabled server is used silently. The proxy still counts the disabled server's tools for the request:
[MCPProxy] Built registry with N tools from M servers
Severity
Major (security/governance control broken, no workaround other than deleting the server).
Root cause
is_enabled is enforced at assignment / listing time but not at runtime. The full runtime chain never filters by server.is_enabled:
- Disabling only updates the server row;
assistant_mcp_servers rows remain (mcp_server_settings_service.py:182-183).
Assistants.mcp_servers is a plain join with no is_enabled filter (assistant_table.py:66-67).
- The assistant MCP loader attaches tool overrides but never drops a server for being disabled (
space_repo.py:788-920).
ask() loads the assistant via the space and uses its attached servers (assistant_service.py:1513-1514).
- For any non-governed assistant,
mcp_servers_override stays None (assistant_service.py:1560-1596), so assistant.ask uses self.mcp_servers as-is (assistant.py:385-388, 407).
completion_service passes the list straight to the proxy with no filter (completion_service.py:259-264).
- The proxy factory builds credentials for, and connects, every server it is handed (
mcp_proxy_factory.py:82-96); its "already filtered by permissions" comment is an unenforced assumption.
- The proxy tool registry exposes a server's tools gating only on the tool flag, never
server.is_enabled (mcp_proxy_session.py:191-210).
Asymmetry confirming this is a bug, not a design choice: the governed personal-default path re-resolves its allowed server set every turn and does filter is_enabled (effective_config_service.py:101-105 → [s for s in servers if s.is_enabled]). So the same disable is honored for governed personal default assistants but ignored for all shared/org-space assistants and non-default personal assistants.
Impact
A security/governance control that silently fails to revoke. An admin who disables a server believing it has been cut off (e.g. it was found to be exfiltrating data, its operator was deprovisioned, or it is being decommissioned) has not actually stopped already-attached assistants from continuing to connect to it and send data to it. Precondition: the server was attached (with tools enabled) before being disabled.
Suggested fix
Filter by is_enabled at runtime. The cheapest spots are the ask path (the override computation in assistant_service.py / assistant.py:385-388) or the loader (space_repo.py:788-920): drop any server where not server.is_enabled. The MCPServer entity already carries the flag (mcp_server_table.py:39), so it is a one-line guard.
Environment
- Branch / commit:
develop @ 084614c2
- Backend:
intric (FastAPI / SQLAlchemy)
Related: #500 (same underlying inconsistency — MCP authorization is computed differently across the assignment, listing, and runtime layers).
Description
Disabling an MCP server at the organization level (
MCPServers.is_enabled = false) does not stop assistants that already have it attached from using it at chat time. Disabling makes the server disappear from pickers and blocks new attachments, but existing assistant → server associations are never pruned and the runtime never re-checksis_enabled. The disabled server keeps connecting and its tools keep being exposed to the model.The only ways to actually revoke a server are to delete it (the
assistant_mcp_serversrows are removed by FK cascade) or to manually remove it from every assistant.Steps to reproduce
is_enabled = false).Expected behavior
Disabling a server should revoke it everywhere: assistants should immediately stop being able to call it, regardless of stored associations. (This is already how the governed personal default assistant path behaves — see the asymmetry below — so the two paths are inconsistent.)
Screenshots / logs
No error is produced — that is the problem; the disabled server is used silently. The proxy still counts the disabled server's tools for the request:
Severity
Major (security/governance control broken, no workaround other than deleting the server).
Root cause
is_enabledis enforced at assignment / listing time but not at runtime. The full runtime chain never filters byserver.is_enabled:assistant_mcp_serversrows remain (mcp_server_settings_service.py:182-183).Assistants.mcp_serversis a plain join with nois_enabledfilter (assistant_table.py:66-67).space_repo.py:788-920).ask()loads the assistant via the space and uses its attached servers (assistant_service.py:1513-1514).mcp_servers_overridestaysNone(assistant_service.py:1560-1596), soassistant.askusesself.mcp_serversas-is (assistant.py:385-388, 407).completion_servicepasses the list straight to the proxy with no filter (completion_service.py:259-264).mcp_proxy_factory.py:82-96); its "already filtered by permissions" comment is an unenforced assumption.server.is_enabled(mcp_proxy_session.py:191-210).Asymmetry confirming this is a bug, not a design choice: the governed personal-default path re-resolves its allowed server set every turn and does filter
is_enabled(effective_config_service.py:101-105→[s for s in servers if s.is_enabled]). So the same disable is honored for governed personal default assistants but ignored for all shared/org-space assistants and non-default personal assistants.Impact
A security/governance control that silently fails to revoke. An admin who disables a server believing it has been cut off (e.g. it was found to be exfiltrating data, its operator was deprovisioned, or it is being decommissioned) has not actually stopped already-attached assistants from continuing to connect to it and send data to it. Precondition: the server was attached (with tools enabled) before being disabled.
Suggested fix
Filter by
is_enabledat runtime. The cheapest spots are the ask path (the override computation inassistant_service.py/assistant.py:385-388) or the loader (space_repo.py:788-920): drop anyserverwherenot server.is_enabled. TheMCPServerentity already carries the flag (mcp_server_table.py:39), so it is a one-line guard.Environment
develop@084614c2intric(FastAPI / SQLAlchemy)Related: #500 (same underlying inconsistency — MCP authorization is computed differently across the assignment, listing, and runtime layers).