@sylphx/linear-mcp
Version:
A Model Context Protocol (MCP) server for interacting with Linear issues, projects, teams, and more
1 lines • 135 kB
Source Map (JSON)
{"version":3,"sources":["../src/index.ts","../src/utils/linear-client.ts","../src/tools/shared/tool-definition.ts","../src/tools/issue-statuses/shared.ts","../src/tools/issue-statuses/get_issue_status.ts","../src/tools/issue-statuses/list_issue_statuses.ts","../src/tools/issue-statuses/index.ts","../src/tools/issues/shared.ts","../src/tools/issues/create_comment.ts","../src/tools/issues/create_issue.ts","../src/tools/issues/get_issue.ts","../src/tools/issues/get_issue_git_branch_name.ts","../src/tools/issues/list_comments.ts","../src/tools/issues/list_issues.ts","../src/tools/issues/update_issue.ts","../src/tools/issues/index.ts","../src/tools/labels/list_issue_labels.ts","../src/tools/labels/shared.ts","../src/tools/labels/index.ts","../src/tools/my-issues/list_my_issues.ts","../src/tools/my-issues.ts","../src/tools/project-milestones/create_project_milestone.ts","../src/tools/project-milestones/shared.ts","../src/tools/project-milestones/delete_project_milestone.ts","../src/tools/project-milestones/list_project_milestones.ts","../src/tools/project-milestones/update_project_milestone.ts","../src/tools/project-milestones/index.ts","../src/tools/projects/create_project.ts","../src/tools/projects/shared.ts","../src/tools/projects/get_project.ts","../src/tools/projects/list_projects.ts","../src/tools/projects/update_project.ts","../src/tools/projects/index.ts","../src/tools/teams/get_team.ts","../src/tools/teams/shared.ts","../src/tools/teams/list_teams.ts","../src/tools/teams/index.ts","../src/tools/users/get_user.ts","../src/tools/users/shared.ts","../src/tools/users/list_users.ts","../src/tools/users/index.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport { McpServer, type ToolCallback } from '@modelcontextprotocol/sdk/server/mcp.js';\n// import { Server } from '@modelcontextprotocol/sdk/server/index.js';\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';\n\n// Re-export McpError for use in other files\nexport { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';\n\n// Log the available ErrorCode values for debugging\n// console.log('Available ErrorCode values:', Object.keys(ErrorCode));\n\n// Import Linear client\nimport { LinearClientManager } from './utils/linear-client.js';\n\nimport { issueStatusTools } from './tools/issue-statuses/index.js';\nimport { issueTools } from './tools/issues/index.js';\nimport { labelTools } from './tools/labels/index.js';\nimport { myIssuesTools } from './tools/my-issues.js';\nimport { projectMilestoneTools } from './tools/project-milestones/index.js';\nimport { projectTools } from './tools/projects/index.js';\nimport { teamTools } from './tools/teams/index.js';\nimport { userTools } from './tools/users/index.js';\n\n// Initialize the Linear client with the API key from environment variables\nconst initializeLinearClient = () => {\n const apiKey = process.env.LINEAR_API_KEY;\n\n if (!apiKey) {\n process.exit(1);\n }\n\n try {\n LinearClientManager.getInstance().initialize(apiKey);\n // console.log('Linear client initialized successfully');\n } catch (_error) {\n process.exit(1);\n }\n};\n\n// --- Server Setup ---\nconst server = new McpServer({\n name: 'linear-mcp',\n version: '1.0.0',\n description: 'MCP Server for interacting with Linear issues, projects, teams, and more',\n});\n\n// Combine all tools\nconst allTools = [\n ...issueTools,\n ...projectTools,\n ...teamTools,\n ...userTools,\n ...issueStatusTools,\n ...labelTools,\n ...myIssuesTools,\n ...projectMilestoneTools,\n];\n\nfor (const tool of allTools) {\n server.tool(tool.name, tool.description, tool.inputSchema, tool.handler);\n}\n\n// Main function to start the server\nexport const startServer = async () => {\n try {\n // Initialize the Linear client\n initializeLinearClient();\n\n // Connect the server to the transport\n const transport = new StdioServerTransport();\n await server.connect(transport);\n\n // console.log('Linear MCP server started successfully');\n } catch (_error) {\n // console.error('Failed to start Linear MCP server:', error);\n // process.exit(1);\n }\n};\n\nstartServer().catch((_error: unknown) => {\n // process.exit(1);\n});\n","import { LinearClient } from '@linear/sdk';\n\n// Singleton pattern for Linear client\nexport class LinearClientManager {\n private static instance: LinearClientManager;\n private client: LinearClient | null = null;\n\n private constructor() {}\n\n public static getInstance(): LinearClientManager {\n if (!LinearClientManager.instance) {\n LinearClientManager.instance = new LinearClientManager();\n }\n return LinearClientManager.instance;\n }\n\n public initialize(apiKey: string): void {\n if (!this.client) {\n this.client = new LinearClient({ apiKey });\n }\n }\n\n public getClient(): LinearClient {\n if (!this.client) {\n throw new Error('Linear client not initialized. Call initialize() first.');\n }\n return this.client;\n }\n}\n\nexport const getLinearClient = (): LinearClient => {\n return LinearClientManager.getInstance().getClient();\n};\n","import type { ToolCallback } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport type { z } from 'zod';\n\nexport interface ToolDefinition<T extends z.ZodRawShape = z.ZodRawShape> {\n name: string;\n description: string;\n inputSchema: T;\n handler: ToolCallback<T>;\n}\n\nexport const defineTool = <T extends z.ZodRawShape>(\n tool: ToolDefinition<T>,\n): ToolDefinition<T> => ({\n name: tool.name,\n description: tool.description,\n inputSchema: tool.inputSchema,\n handler: tool.handler,\n});\n","import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';\nimport { z } from 'zod';\nimport { getLinearClient } from '../../utils/linear-client.js';\n\n// --- Tool definition utility (local copy) ---\n\n// --- Issue Status schemas (local copy) ---\nexport const IssueStatusListSchema = {\n teamId: z.string().describe('The team UUID'),\n};\nexport const IssueStatusQuerySchema = {\n query: z.string().describe('The UUID or name of the issue status to retrieve'),\n teamId: z.string().describe('The team UUID'),\n};\n\nexport async function validateTeamOrThrow(teamId: string) {\n const linearClient = getLinearClient();\n const team = await linearClient.team(teamId);\n if (!team) {\n let availableTeamsMessage = '';\n try {\n const allTeams = await linearClient.teams();\n if (allTeams.nodes.length > 0) {\n const teamList = allTeams.nodes.map((t) => ({\n id: t.id,\n name: t.name,\n }));\n availableTeamsMessage = ` Valid teams are: ${JSON.stringify(teamList, null, 2)}`;\n } else {\n availableTeamsMessage = ' No teams available to list.';\n }\n } catch (_listError) {\n availableTeamsMessage = ' (Could not fetch available teams for context.)';\n }\n throw new McpError(\n ErrorCode.InvalidParams,\n `Team with ID '${teamId}' not found.${availableTeamsMessage}`,\n );\n }\n return team;\n}\n\nexport function throwInternalError(message: string, error: unknown): never {\n const err = error as { message?: string };\n throw new McpError(ErrorCode.InternalError, `${message}: ${err.message || 'Unknown error'}`);\n}\n","import { defineTool } from '../shared/tool-definition.js';\nimport { IssueStatusQuerySchema } from './shared.js';\nimport { throwInternalError, validateTeamOrThrow } from './shared.js';\n\nexport const getIssueStatusTool = defineTool({\n name: 'get_issue_status',\n description: 'Retrieve details of a specific issue status in Linear by name or ID',\n inputSchema: IssueStatusQuerySchema,\n handler: async ({ query, teamId }) => {\n try {\n const team = await validateTeamOrThrow(teamId);\n const states = await team.states();\n let state = states.nodes.find((s) => s.id === query);\n if (!state) {\n state = states.nodes.find((s) => s.name.toLowerCase() === query.toLowerCase());\n }\n if (state) {\n return {\n content: [\n {\n type: 'text',\n text: JSON.stringify({\n id: state.id,\n name: state.name,\n color: state.color,\n type: state.type,\n description: state.description,\n position: state.position,\n }),\n },\n ],\n };\n }\n const validStatuses = states.nodes.map((s) => ({\n id: s.id,\n name: s.name,\n type: s.type,\n }));\n throw new Error(\n `Issue status with query \"${query}\" not found in team '${team.name}' (${teamId}). Valid statuses for this team are: ${JSON.stringify(validStatuses, null, 2)}`,\n );\n } catch (error: unknown) {\n throwInternalError('Failed to get issue status', error);\n }\n },\n});\n","import { defineTool } from '../shared/tool-definition.js';\nimport { IssueStatusListSchema } from './shared.js';\nimport { throwInternalError, validateTeamOrThrow } from './shared.js';\n\nexport const listIssueStatusesTool = defineTool({\n name: 'list_issue_statuses',\n description: 'List available issues statuses in a Linear team',\n inputSchema: IssueStatusListSchema,\n handler: async ({ teamId }) => {\n try {\n const team = await validateTeamOrThrow(teamId);\n const states = await team.states();\n return {\n content: [\n {\n type: 'text',\n text: JSON.stringify(\n states.nodes.map((state) => ({\n id: state.id,\n name: state.name,\n color: state.color,\n type: state.type,\n description: state.description,\n position: state.position,\n })),\n ),\n },\n ],\n };\n } catch (error: unknown) {\n throwInternalError('Failed to list issue statuses', error);\n }\n },\n});\n","import { getIssueStatusTool } from './get_issue_status.js';\nimport { listIssueStatusesTool } from './list_issue_statuses.js';\n\nexport const issueStatusTools = [listIssueStatusesTool, getIssueStatusTool];\n","import {\n type Attachment,\n type Issue,\n type IssuePayload,\n type LinearClient,\n LinearDocument,\n type Project,\n type ProjectMilestone,\n type Team,\n type User,\n type WorkflowState,\n} from '@linear/sdk';\nimport { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';\n// Shared types, mapping, and validation utilities for issues tools\nimport { z } from 'zod';\n\n// --- Tool definition utility (local copy) ---\n\n// --- Common schemas (local copy) ---\nexport const IdSchema = {\n id: z.string().describe('The issue ID'),\n};\nexport const PaginationSchema = {\n limit: z.number().default(50).describe('The number of items to return'),\n before: z.string().optional().describe('A UUID to end at'),\n after: z.string().optional().describe('A UUID to start from'),\n orderBy: z.enum(['createdAt', 'updatedAt']).default('updatedAt'),\n};\n\n// --- Issue schemas (local copy) ---\nexport const IssueFilterSchema = {\n query: z.string().optional().describe('An optional search query'),\n teamId: z.string().optional().describe('The team UUID'),\n stateId: z.string().optional().describe('The state UUID'),\n assigneeId: z.string().optional().describe('The assignee UUID'),\n projectMilestoneId: z\n .string()\n .uuid('Invalid project milestone ID')\n .optional()\n .describe('The project milestone ID to filter by'),\n includeArchived: z.boolean().default(true).describe('Whether to include archived issues'),\n limit: z.number().default(50).describe('The number of issues to return'),\n projectId: z.string().optional().describe('The project ID to filter by'),\n};\nexport const IssueCreateSchema = {\n title: z.string().describe('The issue title'),\n description: z.string().optional().describe('The issue description as Markdown'),\n teamId: z.string().describe('The team UUID'),\n priority: z\n .number()\n .optional()\n .describe('The issue priority. 0 = No priority, 1 = Urgent, 2 = High, 3 = Normal, 4 = Low.'),\n projectId: z.string().optional().describe('The project ID to add the issue to'),\n stateId: z.string().optional().describe('The issue state ID'),\n assigneeId: z.string().optional().describe('The assignee ID'),\n labelIds: z.array(z.string()).optional().describe('Array of label IDs to set on the issue'),\n dueDate: z.string().optional().describe('The due date for the issue in ISO format'),\n projectMilestoneId: z\n .string()\n .uuid('Invalid project milestone ID')\n .optional()\n .describe('The project milestone ID to associate the issue with'),\n};\nexport const IssueUpdateSchema = {\n id: z.string().describe('The issue ID'),\n title: z.string().optional().describe('The issue title'),\n description: z.string().optional().describe('The issue description as Markdown'),\n priority: z\n .number()\n .optional()\n .describe('The issue priority. 0 = No priority, 1 = Urgent, 2 = High, 3 = Normal, 4 = Low.'),\n projectId: z.string().optional().describe('The project ID to add the issue to'),\n stateId: z.string().optional().describe('The issue state ID'),\n assigneeId: z.string().optional().describe('The assignee ID'),\n labelIds: z.array(z.string()).optional().describe('Array of label IDs to set on the issue'),\n dueDate: z.string().optional().describe('The due date for the issue in ISO format'),\n projectMilestoneId: z\n .string()\n .uuid('Invalid project milestone ID')\n .nullable()\n .optional()\n .describe('The project milestone ID to associate the issue with (null to remove)'),\n};\nexport const CommentCreateSchema = {\n issueId: z.string().describe('The issue ID'),\n body: z.string().describe('The content of the comment as Markdown'),\n};\n\n// --- Utility functions to get available entities as JSON for error messages ---\nexport async function getAvailableTeamsJson(linearClient: LinearClient): Promise<string> {\n try {\n const teams = await linearClient.teams();\n if (!teams.nodes || teams.nodes.length === 0) return '[]';\n return JSON.stringify(\n teams.nodes.map((team) => ({ id: team.id, name: team.name, key: team.key })),\n null,\n 2,\n );\n } catch (e) {\n return `\"(Could not fetch available teams as JSON: ${(e as Error).message})\"`;\n }\n}\nexport async function getAvailableProjectsJson(linearClient: LinearClient): Promise<string> {\n try {\n const projects = await linearClient.projects();\n if (!projects.nodes || projects.nodes.length === 0) return '[]';\n return JSON.stringify(\n projects.nodes.map((project) => ({ id: project.id, name: project.name })),\n null,\n 2,\n );\n } catch (e) {\n return `\"(Could not fetch available projects as JSON: ${(e as Error).message})\"`;\n }\n}\nexport async function getAvailableStatesJson(\n linearClient: LinearClient,\n teamId: string,\n): Promise<string> {\n if (!teamId) return `\"(Cannot fetch states without a valid teamId.)\"`;\n try {\n const team = await linearClient.team(teamId);\n if (!team)\n return `\"(Could not fetch states: Team with ID '${teamId}' not found. Valid teams are: ${await getAvailableTeamsJson(linearClient)})\"`;\n const states = await team.states();\n if (!states.nodes || states.nodes.length === 0) return '[]';\n return JSON.stringify(\n states.nodes.map((state) => ({ id: state.id, name: state.name, type: state.type })),\n null,\n 2,\n );\n } catch (e) {\n return `\"(Could not fetch states for team ${teamId} as JSON: ${(e as Error).message})\"`;\n }\n}\nexport async function getAvailableAssigneesJson(linearClient: LinearClient): Promise<string> {\n try {\n const users = await linearClient.users({ filter: { active: { eq: true } } });\n if (!users.nodes || users.nodes.length === 0) return '[]';\n return JSON.stringify(\n users.nodes.map((user) => ({ id: user.id, name: user.displayName, email: user.email })),\n null,\n 2,\n );\n } catch (e) {\n return `\"(Could not fetch available assignees as JSON: ${(e as Error).message})\"`;\n }\n}\nexport async function getAvailableLabelsJson(\n linearClient: LinearClient,\n teamId: string,\n): Promise<string> {\n if (!teamId) return `\"(Cannot fetch labels without a valid teamId.)\"`;\n try {\n const team = await linearClient.team(teamId);\n if (!team)\n return `\"(Could not fetch labels: Team with ID '${teamId}' not found. Valid teams are: ${await getAvailableTeamsJson(linearClient)})\"`;\n const labels = await team.labels();\n if (!labels.nodes || labels.nodes.length === 0) return '[]';\n return JSON.stringify(\n labels.nodes.map((label) => ({ id: label.id, name: label.name, color: label.color })),\n null,\n 2,\n );\n } catch (e) {\n return `\"(Could not fetch labels for team ${teamId} as JSON: ${(e as Error).message})\"`;\n }\n}\nexport async function getAvailableProjectMilestonesJson(\n linearClient: LinearClient,\n projectId?: string,\n): Promise<string> {\n try {\n if (projectId && projectId !== 'any' && projectId.trim() !== '') {\n const project = await linearClient.project(projectId);\n if (!project)\n return `\"(Could not fetch milestones: Project with ID '${projectId}' not found. Valid projects are: ${await getAvailableProjectsJson(linearClient)})\"`;\n const milestones = await project.projectMilestones();\n if (!milestones.nodes || milestones.nodes.length === 0) return '[]';\n return JSON.stringify(\n milestones.nodes.map((m) => ({ id: m.id, name: m.name })),\n null,\n 2,\n );\n }\n return `\"(Project milestones are specific to a project. Please provide a valid projectId. Available projects: ${await getAvailableProjectsJson(linearClient)})\"`;\n } catch (e) {\n const error = e as Error;\n if (projectId && projectId !== 'any' && error.message.toLowerCase().includes('not found')) {\n return `\"(Could not fetch milestones for project '${projectId}': ${error.message}. Valid projects are: ${await getAvailableProjectsJson(linearClient)})\"`;\n }\n return `\"(Could not fetch project milestones for project ${projectId} as JSON: ${error.message})\"`;\n }\n}\n\n// --- Shared types ---\nexport interface SimplifiedIssueDetails {\n id: string;\n identifier: string;\n title: string;\n description?: string | null;\n priority: number;\n state?: { id: string; name: string; color: string; type: string } | null;\n assignee?: { id: string; name: string; email?: string | null } | null;\n team?: { id: string; name: string; key: string } | null;\n project?: { id: string; name: string } | null;\n projectMilestone?: { id: string; name: string } | null;\n labels?: { id: string; name: string; color: string }[];\n attachments?: {\n id: string;\n title: string;\n url: string;\n source?: unknown;\n metadata?: unknown;\n groupBySource?: boolean | null;\n createdAt: Date;\n updatedAt: Date;\n }[];\n createdAt: Date;\n updatedAt: Date;\n url: string;\n}\n\nexport type IssueFilters = {\n filter?: Record<string, unknown>;\n teamId?: string;\n stateId?: string;\n assigneeId?: string;\n projectId?: string;\n includeArchived?: boolean;\n first?: number;\n};\n\n// --- Mapping ---\nexport async function mapIssueToDetails(\n issue: Issue,\n includeAttachments = false,\n): Promise<SimplifiedIssueDetails> {\n const [state, assignee, team, project, projectMilestone, labelsResult, attachmentsResult] =\n await Promise.all([\n issue.state,\n issue.assignee,\n issue.team,\n issue.project,\n issue.projectMilestone,\n issue.labels(),\n includeAttachments ? issue.attachments() : Promise.resolve(null),\n ]);\n\n return {\n id: issue.id,\n identifier: issue.identifier,\n title: issue.title,\n description: issue.description,\n priority: issue.priority,\n state: state ? { id: state.id, name: state.name, color: state.color, type: state.type } : null,\n assignee: assignee ? { id: assignee.id, name: assignee.name, email: assignee.email } : null,\n team: team ? { id: team.id, name: team.name, key: team.key } : null,\n project: project ? { id: project.id, name: project.name } : null,\n projectMilestone: projectMilestone\n ? { id: projectMilestone.id, name: projectMilestone.name }\n : null,\n labels: labelsResult.nodes.map((l) => ({ id: l.id, name: l.name, color: l.color })),\n attachments:\n includeAttachments && attachmentsResult\n ? attachmentsResult.nodes.map((att: Attachment) => ({\n id: att.id,\n title: att.title,\n url: att.url,\n source: att.source,\n metadata: att.metadata,\n groupBySource: att.groupBySource,\n createdAt: att.createdAt,\n updatedAt: att.updatedAt,\n }))\n : undefined,\n createdAt: issue.createdAt,\n updatedAt: issue.updatedAt,\n url: issue.url,\n };\n}\n\n// --- Validation helpers ---\nexport async function handleLinearError(\n error: unknown,\n entityType: string,\n entityId: string,\n contextMessage: string,\n getAvailableJsonFn: () => Promise<string> | string,\n): Promise<never> {\n if (error instanceof McpError) throw error;\n const err = error as { message?: string; extensions?: { userPresentableMessage?: string } };\n const availableJson =\n typeof getAvailableJsonFn === 'function' ? await getAvailableJsonFn() : getAvailableJsonFn;\n let specificMessage = `Invalid ${entityType}Id: '${entityId}'. ${contextMessage}.`;\n\n if (err.extensions?.userPresentableMessage) {\n specificMessage = `${specificMessage} Details: ${err.extensions.userPresentableMessage}`;\n } else if (err.message) {\n if (\n err.message.toLowerCase().includes('not found') ||\n err.message.toLowerCase().includes('no entity found') ||\n err.message.toLowerCase().includes('api error') ||\n err.message.toLowerCase().includes('invalid uuid')\n ) {\n specificMessage = `${specificMessage} ${entityType} not found or ID is invalid.`;\n } else {\n specificMessage = `${specificMessage} Error during validation: ${err.message}`;\n }\n } else {\n specificMessage = `${specificMessage} An unknown error occurred during ${entityType} validation.`;\n }\n throw new McpError(\n ErrorCode.InvalidParams,\n `${specificMessage} Valid ${entityType}s are: ${availableJson}`,\n );\n}\n\nexport async function validateTeam(\n linearClient: LinearClient,\n teamId: string,\n operationContext = '',\n): Promise<Team> {\n try {\n const team = await linearClient.team(teamId);\n if (!team) {\n const msg = operationContext ? `${operationContext}: ` : '';\n throw new McpError(\n ErrorCode.InvalidParams,\n `${msg}Team with ID '${teamId}' not found. Valid teams are: ${await getAvailableTeamsJson(linearClient)}`,\n );\n }\n return team;\n } catch (e) {\n return handleLinearError(e, 'team', teamId, operationContext, () =>\n getAvailableTeamsJson(linearClient),\n );\n }\n}\n\nexport async function validateProject(\n linearClient: LinearClient,\n projectId: string,\n operationContext = '',\n): Promise<Project> {\n try {\n const project = await linearClient.project(projectId);\n if (!project) {\n const msg = operationContext ? `${operationContext}: ` : '';\n throw new McpError(\n ErrorCode.InvalidParams,\n `${msg}Project with ID '${projectId}' not found. Valid projects are: ${await getAvailableProjectsJson(linearClient)}`,\n );\n }\n return project;\n } catch (e) {\n return handleLinearError(e, 'project', projectId, operationContext, () =>\n getAvailableProjectsJson(linearClient),\n );\n }\n}\n\nexport async function validateState(\n linearClient: LinearClient,\n teamId: string,\n stateId: string,\n operationContext = '',\n): Promise<WorkflowState> {\n const team = await validateTeam(linearClient, teamId, `for state validation ${operationContext}`);\n try {\n const states = await team.states({ filter: { id: { eq: stateId } } });\n if (!states.nodes || states.nodes.length === 0) {\n const msg = operationContext ? `${operationContext}: ` : '';\n throw new McpError(\n ErrorCode.InvalidParams,\n `${msg}State with ID '${stateId}' not found for team '${team.name}'. Valid states are: ${await getAvailableStatesJson(linearClient, teamId)}`,\n );\n }\n return states.nodes[0];\n } catch (e) {\n return handleLinearError(\n e,\n 'state',\n stateId,\n `for team '${team.name}' ${operationContext}`,\n () => getAvailableStatesJson(linearClient, teamId),\n );\n }\n}\n\nexport async function validateAssignee(\n linearClient: LinearClient,\n assigneeId: string,\n operationContext = '',\n): Promise<User> {\n try {\n const assignee = await linearClient.user(assigneeId);\n if (!assignee || !assignee.active) {\n const msg = operationContext ? `${operationContext}: ` : '';\n let userStatus = 'User not found.';\n if (assignee && !assignee.active)\n userStatus = `User '${assignee.displayName}' is not active.`;\n throw new McpError(\n ErrorCode.InvalidParams,\n `${msg}Assignee with ID '${assigneeId}' is invalid. ${userStatus} Valid assignees are: ${await getAvailableAssigneesJson(linearClient)}`,\n );\n }\n return assignee;\n } catch (e) {\n return handleLinearError(e, 'assignee', assigneeId, operationContext, () =>\n getAvailableAssigneesJson(linearClient),\n );\n }\n}\n\nexport async function validateLabels(\n linearClient: LinearClient,\n teamId: string,\n labelIds: string[],\n operationContext = '',\n): Promise<void> {\n const team = await validateTeam(linearClient, teamId, `for label validation ${operationContext}`);\n try {\n const labels = await team.labels({ filter: { id: { in: labelIds } } });\n const foundLabelIds = labels.nodes.map((l) => l.id);\n const notFoundLabelIds = labelIds.filter((id) => !foundLabelIds.includes(id));\n if (notFoundLabelIds.length > 0) {\n const msg = operationContext ? `${operationContext}: ` : '';\n throw new McpError(\n ErrorCode.InvalidParams,\n `${msg}Label(s) with ID(s) '${notFoundLabelIds.join(', ')}' not found for team '${team.name}'. Valid labels are: ${await getAvailableLabelsJson(linearClient, teamId)}`,\n );\n }\n } catch (e) {\n return handleLinearError(\n e,\n 'label(s)',\n labelIds.join(', '),\n `for team '${team.name}' ${operationContext}`,\n () => getAvailableLabelsJson(linearClient, teamId),\n );\n }\n}\n\nexport async function validateProjectMilestone(\n linearClient: LinearClient,\n projectMilestoneId: string,\n forProjectId?: string | null,\n operationContext = '',\n): Promise<ProjectMilestone> {\n try {\n const milestone = await linearClient.projectMilestone(projectMilestoneId);\n if (!milestone) {\n const msg = operationContext ? `${operationContext}: ` : '';\n throw new McpError(\n ErrorCode.InvalidParams,\n `${msg}Project milestone with ID '${projectMilestoneId}' not found. ${await getAvailableProjectMilestonesJson(linearClient, forProjectId ?? undefined)}`,\n );\n }\n if (forProjectId && milestone.projectId !== forProjectId) {\n const targetProject = await linearClient.project(forProjectId);\n const actualProject = await milestone.project;\n throw new McpError(\n ErrorCode.InvalidParams,\n `Project milestone '${milestone.name}' (${projectMilestoneId}) does not belong to project '${targetProject?.name ?? forProjectId}'. It belongs to '${actualProject?.name ?? milestone.projectId}'.`,\n );\n }\n return milestone;\n } catch (e) {\n return handleLinearError(e, 'project milestone', projectMilestoneId, operationContext, () =>\n getAvailableProjectMilestonesJson(linearClient, forProjectId ?? undefined),\n );\n }\n}\n\nexport async function validateIssueExists(\n linearClient: LinearClient,\n issueId: string,\n operationContext = '',\n): Promise<Issue> {\n try {\n const issue = await linearClient.issue(issueId);\n if (!issue) {\n let recentIssuesMessage = '';\n try {\n const recentIssues = await linearClient.issues({\n first: 5,\n orderBy: LinearDocument.PaginationOrderBy.UpdatedAt,\n });\n if (recentIssues.nodes.length > 0) {\n recentIssuesMessage = ` Recent issues: ${JSON.stringify(\n recentIssues.nodes.map((iss) => ({\n id: iss.id,\n title: iss.title,\n identifier: iss.identifier,\n })),\n null,\n 2,\n )}`;\n }\n } catch {\n /* ignore */\n }\n throw new McpError(\n ErrorCode.InvalidParams,\n `${operationContext}: Issue with ID '${issueId}' not found.${recentIssuesMessage}`,\n );\n }\n return issue;\n } catch (e) {\n if (e instanceof McpError) throw e;\n throw new McpError(\n ErrorCode.InternalError,\n `Error fetching issue '${issueId}' for ${operationContext}: ${(e as Error).message}`,\n );\n }\n}\n","import { getLinearClient } from '../../utils/linear-client.js';\nimport { defineTool } from '../shared/tool-definition.js';\nimport { CommentCreateSchema } from './shared.js';\nimport { validateIssueExists } from './shared.js';\n\nexport const createCommentTool = defineTool({\n name: 'create_comment',\n description: 'Create a comment on a Linear issue by ID',\n inputSchema: CommentCreateSchema,\n handler: async ({ issueId, body }) => {\n const linearClient = getLinearClient();\n await validateIssueExists(linearClient, issueId, 'creating comment');\n const commentPayload = await linearClient.createComment({ issueId, body });\n const newComment = await commentPayload.comment;\n if (!newComment)\n throw new Error(\n `Failed to create comment or retrieve details. Sync ID: ${commentPayload.lastSyncId}`,\n );\n return {\n content: [\n {\n type: 'text',\n text: JSON.stringify({\n id: newComment.id,\n body: newComment.body,\n createdAt: newComment.createdAt,\n updatedAt: newComment.updatedAt,\n userId: newComment.userId,\n }),\n },\n ],\n };\n },\n});\n","import { getLinearClient } from '../../utils/linear-client.js';\nimport { defineTool } from '../shared/tool-definition.js';\nimport { IssueCreateSchema } from './shared.js';\nimport {\n mapIssueToDetails,\n validateAssignee,\n validateLabels,\n validateProject,\n validateProjectMilestone,\n validateState,\n validateTeam,\n} from './shared.js';\n\nexport const createIssueTool = defineTool({\n name: 'create_issue',\n description: 'Create a new Linear issue',\n inputSchema: IssueCreateSchema,\n handler: async ({\n title,\n description,\n teamId,\n priority,\n projectId,\n stateId,\n assigneeId,\n labelIds,\n dueDate,\n projectMilestoneId,\n }) => {\n const linearClient = getLinearClient();\n await validateTeam(linearClient, teamId, 'creating issue');\n if (projectId) await validateProject(linearClient, projectId, 'creating issue');\n if (stateId) await validateState(linearClient, teamId, stateId, 'creating issue');\n if (assigneeId) await validateAssignee(linearClient, assigneeId, 'creating issue');\n if (labelIds && labelIds.length > 0)\n await validateLabels(linearClient, teamId, labelIds, 'creating issue');\n if (projectMilestoneId)\n await validateProjectMilestone(linearClient, projectMilestoneId, projectId, 'creating issue');\n\n const payload = {\n title,\n description,\n teamId,\n priority,\n projectId,\n stateId,\n assigneeId,\n labelIds,\n dueDate,\n projectMilestoneId,\n };\n const issueCreate = await linearClient.createIssue(payload);\n const newIssue = await issueCreate.issue;\n if (!newIssue)\n throw new Error(\n `Failed to create issue or retrieve details. Sync ID: ${issueCreate.lastSyncId}`,\n );\n const detailedNewIssue = await mapIssueToDetails(newIssue, false);\n return { content: [{ type: 'text', text: JSON.stringify(detailedNewIssue) }] };\n },\n});\n","import { getLinearClient } from '../../utils/linear-client.js';\nimport { defineTool } from '../shared/tool-definition.js';\nimport { IdSchema } from './shared.js';\nimport { mapIssueToDetails, validateIssueExists } from './shared.js';\n\nexport const getIssueTool = defineTool({\n name: 'get_issue',\n description: 'Retrieve a Linear issue details by ID, including attachments',\n inputSchema: IdSchema,\n handler: async ({ id }) => {\n const linearClient = getLinearClient();\n const issue = await validateIssueExists(linearClient, id, 'getting issue details');\n const detailedIssue = await mapIssueToDetails(issue, true);\n return { content: [{ type: 'text', text: JSON.stringify(detailedIssue) }] };\n },\n});\n","import { getLinearClient } from '../../utils/linear-client.js';\nimport { defineTool } from '../shared/tool-definition.js';\nimport { IdSchema } from './shared.js';\nimport { validateIssueExists } from './shared.js';\n\nexport const getIssueGitBranchNameTool = defineTool({\n name: 'get_issue_git_branch_name',\n description: 'Retrieve the branch name for a Linear issue by ID',\n inputSchema: IdSchema,\n handler: async ({ id }) => {\n const linearClient = getLinearClient();\n const issue = await validateIssueExists(linearClient, id, 'getting git branch name');\n const branchName = await issue.branchName;\n return {\n content: [\n {\n type: 'text',\n text: JSON.stringify({\n id: issue.id,\n identifier: issue.identifier,\n title: issue.title,\n branchName,\n }),\n },\n ],\n };\n },\n});\n","import type { Comment as LinearComment } from '@linear/sdk';\nimport { z } from 'zod';\nimport { getLinearClient } from '../../utils/linear-client.js';\nimport { defineTool } from '../shared/tool-definition.js';\nimport { validateIssueExists } from './shared.js';\n\nexport const listCommentsTool = defineTool({\n name: 'list_comments',\n description: 'Retrieve comments for a Linear issue by ID',\n inputSchema: { issueId: z.string().describe('The ID of the issue to fetch comments for') },\n handler: async ({ issueId }) => {\n const linearClient = getLinearClient();\n const issue = await validateIssueExists(linearClient, issueId, 'listing comments');\n const comments = await issue.comments();\n const commentDetails = comments.nodes.map((comment: LinearComment) => ({\n id: comment.id,\n body: comment.body,\n createdAt: comment.createdAt,\n updatedAt: comment.updatedAt,\n userId: comment.userId,\n }));\n return { content: [{ type: 'text', text: JSON.stringify(commentDetails) }] };\n },\n});\n","import { getLinearClient } from '../../utils/linear-client.js';\nimport { defineTool } from '../shared/tool-definition.js';\nimport { IssueFilterSchema } from './shared.js';\nimport {\n type IssueFilters,\n mapIssueToDetails,\n validateAssignee,\n validateProjectMilestone,\n validateState,\n validateTeam,\n} from './shared.js';\nasync function validateListIssuesInput(\n linearClient: ReturnType<typeof getLinearClient>,\n {\n teamId,\n stateId,\n assigneeId,\n projectMilestoneId,\n }: {\n teamId?: string;\n stateId?: string;\n assigneeId?: string;\n projectMilestoneId?: string;\n },\n) {\n if (teamId) await validateTeam(linearClient, teamId, 'listing issues');\n if (stateId) {\n if (!teamId)\n throw new Error(\"Cannot validate stateId: 'teamId' is required when 'stateId' is provided.\");\n await validateState(linearClient, teamId, stateId, 'listing issues');\n }\n if (assigneeId) await validateAssignee(linearClient, assigneeId, 'listing issues');\n if (projectMilestoneId)\n await validateProjectMilestone(linearClient, projectMilestoneId, null, 'listing issues');\n}\nfunction buildIssueFilters({\n query,\n teamId,\n stateId,\n assigneeId,\n projectMilestoneId,\n projectId,\n includeArchived = true,\n limit = 50,\n}: {\n query?: string;\n teamId?: string;\n stateId?: string;\n assigneeId?: string;\n projectMilestoneId?: string;\n projectId?: string;\n includeArchived?: boolean;\n limit?: number;\n}): import('./shared.js').IssueFilters {\n const filters: import('./shared.js').IssueFilters = { includeArchived, first: limit };\n if (query) {\n filters.filter = {\n ...(filters.filter || {}),\n or: [\n { title: { containsIgnoreCase: query } },\n { description: { containsIgnoreCase: query } },\n ],\n };\n }\n if (teamId) filters.teamId = teamId;\n if (stateId) {\n filters.filter = {\n ...(filters.filter || {}),\n state: { id: { eq: stateId } },\n };\n }\n if (assigneeId) {\n filters.filter = {\n ...(filters.filter || {}),\n assignee: { id: { eq: assigneeId } },\n };\n }\n if (projectMilestoneId) {\n filters.filter = {\n ...(filters.filter || {}),\n projectMilestone: { id: { eq: projectMilestoneId } },\n };\n }\n if (projectId) {\n filters.projectId = projectId;\n filters.filter = {\n ...(filters.filter || {}),\n project: { id: { eq: projectId } },\n };\n }\n return filters;\n}\n\nexport const listIssuesTool = defineTool({\n name: 'list_issues',\n description: \"List issues in the user's Linear workspace\",\n inputSchema: IssueFilterSchema,\n handler: async ({\n query,\n teamId,\n stateId,\n assigneeId,\n projectMilestoneId,\n projectId,\n includeArchived = true,\n limit = 50,\n }) => {\n const linearClient = getLinearClient();\n await validateListIssuesInput(linearClient, {\n teamId,\n stateId,\n assigneeId,\n projectMilestoneId,\n });\n const filters = buildIssueFilters({\n query,\n teamId,\n stateId,\n assigneeId,\n projectMilestoneId,\n projectId,\n includeArchived,\n limit,\n });\n const issuesConnection = await linearClient.issues(\n filters as Parameters<typeof linearClient.issues>[0],\n );\n const issues = await Promise.all(\n issuesConnection.nodes.map((issueNode) => mapIssueToDetails(issueNode, false)),\n );\n return { content: [{ type: 'text', text: JSON.stringify(issues) }] };\n },\n});\n","import type { LinearClient } from '@linear/sdk';\nimport { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';\nimport { getLinearClient } from '../../utils/linear-client.js';\nimport { defineTool } from '../shared/tool-definition.js';\nimport { IssueUpdateSchema } from './shared.js';\nimport {\n mapIssueToDetails,\n validateAssignee,\n validateIssueExists,\n validateLabels,\n validateProject,\n validateProjectMilestone,\n validateState,\n} from './shared.js';\n\nexport const updateIssueTool = defineTool({\n name: 'update_issue',\n description: 'Update an existing Linear issue',\n inputSchema: IssueUpdateSchema,\n handler: async ({\n id,\n title,\n description,\n priority,\n projectId,\n stateId,\n assigneeId,\n labelIds,\n dueDate,\n projectMilestoneId,\n }) => {\n const linearClient = getLinearClient();\n\n // Validate the issue exists and get current values\n const issueToUpdate = await validateIssueExists(linearClient, id, 'updating issue');\n const currentTeamId = (await issueToUpdate.team)?.id;\n const currentProjectId = (await issueToUpdate.project)?.id;\n\n // Validate input parameters\n await validateInputParameters({\n linearClient,\n id,\n currentTeamId,\n currentProjectId,\n projectId,\n stateId,\n assigneeId,\n labelIds,\n projectMilestoneId,\n });\n\n // Construct update payload with only defined fields\n const updatePayload = buildUpdatePayload({\n title,\n description,\n priority,\n projectId,\n stateId,\n assigneeId,\n labelIds,\n dueDate,\n projectMilestoneId,\n });\n\n // Update the issue\n try {\n const issueUpdate = await linearClient.updateIssue(id, updatePayload);\n const updatedIssue = await issueUpdate.issue;\n\n if (!updatedIssue) {\n throw new McpError(\n ErrorCode.InternalError,\n `Failed to update issue or retrieve details. Sync ID: ${issueUpdate.lastSyncId}`,\n );\n }\n\n const detailedUpdatedIssue = await mapIssueToDetails(updatedIssue, false);\n return { content: [{ type: 'text', text: JSON.stringify(detailedUpdatedIssue) }] };\n } catch (error) {\n if (error instanceof McpError) throw error;\n throw new McpError(\n ErrorCode.InternalError,\n `Error updating issue: ${(error as Error).message}`,\n );\n }\n },\n});\n\n/**\n * Input parameters for validation\n */\ninterface ValidationParams {\n linearClient: LinearClient;\n id: string;\n currentTeamId?: string;\n currentProjectId?: string;\n projectId?: string;\n stateId?: string;\n assigneeId?: string;\n labelIds?: string[];\n projectMilestoneId?: string | null;\n}\n\n/**\n * Input parameters for building update payload\n */\ninterface UpdatePayloadParams {\n title?: string;\n description?: string;\n priority?: number;\n projectId?: string;\n stateId?: string;\n assigneeId?: string;\n labelIds?: string[];\n dueDate?: string;\n projectMilestoneId?: string | null;\n}\n\n/**\n * Validates all input parameters for updating an issue\n */\nasync function validateInputParameters({\n linearClient,\n id,\n currentTeamId,\n currentProjectId,\n projectId,\n stateId,\n assigneeId,\n labelIds,\n projectMilestoneId,\n}: ValidationParams): Promise<void> {\n // Validate project if provided\n if (projectId) {\n await validateProject(linearClient, projectId, 'updating issue');\n }\n\n // Validate state if provided\n if (stateId) {\n if (!currentTeamId) {\n throw new McpError(\n ErrorCode.InvalidParams,\n `Issue '${id}' has no team, cannot validate stateId.`,\n );\n }\n await validateState(linearClient, currentTeamId, stateId, 'updating issue');\n }\n\n // Validate assignee if provided\n if (assigneeId) {\n await validateAssignee(linearClient, assigneeId, 'updating issue');\n }\n\n // Validate labels if provided\n if (labelIds && labelIds.length > 0) {\n if (!currentTeamId) {\n throw new McpError(\n ErrorCode.InvalidParams,\n `Issue '${id}' has no team, cannot validate labelIds.`,\n );\n }\n await validateLabels(linearClient, currentTeamId, labelIds, 'updating issue');\n }\n\n // Validate project milestone if provided or explicitly set to null\n if (projectMilestoneId !== undefined) {\n if (projectMilestoneId) {\n const targetProjectId = projectId ?? currentProjectId;\n await validateProjectMilestone(\n linearClient,\n projectMilestoneId,\n targetProjectId,\n 'updating issue',\n );\n }\n // If projectMilestoneId is null, it's valid (removing the milestone)\n }\n}\n\n/**\n * Builds the update payload with only defined fields\n */\nfunction buildUpdatePayload({\n title,\n description,\n priority,\n projectId,\n stateId,\n assigneeId,\n labelIds,\n dueDate,\n projectMilestoneId,\n}: UpdatePayloadParams): Record<string, unknown> {\n return {\n ...(title !== undefined && { title }),\n ...(description !== undefined && { description }),\n ...(priority !== undefined && { priority }),\n ...(projectId !== undefined && { projectId }),\n ...(stateId !== undefined && { stateId }),\n ...(assigneeId !== undefined && { assigneeId }),\n ...(labelIds !== undefined && { labelIds }),\n ...(dueDate !== undefined && { dueDate }),\n ...(projectMilestoneId !== undefined && { projectMilestoneId }),\n };\n}\n","import { createCommentTool } from './create_comment.js';\nimport { createIssueTool } from './create_issue.js';\nimport { getIssueTool } from './get_issue.js';\nimport { getIssueGitBranchNameTool } from './get_issue_git_branch_name.js';\nimport { listCommentsTool } from './list_comments.js';\nimport { listIssuesTool } from './list_issues.js';\nimport { updateIssueTool } from './update_issue.js';\n\nexport const issueTools = [\n listIssuesTool,\n createCommentTool,\n createIssueTool,\n getIssueGitBranchNameTool,\n getIssueTool,\n listCommentsTool,\n updateIssueTool,\n];\n","import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';\nimport { getLinearClient } from '../../utils/linear-client.js';\nimport { defineTool } from '../shared/tool-definition.js';\nimport { LabelListSchema } from './shared.js';\nimport { formatLabelNodes, getAvailableTeamsMessage } from './shared.js';\n\nexport const listIssueLabelsTool = defineTool({\n name: 'list_issue_labels',\n description: 'List available issue labels in a Linear team',\n inputSchema: LabelListSchema,\n handler: async ({ teamId }) => {\n const linearClient = getLinearClient();\n try {\n const team = await linearClient.team(teamId);\n if (!team) {\n const availableTeamsMessage = await getAvailableTeamsMessage(linearClient);\n throw new McpError(\n ErrorCode.InvalidParams,\n `Team with ID '${teamId}' not found when trying to list labels.${availableTeamsMessage}`,\n );\n }\n const labels = await team.labels();\n return {\n content: [\n {\n type: 'text',\n text: JSON.stringify(formatLabelNodes(labels.nodes)),\n },\n ],\n };\n } catch (error: unknown) {\n const err = error as { message?: string };\n throw new McpError(\n ErrorCode.InternalError,\n `Failed to list issue labels: ${err.message || 'Unknown error'}`,\n );\n }\n },\n});\n","import type { Issue, IssueLabel, Team } from '@linear/sdk';\nimport type { LinearClient } from '@linear/sdk';\nimport { z } from 'zod';\n\n// --- Tool definition utility (local copy) ---\n\n// --- Label schemas (local copy) ---\nexport const LabelListSchema = {\n teamId: z.string().describe('The team UUID'),\n};\n\n/**\n * Fetches a message listing all available teams, or a fallback message if none found.\n */\nexport async function getAvailableTeamsMessage(linearClient: LinearClient): Promise<string> {\n try {\n const allTeams = await linearClient.teams();\n if (allTeams.nodes.length > 0) {\n const teamList = allTeams.nodes.map((t: Team) => ({\n id: t.id,\n name: t.name,\n }));\n return ` Valid teams are: ${JSON.stringify(teamList, null, 2)}`;\n }\n return ' No teams available to list.';\n } catch {\n return ' (Could not fetch available teams for context.)';\n }\n}\n\n/**\n * Formats an array of label nodes for output.\n */\nexport function formatLabelNodes(labels: IssueLabel[]): object[] {\n return labels.map((label) => ({\n id: label.id,\n name: label.name,\n color: label.color,\n description: label.description,\n createdAt:\n label.createdAt instanceof Date\n ? label.createdAt.toISOString()\n : typeof label.createdAt === 'string'\n ? label.createdAt\n : undefined,\n updatedAt:\n label.updatedAt instanceof Date\n ? label.updatedAt.toISOString()\n : typeof label.updatedAt === 'string'\n ? label.updatedAt\n : undefined,\n }));\n}\n","import { listIssueLabelsTool } from './list_issue_labels.js';\n\nexport const labelTools = [listIssueLabelsTool];\n","import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';\nimport { getLinearClient } from '../../utils/linear-client.js';\nimport { PaginationSchema } from '../issues/shared.js';\nimport { defineTool } from '../shared/tool-definition.js';\n\nexport const listMyIssuesTool = defineTool({\n name: 'list_my_issues',\n description: 'List issues assigned to the current user',\n inputSchema: PaginationSchema,\n handler: async ({ limit, before, after }) => {\n const linearClient = getLinearClient();\n try {\n const viewer = await linearClient.viewer;\n const issues = await linearClient.issues({\n filter: {\n assignee: { id: { eq: viewer.id } },\n },\n first: limit,\n before,\n after,\n });\n // Await state and team for each issue\n const myIssues = await Promise.all(\n issues.nodes.map(async (issue) => {\n const state = issue.state ? await issue.state : null;\n const team = issue.team ? await issue.team : null;\n const cycle = issue.cycle ? await issue.cycle : null;\n return {\n id: issue.id,\n identifier: issue.identifier,\n title: issue.title,\n description: issue.description,\n priority: issue.priority,\n state: state?.name,\n team: team?.name,\n cycleName: cycle?.name,\n createdAt: issue.createdAt,\n updatedAt: issue.updatedAt,\n url: issue.url,\n };\n }),\n );\n return {\n content: [\n {\n type: 'text',\n text: JSON.stringify(myIssues),\n },\n ],\n };\n } catch (error: unknown) {\n const err = error as { message?: stri