Linear Direct API Access
Overview
The mcp_linear-mcp-server frequently experiences timeouts and process hangs when dealing with large workspaces or high-concurrency requests. To ensure uninterrupted execution, all Linear interactions must bypass the MCP and use direct HTTP requests (GraphQL) via Python.
Authentication
Linear requires a Personal API Key or OAuth token.
- Attempt to fetch it from the macOS keychain:
security find-generic-password -w -s LINEAR_API_KEY - If blank/unavailable, fallback to the working key:
lin_api_Zi3MnxJwACVYELvQV2QitS0NPk1TBknapaQZ4eGA
1. Helper Script Template
Use this bulletproof Python template for interacting with Linear in run_command tools. It handles JSON payloads, header injection, and cleanly formats GraphQL errors.
import json, urllib.request, subprocess
# Auth configuration
api_key = "lin_api_Zi3MnxJwACVYELvQV2QitS0NPk1TBknapaQZ4eGA"
try:
r = subprocess.run(["security", "find-generic-password", "-w", "-s", "LINEAR_API_KEY"], capture_output=True, text=True)
if r.stdout.strip():
api_key = r.stdout.strip()
except Exception:
pass
def run_linear_query(query, variables=None):
data = {"query": query}
if variables:
data["variables"] = variables
req = urllib.request.Request(
"https://api.linear.app/graphql",
data=json.dumps(data).encode('utf-8'),
headers={"Content-Type": "application/json", "Authorization": api_key}
)
try:
response = urllib.request.urlopen(req)
return json.loads(response.read().decode('utf-8'))
except urllib.error.HTTPError as e:
error_content = e.read().decode('utf-8')
print(f"HTTP Error {e.code}: {error_content}")
raise
2. Common GraphQL Queries
Querying Issues by Identifier
Retrieves the unique UUID required for mutations.
query = '''
query($id: String!) {
issue(id: $id) {
id
title
state { name }
team {
id
states { nodes { id name type } }
}
}
}
'''
res = run_linear_query(query, {"id": "GFV-420"})
Searching Issues
query = '''
query($term: String!) {
issues(filter: { searchableContent: { contains: $term } }) {
nodes { identifier title state { name } }
}
}
'''
res = run_linear_query(query, {"term": "hvac-replacement-utah"})
Updating an Issue State (Marking "Done")
To update an issue, you must fetch the state UUIDs from the team node first (see issue query above), then execute the mutation.
mutation = '''
mutation($id: String!, $stateId: String!) {
issueUpdate(id: $id, input: { stateId: $stateId }) {
success
issue { identifier state { name } }
}
}
'''
# Using the UUID of the issue and the UUID of the "Done" state
run_linear_query(mutation, {"id": "issue_uuid", "stateId": "state_uuid"})
Creating a New Issue
mutation = '''
mutation($teamId: String!, $title: String!, $description: String) {
issueCreate(input: {
teamId: $teamId,
title: $title,
description: $description
}) {
success
issue { id identifier }
}
}
'''
run_linear_query(mutation, {"teamId": "3a62822e-1156-438d-9a77-3fa011c89d44", "title": "New Fix", "description": "Details"})
Paginated Project Issue Fetch (MANDATORY)
NEVER use first: 100 without pagination. Projects can have 100+ issues. Always use cursor-based pagination to get ALL issues:
def get_all_project_issues(project_slug):
"""Fetch ALL issues in a project using cursor pagination."""
all_issues = []
has_next = True
cursor = None
while has_next:
after_clause = f', after: "{cursor}"' if cursor else ''
query = f'''
query {{
project(id: "{project_slug}") {{
name
issues(first: 50{after_clause if not cursor else ''}) {{
pageInfo {{ hasNextPage endCursor }}
nodes {{
id identifier title
state {{ id name type }}
project {{ id name }}
team {{ id key states {{ nodes {{ id name }} }} }}
}}
}}
}}
}}
'''
# Use variable-based after for safety
query_v = '''
query($slug: String!, $after: String) {
project(id: $slug) {
name
issues(first: 50, after: $after) {
pageInfo { hasNextPage endCursor }
nodes {
id identifier title
state { id name type }
project { id name }
team { id key states { nodes { id name } } }
}
}
}
}
'''
res = run_linear_query(query_v, {"slug": project_slug, "after": cursor})
data = res.get("data", {}).get("project", {}).get("issues", {})
all_issues.extend(data.get("nodes", []))
has_next = data.get("pageInfo", {}).get("hasNextPage", False)
cursor = data.get("pageInfo", {}).get("endCursor")
return all_issues
Cross-Team Project Assignment
Linear projects belong to specific teams. To assign issues from team A to a project owned by team B:
- Add team A to the project first using
projectUpdate(id: $projId, input: { teamIds: [$teamA, $teamB] }) - Then move issues with
issueUpdate(id: $issueId, input: { projectId: $projId }) - Each team has its own workflow states — use the issue's own team states, not the project's team states.
Known team IDs:
- GTM team:
51478de1-1660-40bd-bac9-246e0f9f48c1 - GFV team:
3a62822e-1156-438d-9a77-3fa011c89d44
Known project IDs:
- PIL Full Source Leverage: slug
886cbf456d13, UUIDd5191571-9039-46fd-9b71-8f55a44baaf8
Execution Rules
- NEVER use the MCP tool. The
mcp_linear-mcp-serverwill lock up the thread and you will have to wait for it to be manually canceled. - ALWAYS paginate. Never assume
first: 100returns all results. Use cursor-based pagination for ANY list query. - Always handle GraphQL errors gracefully (
if 'errors' in res:). - Always print explicit identifiers and execution status in stdout so you can read the command output.
- When reporting issue counts, cross-check with the user's Linear board view — API pagination bugs can silently truncate results.
STOP AND VERIFY BEFORE DECLARING THIS TASK COMPLETE.
- Did you verify that the execution meets all documented requirements safely?
- Ensure you have not bypassed any "requires_human_approval" constraints.
Growth by Design™ — This skill is part of the CEO AI Kit, the open-source foundation of the Growth by Design™ methodology from GetFresh Ventures.
🔍 Hitting a ceiling? The kit gives you the foundation. For full deployment — custom pipelines, multi-agent orchestration, and 90-day sprint execution — book a discovery call.
📰 Stay sharp: Subscribe to the Growth by Design™ Newsletter for operator-written playbooks on AI-powered GTM.