Keep the removed-connection note server-owned

Only removing a connection notes it on a kept tool. A config save through
update_tool or update_tool_config carries the stored note and ignores the
client's copy, so it can neither fake nor clear it; creating a tool, an
MCP server save and an agent import start without one. The note is now
written even for a connection with no catalog key, and a kept tool its
owner gave credentials of its own since runs again.
This commit is contained in:
arc53-machine committed 2026-09-29 18:12:54 +01:00
1 parent d04f76e97a
commit 3cab8af753
6 files changed
+79 -10

No files matched your search

+3 -1
View File
@@ -38,6 +38,7 @@ from docsgpt.agents.default_tools import (
from docsgpt.api import api
from docsgpt.api.pat.rules import allowed_ids
from docsgpt.api.user.resource_access import AccessDenied, require
from docsgpt.connectors.resolve import carry_removed_connection
from docsgpt.core.model_utils import validate_model_id
from docsgpt.core.url_validation import SSRFError, validate_url
from docsgpt.security.safe_url import UnsafeUserUrlError, validate_user_base_url
@@ -1138,7 +1139,8 @@ def _create_tool_from_spec(conn, user: str, tool: dict, secrets: dict, warnings:
warnings.append(f"Tool type '{tool_type}' not available on this instance; skipped")
return None
config_requirements = inst.get_config_requirements() or {}
config = dict(tool.get("config") or {})
# An imported tool starts with no note that a connection was removed.
config = carry_removed_connection(dict(tool.get("config") or {}), None)
config.update(secrets or {})
if tool_type == "api_tool":
label = tool.get("display_name") or tool.get("name") or tool_type
+6 -2
View File
@@ -19,6 +19,7 @@ from docsgpt.api.user.tools.routes import (
transform_actions,
)
from docsgpt.cache import get_redis_instance
from docsgpt.connectors.resolve import REMOVED_CONNECTION_KEY
from docsgpt.core.url_validation import SSRFError, validate_url
from docsgpt.security.encryption import decrypt_credentials, encrypt_credentials
from docsgpt.storage.db.repositories.connector_sessions import (
@@ -37,8 +38,10 @@ def _sanitize_mcp_transport(config):
"""Normalise and validate the transport_type field.
Strips ``command`` / ``args`` keys that are only valid for local STDIO
transports, and ``connection_id``, which only the tool executor sets
(it picks whose MCP tokens the tool uses). Returns the cleaned
transports, ``connection_id``, which only the tool executor sets
(it picks whose MCP tokens the tool uses), and the note that a
connection was removed, which only removing one writes (a save stores
a fresh config, so it drops that note too). Returns the cleaned
transport type string.
"""
transport_type = (config.get("transport_type") or "auto").lower()
@@ -47,6 +50,7 @@ def _sanitize_mcp_transport(config):
config.pop("command", None)
config.pop("args", None)
config.pop("connection_id", None)
config.pop(REMOVED_CONNECTION_KEY, None)
config["transport_type"] = transport_type
return transport_type
+8
View File
@@ -35,6 +35,7 @@ from docsgpt.api.user.resource_access import (
)
from docsgpt.api.user.team_sharing import visible_with_access
from docsgpt.connectors.catalog import base_url, definition_for_tool
from docsgpt.connectors.resolve import carry_removed_connection
from docsgpt.connectors.service import account_tool_names
from docsgpt.connectors.permissions import owner_credential_writes
from docsgpt.core.settings import settings
@@ -521,12 +522,16 @@ def _prepare_tool_config(tool_doc: dict, new_config: dict, config_requirements:
Handles the three secret stores: ``config_requirements`` secrets
(``encrypted_credentials``), api_tool header/query values, and the MCP
origin-change rule (a new scheme, host or port drops stored credentials).
A removed connection's note is carried over from the stored config; the
client's copy is ignored.
Raises:
CredentialsRequired: the MCP origin changed and no new secret arrived.
"""
owner_id = tool_doc["user_id"]
existing_config = tool_doc.get("config") or {}
# The note that its connection was removed is the server's to keep.
new_config = carry_removed_connection(new_config, existing_config)
if tool_doc.get("name") == "api_tool":
return _seal_api_tool_secrets(new_config, existing_config, owner_id)
moved = tool_doc.get("name") == "mcp_tool" and _mcp_origin_changed(new_config, existing_config)
@@ -834,6 +839,9 @@ class CreateTool(Resource):
missing_fields = check_required_fields(data, required_fields)
if missing_fields:
return missing_fields
if isinstance(data.get("config"), dict):
# Only removing a connection notes that it was removed.
data["config"] = carry_removed_connection(data["config"], None)
try:
if data["name"] == "mcp_tool":
server_url = (data.get("config", {}).get("server_url") or "").strip()