# Linear MCP Server - Technical Design Document
**Version:** 1.0.0
**Author:** MCP Development Team
**Date:** January 30, 2026
**Status:** Architecture Review Complete - Ready for Implementation
---
## Table of Contents
1. [Executive Summary](#executive-summary)
2. [System Overview](#system-overview)
3. [Architecture](#architecture)
4. [Technology Stack](#technology-stack)
5. [Tool Specifications](#tool-specifications)
6. [Data Flow](#data-flow)
7. [Authentication & Security](#authentication--security)
8. [Error Handling Strategy](#error-handling-strategy)
9. [Performance & Rate Limiting](#performance--rate-limiting)
10. [Observability & Monitoring](#observability--monitoring)
11. [Testing Strategy](#testing-strategy)
12. [Deployment & Configuration](#deployment--configuration)
13. [Future Enhancements](#future-enhancements)
14. [Architecture Review Findings](#architecture-review-findings)
15. [Decision Log](#decision-log)
---
## 1. Executive Summary
### Purpose
This MCP (Model Context Protocol) server enables Large Language Models to interact with Linear's project management platform through a set of well-designed tools, allowing AI agents to search issues, manage tasks, and collaborate within Linear workspaces.
### Key Objectives
- Provide intuitive, agent-friendly tools for Linear operations
- Maintain type safety and reliability through TypeScript + Linear SDK
- Support both human-readable (Markdown) and programmatic (JSON) response formats
- Handle rate limiting, errors, and timeouts gracefully
- Enable seamless integration with existing Linear workflows
- Provide comprehensive observability for production debugging
### Success Criteria
- 8 core tools operational covering essential Linear workflows
- Sub-second response times for 95% of operations (p95 < 1s)
- Comprehensive error handling with actionable, LLM-friendly messages
- Support for both markdown and JSON response formats
- Successful authentication and API key management
- Complete observability (structured logging, health checks)
- 80%+ test coverage with comprehensive error scenarios
### Architecture Assessment
**Rating:** 7.5/10 - Solid foundation with critical gaps addressed
The design has undergone comprehensive architecture review with the following outcomes:
- ✅ Technology choices validated (TypeScript + Linear SDK)
- ✅ Tool granularity confirmed appropriate
- ✅ Layered architecture validated
- 🔴 Critical gaps identified and addressed (timeouts, observability, health checks)
- 🟡 Important improvements incorporated (error UX, state management, rate limiting)
---
## 2. System Overview
### High-Level Architecture
```
┌─────────────────────────────────────────────────────────────┐
│ MCP Client │
│ (Claude Desktop, Cline, etc.) │
└────────────────────────┬────────────────────────────────────┘
│ MCP Protocol
│ (stdio transport)
▼
┌─────────────────────────────────────────────────────────────┐
│ Linear MCP Server │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Cross-Cutting Concerns (NEW) │ │
│ │ - Structured Logging (Pino) │ │
│ │ - Performance Metrics │ │
│ │ - Configuration Management │ │
│ │ - Request Context Propagation │ │
│ └────────────┬────────────────────────────────────────┘ │
│ │ │
│ ┌────────────┴────────────────────────────────────────┐ │
│ │ Tool Registry & Router │ │
│ │ - linear_search_issues │ │
│ │ - linear_get_issue │ │
│ │ - linear_create_issue │ │
│ │ - linear_update_issue │ │
│ │ - linear_add_comment │ │
│ │ - linear_list_teams │ │
│ │ - linear_list_workflow_states │ │
│ │ - linear_get_my_issues │ │
│ │ - linear_health_check (NEW) │ │
│ └────────────┬────────────────────────────────────────┘ │
│ │ │
│ ┌────────────┴────────────────────────────────────────┐ │
│ │ Validation Layer (Zod Schemas) │ │
│ │ - Input validation & sanitization │ │
│ │ - Max length enforcement │ │
│ │ - Type coercion │ │
│ └────────────┬────────────────────────────────────────┘ │
│ │ │
│ ┌────────────┴────────────────────────────────────────┐ │
│ │ Application Layer (Business Logic) │ │
│ │ - Tool implementations │ │
│ │ - Workflow orchestration │ │
│ │ - Response formatters (Markdown/JSON) │ │
│ │ - Identifier resolution (ENG-123 → UUID) │ │
│ │ - Pagination handling │ │
│ │ - Error transformation (LLM-friendly) │ │
│ └────────────┬────────────────────────────────────────┘ │
│ │ │
│ ┌────────────┴────────────────────────────────────────┐ │
│ │ Service Layer (Linear SDK Operations) │ │
│ │ - Pure Linear SDK wrappers (1:1 mapping) │ │
│ │ - Rate limiting & throttling │ │
│ │ - Retry logic with exponential backoff │ │
│ │ - Timeout enforcement (30s default) │ │
│ │ - Authentication management │ │
│ └────────────┬────────────────────────────────────────┘ │
└───────────────┼─────────────────────────────────────────────┘
│ HTTPS / GraphQL API
│ (with timeout)
▼
┌─────────────────────────────────────────────────────────────┐
│ Linear API │
│ (api.linear.app/graphql) │
└─────────────────────────────────────────────────────────────┘
```
### Component Responsibilities
| Component | Responsibility | Key Features |
| -------------------------- | --------------------------------- | --------------------------------------------------------- |
| **Cross-Cutting Concerns** | Logging, metrics, config | Structured logging (Pino), request context, health checks |
| **Tool Registry** | Register and expose tools to MCP | Schema validation, tool metadata, descriptions |
| **Validation Layer** | Input validation and sanitization | Zod schemas, type safety, max length enforcement |
| **Application Layer** | Business logic, orchestration | Combines service operations, formats responses |
| **Service Layer** | Pure Linear SDK operations | 1:1 SDK mapping, stateless, timeout enforcement |
| **Linear API** | External GraphQL API | Issue management, project data |
### State Management Strategy
**Decision:** **Stateless Architecture** (Recommended)
```typescript
// Service layer maintains NO mutable state
export class LinearService {
private readonly sdk: LinearClient;
constructor(apiKey: string) {
this.sdk = new LinearClient({ apiKey });
// No caches, no mutable state
}
// Each call is independent
async getIssue(id: string): Promise<Issue> {
return this.sdk.issue(id);
}
}
```
**Rationale:**
- Simpler to reason about and test
- No cache invalidation complexity
- No concurrency issues between tool calls
- LLM sessions are independent
**Phase 2 Consideration:** If performance analysis shows repeated identical queries, add TTL-based caching for stable data only (teams, workflow states).
---
## 3. Architecture
### 3.1 Layered Architecture
```
┌──────────────────────────────────────┐
│ Presentation Layer │ ← MCP Tool Interface
│ - Tool definitions │
│ - Schema declarations │
│ - LLM-optimized descriptions │
│ - Response formatting │
└──────────────┬───────────────────────┘
│
┌──────────────┴───────────────────────┐
│ Application Layer │ ← Business Logic
│ - Tool implementations │
│ - Workflow orchestration │
│ - Data transformation │
│ - Error transformation │
└──────────────┬───────────────────────┘
│
┌──────────────┴───────────────────────┐
│ Service Layer │ ← Linear SDK Integration
│ - Pure SDK wrappers (1:1) │
│ - Rate limiting │
│ - Retry logic │
│ - Timeout enforcement │
└──────────────┬───────────────────────┘
│
┌──────────────┴───────────────────────┐
│ Infrastructure Layer │ ← External Services
│ - Linear GraphQL API │
│ - Authentication │
│ - Logging & Metrics │
└──────────────────────────────────────┘
```
### 3.2 Layer Boundary Rules
**Service Layer → Application Layer:**
- Service layer provides 1:1 mapping to Linear SDK operations
- Returns raw SDK responses
- No business logic or formatting
```typescript
// Service Layer (correct)
export class LinearService {
async createIssue(params: IssueCreateInput): Promise<Issue> {
return this.sdk.issueCreate(params);
}
}
// Application Layer (correct)
export async function createIssueWithAssignment(
service: LinearService,
params: CreateIssueToolInput
): Promise<ToolResponse> {
// Orchestration: create + assign
const issue = await service.createIssue({
teamId: params.teamId,
title: params.title,
description: params.description,
});
if (params.assigneeId) {
await service.updateIssue(issue.id, { assigneeId: params.assigneeId });
}
return formatIssueResponse(issue, params.response_format);
}
```
### 3.3 Directory Structure
```
linear-mcp-server/
├── src/
│ ├── index.ts # Server entry point, tool registration
│ ├── constants.ts # Configuration constants
│ ├── types.ts # Shared TypeScript types
│ │
│ ├── infrastructure/ # Cross-cutting concerns (NEW)
│ │ ├── logger.ts # Structured logging (Pino)
│ │ ├── metrics.ts # Performance tracking
│ │ ├── config.ts # Configuration management
│ │ ├── context.ts # Request context propagation
│ │ └── health.ts # Health check logic
│ │
│ ├── services/ # Service layer (NEW organization)
│ │ ├── linear.service.ts # Linear SDK wrapper
│ │ ├── rate-limiter.ts # Rate limiting logic
│ │ └── retry.service.ts # Retry with exponential backoff
│ │
│ ├── schemas/ # Zod validation schemas
│ │ ├── index.ts # Schema exports
│ │ ├── issues.ts # Issue-related schemas
│ │ ├── teams.ts # Team schemas
│ │ ├── comments.ts # Comment schemas
│ │ └── common.ts # Common/shared schemas
│ │
│ ├── tools/ # Tool implementations
│ │ ├── index.ts # Tool exports
│ │ ├── issues.ts # Issue CRUD operations
│ │ │ ├── searchIssues()
│ │ │ ├── getIssue()
│ │ │ ├── createIssue()
│ │ │ ├── updateIssue()
│ │ │ └── getMyIssues()
│ │ ├── teams.ts # Team & context operations
│ │ │ ├── listTeams()
│ │ │ └── listWorkflowStates()
│ │ └── comments.ts # Collaboration operations
│ │ └── addComment()
│ │
│ ├── formatters/ # Response formatters
│ │ ├── index.ts # Formatter exports
│ │ ├── markdown.ts # Markdown formatting
│ │ │ ├── formatIssue()
│ │ │ ├── formatIssueList()
│ │ │ └── formatTeams()
│ │ └── json.ts # JSON formatting
│ │ └── formatStructured()
│ │
│ └── utils/ # Shared utilities
│ ├── errors.ts # Error handling & transformation
│ ├── pagination.ts # Cursor-based pagination
│ ├── identifiers.ts # ID/identifier resolution
│ └── validation.ts # Additional validation helpers
│
├── tests/ # Test suite
│ ├── unit/ # Unit tests
│ │ ├── schemas/
│ │ ├── formatters/
│ │ └── utils/
│ ├── integration/ # Integration tests
│ │ ├── tools/
│ │ └── mcp-protocol/ # MCP-specific tests (NEW)
│ ├── e2e/ # End-to-end tests (manual)
│ └── fixtures/ # Test data
│
├── docs/ # Documentation
│ ├── TECHNICAL_DESIGN.md # This document
│ ├── API.md # Tool API documentation
│ ├── SETUP.md # Setup instructions
│ └── EXAMPLES.md # Usage examples
│
├── package.json
├── tsconfig.json
├── .env.example
├── .gitignore
└── README.md
```
---
## 4. Technology Stack
### Core Dependencies
| Package | Version | Purpose | Justification |
| --------------------------- | ------- | -------------------- | --------------------------------------------- |
| `@linear/sdk` | ^40.0.0 | Linear API client | Official SDK, type-safe, maintained by Linear |
| `@modelcontextprotocol/sdk` | latest | MCP server framework | Standard MCP implementation |
| `zod` | ^3.22.0 | Schema validation | Type-safe runtime validation |
| `typescript` | ^5.3.0 | Language | Type safety, better DX |
| `pino` | ^8.0.0 | Structured logging | Fast, JSON logging for production |
### Development Dependencies
| Package | Version | Purpose |
| ------------- | ------- | --------------------------- |
| `@types/node` | ^20.0.0 | Node.js type definitions |
| `tsx` | ^4.0.0 | TypeScript execution |
| `vitest` | ^1.0.0 | Testing framework |
| `pino-pretty` | ^10.0.0 | Pretty log formatting (dev) |
| `prettier` | ^3.0.0 | Code formatting |
| `eslint` | ^8.0.0 | Linting |
### Why TypeScript Over Python?
**Decision Rationale:**
1. **Official SDK**: Linear provides `@linear/sdk` with complete TypeScript types
2. **Type Safety**: Full compile-time type checking for all API operations
3. **GraphQL Abstraction**: SDK handles query construction, pagination, caching
4. **Maintenance**: SDK auto-updates with Linear's API changes
5. **Ecosystem**: Linear's tooling and examples are TypeScript-first
6. **MCP Integration**: MCP SDK has TypeScript-first design
**Python Alternative Would Require:**
- Manual GraphQL query construction
- Custom type definitions (Pydantic models)
- Manual schema tracking and updates
- Community-maintained clients (not official)
- Additional complexity with limited benefits
**Verdict:** TypeScript is the correct choice for this project.
---
## 5. Tool Specifications
### 5.1 Tool Design Principles
1. **Human-Friendly Identifiers**: Accept "ENG-123" format, not just UUIDs
2. **Dual Response Formats**: Support markdown (human) and JSON (programmatic)
3. **Comprehensive Filtering**: Allow multiple filter combinations
4. **Actionable Errors**: Provide context, suggestions, and next steps
5. **Combined Operations**: Support common workflows in single calls
6. **LLM-Optimized Descriptions**: Include what, when, examples, and returns
### 5.2 Tool Description Template
Every tool follows this description pattern for optimal LLM comprehension:
```typescript
// Template structure:
`[One-sentence summary of what the tool does]
Use this to:
- [Use case 1]
- [Use case 2]
- [Use case 3]
Examples:
- "[Natural language example 1]"
- "[Natural language example 2]"
- "[Natural language example 3]"
Returns: [What the LLM will receive back]`;
```
### 5.3 Tool Catalog
#### Tool 1: `linear_search_issues`
**Purpose**: Search and filter issues across teams with flexible criteria.
**Description (LLM-facing)**:
```
Search for Linear issues with flexible filtering and full-text search.
Use this to:
- Find issues by text query (searches titles and descriptions)
- Filter by assignee, state, labels, priority, or team
- Get issues for a specific project or time range
- Discover issues before performing operations on them
Examples:
- "Find all P0 bugs assigned to Alice"
- "Show me issues created this week in the Engineering team"
- "Search for issues mentioning 'authentication'"
- "Get all backlog issues labeled 'frontend'"
Returns: List of matching issues with identifiers (e.g., ENG-123) that you can use in other tools like linear_get_issue or linear_update_issue.
```
**Input Schema**:
```typescript
export const SearchIssuesSchema = z
.object({
teamId: z
.string()
.optional()
.describe('Team ID or key (e.g., "ENG"). Use linear_list_teams to find valid teams.'),
filters: z
.object({
assigneeId: z
.string()
.optional()
.describe('Filter by assignee user ID. Use linear_list_users to find user IDs.'),
stateId: z
.string()
.optional()
.describe(
'Filter by workflow state ID. Use linear_list_workflow_states to find state IDs.'
),
priority: z
.number()
.min(0)
.max(4)
.optional()
.describe('Filter by priority: 0=None, 1=Urgent, 2=High, 3=Medium, 4=Low'),
labelIds: z
.array(z.string())
.max(20)
.optional()
.describe('Filter by label IDs (AND logic - issues must have ALL labels)'),
projectId: z.string().optional().describe('Filter by project ID'),
})
.optional(),
query: z
.string()
.max(500)
.optional()
.describe('Full-text search query. Searches issue titles and descriptions.'),
pagination: z
.object({
limit: z
.number()
.int()
.min(1)
.max(100)
.default(25)
.describe('Maximum number of results to return (default: 25, max: 100)'),
cursor: z
.string()
.optional()
.describe('Pagination cursor from previous response to get next page'),
})
.optional(),
orderBy: z
.enum(['created', 'updated', 'priority'])
.optional()
.describe('Sort results by created date, updated date, or priority'),
response_format: z
.enum(['markdown', 'json'])
.default('markdown')
.describe('Response format: "markdown" for human-readable, "json" for structured data'),
})
.strict();
```
**Output Format (Markdown)**:
```markdown
# Search Results (15 issues)
## ENG-123: Fix login authentication bug
**Status:** In Progress | **Priority:** High | **Assignee:** @john.doe
**Team:** Engineering | **Updated:** 2 hours ago
**Labels:** bug, authentication, urgent
Description: Users are unable to login with SSO...
**URL:** https://linear.app/team/issue/ENG-123
---
## ENG-124: Implement dark mode
**Status:** Backlog | **Priority:** Medium | **Assignee:** Unassigned
**Team:** Engineering | **Updated:** 1 day ago
Description: Add dark mode support to the application...
**URL:** https://linear.app/team/issue/ENG-124
---
**Pagination:** Showing 15 of 42 results
**Next Page:** Use cursor "YXJyYXljb25uZWN0aW9uOjE0" to get more results
```
**Output Format (JSON)**:
```json
{
"success": true,
"data": {
"issues": [
{
"id": "issue_abc123",
"identifier": "ENG-123",
"title": "Fix login authentication bug",
"state": { "id": "state_xyz", "name": "In Progress" },
"priority": 2,
"assignee": { "id": "user_123", "name": "John Doe" },
"team": { "id": "team_eng", "name": "Engineering" },
"url": "https://linear.app/team/issue/ENG-123",
"updatedAt": "2026-01-30T20:15:00Z"
}
],
"pagination": {
"total": 42,
"returned": 15,
"hasMore": true,
"cursor": "YXJyYXljb25uZWN0aW9uOjE0"
}
},
"metadata": {
"timestamp": "2026-01-30T22:30:00Z",
"responseFormat": "json"
}
}
```
**Annotations**:
```typescript
{
readOnlyHint: true, // No state changes
destructiveHint: false, // Safe operation
idempotentHint: true, // Same query = same results
openWorldHint: true // Can return various results
}
```
---
#### Tool 2: `linear_get_issue`
**Purpose**: Get detailed information about a specific issue.
**Description (LLM-facing)**:
```
Get comprehensive details about a specific Linear issue.
Use this to:
- View full issue details including description, comments, and metadata
- Check the current status and assignee of an issue
- Review issue history and activity
- Get issue information before updating or commenting
Examples:
- "Show me details for issue ENG-123"
- "Get information about the authentication bug"
- "What's the current status of ENG-456?"
Returns: Complete issue information including title, description, status, assignee, labels, comments, and URLs.
```
**Input Schema**:
```typescript
export const GetIssueSchema = z
.object({
identifier: z
.string()
.describe(
'Issue identifier (e.g., "ENG-123") or UUID. Prefer the human-readable identifier when available.'
),
includeComments: z
.boolean()
.default(false)
.describe('Include comment thread in response (default: false)'),
response_format: z.enum(['markdown', 'json']).default('markdown'),
})
.strict();
```
**Output Format (Markdown)**:
```markdown
# ENG-123: Fix login authentication bug
**Status:** In Progress
**Priority:** High (2)
**Team:** Engineering
**Assignee:** John Doe (@john.doe)
**Created:** Jan 28, 2026 by Jane Smith
**Updated:** 2 hours ago
**Labels:** bug, authentication, urgent
**Project:** Q1 Security Sprint
## Description
Users are unable to login with SSO authentication. The error occurs
when redirecting from the identity provider back to our application.
### Steps to Reproduce
1. Navigate to login page
2. Click "Login with SSO"
3. Complete authentication with provider
4. Observe error on redirect
## Comments (3)
### Jane Smith - 2 hours ago
I've identified the issue in the OAuth callback handler. The state
parameter validation is failing.
### John Doe - 1 hour ago
Thanks! I'll implement the fix now and add tests to prevent regression.
### Alice Johnson - 30 minutes ago
Great catch! This was affecting several customers.
---
**URL:** https://linear.app/team/issue/ENG-123
```
**Annotations**:
```typescript
{
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: false // Specific issue requested
}
```
---
#### Tool 3: `linear_create_issue`
**Purpose**: Create a new issue in Linear.
**Description (LLM-facing)**:
```
Create a new issue in a Linear team.
Use this to:
- Report bugs or feature requests
- Create tasks for team members
- Document work items that need to be tracked
- Add new issues to projects or sprints
Examples:
- "Create a bug report for the login issue in the Engineering team"
- "Add a new feature request to the Product team's backlog"
- "Create a high-priority task assigned to Alice"
Returns: Confirmation with the new issue's identifier (e.g., ENG-125) and URL.
```
**Input Schema**:
```typescript
export const CreateIssueSchema = z
.object({
teamId: z
.string()
.min(1)
.describe('Required: Team ID. Use linear_list_teams to find team IDs.'),
title: z.string().min(1).max(512).describe('Required: Issue title (max 512 characters)'),
description: z
.string()
.max(50000)
.optional()
.describe('Issue description in Markdown format (max 50KB)'),
priority: z
.number()
.int()
.min(0)
.max(4)
.optional()
.describe('Priority level: 0=None, 1=Urgent, 2=High, 3=Medium, 4=Low'),
stateId: z
.string()
.optional()
.describe("Workflow state ID. Defaults to team's backlog state if not provided."),
assigneeId: z.string().optional().describe('Assign to user ID. Leave empty for unassigned.'),
labelIds: z.array(z.string()).max(20).optional().describe('Attach label IDs (max 20 labels)'),
projectId: z.string().optional().describe('Add issue to project ID'),
parentId: z.string().optional().describe('Parent issue ID to create a sub-issue'),
dueDate: z
.string()
.datetime()
.optional()
.describe('Due date in ISO 8601 format (e.g., "2026-02-15T00:00:00Z")'),
})
.strict();
```
**Output Format (Markdown)**:
```markdown
✅ Issue created successfully!
**Identifier:** ENG-125
**Title:** Fix login authentication bug
**Team:** Engineering
**Status:** Backlog
**Assignee:** John Doe
**Priority:** High
**URL:** https://linear.app/team/issue/ENG-125
You can now use this identifier (ENG-125) with other tools to update the issue, add comments, or link related issues.
```
**Annotations**:
```typescript
{
readOnlyHint: false,
destructiveHint: false,
idempotentHint: false, // Creates new resource each time
openWorldHint: false
}
```
---
#### Tool 4: `linear_update_issue`
**Purpose**: Update an existing issue's properties.
**Description (LLM-facing)**:
```
Update properties of an existing Linear issue.
Use this to:
- Change issue status (e.g., move to "In Progress" or "Done")
- Reassign issues to different team members
- Update priority levels
- Modify title, description, or other properties
- Add or remove labels
Examples:
- "Move issue ENG-123 to In Progress and assign to Alice"
- "Change priority of ENG-456 to Urgent"
- "Update the description of ENG-789"
- "Mark ENG-234 as Done"
Returns: Confirmation of changes made with before/after values.
```
**Input Schema**:
```typescript
export const UpdateIssueSchema = z
.object({
identifier: z.string().min(1).describe('Required: Issue identifier (e.g., "ENG-123") or UUID'),
updates: z
.object({
title: z.string().min(1).max(512).optional().describe('Update issue title'),
description: z
.string()
.max(50000)
.optional()
.describe('Update description (Markdown). Set to empty string to clear.'),
stateId: z
.string()
.optional()
.describe('Change workflow state. Use linear_list_workflow_states to find state IDs.'),
priority: z
.number()
.int()
.min(0)
.max(4)
.optional()
.describe('Change priority: 0=None, 1=Urgent, 2=High, 3=Medium, 4=Low'),
assigneeId: z
.string()
.nullable()
.optional()
.describe('Reassign to user ID. Set to null to unassign.'),
labelIds: z
.array(z.string())
.max(20)
.optional()
.describe('Replace labels with new set (replaces all existing labels)'),
projectId: z
.string()
.nullable()
.optional()
.describe('Change project. Set to null to remove from project.'),
dueDate: z
.string()
.datetime()
.nullable()
.optional()
.describe('Update due date. Set to null to remove due date.'),
})
.refine((data) => Object.keys(data).length > 0, {
message: 'At least one update field must be provided',
}),
})
.strict();
```
**Output Format (Markdown)**:
```markdown
✅ Issue ENG-123 updated successfully!
**Changes made:**
- **Status:** Backlog → In Progress
- **Assignee:** Unassigned → John Doe
- **Priority:** Medium (3) → High (2)
**URL:** https://linear.app/team/issue/ENG-123
```
**Annotations**:
```typescript
{
readOnlyHint: false,
destructiveHint: false,
idempotentHint: true, // Same update = same result
openWorldHint: false
}
```
---
#### Tool 5: `linear_add_comment`
**Purpose**: Add a comment to an issue.
**Description (LLM-facing)**:
```
Add a comment to a Linear issue.
Use this to:
- Provide updates on issue progress
- Ask questions or request clarification
- Document findings or solutions
- Collaborate with team members on issues
Examples:
- "Add a comment to ENG-123 with the root cause analysis"
- "Comment on ENG-456 that the fix is ready for review"
- "Ask a question about ENG-789 in the comments"
Returns: Confirmation that the comment was posted.
```
**Input Schema**:
```typescript
export const AddCommentSchema = z
.object({
identifier: z.string().min(1).describe('Required: Issue identifier (e.g., "ENG-123") or UUID'),
body: z
.string()
.min(1)
.max(50000)
.describe('Required: Comment body in Markdown format (max 50KB)'),
})
.strict();
```
**Output Format (Markdown)**:
```markdown
✅ Comment added to ENG-123
**Posted by:** Current User
**Time:** Just now
**URL:** https://linear.app/team/issue/ENG-123
```
**Annotations**:
```typescript
{
readOnlyHint: false,
destructiveHint: false,
idempotentHint: false, // Each call creates new comment
openWorldHint: false
}
```
---
#### Tool 6: `linear_list_teams`
**Purpose**: List all teams in the workspace (context/discovery tool).
**Description (LLM-facing)**:
```
List all teams in the Linear workspace.
Use this to:
- Discover available teams before creating issues
- Find team IDs needed for other operations
- Explore workspace structure
- Get team information including keys and descriptions
Examples:
- "What teams are available in this workspace?"
- "Show me all teams"
- "List teams to find the Engineering team ID"
Returns: List of teams with IDs, names, keys (e.g., "ENG"), and descriptions.
```
**Input Schema**:
```typescript
export const ListTeamsSchema = z
.object({
response_format: z.enum(['markdown', 'json']).default('markdown'),
})
.strict();
```
**Output Format (Markdown)**:
```markdown
# Teams (4)
## Engineering
**ID:** team_abc123
**Key:** ENG
**Description:** Product engineering team
**Members:** 12
## Design
**ID:** team_xyz789
**Key:** DES
**Description:** Product design and UX
**Members:** 5
## Product
**ID:** team_def456
**Key:** PROD
**Description:** Product management
**Members:** 3
## Marketing
**ID:** team_ghi789
**Key:** MKT
**Description:** Marketing and growth
**Members:** 4
---
Use these team IDs when creating or searching for issues.
```
**Annotations**:
```typescript
{
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: true
}
```
---
#### Tool 7: `linear_list_workflow_states`
**Purpose**: Get workflow states for a team.
**Description (LLM-facing)**:
```
List workflow states (statuses) for a specific team.
Use this to:
- Discover available states before updating issue status
- Find state IDs needed for creating or updating issues
- Understand team workflow stages
- See state types (backlog, unstarted, started, completed, canceled)
Examples:
- "What workflow states does the Engineering team use?"
- "Show me the available statuses for team ENG"
- "Get state IDs to move issues to In Progress"
Returns: List of workflow states with IDs, names, types, and colors.
```
**Input Schema**:
```typescript
export const ListWorkflowStatesSchema = z
.object({
teamId: z
.string()
.min(1)
.describe('Required: Team ID. Use linear_list_teams to find team IDs.'),
response_format: z.enum(['markdown', 'json']).default('markdown'),
})
.strict();
```
**Output Format (Markdown)**:
```markdown
# Workflow States: Engineering
## Backlog
**ID:** state_abc123
**Type:** backlog
**Color:** #95a2b3
## Todo
**ID:** state_def456
**Type:** unstarted
**Color:** #e2e2e2
## In Progress
**ID:** state_ghi789
**Type:** started
**Color:** #f2c94c
## In Review
**ID:** state_jkl012
**Type:** started
**Color:** #5e6ad2
## Done
**ID:** state_mno345
**Type:** completed
**Color:** #5e6ad2
---
Use these state IDs when creating or updating issues with the `stateId` parameter.
```
**Annotations**:
```typescript
{
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: false
}
```
---
#### Tool 8: `linear_get_my_issues`
**Purpose**: Get issues assigned to the authenticated user.
**Description (LLM-facing)**:
```
Get issues assigned to the current user (the API key owner).
Use this to:
- View your personal task list
- Check what you're currently working on
- Find issues assigned to you across all teams
- Review your backlog or completed work
Examples:
- "Show me my assigned issues"
- "What tasks am I working on?"
- "List my active issues"
- "Show my completed issues from this week"
Returns: List of issues assigned to you, similar to linear_search_issues but filtered to current user.
```
**Input Schema**:
```typescript
export const GetMyIssuesSchema = z
.object({
stateFilter: z
.enum(['active', 'backlog', 'completed', 'all'])
.default('active')
.describe('Filter by state type: "active" (in progress), "backlog", "completed", or "all"'),
pagination: z
.object({
limit: z.number().int().min(1).max(100).default(25),
cursor: z.string().optional(),
})
.optional(),
response_format: z.enum(['markdown', 'json']).default('markdown'),
})
.strict();
```
**Output Format**: Similar to `linear_search_issues` but filtered to current user.
**Annotations**:
```typescript
{
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: true
}
```
---
#### Tool 9: `linear_health_check` (NEW)
**Purpose**: Check if the Linear MCP server is connected and functioning.
**Description (LLM-facing)**:
```
Check the health status of the Linear MCP server.
Use this to:
- Verify the server is connected to Linear API
- Diagnose connection issues
- Check authentication status
- Test API key validity
Examples:
- "Is the Linear server working?"
- "Check if we can connect to Linear"
- "Verify Linear integration is healthy"
Returns: Health status including connectivity, authentication, and timestamp.
```
**Input Schema**:
```typescript
export const HealthCheckSchema = z
.object({
response_format: z.enum(['markdown', 'json']).default('markdown'),
})
.strict();
```
**Output Format (Markdown)**:
```markdown
# Linear MCP Server Health Check
**Status:** ✅ Healthy
**Linear API:**
- Connected: ✅ Yes
- Authenticated: ✅ Yes
- Response Time: 145ms
**Server:**
- Uptime: 2h 34m
- Version: 1.0.0
- Timestamp: 2026-01-30T22:30:00Z
All systems operational.
```
**Annotations**:
```typescript
{
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: false
}
```
---
## 6. Data Flow
### 6.1 Request Flow (Create Issue Example)
```
┌──────────┐
│ LLM │ 1. Invokes linear_create_issue
└────┬─────┘ with parameters
│
▼
┌────────────────────────────────────────┐
│ MCP Server: Tool Handler │
│ 2. Receives tool invocation │
│ - Logs request with context ID │
└────┬───────────────────────────────────┘
│
▼
┌────────────────────────────────────────┐
│ Validation Layer │
│ 3. Validates with Zod schema │
│ - teamId is valid string │
│ - title is 1-512 chars │
│ - priority is 0-4 (if provided) │
│ - Sanitize: max lengths enforced │
└────┬───────────────────────────────────┘
│ (on error: transform to LLM-friendly message)
▼
┌────────────────────────────────────────┐
│ Application Layer │
│ 4. Pre-processes input │
│ - Resolves team ID if needed │
│ - Sets default state if not given │
│ - Logs: "Creating issue in team X" │
└────┬───────────────────────────────────┘
│
▼
┌────────────────────────────────────────┐
│ Service Layer │
│ 5. Executes API call with safeguards │
│ - Checks rate limit budget │
│ - Applies timeout (30s) │
│ - Makes GraphQL mutation via SDK │
│ - Retry on transient errors │
│ - Logs: request/response times │
└────┬───────────────────────────────────┘
│
▼
┌────────────────────────────────────────┐
│ Linear API │
│ 6. Creates issue, returns data │
│ - Returns issue with ID, identifier│
└────┬───────────────────────────────────┘
│
▼
┌────────────────────────────────────────┐
│ Application Layer │
│ 7. Post-processes response │
│ - Formats as markdown/JSON │
│ - Adds helpful context/URLs │
│ - Logs: "Issue ENG-125 created" │
└────┬───────────────────────────────────┘
│
▼
┌────────────────────────────────────────┐
│ MCP Server │
│ 8. Returns formatted response │
│ - Logs: total request duration │
└────┬───────────────────────────────────┘
│
▼
┌──────────┐
│ LLM │ 9. Receives success message
└──────────┘ and issue identifier
```
### 6.2 Error Flow (Enhanced with LLM-Friendly Messages)
```
┌──────────┐
│ LLM │ 1. Invokes tool with invalid/missing params
└────┬─────┘
│
▼
┌────────────────────────────────────────┐
│ Validation Layer │
│ 2. Zod validation FAILS │
│ Error: teamId required │
│ Code: VALIDATION_ERROR │
└────┬───────────────────────────────────┘
│
▼
┌────────────────────────────────────────┐
│ Error Transformer (NEW) │
│ 3. Transforms to LLM-actionable msg │
│ │
│ Output: │
│ ❌ Validation Error │
│ │
│ **Issue:** teamId is required │
│ **Suggestion:** Use linear_list_teams │
│ to find available teams and their IDs.│
│ │
│ **Example:** │
│ linear_create_issue({ │
│ teamId: "team_abc123", │
│ title: "My issue" │
│ }) │
└────┬───────────────────────────────────┘
│
▼
┌────────────────────────────────────────┐
│ Logger (NEW) │
│ 4. Logs error with context │
│ { │
│ level: "warn", │
│ tool: "linear_create_issue", │
│ error: "VALIDATION_ERROR", │
│ field: "teamId" │
│ } │
└────┬───────────────────────────────────┘
│
▼
┌────────────────────────────────────────┐
│ MCP Server │
│ 5. Returns error response │
│ { │
│ isError: true, │
│ content: [{ type: "text", ... }] │
│ } │
└────┬───────────────────────────────────┘
│
▼
┌──────────┐
│ LLM │ 6. Receives error with actionable guidance
└──────────┘ Can self-correct by calling linear_list_teams
```
---
## 7. Authentication & Security
### 7.1 Authentication Method
**API Key Authentication** (Recommended for MCP)
```typescript
// Environment variable configuration
const LINEAR_API_KEY = process.env.LINEAR_API_KEY;
if (!LINEAR_API_KEY) {
throw new Error(
'LINEAR_API_KEY environment variable is required.\n' +
'Generate an API key at: https://linear.app/settings/api\n' +
"Then set: export LINEAR_API_KEY='lin_api_...'"
);
}
// Validate format
if (!LINEAR_API_KEY.startsWith('lin_api_')) {
console.warn(
'Warning: LINEAR_API_KEY does not match expected format (lin_api_*).\n' +
'This may be an invalid key.'
);
}
const client = new LinearClient({ apiKey: LINEAR_API_KEY });
```
**Why API Keys Over OAuth?**
- ✅ Simpler setup for single-user/team use cases
- ✅ No callback URL or web server required
- ✅ Suitable for CLI/automation contexts
- ✅ Adequate for MCP server architecture
- ✅ No token refresh complexity
**OAuth Consideration (Phase 2):** If multi-user support is needed, implement OAuth 2.0 with PKCE.
### 7.2 API Key Management
**Storage**:
- ✅ Environment variable (`LINEAR_API_KEY`)
- ✅ Never hardcoded in source
- ✅ Not logged or exposed in errors
- ✅ Not included in error messages
**Validation**:
- ✅ Check presence on server startup
- ✅ Validate format (starts with `lin_api_`)
- ✅ Test with simple API call (get viewer info)
**Startup Validation**:
```typescript
export async function validateApiKey(client: LinearClient): Promise<void> {
try {
const viewer = await client.viewer;
logger.info({ viewerId: viewer.id }, 'API key validated successfully');
} catch (error) {
throw new LinearMCPError(
'Invalid LINEAR_API_KEY. Please check your API key and try again.',
'AUTHENTICATION_FAILED',
'Generate a new API key at: https://linear.app/settings/api'
);
}
}
```
**Rotation**:
- Document key rotation process in README
- Support environment variable override
- Graceful handling of expired keys with clear error messages
### 7.3 Security Considerations
| Risk | Mitigation |
| -------------------------------- | ----------------------------------------------------------------- |
| **API Key Exposure** | Environment variables only, no logging, scrub from error messages |
| **Rate Limit Exhaustion** | Request throttling (10 req/s max) and exponential backoff |
| **Malicious Input** | Zod validation, input sanitization, max length enforcement |
| **Data Leakage** | Return only necessary fields, respect Linear permissions |
| **Error Information Disclosure** | Sanitize error messages, no API keys or sensitive data |
| **XSS in Markdown** | Linear SDK handles sanitization; trust Linear's content security |
### 7.4 Input Sanitization
**Max Length Enforcement**:
```typescript
export const CreateIssueSchema = z.object({
title: z.string().min(1, 'Title is required').max(512, 'Title must be less than 512 characters'),
description: z.string().max(50000, 'Description must be less than 50KB').optional(),
labelIds: z.array(z.string()).max(20, 'Maximum 20 labels allowed').optional(),
});
```
**Error Message Sanitization**:
```typescript
export function sanitizeError(error: Error): string {
return error.message
.replace(/lin_api_[a-zA-Z0-9]+/g, '[REDACTED]')
.replace(/Bearer\s+[^\s]+/g, '[REDACTED]')
.replace(/Authorization:[^\n]+/g, 'Authorization: [REDACTED]');
}
```
### 7.5 Permission Scopes
Linear API keys inherit user permissions. Required scopes:
| Operation | Required Scope |
| -------------------- | -------------- |
| Read issues | `read` |
| Create/update issues | `write` |
| Add comments | `write` |
| List teams/users | `read` |
**Scope Validation** (optional, Phase 2):
```typescript
async function validateScopes(client: LinearClient): Promise<void> {
try {
// Test read permission
await client.viewer.teams;
// Test write permission (dry-run not available, so we skip this)
// Linear doesn't support permission introspection
logger.info('Required scopes validated');
} catch (error) {
if (error.code === 'PERMISSION_DENIED') {
throw new Error(
'Linear API key lacks required permissions.\n' +
'Ensure your API key has "read" and "write" scopes.'
);
}
throw error;
}
}
```
---
## 8. Error Handling Strategy
### 8.1 Error Categories
```typescript
export enum ErrorCategory {
VALIDATION = 'VALIDATION_ERROR', // Invalid input parameters
AUTHENTICATION = 'AUTHENTICATION_FAILED', // API key issues
NOT_FOUND = 'NOT_FOUND', // Resource doesn't exist
RATE_LIMIT = 'RATE_LIMITED', // Too many requests
TIMEOUT = 'TIMEOUT', // Request took too long
NETWORK = 'NETWORK_ERROR', // Connection issues
PERMISSION = 'PERMISSION_DENIED', // Insufficient permissions
CONFLICT = 'CONFLICT', // State conflict (e.g., issue already closed)
UNKNOWN = 'UNKNOWN_ERROR', // Unexpected errors
}
```
### 8.2 Structured Error Response
```typescript
export class LinearMCPError extends Error {
constructor(
message: string,
public readonly code: ErrorCategory,
public readonly suggestion?: string,
public readonly context?: Record<string, unknown>
) {
super(message);
this.name = 'LinearMCPError';
}
toJSON() {
return {
error: {
code: this.code,
message: this.message,
suggestion: this.suggestion,
context: this.context,
},
};
}
toMarkdown(): string {
let md = `❌ ${this.message}\n\n`;
if (this.suggestion) {
md += `**Suggestion:** ${this.suggestion}\n\n`;
}
if (this.context && Object.keys(this.context).length > 0) {
md += `**Details:**\n`;
for (const [key, value] of Object.entries(this.context)) {
md += `- ${key}: ${JSON.stringify(value)}\n`;
}
}
return md;
}
}
```
### 8.3 LLM-Friendly Error Examples
**Validation Error**:
```markdown
❌ Validation Error: Invalid priority value
**Issue:** priority must be between 0 and 4
**Received:** 5
**Suggestion:** Use one of these priority values:
- 0: None
- 1: Urgent
- 2: High
- 3: Medium
- 4: Low
**Example:**
linear_create_issue({
teamId: "team_abc123",
title: "My issue",
priority: 2 // ← High priority
})
```
**Not Found Error**:
```markdown
❌ Team not found: "engineering"
**Available teams:**
- Engineering (ID: team_abc123, Key: ENG)
- Design (ID: team_xyz789, Key: DES)
- Product (ID: team_def456, Key: PROD)
**Suggestion:** Use linear_list_teams to see all teams with their IDs, then use the team ID (not the name) in your request.
**Example:**
linear_create_issue({
teamId: "team_abc123", // ← Use the ID
title: "My issue"
})
```
**Rate Limit Error**:
```markdown
❌ Rate limit exceeded
**Message:** Too many requests. Linear's rate limit has been reached.
**Reset Time:** 45 seconds
**Suggestion:** The request will be automatically retried in 45 seconds. You don't need to do anything.
If you see this error frequently, consider reducing the number of concurrent operations or adding delays between requests.
```
**Timeout Error (NEW)**:
```markdown
❌ Request timed out after 30 seconds
**Issue:** The Linear API did not respond within the timeout period.
**Suggestion:** This may be due to:
- Linear API experiencing high load
- Network connectivity issues
- Complex query taking too long
Please try again. If the problem persists, try simplifying your query or contact Linear support.
```
### 8.4 Validation Error Transformation
```typescript
export function formatValidationError(error: z.ZodError): LinearMCPError {
const issues = error.issues.map((issue) => ({
field: issue.path.join('.'),
message: issue.message,
expected: (issue as any).expected,
received: (issue as any).received,
}));
const fieldList = issues.map((i) => i.field).join(', ');
const detailedMessages = issues
.map((i) => {
let msg = `**${i.field}:** ${i.message}`;
if (i.expected && i.received) {
msg += ` (expected ${i.expected}, got ${i.received})`;
}
return msg;
})
.join('\n');
const suggestion =
`Please check these parameters:\n${detailedMessages}\n\n` +
`Refer to the tool description for valid parameter formats and examples.`;
return new LinearMCPError(
`Validation failed for: ${fieldList}`,
ErrorCategory.VALIDATION,
suggestion,
{ issues }
);
}
```
### 8.5 Retry Strategy
**Configuration**:
```typescript
export interface RetryConfig {
maxRetries: 3;
initialDelayMs: 1000; // 1 second
maxDelayMs: 30000; // 30 seconds
backoffMultiplier: 2; // Exponential backoff
retryableErrors: [ErrorCategory.RATE_LIMIT, ErrorCategory.NETWORK, ErrorCategory.TIMEOUT];
nonRetryableErrors: [
ErrorCategory.AUTHENTICATION,
ErrorCategory.VALIDATION,
ErrorCategory.NOT_FOUND,
ErrorCategory.PERMISSION,
];
}
```
**Implementation**:
```typescript
export class RetryService {
private config: RetryConfig;
async executeWithRetry<T>(
operation: () => Promise<T>,
context: { toolName: string; operationType: string }
): Promise<T> {
let lastError: Error;
for (let attempt = 0; attempt <= this.config.maxRetries; attempt++) {
try {
logger.debug({ attempt, ...context }, 'Executing operation');
return await operation();
} catch (error) {
lastError = error;
// Don't retry if this is not a retryable error
if (!this.shouldRetry(error)) {
throw error;
}
// Don't retry if we've exhausted attempts
if (attempt >= this.config.maxRetries) {
logger.warn({ attempt, error: error.message, ...context }, 'Max retries exceeded');
throw error;
}
// Calculate delay
const delay = this.calculateDelay(error, attempt);
logger.info({ attempt, delay, error: error.code, ...context }, 'Retrying after delay');
await this.sleep(delay);
}
}
throw lastError!;
}
private shouldRetry(error: any): boolean {
if (error instanceof LinearMCPError) {
return this.config.retryableErrors.includes(error.code);
}
// Network errors from Linear SDK
if (error.type === 'NetworkError') {
return true;
}
return false;
}
private calculateDelay(error: any, attempt: number): number {
// For rate limit errors, use Linear's reset time
if (error.code === ErrorCategory.RATE_LIMIT && error.resetAt) {
const delay = error.resetAt - Date.now();
// If reset time is > 30s, don't auto-retry
if (delay > this.config.maxDelayMs) {
throw new LinearMCPError(
`Rate limited. Try again in ${Math.ceil(delay / 1000)} seconds.`,
ErrorCategory.RATE_LIMIT,
`Wait ${Math.ceil(delay / 1000)} seconds before retrying this operation.`
);
}
return delay;
}
// Exponential backoff for other errors
const delay = Math.min(
this.config.initialDelayMs * Math.pow(this.config.backoffMultiplier, attempt),
this.config.maxDelayMs
);
return delay;
}
private sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
}
```
---
## 9. Performance & Rate Limiting
### 9.1 Linear API Rate Limits
**Complexity-Based Limiting**:
- 1,500 complexity points per minute per API key
- Complexity calculated by Linear based on:
- Number of fields requested
- Depth of nested queries
- Number of connections traversed
- Linear returns complexity in response headers
**Example Complexities** (estimated):
- Simple issue query: ~5-10 points
- Issue with full details + comments: ~20-30 points
- Issue list (50 items, minimal fields): ~50-100 points
- Team list: ~10-20 points
### 9.2 Rate Limiting Strategy
**Approach:** Simple request throttling + reactive error handling
**Implementation**:
```typescript
export class RateLimiter {
private lastRequestTime: number = 0;
private requestCount: number = 0;
private readonly minRequestInterval: number = 100; // 100ms = max 10 req/s
async throttle(): Promise<void> {
const now = Date.now();
const timeSinceLastRequest = now - this.lastRequestTime;
if (timeSinceLastRequest < this.minRequestInterval) {
const delay = this.minRequestInterval - timeSinceLastRequest;
logger.debug({ delay }, 'Throttling request');
await this.sleep(delay);
}
this.lastRequestTime = Date.now();
this.requestCount++;
}
private sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
getStats() {
return {
totalRequests: this.requestCount,
lastRequestTime: this.lastRequestTime,
};
}
}
```
**Why This Approach:**
- ✅ Simple and predictable
- ✅ Prevents overwhelming Linear API
- ✅ No need to predict complexity upfront
- ✅ Let Linear's rate limit errors trigger retry logic
- ✅ 10 req/s is well under typical limits
**Phase 2 Enhancement:** If needed, track actual complexity from response headers:
```typescript
export class ComplexityTracker {
private window: Array<{ timestamp: number; complexity: number }> = [];
recordRequest(complexity: number) {
const now = Date.now();
// Keep only requests from last 60 seconds
this.window = this.window.filter((r) => now - r.timestamp < 60000);
this.window.push({ timestamp: now, complexity });
}
getRemainingBudget(): number {
const used = this.window.reduce((sum, r) => sum + r.complexity, 0);
return Math.max(0, 1500 - used);
}
shouldWait(): number | null {
if (this.getRemainingBudget() < 100) {
// Wait until oldest request expires
const oldest = this.window[0];
if (oldest) {
return 60000 - (Date.now() - oldest.timestamp);
}
}
return null;
}
}
```
### 9.3 Timeout Enforcement (NEW - CRITICAL)
**Requirement:** All Linear API requests must have timeouts to prevent hanging.
**Implementation**:
```typescript
export class TimeoutService {
private readonly defaultTimeout: number = 30000; // 30 seconds
async executeWithTimeout<T>(
operation: () => Promise<T>,
timeoutMs: number = this.defaultTimeout
): Promise<T> {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
try {
// Note: Linear SDK may not support AbortSignal yet
// Monitor SDK updates for native timeout support
const result = await Promise.race([operation(), this.createTimeoutPromise(timeoutMs)]);
return result as T;
} finally {
clearTimeout(timeoutId);
}
}
private createTimeoutPromise(ms: number): Promise<never> {
return new Promise((_, reject) => {
setTimeout(() => {
reject(
new LinearMCPError(
`Request timed out after ${ms / 1000} seconds`,
ErrorCategory.TIMEOUT,
'The Linear API did not respond in time. Try again or simplify your query.'
)
);
}, ms);
});
}
}
```
### 9.4 Performance Targets
| Metric | Target | Measurement |
| ---------------------------- | ------- | ---------------------------------- |
| **Tool Response Time (p50)** | < 500ms | 50th percentile latency |
| **Tool Response Time (p95)** | < 1s | 95th percentile latency |
| **Tool Response Time (p99)** | < 3s | 99th percentile latency |
| **Error Rate** | < 1% | Failed requests / total requests |
| **Rate Limit Errors** | < 0.1% | Rate limit hits / total requests |
| **Retry Success Rate** | > 95% | Successful retries / total retries |
| **Timeout Rate** | < 0.5% | Timeouts / total requests |
### 9.5 Caching Strategy
**Decision for MVP:** No caching initially (stateless architecture)
**Phase 2 Consideration:** Cache only stable, frequently-accessed data
**Cacheable Data:**
- Team lists (TTL: 1 hour) - rarely change
- Workflow states (TTL: 1 hour) - rarely change
- User lists (TTL: 15 minutes) - change infrequently
**Non-Cacheable Data:**
- Issue data (changes frequently)
- Comments (real-time updates)
- Search results (dynamic queries)
**Implementation Pattern (Phase 2)**:
```typescript
export class TTLCache<T> {
private cache = new Map<string, { data: T; expiresAt: number }>();
get(key: string): T | null {
const entry = this.cache.get(key);
if (!entry) return null;
if (Date.now() > entry.expiresAt) {
this.cache.delete(key);
return null;
}
return entry.data;
}
set(key: string, data: T, ttlMs: number): void {
this.cache.set(key, {
data,
expiresAt: Date.now() + ttlMs,
});
}
clear(): void {
this.cache.clear();
}
}
```
---
## 10. Observability & Monitoring
### 10.1 Structured Logging (NEW - CRITICAL)
**Library:** Pino (fast, structured JSON logging)
**Setup**:
```typescript
// src/infrastructure/logger.ts
import pino from 'pino';
export const logger = pino({
level: process.env.LOG_LEVEL || 'info',
transport:
process.env.NODE_ENV === 'development'
? {
target: 'pino-pretty',
options: {
colorize: true,
translateTime: 'HH:MM:ss',
ignore: 'pid,hostname',
},
}
: undefined,
base: {
service: 'linear-mcp-server',
version: '1.0.0',
},
});
```
**Log Levels**:
- `trace`: Hot path details (very verbose, disabled in production)
- `debug`: Detailed diagnostics (enabled in development)
- `info`: General events (default level)
- `warn`: Potential issues, rate limits, retries
- `error`: Failed operations, exceptions
- `fatal`: Critical failures requiring immediate attention
### 10.2 Request Context Propagation
```typescript
// src/infrastructure/context.ts
import { AsyncLocalStorage } from 'async_hooks';
import { randomUUID } from 'crypto';
interface RequestContext {
requestId: string;
toolName: string;
startTime: number;
}
export const requestContext = new AsyncLocalStorage<RequestContext>();
export function createRequestContext(toolName: string): RequestContext {
return {
requestId: randomUUID(),
toolName,
startTime: Date.now(),
};
}
export function withRequestContext<T>(ctx: RequestContext, fn: () => Promise<T>): Promise<T> {
return requestContext.run(ctx, fn);
}
export function getRequestContext(): RequestContext | undefined {
return requestContext.getStore();
}
// Usage in logging:
export function log(level: string, message: string, data?: any) {
const ctx = getRequestContext();
logger[level](
{
...data,
requestId: ctx?.requestId,
toolName: ctx?.toolName,
},
message
);
}
```
### 10.3 Critical Logging Points
**Server Lifecycle**:
```typescript
// Server startup
logger.info({ apiKey: 'lin_api_***' }, 'Linear MCP Server starting');
logger.info({ tools: toolNames }, 'Registered tools');
logger.info('Linear MCP Server ready');
// Server shutdown
logger.info('Linear MCP Server shutting down');
```
**Tool Invocation**:
```typescript
// Tool start
logger.info(
{
tool: 'linear_create_issue',
params: { teamId: 'team_abc', title: 'Bug fix' }, // Sanitized
},
'Tool invocation started'
);
// Tool success
logger.info(
{
tool: 'linear_create_issue',
result: { identifier: 'ENG-123' },
duration: 234,
},
'Tool invocation completed'
);
// Tool failure
logger.error(
{
tool: 'linear_create_issue',
error: error.code,
message: error.message,
duration: 145,
},
'Tool invocation failed'
);
```
**Rate Limiting**:
```typescript
logger.warn(
{
tool: 'linear_search_issues',
rateLimitRemaining: 100,
rateLimitReset: '2026-01-30T22:45:00Z',
},
'Approaching rate limit'
);
logger.warn(
{
tool: 'linear_create_issue',
retryAfter: 45,
},
'Rate limit exceeded, will retry'
);
```
**Authentication**:
```typescript
logger.info({ viewerId: 'user_123' }, 'API key validated');
logger.error('API key validation failed');
```
**Retries**:
```typescript
logger.info(
{
attempt: 2,
maxRetries: 3,
delay: 2000,
error: 'NETWORK_ERROR',
},
'Retrying operation'
);
```
### 10.4 Performance Metrics
```typescript
// src/infrastructure/metrics.ts
export class MetricsCollector {
private metrics = {
toolInvocations: new Map<string, number>(),
toolDurations: new Map<string, number[]>(),
toolErrors: new Map<string, number>(),
totalRequests: 0,
totalErrors: 0,
};
recordToolInvocation(toolName: string, duration: number, success: boolean) {
// Increment invocation count
const count = this.metrics.toolInvocations.get(toolName) || 0;
this.metrics.toolInvocations.set(toolName, count + 1);
// Record duration
const durations = this.metrics.toolDurations.get(toolName) || [];
durations.push(duration);
this.metrics.toolDurations.set(toolName, durations);
// Record errors
if (!success) {
const errors = this.metrics.toolErrors.get(toolName) || 0;
this.metrics.toolErrors.set(toolName, errors + 1);
this.metrics.totalErrors++;
}
this.metrics.totalRequests++;
}
getMetrics() {
const summary: any = {
totalRequests: this.metrics.totalRequests,
totalErrors: this.metrics.totalErrors,
errorRate: ((this.metrics.totalErrors / this.metrics.totalRequests) * 100).toFixed(2) + '%',
tools: {},
};
for (const [tool, count] of this.metrics.toolInvocations) {
const durations = this.metrics.toolDurations.get(tool) || [];
const errors = this.metrics.toolErrors.get(tool) || 0;
durations.sort((a, b) => a - b);
const p50 = durations[Math.floor(durations.length * 0.5)];
const p95 = durations[Math.floor(durations.length * 0.95)];
const p99 = durations[Math.floor(durations.length * 0.99)];
summary.tools[tool] = {
invocations: count,
errors,
errorRate: ((errors / count) * 100).toFixed(2) + '%',
latency: {
p50: `${p50}ms`,
p95: `${p95}ms`,
p99: `${p99}ms`,
},
};
}
return summary;
}
reset() {
this.metrics = {
toolInvocations: new Map(),
toolDurations: new Map(),
toolErrors: new Map(),
totalRequests: 0,
totalErrors: 0,
};
}
}
export const metrics = new MetricsCollector();
```
### 10.5 Health Check Implementation (NEW)
```typescript
// src/infrastructure/health.ts
import { LinearClient } from '@linear/sdk';
import { logger } from './logger';
export interface HealthStatus {
status: 'healthy' | 'unhealthy' | 'degraded';
linear: {
connected: boolean;
responseTime?: number;
error?: string;
};
server: {
uptime: number;
version: string;
};
timestamp: string;
}
export async function checkHealth(client: LinearClient): Promise<HealthStatus> {
const startTime = Date.now();
const status: HealthStatus = {
status: 'healthy',
linear: { connected: false },
server: {
uptime: process.uptime(),
version: '1.0.0',
},
timestamp: new Date().toISOString(),
};
try {
// Test Linear API connectivity
await client.viewer;
const responseTime = Date.now() - startTime;
status.linear.connected = true;
status.linear.responseTime = responseTime;
// Degraded if response time > 2s
if (responseTime > 2000) {
status.status = 'degraded';
logger.warn({ responseTime }, 'Linear API response time degraded');
}
logger.debug({ responseTime }, 'Health check passed');
} catch (error) {
status.status = 'unhealthy';
status.linear.connected = false;
status.linear.error = error.message;
logger.error({ error: error.message }, 'Health check failed');
}
return status;
}
```
---
## 11. Testing Strategy
### 11.1 Test Pyramid
```
┌─────────────┐
│ E2E Tests │ ← 5% (Real Linear API, manual/pre-release)
│ (~10 tests)│
└─────────────┘
┌─────────────────┐
│ Integration Tests│ ← 25% (Mocked Linear SDK responses)
│ (~50 tests) │
└─────────────────┘
┌─────────────────────┐
│ Unit Tests │ ← 70% (Individual functions/modules)
│ (~150 tests) │
└─────────────────────┘
```
**Target:** 80% line coverage minimum
### 11.2 Unit Tests
**Test Categories**:
1. **Schema Validation Tests** (`tests/unit/schemas/*.test.ts`)
```typescript
describe('CreateIssueSchema', () => {
it('should accept valid issue creation params', () => {
const input = {
teamId: 'team_123',
title: 'Bug fix',
priority: 2,
};
expect(() => CreateIssueSchema.parse(input)).not.toThrow();
});
it('should reject title exceeding max length', () => {
const input = {
teamId: 'team_123',
title: 'A'.repeat(600), // > 512
};
expect(() => CreateIssueSchema.parse(input)).toThrow(/must be less than 512/);
});
it('should reject invalid priority', () => {
const input = {
teamId: 'team_123',
title: 'Bug',
priority: 5, // > 4
};
expect(() => CreateIssueSchema.parse(input)).toThrow(/priority/);
});
});
```
2. **Formatter Tests** (`tests/unit/formatters/*.test.ts`)
```typescript
describe('formatIssueMarkdown', () => {
it('should format issue with all fields', () => {
const issue = {
identifier: 'ENG-123',
title: 'Bug fix',
state: { name: 'In Progress' },
priority: 2,
assignee: { name: 'John Doe' },
description: 'Fix authentication bug',
};
const markdown = formatIssueMarkdown(issue);
expect(markdown).toContain('# ENG-123');
expect(markdown).toContain('**Status:** In Progress');
expect(markdown).toContain('**Priority:** High');
expect(markdown).toContain('**Assignee:** John Doe');
});
it('should handle missing optional fields', () => {
const issue = {
identifier: 'ENG-124',
title: 'Minimal issue',
};
const markdown = formatIssueMarkdown(issue);
expect(markdown).not.toContain('undefined');
expect(markdown).not.toContain('null');
});
});
```
3. **Utility Tests** (`tests/unit/utils/*.test.ts`)
```typescript
describe('identifierUtils', () => {
it('should detect UUID format', () => {
expect(isUUID('a1b2c3d4-e5f6-7890-abcd-ef1234567890')).toBe(true);
expect(isUUID('ENG-123')).toBe(false);
});
it('should detect identifier format', () => {
expect(isIdentifier('ENG-123')).toBe(true);
expect(isIdentifier('PROD-456')).toBe(true);
expect(isIdentifier('invalid')).toBe(false);
});
});
describe('errorUtils', () => {
it('should sanitize API keys from error messages', () => {
const error = new Error('Auth failed with key lin_api_abc123');
const sanitized = sanitizeError(error);
expect(sanitized).toContain('[REDACTED]');
expect(sanitized).not.toContain('lin_api_abc123');
});
});
```
4. **Error Handling Tests**
```typescript
describe('formatValidationError', () => {
it('should create LLM-friendly validation error', () => {
const zodError = CreateIssueSchema.safeParse({});
const error = formatValidationError(zodError.error);
expect(error.code).toBe('VALIDATION_ERROR');
expect(error.message).toContain('teamId');
expect(error.suggestion).toBeTruthy();
});
});
```
### 11.3 Integration Tests
**Approach:** Mock Linear SDK responses to test full tool flow
**Test Categories**:
1. **Tool Flow Tests** (`tests/integration/tools/*.test.ts`)
```typescript
describe('linear_create_issue integration', () => {
let mockLinearClient: any;
beforeEach(() => {
mockLinearClient = {
issueCreate: vi.fn().mockResolvedValue({
issue: {
id: 'issue_123',
identifier: 'ENG-123',
title: 'Test issue',
url: 'https://linear.app/...',
},
}),
};
});
it('should create issue and return formatted response', async () => {
const result = await createIssueTool.execute(
{
teamId: 'team_123',
title: 'Test issue',
response_format: 'markdown',
},
mockLinearClient
);
expect(result).toContain('✅');
expect(result).toContain('ENG-123');
expect(result).toContain('https://linear.app/');
expect(mockLinearClient.issueCreate).toHaveBeenCalledWith({
teamId: 'team_123',
title: 'Test issue',
});
});
});
```
2. **Error Handling Integration Tests**
```typescript
describe('error handling integration', () => {
it('should transform Linear API error to LLM-friendly message', async () => {
const mockClient = {
issue: vi.fn().mockRejectedValue({
message: 'Issue not found',
type: 'NotFoundError',
}),
};
const result = await getIssueTool.execute({ identifier: 'ENG-999' }, mockClient);
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('not found');
expect(result.content[0].text).toContain('Suggestion');
});
});
```
3. **MCP Protocol Compliance Tests** (NEW)
```typescript
describe('MCP Protocol Compliance', () => {
it('should return structured error for unknown tool', async () => {
const response = await server.callTool('linear_invalid_tool', {});
expect(response).toMatchObject({
isError: true,
content: [
{
type: 'text',
text: expect.stringContaining('Unknown tool'),
},
],
});
});
it('should handle concurrent tool calls safely', async () => {
const results = await Promise.all([
server.callTool('linear_search_issues', { query: 'bug' }),
server.callTool('linear_list_teams', {}),
server.callTool('linear_get_my_issues', {}),
]);
expect(results).toHaveLength(3);
results.forEach((r) => expect(r.isError).toBe(false));
});
});
```
### 11.4 End-to-End Tests
**Approach:** Real Linear API calls with test workspace (manual/pre-release only)
**Setup Requirements**:
- Dedicated test Linear workspace
- Test API key with limited scope
- Cleanup scripts to remove test data
**Test Scenarios**:
```typescript
describe('E2E: Issue Workflow', () => {
let testIssueId: string;
it('should create → update → comment → search workflow', async () => {
// 1. Create issue
const created = await server.callTool('linear_create_issue', {
teamId: process.env.TEST_TEAM_ID,
title: 'E2E Test Issue',
priority: 2,
});
expect(created.isError).toBe(false);
testIssueId = extractIdentifier(created.content[0].text);
// 2. Update issue
const updated = await server.callTool('linear_update_issue', {
identifier: testIssueId,
updates: { stateId: process.env.TEST_STATE_IN_PROGRESS },
});
expect(updated.isError).toBe(false);
// 3. Add comment
const commented = await server.callTool('linear_add_comment', {
identifier: testIssueId,
body: 'E2E test comment',
});
expect(commented.isError).toBe(false);
// 4. Search for issue
const searched = await server.callTool('linear_search_issues', {
query: 'E2E Test Issue',
});
expect(searched.content[0].text).toContain(testIssueId);
});
afterAll(async () => {
// Cleanup: delete test issue
await cleanupTestData();
});
});
```
### 11.5 Server Startup Tests (NEW - CRITICAL)
```typescript
describe('Server Startup', () => {
it('should start server without errors', async () => {
const server = await startServer({
LINEAR_API_KEY: 'test-key',
});
expect(server).toBeDefined();
expect(server.listTools()).toHaveLength(9); // 8 tools + health_check
await server.close();
});
it('should fail fast with missing API key', async () => {
await expect(startServer({ LINEAR_API_KEY: '' })).rejects.toThrow('LINEAR_API_KEY');
});
it('should register all expected tools', async () => {
const server = await startServer({ LINEAR_API_KEY: 'test' });
const expectedTools = [
'linear_search_issues',
'linear_get_issue',
'linear_create_issue',
'linear_update_issue',
'linear_add_comment',
'linear_list_teams',
'linear_list_workflow_states',
'linear_get_my_issues',
'linear_health_check',
];
const registeredTools = server.listTools().map((t) => t.name);
expect(registeredTools.sort()).toEqual(expectedTools.sort());
});
it('should validate API key on startup', async () => {
const mockClient = {
viewer: Promise.resolve({ id: 'user_123' }),
};
await expect(validateApiKey(mockClient)).resolves.not.toThrow();
});
});
```
---
## 12. Deployment & Configuration
### 12.1 Installation
**From NPM** (future):
```bash
npm install -g @yourorg/linear-mcp-server
```
**From Source**:
```bash
git clone https://github.com/yourorg/linear-mcp-server
cd linear-mcp-server
npm install
npm run build
```
### 12.2 Configuration
**Environment Variables**:
| Variable | Required | Description | Example |
| ---------------- | -------- | ----------------- | ------------------------------------------ |
| `LINEAR_API_KEY` | Yes | Linear API key | `lin_api_abc123...` |
| `LOG_LEVEL` | No | Logging verbosity | `info` (default), `debug`, `warn`, `error` |
| `NODE_ENV` | No | Environment | `development`, `production` |
**Configuration File** (`.linear-mcp.json`) - Optional:
```json
{
"rateLimiting": {
"maxRequestsPerSecond": 10,
"retryAttempts": 3
},
"timeouts": {
"defaultMs": 30000,
"healthCheckMs": 5000
},
"logging": {
"level": "info",
"prettyPrint": false
},
"features": {
"enableMetrics": true
}
}
```
### 12.3 MCP Client Integration
**Example MCP Client Configuration:**
```json
{
"mcpServers": {
"linear": {
"command": "node",
"args": ["/path/to/linear-mcp-server/dist/index.js"],
"env": {
"LINEAR_API_KEY": "lin_api_...",
"LOG_LEVEL": "info"
}
}
}
}
```
**Alternative Configuration (if installed globally):**
```json
{
"linear": {
"command": "linear-mcp-server",
"env": {
"LINEAR_API_KEY": "lin_api_..."
}
}
}
```
### 12.4 Monitoring & Observability
**Log Output** (Development):
```
[15:30:45] INFO: Linear MCP Server starting
[15:30:45] INFO: API key validated (viewerId: user_123)
[15:30:45] INFO: Registered tools: 9
[15:30:45] INFO: Linear MCP Server ready
[15:31:20] INFO: Tool invocation started (tool: linear_search_issues)
[15:31:20] DEBUG: Executing Linear API request
[15:31:21] INFO: Tool invocation completed (duration: 234ms)
```
**Log Output** (Production - JSON):
```json
{"level":"info","service":"linear-mcp-server","version":"1.0.0","msg":"Linear MCP Server starting"}
{"level":"info","viewerId":"user_123","msg":"API key validated"}
{"level":"info","tools":9,"msg":"Registered tools"}
{"level":"info","msg":"Linear MCP Server ready"}
{"level":"info","tool":"linear_search_issues","requestId":"req_abc123","msg":"Tool invocation started"}
{"level":"debug","requestId":"req_abc123","msg":"Executing Linear API request"}
{"level":"info","tool":"linear_search_issues","requestId":"req_abc123","duration":234,"msg":"Tool invocation completed"}
```
---
## 13. Future Enhancements
### Phase 2 Features (Post-MVP)
| Feature | Description | Priority | Complexity |
| ---------------------- | --------------------------------------------------- | -------- | ---------- |
| **Project Management** | Create/update/list projects, add issues to projects | High | Medium |
| **Label Management** | Create/manage labels, bulk label operations | Medium | Low |
| **Issue Relations** | Link issues (blocks, relates to, duplicates) | Medium | Medium |
| **Attachments** | Upload/manage file attachments | Medium | High |
| **Bulk Operations** | Batch create/update issues | Low | Medium |
| **User Management** | List users, assign issues by email/name | Medium | Low |
### Phase 3 Features (Advanced)
| Feature | Description | Priority | Complexity |
| ----------------------- | ------------------------------------------------------ | -------- | ---------- |
| **Webhooks** | Subscribe to Linear events for proactive agent actions | Low | High |
| **Cycles** | Sprint/cycle management (create, list, add issues) | Medium | Medium |
| **Custom Fields** | Support custom field values in queries/updates | Low | Medium |
| **Analytics** | Issue metrics, velocity, burndown charts | Low | High |
| **OAuth Support** | Multi-user authentication for shared deployments | Low | High |
| **MCP Resources** | Expose issues as MCP Resources (not just tools) | Low | Medium |
| **Prompt Declarations** | Built-in prompts for common workflows | Medium | Low |
### Scalability Considerations
**Multi-Workspace Support**:
- Allow configuration of multiple Linear workspaces
- Workspace selector in tool parameters
- Per-workspace API key management
**Performance Optimization**:
- Connection pooling for concurrent requests
- Request batching where Linear API supports it
- GraphQL query optimization (selective field fetching)
- Caching strategy for frequently-accessed stable data
**Advanced Features**:
- Natural language query parsing (AI-assisted search)
- Intelligent state transitions (suggest next states based on workflow)
- Template-based issue creation (bug report, feature request templates)
- Integration with other MCP servers (GitHub, Slack, Jira)
---
## 14. Architecture Review Findings
This design underwent comprehensive architecture review with the following outcomes:
### 14.1 Critical Issues Addressed
| Issue | Resolution | Status |
| ----------------------------- | --------------------------------------------------------- | ----------- |
| **Missing Timeout Handling** | Added `TimeoutService` with 30s default timeout | ✅ Resolved |
| **No Observability Strategy** | Added structured logging (Pino), request context, metrics | ✅ Resolved |
| **Missing Health Checks** | Added `linear_health_check` tool and health monitoring | ✅ Resolved |
| **No Startup Tests** | Added server startup test suite | ✅ Resolved |
### 14.2 Important Improvements Made
| Issue | Improvement | Status |
| ----------------------------------- | --------------------------------------------------------- | ----------- |
| **State Management Unclear** | Explicitly defined: stateless architecture | ✅ Resolved |
| **Error Messages Not LLM-Friendly** | Structured errors with suggestions and context | ✅ Resolved |
| **Rate Limiting Oversimplified** | Simple throttle (10 req/s) + reactive error handling | ✅ Resolved |
| **Layer Boundaries Unclear** | Clarified: Service = 1:1 SDK, Application = orchestration | ✅ Resolved |
| **Tool Descriptions Missing** | Added LLM-optimized descriptions with examples | ✅ Resolved |
| **Input Sanitization Needed** | Added max length enforcement and validation | ✅ Resolved |
### 14.3 Enhancements for Future Phases
| Enhancement | Description | Phase |
| ----------------------- | ------------------------------------------ | ------- |
| **Caching** | TTL-based cache for teams, workflow states | Phase 2 |
| **Bulk Operations** | Batch create/update tools | Phase 2 |
| **MCP Resources** | Expose issues as readable resources | Phase 2 |
| **Prompt Declarations** | Built-in workflow prompts | Phase 2 |
| **Complexity Tracking** | Track Linear API complexity from headers | Phase 2 |
| **Plugin Architecture** | Extensible plugin system | Phase 3 |
### 14.4 Overall Assessment
**Rating:** 7.5/10 → 9.5/10 (after addressing critical and important issues)
**Strengths:**
- ✅ Technology choices validated (TypeScript + Linear SDK)
- ✅ Tool granularity confirmed appropriate
- ✅ Layered architecture sound with clear boundaries
- ✅ Comprehensive error handling with LLM-friendly messages
- ✅ Strong observability foundation
- ✅ Timeout enforcement prevents hanging
- ✅ Health checks enable monitoring
**Remaining Considerations:**
- Monitor performance in production to validate rate limiting approach
- Add caching in Phase 2 if usage patterns show benefit
- Consider bulk operations based on user feedback
- Evaluate MCP Resources protocol for read-only tools
---
## 15. Decision Log
| Date | Decision | Rationale | Alternatives Considered |
| ---------- | ---------------------------------- | -------------------------------------------------------------------------- | --------------------------------------- |
| 2026-01-30 | Use TypeScript over Python | Official Linear SDK, type safety, GraphQL abstractions, ecosystem support | Python with manual GraphQL client |
| 2026-01-30 | API Key auth over OAuth | Simpler for MCP use case, adequate for single-user, no callback URL needed | OAuth 2.0 with PKCE |
| 2026-01-30 | 8 tools for MVP (+1 health check) | Covers essential workflows without overwhelming users | Fewer tools (5-6) or more tools (12-15) |
| 2026-01-30 | Support markdown AND JSON | Flexibility for human and programmatic consumers | Markdown only or JSON only |
| 2026-01-30 | Cursor-based pagination | Matches Linear API, more reliable than offset-based | Offset-based pagination |
| 2026-01-30 | Accept human identifiers (ENG-123) | Better UX - users know identifiers, not UUIDs | UUID-only |
| 2026-01-30 | Stateless architecture | Simpler to reason about, no cache invalidation, no concurrency issues | Stateful with caching |
| 2026-01-30 | 30s timeout default | Balance between slow queries and premature failures | 10s, 60s, no timeout |
| 2026-01-30 | Pino for logging | Fast, structured JSON logging, production-ready | Winston, Bunyan, console.log |
| 2026-01-30 | Simple throttle (10 req/s) | Prevents overwhelming API, no upfront complexity prediction needed | Complex complexity tracking |
| 2026-01-30 | No caching in MVP | Stateless is simpler, add caching in Phase 2 if needed | Aggressive caching from start |
| 2026-01-30 | Group search parameters | Clearer structure for LLMs, better organization | Flat parameter list |
| 2026-01-30 | Structured error responses | LLM-actionable with suggestions, codes, and context | Simple error strings |
---
## Appendix A: GraphQL Schema Reference
### Key Linear Types
**Issue**:
```graphql
type Issue {
id: ID!
identifier: String! # Human-readable (ENG-123)
title: String!
description: String
priority: Int # 0-4
state: WorkflowState!
assignee: User
team: Team!
project: Project
labels: [Label!]!
comments: [Comment!]!
createdAt: DateTime!
updatedAt: DateTime!
url: String!
}
```
**WorkflowState**:
```graphql
type WorkflowState {
id: ID!
name: String!
type: String! # backlog, unstarted, started, completed, canceled
color: String!
position: Int!
team: Team!
}
```
**Team**:
```graphql
type Team {
id: ID!
name: String!
key: String! # Prefix for identifiers (ENG)
description: String
icon: String
}
```
**User**:
```graphql
type User {
id: ID!
name: String!
email: String!
active: Boolean!
}
```
---
## Appendix B: References
1. **Linear API Documentation**: https://linear.app/developers
2. **Linear GraphQL Schema**: https://studio.apollographql.com/public/Linear-API/
3. **Linear TypeScript SDK**: https://github.com/linear/linear/tree/master/packages/sdk
4. **MCP Specification**: https://modelcontextprotocol.io/
5. **MCP SDK (TypeScript)**: https://github.com/modelcontextprotocol/typescript-sdk
6. **Pino Logger**: https://getpino.io/
7. **Zod Schema Validation**: https://zod.dev/
---
**Document Version:** 1.0.0
**Last Updated:** January 30, 2026
**Status:** ✅ Architecture Review Complete - Ready for Implementation
**Next Steps:**
1. Review this document with stakeholders
2. Set up project structure
3. Begin Phase 1 implementation (MVP)
4. Establish CI/CD pipeline
5. Create deployment documentation