mirror of
https://github.com/tiennm99/DocsGPT.git
synced 2026-10-14 04:15:35 +00:00
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:
1 parent
805e65eeb4
commit
03d1b10897
9 files changed
+1168
-16
No files matched your search
@@ -326,6 +326,14 @@ def _start_sync(user_id: str, row: dict, sync: dict):
|
||||
return ("Pick a GitHub repository", 400)
|
||||
items = {**items, "repo_url": repo}
|
||||
name = name or repo
|
||||
elif definition.sync_ingestor == "linear":
|
||||
from docsgpt.connectors import linear
|
||||
|
||||
try:
|
||||
items = linear.normalize_selection(items)
|
||||
except ValueError as err:
|
||||
return (str(err), 400)
|
||||
name = name or linear.selection_name(items)
|
||||
name = name or definition.name
|
||||
# Validate before claiming the idempotency key: a rejected request must
|
||||
# leave the key free for the corrected retry.
|
||||
@@ -346,11 +354,15 @@ def _start_sync(user_id: str, row: dict, sync: dict):
|
||||
"sync_frequency": frequency,
|
||||
}
|
||||
else:
|
||||
fields = {f.key for f in definition.setup_fields}
|
||||
source_data = {k: v for k, v in items.items() if k in fields and v not in (None, "")}
|
||||
missing = [f.label for f in definition.setup_fields if f.required and f.key not in source_data]
|
||||
if missing:
|
||||
return (f"Missing: {', '.join(missing)}", 400)
|
||||
if definition.sync_ingestor == "linear":
|
||||
# The teams and projects picked, read with the connection's MCP sign-in.
|
||||
source_data = items
|
||||
else:
|
||||
fields = {f.key for f in definition.setup_fields}
|
||||
source_data = {k: v for k, v in items.items() if k in fields and v not in (None, "")}
|
||||
missing = [f.label for f in definition.setup_fields if f.required and f.key not in source_data]
|
||||
if missing:
|
||||
return (f"Missing: {', '.join(missing)}", 400)
|
||||
task_fn = ingest_remote
|
||||
kwargs = {
|
||||
"source_data": source_data,
|
||||
@@ -418,6 +430,36 @@ class ConnectionRepositories(Resource):
|
||||
)
|
||||
|
||||
|
||||
@connections_ns.route("/connections/<string:connection_id>/linear")
|
||||
class LinearWorkspace(Resource):
|
||||
@api.doc(
|
||||
description=(
|
||||
"Linear: the teams and projects the connection can see, for the sync picker. "
|
||||
"Read through Linear's MCP server with the connection's sign-in."
|
||||
)
|
||||
)
|
||||
def get(self, connection_id: str):
|
||||
from docsgpt.connectors import linear, mcp
|
||||
|
||||
user_id = _user_id()
|
||||
if not user_id:
|
||||
return _unauthorized()
|
||||
with db_readonly() as conn:
|
||||
row = _owned(conn, connection_id, user_id)
|
||||
if row is None or catalog.connector_key_for_row(row) != linear.LINEAR_CONNECTOR:
|
||||
return _not_found()
|
||||
try:
|
||||
workspace = mcp.run_connection_session(row, linear.mcp_url(), linear.list_workspace)
|
||||
except service.ConnectionUnavailable:
|
||||
return _error("Reconnect to continue", 409, code="reconnect")
|
||||
except service.TransientConnectionError:
|
||||
return _error("Linear is not responding. Try again.", 503)
|
||||
except Exception as err:
|
||||
current_app.logger.error(f"Error listing Linear teams: {err}", exc_info=True)
|
||||
return _error("Failed to list Linear teams", 502)
|
||||
return make_response(jsonify({"success": True, **workspace}), 200)
|
||||
|
||||
|
||||
@connections_ns.route("/connections/<string:connection_id>/reconnect")
|
||||
class ConnectionReconnect(Resource):
|
||||
@api.doc(
|
||||
|
||||
Reference in new issue
Block a user