This document outlines the architecture of the Linear MCP (Model Context Protocol) implementation.
The Linear MCP provides a type-safe, modular interface to the Linear API. It abstracts away the complexity of GraphQL operations while providing a clean, domain-driven API surface through MCP tools.
The codebase is organized around business domains:
- Authentication
- Issues
- Projects
- Teams
- Users
Each domain has its own set of:
- Handlers (for MCP tool operations)
- Types
- Tests
The codebase follows a layered architecture pattern:
src/
├── core/ # Core infrastructure
│ ├── handlers/ # Base handler and factory
│ │ ├── base.handler.ts
│ │ └── handler.factory.ts
│ ├── types/ # Shared type definitions
│ │ ├── tool.types.ts # MCP tool schemas
│ │ └── common.types.ts
│ └── interfaces/ # Core interfaces
│ └── tool-handler.interface.ts
│
├── features/ # Feature modules by domain
│ ├── auth/ # Authentication
│ │ └── handlers/
│ ├── issues/ # Issue management
│ │ └── handlers/
│ ├── projects/ # Project operations
│ │ └── handlers/
│ ├── teams/ # Team operations
│ │ └── handlers/
│ └── users/ # User operations
│ └── handlers/
│
├── infrastructure/ # Infrastructure concerns
│ ├── graphql/ # GraphQL implementation
│ │ ├── operations/ # GraphQL operations by domain
│ │ └── fragments/ # Shared GraphQL fragments
│ └── http/ # HTTP client
│
└── utils/ # Shared utilities
├── logger.ts # Logging system
└── config.ts # Configuration management
The handler system provides a clean separation of concerns for MCP tool operations:
// Base handler with shared functionality
abstract class BaseHandler {
protected verifyAuth(): LinearGraphQLClient;
protected createResponse(text: string): BaseToolResponse;
protected createJsonResponse(data: unknown): BaseToolResponse;
protected handleError(error: unknown, operation: string): never;
protected validateRequiredParams(params: Record<string, unknown>, required: string[]): void;
}
// Feature-specific handlers extend the base
class IssueHandler extends BaseHandler {
handleCreateIssue(args: any): Promise<BaseToolResponse>;
handleSearchIssues(args: any): Promise<BaseToolResponse>;
// ... other issue operations
}
// Factory for managing handlers
class HandlerFactory {
private authHandler: AuthHandler;
private issueHandler: IssueHandler;
// ... other handlers
getHandlerForTool(toolName: string): { handler: BaseHandler; method: string };
}
The authentication system supports both PAT and OAuth flows:
class AuthHandler extends BaseHandler {
handleAuth(args: any): Promise<BaseToolResponse>;
handleAuthCallback(args: any): Promise<BaseToolResponse>;
}
interface AuthConfig {
type: 'pat' | 'oauth';
accessToken?: string;
clientId?: string;
clientSecret?: string;
redirectUri?: string;
}
The GraphQL layer provides domain-specific operations with atomic and composite patterns:
class LinearGraphQLClient {
// Execute GraphQL operations
async execute<T>(document: DocumentNode, variables?: any): Promise<T>;
// Atomic operations
async createProject(input: ProjectInput): Promise<ProjectResponse>;
async createBatchIssues(issues: CreateIssueInput[]): Promise<IssueBatchResponse>;
// Composite operations (built from atomic operations)
async createProjectWithIssues(
projectInput: ProjectInput,
issues: CreateIssueInput[]
): Promise<ProjectResponse> {
// Creates project first, then creates issues with project reference
const project = await this.createProject(projectInput);
const issuesWithProject = issues.map(issue => ({
...issue,
projectId: project.projectCreate.project.id
}));
const batchResult = await this.createBatchIssues(issuesWithProject);
return { projectCreate: project.projectCreate, issueBatchCreate: batchResult.issueBatchCreate };
}
}
This pattern ensures:
- Clear separation between atomic and composite operations
- Type safety through the entire operation chain
- Proper error handling at each step
- Reusable atomic operations
Errors are handled consistently through the MCP error system:
interface BaseToolResponse {
content: Array<{
type: string;
text: string;
}>;
}
interface ErrorToolResponse extends BaseToolResponse {
isError: true;
}
-
Handler Organization
- Each domain has its own handler
- Handlers extend BaseHandler
- Keep handler methods focused and single-purpose
-
Type Safety
- Define tool schemas in tool.types.ts
- Use interfaces for handler methods
- Minimize use of 'any' type
-
Error Handling
- Use BaseHandler error methods
- Provide clear error messages
- Include operation context in errors
-
Testing
- Test each handler independently
- Use integration tests for full flows
- Mock GraphQL responses
- Replace all 'any' types with proper interfaces
- Generate types from GraphQL schema
- Add runtime type checking
- Implement JSON schema validation for inputs
- Improve error messages for validation failures
- Implement true batch mutations for bulk operations
- Pre-import and cache GraphQL operations
- Add query batching for related operations
- Implement proper error handling for GraphQL errors
- Move to GraphQL code generation
- Add operation validation
- Add comprehensive input validation
- Implement response caching with invalidation
- Add retry logic with backoff strategy
- Add handler lifecycle hooks
- Improve error context and debugging
- Complete OAuth flow with proper state management
- Add token refresh with automatic retry
- Implement secure token storage
- Add proper error handling for OAuth flows
- Support multiple OAuth scopes
- Implement true batching for bulk operations
- Move to code generation for type safety
- Add operation validation and optimization
- Implement proper error handling
- Add query complexity analysis
- Split authentication into separate implementations:
interface ILinearAuth { initialize(config: AuthConfig): void; isAuthenticated(): boolean; getClient(): LinearClient; } class OAuthLinearAuth implements ILinearAuth { // OAuth-specific implementation } class PatLinearAuth implements ILinearAuth { // PAT-specific implementation }
- Implement caching for frequently accessed data:
interface CacheConfig { ttl: number; maxSize: number; } class CacheManager { private cache: Map<string, CacheEntry>; get<T>(key: string): T | undefined; set<T>(key: string, value: T, ttl?: number): void; invalidate(pattern: string): void; }
- Add rate limiting middleware:
class RateLimiter { private readonly limits: Map<string, number>; private readonly windowMs: number; async checkLimit(operation: string): Promise<boolean>; async waitForAvailability(operation: string): Promise<void>; }
- Implement domain-specific error types:
class LinearApiError extends Error { constructor( public code: string, public operation: string, message: string ) { super(message); } }
When contributing to this codebase:
- Follow the handler pattern
- Maintain domain separation
- Add tests for new handlers
- Update tool schemas
- Keep handlers focused
- Document new tools
Keep related code together:
features/issues/
├── handlers/ # Issue-related handlers
│ └── issue.handler.ts
├── types/ # Issue-specific types
│ └── issue.types.ts
└── __tests__/ # Tests
├── issue.test.ts
└── issue.integration.test.ts
- Keep dependencies minimal
- Use peer dependencies where appropriate
- Lock dependency versions
- Document breaking changes