Files
arc53-machine b692eaf7ad 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.
2026-09-28 17:30:53 +01:00

69 lines
2.2 KiB
Python

"""Which connector a retrieved chunk came from, for "From Google Drive" citations.
Only the connector's key and display name are attached, never the account
behind the connection, so shared conversations show the same attribution
without revealing whose Drive it was.
"""
from __future__ import annotations
import logging
from typing import Optional
from sqlalchemy import text
from docsgpt.storage.db.base_repository import looks_like_uuid
from docsgpt.storage.db.session import db_readonly
logger = logging.getLogger(__name__)
def connector_labels(source_ids: list[str]) -> dict[str, dict]:
"""``{source_id: {"connector_key", "connector_name"}}`` for synced sources.
Sources that do not come from a connection are left out. Never raises:
attribution is decoration, and a lookup failure must not break an answer.
"""
from docsgpt.connectors import service
ids = [str(i) for i in source_ids if i and looks_like_uuid(str(i))]
if not ids:
return {}
try:
with db_readonly() as conn:
rows = conn.execute(
text(
"SELECT s.id AS source_id, cs.* FROM sources s "
"JOIN connector_sessions cs ON cs.id = s.connection_id "
"WHERE s.id = ANY(CAST(:ids AS uuid[]))"
),
{"ids": ids},
).fetchall()
except Exception:
logger.warning("connector attribution lookup failed", exc_info=True)
return {}
labels = {}
for row in rows:
data = dict(row._mapping)
public = service.serialize_connection({**data, "id": data["id"]})
labels[str(data["source_id"])] = {
"connector_key": public["connector_key"],
"connector_name": public["name"],
}
return labels
class ConnectorLabelCache:
"""Per-search memo so each source is looked up once."""
def __init__(self) -> None:
self._labels: dict[str, dict] = {}
def for_source(self, source_id: Optional[str]) -> dict:
if not source_id:
return {}
key = str(source_id)
if key not in self._labels:
self._labels[key] = connector_labels([key]).get(key, {})
return self._labels[key]