Add MCP presets, admin connector policies and connector attribution

Six vendor-run MCP servers (Notion, Linear, Atlassian, Sentry, Asana,
Stripe) join the catalog as presets from docsgpt/connectors/presets/
mcp.yaml; existing connections to those servers show under them.

connector_policies lets an admin turn a connector off or force whose
account every share uses, and app_metadata's
connectors.allow_custom_mcp turns custom MCP servers off. Both are
enforced on the server: new connections, OAuth sign-ins, MCP test and
save, and the tools of a disabled connector stop resolving.
GET/PUT /api/admin/connectors reads and changes them.

A shared tool's owner picks owner or member credentials with
PUT /api/connections/tools/<id>/credential-mode, and a member running
the owner's account always confirms write actions.

Tool-call events and retrieved chunks name the connector they came
from (key and display name, never the account), and tool calls keep
those fields when the conversation is reloaded.
This commit is contained in:
arc53-machine committed 2026-09-28 17:30:53 +01:00
1 parent 6e2ee43944
commit b692eaf7ad
20 files changed
+844 -4

No files matched your search

+2
View File
@@ -482,6 +482,8 @@ def _remote_credentials(user, source, config):
try:
with db_session() as conn:
row, _ = service.create_api_key_connection(conn, user, definition, provided)
except service.ConnectorDisabled as err:
return None, None, make_response(jsonify({"success": False, "error": str(err)}), 403)
except (service.EncryptionKeyNotConfigured, ValueError):
return config, None, None
return public, str(row["id"]), None
+31
View File
@@ -112,6 +112,31 @@ def _mcp_connection(user, config, auth_type, auth_credentials, display_name):
return str(row["id"]) if row else None
def _mcp_policy_error(config: dict):
"""A 403 when an admin turned this MCP server's connector off, else None.
A preset's own switch applies to its server; any other server is a
custom connector and needs "Allow custom MCP servers".
"""
from docsgpt.connectors import catalog, service
preset = catalog.preset_for_url(config.get("server_url"))
key = preset.key if preset else "custom_mcp"
try:
with db_readonly() as conn:
service.ensure_connector_allowed(conn, key)
except service.ConnectorDisabled:
return make_response(
jsonify({"success": False, "error": "This MCP server is turned off by an admin", "code": "disabled"}),
403,
)
except Exception:
# The switch is an admin preference; when the policy table cannot be
# read, saving the tool (which needs the database) fails on its own.
current_app.logger.warning("Could not read connector policies", exc_info=True)
return None
@tools_mcp_ns.route("/mcp_server/test")
class TestMCPServerConfig(Resource):
@api.expect(
@@ -147,6 +172,9 @@ class TestMCPServerConfig(Resource):
)
_validate_mcp_server_url(config)
policy_error = _mcp_policy_error(config)
if policy_error is not None:
return policy_error
auth_credentials = _extract_auth_credentials(config)
test_config = config.copy()
@@ -243,6 +271,9 @@ class MCPServerSave(Resource):
)
_validate_mcp_server_url(config)
policy_error = _mcp_policy_error(config)
if policy_error is not None:
return policy_error
auth_credentials = _extract_auth_credentials(config)
auth_type = config.get("auth_type", "none")
+2
View File
@@ -470,6 +470,8 @@ def _create_connected_tool(user, data, definition, tool_instance):
)
except connection_service.EncryptionKeyNotConfigured:
return None
except connection_service.ConnectorDisabled as err:
return make_response(jsonify({"success": False, "message": str(err)}), 403)
except ValueError as err:
return make_response(jsonify({"success": False, "message": str(err)}), 400)
return make_response(jsonify({"id": str(created["id"]), "connection_id": str(connection["id"])}), 200)