$npx -y skills add AlexAI-MCP/hermes-CCC --skill linearLinear project management — create issues, manage cycles, update projects, and query team progress via Linear API or MCP.
| 1 | # Linear Project Management |
| 2 | |
| 3 | Manage Linear issues, cycles, projects, and team workflows via the Linear API (GraphQL) or MCP. |
| 4 | |
| 5 | ## Setup |
| 6 | |
| 7 | ```bash |
| 8 | # Get API key from Linear: Settings → API → Personal API Keys |
| 9 | export LINEAR_API_KEY="lin_api_..." |
| 10 | ``` |
| 11 | |
| 12 | ```python |
| 13 | import requests |
| 14 | |
| 15 | LINEAR_API = "https://api.linear.app/graphql" |
| 16 | HEADERS = { |
| 17 | "Authorization": f"Bearer {LINEAR_API_KEY}", |
| 18 | "Content-Type": "application/json" |
| 19 | } |
| 20 | |
| 21 | def linear_query(query, variables=None): |
| 22 | resp = requests.post(LINEAR_API, json={"query": query, "variables": variables or {}}, headers=HEADERS) |
| 23 | return resp.json()["data"] |
| 24 | ``` |
| 25 | |
| 26 | --- |
| 27 | |
| 28 | ## List Issues |
| 29 | |
| 30 | ```python |
| 31 | query = """ |
| 32 | query { |
| 33 | issues(filter: {assignee: {isMe: {eq: true}}, state: {type: {nin: ["completed", "cancelled"]}}}) { |
| 34 | nodes { |
| 35 | id title priority state { name } team { name } |
| 36 | } |
| 37 | } |
| 38 | } |
| 39 | """ |
| 40 | data = linear_query(query) |
| 41 | for issue in data["issues"]["nodes"]: |
| 42 | print(f"[{issue['state']['name']}] {issue['title']}") |
| 43 | ``` |
| 44 | |
| 45 | --- |
| 46 | |
| 47 | ## Create Issue |
| 48 | |
| 49 | ```python |
| 50 | mutation = """ |
| 51 | mutation CreateIssue($title: String!, $teamId: String!, $description: String, $priority: Int) { |
| 52 | issueCreate(input: { |
| 53 | title: $title |
| 54 | teamId: $teamId |
| 55 | description: $description |
| 56 | priority: $priority |
| 57 | }) { |
| 58 | success |
| 59 | issue { id title url } |
| 60 | } |
| 61 | } |
| 62 | """ |
| 63 | |
| 64 | data = linear_query(mutation, { |
| 65 | "title": "Fix auth bug in production", |
| 66 | "teamId": "TEAM_ID", |
| 67 | "description": "Users getting 401 on refresh token", |
| 68 | "priority": 1 # 0=no priority, 1=urgent, 2=high, 3=medium, 4=low |
| 69 | }) |
| 70 | print(data["issueCreate"]["issue"]["url"]) |
| 71 | ``` |
| 72 | |
| 73 | --- |
| 74 | |
| 75 | ## Get Teams |
| 76 | |
| 77 | ```python |
| 78 | teams_query = """ |
| 79 | query { teams { nodes { id name key } } } |
| 80 | """ |
| 81 | data = linear_query(teams_query) |
| 82 | for team in data["teams"]["nodes"]: |
| 83 | print(f"{team['key']}: {team['name']} ({team['id']})") |
| 84 | ``` |
| 85 | |
| 86 | --- |
| 87 | |
| 88 | ## Update Issue |
| 89 | |
| 90 | ```python |
| 91 | update_mutation = """ |
| 92 | mutation UpdateIssue($id: String!, $stateId: String) { |
| 93 | issueUpdate(id: $id, input: { stateId: $stateId }) { |
| 94 | success |
| 95 | } |
| 96 | } |
| 97 | """ |
| 98 | linear_query(update_mutation, {"id": "ISSUE_ID", "stateId": "STATE_ID"}) |
| 99 | ``` |
| 100 | |
| 101 | --- |
| 102 | |
| 103 | ## Search Issues |
| 104 | |
| 105 | ```python |
| 106 | search_query = """ |
| 107 | query SearchIssues($query: String!) { |
| 108 | issueSearch(query: $query) { |
| 109 | nodes { id title state { name } url } |
| 110 | } |
| 111 | } |
| 112 | """ |
| 113 | data = linear_query(search_query, {"query": "auth bug"}) |
| 114 | ``` |
| 115 | |
| 116 | --- |
| 117 | |
| 118 | ## Cycles (Sprints) |
| 119 | |
| 120 | ```python |
| 121 | cycles_query = """ |
| 122 | query { |
| 123 | cycles(filter: {isActive: {eq: true}}) { |
| 124 | nodes { |
| 125 | id name number startsAt endsAt |
| 126 | issues { nodes { id title state { name } } } |
| 127 | } |
| 128 | } |
| 129 | } |
| 130 | """ |
| 131 | data = linear_query(cycles_query) |
| 132 | ``` |
| 133 | |
| 134 | --- |
| 135 | |
| 136 | ## MCP Integration |
| 137 | |
| 138 | If Linear MCP is installed in `.mcp.json`: |
| 139 | ```json |
| 140 | { |
| 141 | "mcpServers": { |
| 142 | "linear": { |
| 143 | "command": "npx", |
| 144 | "args": ["-y", "@modelcontextprotocol/server-linear"], |
| 145 | "env": {"LINEAR_API_KEY": "lin_api_..."} |
| 146 | } |
| 147 | } |
| 148 | } |
| 149 | ``` |
| 150 | |
| 151 | Then Claude can use Linear tools directly without GraphQL code. |
| 152 | |
| 153 | --- |
| 154 | |
| 155 | ## Webhooks (receive events) |
| 156 | |
| 157 | ```python |
| 158 | # Linear sends POST to your endpoint on issue create/update |
| 159 | # Payload includes: type, action, data.issue |
| 160 | # Verify with: X-Linear-Signature header |
| 161 | import hmac, hashlib |
| 162 | |
| 163 | def verify_signature(payload, signature, secret): |
| 164 | expected = hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest() |
| 165 | return hmac.compare_digest(f"sha256={expected}", signature) |
| 166 | ``` |