# Linear Integration Tool-specific patterns for integrating Linear issue tracking into status reports via the **streamlinear MCP server** (`github:obra/streamlinear`). > **Important**: This guide is specifically for the streamlinear MCP, not the official Linear MCP. The streamlinear server uses a single `mcp__linear__linear` tool with action-based dispatch rather than separate tools per operation. ## Overview Linear provides issue tracking with team-based organization, project management, and rich metadata. Status reports should surface recently active issues relevant to current work context. ## Streamlinear MCP Tool All Linear operations go through a single tool with an `action` parameter: ```typescript // Search your active issues await mcp__linear__linear({ action: 'search' }); // Search with text query await mcp__linear__linear({ action: 'search', query: 'authentication bug' }); // Search with filters await mcp__linear__linear({ action: 'search', query: { team: 'BLZ', state: 'In Progress', assignee: 'me' } }); // Get issue details await mcp__linear__linear({ action: 'get', id: 'BLZ-123' // Also accepts URLs or UUIDs }); // Update issue await mcp__linear__linear({ action: 'update', id: 'BLZ-123', state: 'Done' }); // Add comment await mcp__linear__linear({ action: 'comment', id: 'BLZ-123', body: 'Fixed in commit abc123' }); // Create issue await mcp__linear__linear({ action: 'create', title: 'Bug title', team: 'BLZ', body: 'Description here', priority: 2 }); // Raw GraphQL for advanced queries await mcp__linear__linear({ action: 'graphql', graphql: 'query { teams { nodes { id key name } } }' }); ``` ## Action Reference | Action | Purpose | Key Parameters | |--------|---------|----------------| | `search` | Find issues | `query` (string or object with filters) | | `get` | Issue details | `id` (identifier, URL, or UUID) | | `update` | Change issue | `id`, `state`, `priority`, `assignee`, `labels` | | `comment` | Add comment | `id`, `body` | | `create` | New issue | `title`, `team`, `body`, `priority`, `labels` | | `graphql` | Raw queries | `graphql`, `variables` | | `help` | Full docs | (none) | ## Priority Values | Value | Meaning | |-------|---------| | 0 | None | | 1 | Urgent | | 2 | High | | 3 | Medium | | 4 | Low | ## Data Gathering ### Issue Listing ```typescript interface LinearIssue { identifier: string; // "BLZ-123" title: string; state: { name: string; // "In Progress", "Done", etc. type: string; // "started", "completed", etc. }; priority: number; // 0-4 (0=none, 1=urgent, 2=high, 3=normal, 4=low) assignee?: { name: string; email: string; }; labels: Array<{ name: string; color: string; }>; createdAt: string; updatedAt: string; url: string; } async function fetchTeamIssues(teamKey: string): Promise { const result = await mcp__linear__linear({ action: 'search', query: { team: teamKey } }); return result.issues; } async function fetchMyActiveIssues(): Promise { const result = await mcp__linear__linear({ action: 'search' }); return result.issues; } ``` ### Advanced Queries with GraphQL For complex filtering not supported by the search action, use GraphQL: ```typescript // Get all teams async function listTeams(): Promise> { const result = await mcp__linear__linear({ action: 'graphql', graphql: 'query { teams { nodes { id key name } } }' }); return result.teams.nodes; } // Get issues updated in last N days across all teams async function fetchRecentIssues(daysBack: number = 7): Promise { const result = await mcp__linear__linear({ action: 'graphql', graphql: ` query { viewer { assignedIssues( filter: { state: { type: { nin: ["completed", "canceled"] } } } first: 30 orderBy: updatedAt ) { nodes { identifier title state { name type } team { key } priority updatedAt url } } } } ` }); return result.viewer.assignedIssues.nodes; } // Filter by state type async function fetchIssuesByStateType( stateType: 'unstarted' | 'started' | 'completed' | 'canceled' ): Promise { const result = await mcp__linear__linear({ action: 'graphql', graphql: ` query($stateType: String!) { issues( filter: { state: { type: { eq: $stateType } } } first: 50 ) { nodes { identifier title state { name type } team { key } priority } } } `, variables: { stateType } }); return result.issues.nodes; } ``` ### Context-Aware Filtering Map repository to Linear team/project: ```typescript interface LinearContext { filterBy: 'team' | 'project' | 'query'; team?: string; // Team key (e.g., "BLZ") project?: string; query?: string; } interface RepoMapping { path: string; pattern?: boolean; // If true, path supports wildcards linear: LinearContext; } interface LinearConfig { mappings: RepoMapping[]; defaults: { daysBack: number; limit: number; }; } ``` Example configuration: ```json { "mappings": [ { "path": "/Users/mg/Developer/outfitter/blz", "linear": { "filterBy": "team", "team": "BLZ" } }, { "path": "/Users/mg/Developer/*", "pattern": true, "linear": { "filterBy": "query", "query": "outfitter" } } ], "defaults": { "daysBack": 7, "limit": 10 } } ``` ### Context Resolution ```typescript async function resolveLinearContext(cwd: string, config: LinearConfig): Promise { // Try exact path match first for (const mapping of config.mappings) { if (!mapping.pattern && mapping.path === cwd) { return mapping.linear; } } // Try pattern match for (const mapping of config.mappings) { if (mapping.pattern) { const regex = new RegExp('^' + mapping.path.replace(/\*/g, '.*') + '$'); if (regex.test(cwd)) { return mapping.linear; } } } // Fallback: query-based search using repo name const repoName = await getRepoName(cwd); if (repoName) { return { filterBy: 'query', query: repoName.split('/')[1] // Extract short name from "owner/repo" }; } return null; } ``` ## Presentation Templates ### Issue Section ``` LINEAR ISSUES (Recent Activity - {team_name}) {count} issues updated in last {period} {issue_identifier}: {title} [{state}] Priority: {priority_label} | Assignee: {assignee_name} Labels: {label_list} Updated: {relative_time} {issue_url} ``` ### Priority Formatting ```typescript function formatPriority(priority: number): string { const labels: Record = { 0: 'None', 1: 'Urgent', 2: 'High', 3: 'Medium', 4: 'Low' }; return labels[priority] || 'None'; } ``` ### Example Output ``` LINEAR ISSUES (Recent Activity - BLZ Team) 5 issues updated in last 7 days BLZ-162: Implement authentication middleware [In Progress] Priority: High | Assignee: Alice Smith Labels: backend, security Updated: 3 hours ago https://linear.app/outfitter/issue/BLZ-162 BLZ-161: Fix user validation bug [Done] Priority: Urgent | Assignee: Bob Jones Labels: bug, backend Updated: 5 hours ago https://linear.app/outfitter/issue/BLZ-161 BLZ-158: Update dependencies [Todo] Priority: Low | Assignee: Unassigned Labels: maintenance Updated: 2 days ago https://linear.app/outfitter/issue/BLZ-158 ``` ## Cross-Referencing ### Link Issues to PRs Extract issue references from PR titles/bodies: ```typescript function extractIssueReferences(text: string): string[] { // Pattern: "BLZ-123" or "[BLZ-123]" or "BLZ-123:" const pattern = /\[?([A-Z]{2,}-\d+)\]?:?/g; const matches = text.matchAll(pattern); return Array.from(matches, m => m[1]); } async function linkIssuesToPRs( issues: LinearIssue[], prs: GitHubPR[] ): Promise> { const issueMap = new Map(); for (const issue of issues) { const relatedPRs = prs.filter(pr => { const refs = extractIssueReferences(pr.title + ' ' + pr.body); return refs.includes(issue.identifier); }); if (relatedPRs.length > 0) { issueMap.set(issue.identifier, relatedPRs); } } return issueMap; } ``` ### Annotate Issues with PR Status ``` LINEAR ISSUES (with PR Status) BLZ-162: Implement authentication middleware [In Progress] Priority: High | Assignee: Alice Smith PRs: #156 (Approved, CI passing) Updated: 3 hours ago BLZ-161: Fix user validation bug [Done] Priority: Urgent | Assignee: Bob Jones PRs: #155 (CI failing, changes requested) Updated: 5 hours ago ``` ## State Matching The streamlinear MCP supports fuzzy state matching: ```typescript // These all work: await mcp__linear__linear({ action: 'update', id: 'BLZ-123', state: 'done' }); await mcp__linear__linear({ action: 'update', id: 'BLZ-123', state: 'Done' }); await mcp__linear__linear({ action: 'update', id: 'BLZ-123', state: 'in prog' }); await mcp__linear__linear({ action: 'update', id: 'BLZ-123', state: 'In Progress' }); ``` ## Error Handling ### MCP Availability ```typescript async function checkLinearMCPAvailable(): Promise { try { await mcp__linear__linear({ action: 'search' }); return true; } catch (error) { console.warn('Linear MCP not available:', error.message); return false; } } ``` ### Graceful Degradation ```typescript async function fetchLinearIssuesSafe( context: LinearContext | null ): Promise { if (!context) { console.log('No Linear context for current repo'); return null; } const available = await checkLinearMCPAvailable(); if (!available) { console.log('Linear MCP not available, skipping issue section'); return null; } try { if (context.filterBy === 'team' && context.team) { return await fetchTeamIssues(context.team); } else if (context.filterBy === 'query' && context.query) { const result = await mcp__linear__linear({ action: 'search', query: context.query }); return result.issues; } return await fetchMyActiveIssues(); } catch (error) { console.error('Failed to fetch Linear issues:', error); return null; } } ``` ## Configuration Management ### Config File Location Store mapping config in skill directory or user config: ``` ~/.config/claude/status-reporting/linear-config.json ``` Or project-specific: ``` .claude/linear-mapping.json ``` ### Loading Configuration ```typescript async function loadLinearConfig(): Promise { const configPaths = [ // User config path.join(os.homedir(), '.config/claude/status-reporting/linear-config.json'), // Project config path.join(process.cwd(), '.claude/linear-mapping.json') ]; for (const configPath of configPaths) { if (await fileExists(configPath)) { const content = await Bun.file(configPath).text(); return JSON.parse(content); } } // Return defaults return { mappings: [], defaults: { daysBack: 7, limit: 10 } }; } ``` ## Best Practices ### Team Key vs Team Name Use team keys (e.g., "BLZ") rather than full names: - Keys are shorter and less prone to typos - The streamlinear MCP expects keys in query filters - Keys are visible in issue identifiers (BLZ-123) Get team keys: ```typescript const result = await mcp__linear__linear({ action: 'graphql', graphql: 'query { teams { nodes { id key name } } }' }); // Returns: [{ id: "uuid", key: "BLZ", name: "BLZ Team" }, ...] ``` ### Relative Time Display ```typescript function formatRelativeTime(isoDate: string): string { const date = new Date(isoDate); const now = new Date(); const diff = now.getTime() - date.getTime(); const minutes = Math.floor(diff / 60000); const hours = Math.floor(diff / 3600000); const days = Math.floor(diff / 86400000); if (minutes < 60) return `${minutes} minutes ago`; if (hours < 24) return `${hours} hours ago`; if (days < 7) return `${days} days ago`; return date.toLocaleDateString(); } ``` ### Issue Prioritization Show high-priority and urgent issues first: ```typescript function sortIssuesByPriority(issues: LinearIssue[]): LinearIssue[] { return issues.sort((a, b) => { // Lower number = higher priority (1=urgent, 2=high, 3=normal, 4=low) // 0=none goes to end const priorityA = a.priority === 0 ? 99 : a.priority; const priorityB = b.priority === 0 ? 99 : b.priority; if (priorityA !== priorityB) { return priorityA - priorityB; } // Same priority: sort by updated time (most recent first) return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime(); }); } ``` ## Integration Points ### With GitHub (see github.md) Correlate Linear issues with GitHub PRs: ```typescript async function correlateLinearWithGitHub( issues: LinearIssue[], prs: GitHubPR[] ): Promise { for (const issue of issues) { // Find PRs referencing this issue const relatedPRs = prs.filter(pr => { const refs = extractIssueReferences(pr.title + ' ' + (pr.body || '')); return refs.includes(issue.identifier); }); if (relatedPRs.length > 0) { issue.relatedPRs = relatedPRs; } } } ``` ### With Graphite (see graphite.md) Show Linear issues alongside stack: ```typescript async function annotateStackWithLinear( stack: StackNode[], issues: LinearIssue[] ): Promise { for (const node of stack) { if (!node.prTitle) continue; const refs = extractIssueReferences(node.prTitle); node.linearIssues = issues.filter(issue => refs.includes(issue.identifier) ); } } ``` ## Troubleshooting ### Linear MCP Not Found Verify the streamlinear MCP server is configured in `~/.claude.json`: ```json { "mcpServers": { "linear": { "type": "stdio", "command": "npx", "args": ["-y", "github:obra/streamlinear"] } } } ``` Ensure `LINEAR_API_TOKEN` is set in your environment. ### No Issues Returned ```typescript // Debug: Check available teams const teams = await mcp__linear__linear({ action: 'graphql', graphql: 'query { teams { nodes { id key name } } }' }); console.log('Available teams:', teams); // Debug: Try broader search const allIssues = await mcp__linear__linear({ action: 'search', query: '' }); console.log('Total issues accessible:', allIssues.length); ``` ### Authentication Issues The streamlinear MCP reads `LINEAR_API_TOKEN` from environment. Verify it's set: ```bash echo $LINEAR_API_TOKEN ``` Generate a new token at: