MCP Server
by agentico-dev
- src
- types
import { LinearClient } from '@linear/sdk';
// Tool argument interfaces
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
export interface LinearSearchFilter {
assignedToUserIds?: string[];
creatorIds?: string[];
}
export interface SearchIssuesArgs {
query: string;
includeRelationships?: boolean;
filter?: {
assignedTo?: string; // User ID or 'me' for self
createdBy?: string; // User ID or 'me' for self
};
}
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[];
}
// 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')));
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';
// 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;
};