Sync Linear issues and documents into Knowledge with the Linear sign-in

The Linear connector now syncs as well as giving agents its tools, from
one connection. Linear's MCP server is its own OAuth issuer, so its
tokens are read through the same MCP tools the agents use (list_issues,
get_issue, list_comments, list_documents) rather than Linear's GraphQL
API, and no OAuth app has to be registered.

A source picks teams and projects, with comments (on by default) and the
projects' documents. Each issue becomes one document with its state,
assignee, priority, labels, description and comments, filed under its
team and citing its Linear URL. Each sync reads up to 500 issues and 100
documents again. /api/connections/<id>/linear lists the teams and
projects to pick from. Sources sync on their schedule with the owner's
connection, and pause when the sign-in needs reconnecting.
This commit is contained in:
arc53-machine committed 2026-09-29 11:32:40 +01:00
1 parent 805e65eeb4
commit 03d1b10897
9 files changed
+1168 -16

No files matched your search

+22 -7
View File
@@ -1512,6 +1512,10 @@ def sync(
return {"status": "success"}
# Remote loaders that can only read with a connection (no public fallback).
_CONNECTION_ONLY_LOADERS = frozenset({"linear"})
def sync_worker(self, frequency):
from sqlalchemy import text as sql_text
@@ -1565,6 +1569,11 @@ def sync_worker(self, frequency):
# notifying) on every schedule until then.
sync_counts["sync_skipped"] += 1
continue
if source_type in _CONNECTION_ONLY_LOADERS and not doc.get("connection_id"):
# Linear is read only with a connection's sign-in; its
# connection was removed and the content kept.
sync_counts["sync_skipped"] += 1
continue
source_data = normalize_remote_data(source_type, doc.get("remote_data"))
if not source_data:
@@ -2217,9 +2226,10 @@ def _with_connection_credentials(source_data, connection_id: str):
S3, Reddit and GitHub sources made from a connection keep their keys on
the connection only, never in ``sources.remote_data``. A JSON string
stays a JSON string and a dict a dict; any other string (a GitHub
repository URL) becomes ``{"url": ...}`` next to the keys. Returns None
when the connection is gone, needs reconnecting, or its connector is
turned off.
repository URL) becomes ``{"url": ...}`` next to the keys. An MCP
sign-in (Linear) gets its ``connection_id`` instead: its tokens stay
with the MCP client, which renews them. Returns None when the
connection is gone, needs reconnecting, or its connector is turned off.
"""
from docsgpt.connectors import service
from docsgpt.storage.db.repositories.connector_sessions import ConnectorSessionsRepository
@@ -2229,10 +2239,15 @@ def _with_connection_credentials(source_data, connection_id: str):
enabled = row is not None and service.connector_enabled(conn, row)
if row is None or not enabled:
return None
try:
credentials = service.access_credentials(row)
except service.ConnectionUnavailable:
return None
if (row.get("auth_kind") or "") == "mcp_oauth":
if service.normalize_status(row) != service.STATUS_CONNECTED:
return None
credentials = {"connection_id": str(row["id"])}
else:
try:
credentials = service.access_credentials(row)
except service.ConnectionUnavailable:
return None
if isinstance(source_data, str):
try:
parsed = json.loads(source_data)