Linear MCP Server
by cosmix
- src
- types
import { LinearClient } from '@linear/sdk';
// Tool argument interfaces
export interface GetProjectsArgs {
nameFilter?: string; // Optional filter by project name
includeArchived?: boolean; // Whether to include archived projects
first?: number; // Pagination: number of items to return (default: 50, max: 100)
after?: string; // Pagination: cursor for fetching next page
}
export interface GetProjectUpdatesArgs {
projectId: string;
includeArchived?: boolean;
first?: number;
after?: string; // Cursor for pagination
createdAfter?: string;
createdBefore?: string;
userId?: string; // User ID or 'me' for self
health?: string;
}
export interface GetTeamsArgs {
nameFilter?: string; // Optional filter to search by team name
}
export interface GetIssueArgs {
issueId: string;
includeRelationships?: boolean; // Include comments, parent/sub-issues, and related issues
}
export interface LinearUser {
id: string;
name: string;
email: string;
}
// Internal type used by the service for actual Linear API calls
// Comparator types for different field types
export interface StringComparators {
eq?: string;
neq?: string;
in?: string[];
nin?: string[];
eqIgnoreCase?: string;
neqIgnoreCase?: string;
startsWith?: string;
notStartsWith?: string;
endsWith?: string;
notEndsWith?: string;
contains?: string;
notContains?: string;
containsIgnoreCase?: string;
notContainsIgnoreCase?: string;
null?: boolean;
}
export interface NumberComparators {
eq?: number;
neq?: number;
in?: number[];
nin?: number[];
lt?: number;
lte?: number;
gt?: number;
gte?: number;
null?: boolean;
}
export interface DateComparators {
eq?: string;
neq?: string;
lt?: string;
lte?: string;
gt?: string;
gte?: string;
null?: boolean;
}
// Issue field filters
export interface IssueFieldFilters {
title?: StringComparators;
description?: StringComparators;
priority?: NumberComparators;
estimate?: NumberComparators;
dueDate?: DateComparators;
createdAt?: DateComparators;
updatedAt?: DateComparators;
completedAt?: DateComparators;
startedAt?: DateComparators;
canceledAt?: DateComparators;
// Relationship filters
assignee?: { id?: StringComparators; name?: StringComparators };
creator?: { id?: StringComparators; name?: StringComparators };
team?: { id?: StringComparators; name?: StringComparators; key?: StringComparators };
state?: { id?: StringComparators; name?: StringComparators; type?: StringComparators };
labels?: { name?: StringComparators; every?: { name?: StringComparators } };
project?: { id?: StringComparators; name?: StringComparators };
}
export interface SearchIssuesArgs {
query: string;
includeRelationships?: boolean;
filter?: IssueFieldFilters & {
// Maintain backward compatibility
assignedTo?: string; // User ID or 'me' for self
createdBy?: string; // User ID or 'me' for self
// Support logical operators
and?: IssueFieldFilters[];
or?: IssueFieldFilters[];
};
projectId?: string; // Filter by project ID (backward compatibility)
projectName?: string; // Filter by project name (backward compatibility)
}
export interface CreateIssueArgs {
teamId?: string; // Optional if parentId is provided
title: string;
description?: string;
parentId?: string; // Optional parent issue ID, not identifier
status?: string;
priority?: number;
assigneeId?: string; // User ID or 'me' for self
labelIds?: string[]; // Optional array of label IDs to attach
}
export interface CreateCommentArgs {
issueId: string; // ID of the issue to comment on
body: string; // Comment content
}
export interface UpdateIssueArgs {
issueId: string; // ID or key of issue to update
title?: string; // New title
description?: string; // New description
status?: string; // New status
priority?: number; // New priority
assigneeId?: string; // User ID or 'me' for self
labelIds?: string[]; // New labels
}
export interface DeleteIssueArgs {
issueId: string; // ID of the issue to delete
}
// Linear data interfaces
export interface LinearComment {
id: string;
body: string;
userId: string;
userName?: string;
createdAt: string;
updatedAt?: string;
}
export interface LinearRelationship {
type: 'parent' | 'sub' | 'related' | 'blocked' | 'blocking' | 'duplicate';
issueId: string;
identifier: string;
title: string;
}
export interface LinearIssue {
id: string;
identifier: string;
title: string;
description?: string | null;
status?: string;
assignee?: string | null;
priority?: number;
createdAt?: string;
updatedAt?: string;
// Enhanced fields that Linear API supports
teamName?: string;
creatorName?: string;
labels?: string[];
estimate?: number;
dueDate?: string;
// Core relationships (always included)
parent?: {
id: string;
identifier: string;
title: string;
};
subIssues: {
id: string;
identifier: string;
title: string;
}[];
// Optional relationships (when includeRelationships=true)
comments?: LinearComment[];
relationships?: LinearRelationship[]; // Other relationships like blocked/blocking/duplicate
// Extracted data
mentionedIssues?: string[]; // Issue identifiers mentioned in description/comments
mentionedUsers?: string[]; // Usernames mentioned in description/comments
}
export interface LinearTeam {
id: string;
name: string;
key: string;
description?: string;
}
export interface LinearIssueSearchResult {
id: string;
identifier: string;
title: string;
status?: string;
assignee?: string | null;
priority?: number;
teamName?: string;
labels?: string[];
}
export interface LinearProjectUpdate {
id: string;
body: string;
createdAt: string;
updatedAt: string;
health?: string;
user: {
id: string;
name: string;
displayName?: string;
email?: string;
avatarUrl?: string;
};
diffMarkdown?: string;
url?: string;
}
export interface LinearProject {
id: string;
name: string;
description?: string;
slugId: string;
icon?: string;
color?: string;
status: {
name: string;
type: string;
};
creator?: {
id: string;
name: string;
};
lead?: {
id: string;
name: string;
};
startDate?: string;
targetDate?: string;
startedAt?: string;
completedAt?: string;
canceledAt?: string;
progress?: number;
health?: string;
teams: {
id: string;
name: string;
key: string;
}[];
}
export interface LinearProjectsResponse {
projects: LinearProject[];
pageInfo: {
hasNextPage: boolean;
endCursor?: string;
};
totalCount: number;
}
export interface LinearProjectUpdateResponse {
projectUpdates: LinearProjectUpdate[];
project: {
id: string;
name: string;
};
pageInfo: {
hasNextPage: boolean;
endCursor?: string;
};
totalCount: number;
}
// Type guards
export const isGetTeamsArgs = (args: unknown): args is GetTeamsArgs =>
typeof args === 'object' &&
args !== null &&
(typeof (args as GetTeamsArgs).nameFilter === 'undefined' ||
typeof (args as GetTeamsArgs).nameFilter === 'string');
export const isGetIssueArgs = (args: unknown): args is GetIssueArgs =>
typeof args === 'object' &&
args !== null &&
typeof (args as GetIssueArgs).issueId === 'string';
export const isSearchIssuesArgs = (args: unknown): args is SearchIssuesArgs =>
typeof args === 'object' &&
args !== null &&
typeof (args as SearchIssuesArgs).query === 'string' &&
(typeof (args as SearchIssuesArgs).filter === 'undefined' ||
(typeof (args as SearchIssuesArgs).filter === 'object' &&
(args as SearchIssuesArgs).filter !== null &&
(typeof (args as SearchIssuesArgs).filter!.assignedTo === 'undefined' ||
typeof (args as SearchIssuesArgs).filter!.assignedTo === 'string') &&
(typeof (args as SearchIssuesArgs).filter!.createdBy === 'undefined' ||
typeof (args as SearchIssuesArgs).filter!.createdBy === 'string'))) &&
(typeof (args as SearchIssuesArgs).projectId === 'undefined' ||
typeof (args as SearchIssuesArgs).projectId === 'string') &&
(typeof (args as SearchIssuesArgs).projectName === 'undefined' ||
typeof (args as SearchIssuesArgs).projectName === 'string');
export const isCreateIssueArgs = (args: unknown): args is CreateIssueArgs =>
typeof args === 'object' &&
args !== null &&
typeof (args as CreateIssueArgs).title === 'string' &&
(typeof (args as CreateIssueArgs).teamId === 'undefined' || typeof (args as CreateIssueArgs).teamId === 'string') &&
(typeof (args as CreateIssueArgs).description === 'undefined' || typeof (args as CreateIssueArgs).description === 'string') &&
(typeof (args as CreateIssueArgs).parentId === 'undefined' || typeof (args as CreateIssueArgs).parentId === 'string') &&
(typeof (args as CreateIssueArgs).status === 'undefined' || typeof (args as CreateIssueArgs).status === 'string') &&
(typeof (args as CreateIssueArgs).priority === 'undefined' || typeof (args as CreateIssueArgs).priority === 'number') &&
(typeof (args as CreateIssueArgs).assigneeId === 'undefined' || typeof (args as CreateIssueArgs).assigneeId === 'string') &&
(typeof (args as CreateIssueArgs).labelIds === 'undefined' || (Array.isArray((args as CreateIssueArgs).labelIds) &&
(args as CreateIssueArgs).labelIds!.every(id => typeof id === 'string'))) &&
// Ensure either teamId or parentId is provided
((args as CreateIssueArgs).teamId !== undefined || (args as CreateIssueArgs).parentId !== undefined);
export const isDeleteIssueArgs = (args: unknown): args is DeleteIssueArgs =>
typeof args === 'object' &&
args !== null &&
typeof (args as DeleteIssueArgs).issueId === 'string';
export const isUpdateIssueArgs = (args: unknown): args is UpdateIssueArgs =>
typeof args === 'object' &&
args !== null &&
typeof (args as UpdateIssueArgs).issueId === 'string' &&
(typeof (args as UpdateIssueArgs).title === 'undefined' || typeof (args as UpdateIssueArgs).title === 'string') &&
(typeof (args as UpdateIssueArgs).description === 'undefined' || typeof (args as UpdateIssueArgs).description === 'string') &&
(typeof (args as UpdateIssueArgs).status === 'undefined' || typeof (args as UpdateIssueArgs).status === 'string') &&
(typeof (args as UpdateIssueArgs).priority === 'undefined' || typeof (args as UpdateIssueArgs).priority === 'number') &&
(typeof (args as UpdateIssueArgs).assigneeId === 'undefined' || typeof (args as UpdateIssueArgs).assigneeId === 'string') &&
(typeof (args as UpdateIssueArgs).labelIds === 'undefined' || (Array.isArray((args as UpdateIssueArgs).labelIds) &&
(args as UpdateIssueArgs).labelIds!.every(id => typeof id === 'string')));
export const isCreateCommentArgs = (args: unknown): args is CreateCommentArgs =>
typeof args === 'object' &&
args !== null &&
typeof (args as CreateCommentArgs).issueId === 'string' &&
typeof (args as CreateCommentArgs).body === 'string';
export const isGetProjectsArgs = (args: unknown): args is GetProjectsArgs =>
typeof args === 'object' &&
args !== null &&
(typeof (args as GetProjectsArgs).nameFilter === 'undefined' ||
typeof (args as GetProjectsArgs).nameFilter === 'string') &&
(typeof (args as GetProjectsArgs).includeArchived === 'undefined' ||
typeof (args as GetProjectsArgs).includeArchived === 'boolean') &&
(typeof (args as GetProjectsArgs).first === 'undefined' ||
typeof (args as GetProjectsArgs).first === 'number') &&
(typeof (args as GetProjectsArgs).after === 'undefined' ||
typeof (args as GetProjectsArgs).after === 'string');
export const isGetProjectUpdatesArgs = (args: unknown): args is GetProjectUpdatesArgs =>
typeof args === 'object' &&
args !== null &&
typeof (args as GetProjectUpdatesArgs).projectId === 'string' &&
(typeof (args as GetProjectUpdatesArgs).includeArchived === 'undefined' ||
typeof (args as GetProjectUpdatesArgs).includeArchived === 'boolean') &&
(typeof (args as GetProjectUpdatesArgs).first === 'undefined' ||
typeof (args as GetProjectUpdatesArgs).first === 'number') &&
(typeof (args as GetProjectUpdatesArgs).after === 'undefined' ||
typeof (args as GetProjectUpdatesArgs).after === 'string') &&
(typeof (args as GetProjectUpdatesArgs).createdAfter === 'undefined' ||
typeof (args as GetProjectUpdatesArgs).createdAfter === 'string') &&
(typeof (args as GetProjectUpdatesArgs).createdBefore === 'undefined' ||
typeof (args as GetProjectUpdatesArgs).createdBefore === 'string') &&
(typeof (args as GetProjectUpdatesArgs).userId === 'undefined' ||
typeof (args as GetProjectUpdatesArgs).userId === 'string') &&
(typeof (args as GetProjectUpdatesArgs).health === 'undefined' ||
typeof (args as GetProjectUpdatesArgs).health === 'string');
// Helper functions for data cleaning
export const extractMentions = (text: string | null | undefined): { issues: string[]; users: string[] } => {
if (!text) return { issues: [], users: [] };
// Linear uses identifiers like ABC-123
const issues = Array.from(text.matchAll(/([A-Z]+-\d+)/g)).map(m => m[1]);
// Linear uses @ mentions
const users = Array.from(text.matchAll(/@([a-zA-Z0-9_-]+)/g)).map(m => m[1]);
return {
issues: [...new Set(issues)], // Deduplicate
users: [...new Set(users)] // Deduplicate
};
};
export const cleanDescription = (description: string | null | undefined): string | null => {
if (!description) return null;
// Remove excessive whitespace
let cleaned = description.replace(/\s+/g, ' ').trim();
// Remove common markdown artifacts while preserving content
cleaned = cleaned
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') // Convert markdown links to just text
.replace(/#{1,6}\s.*?(?:\n|(?=\*\*|__|_|\[|`))/g, '') // Remove headings until newline or next markdown element
.replace(/(\*\*|__)(.*?)\1/g, '$2') // Remove bold markers but keep content
.replace(/(\*|_)(.*?)\1/g, '$2') // Remove italic markers but keep content
.replace(/`([^`]+)`/g, '$1') // Remove inline code markers but keep content
return cleaned;
};