UNPKG

@sylphx/linear-mcp

Version:

A Model Context Protocol (MCP) server for interacting with Linear issues, projects, teams, and more

1,397 lines (1,366 loc) 74.2 kB
#!/usr/bin/env node "use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { ErrorCode: () => import_types19.ErrorCode, McpError: () => import_types19.McpError, startServer: () => startServer }); module.exports = __toCommonJS(index_exports); var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js"); var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js"); var import_types19 = require("@modelcontextprotocol/sdk/types.js"); // src/utils/linear-client.ts var import_sdk = require("@linear/sdk"); var LinearClientManager = class _LinearClientManager { static instance; client = null; constructor() { } static getInstance() { if (!_LinearClientManager.instance) { _LinearClientManager.instance = new _LinearClientManager(); } return _LinearClientManager.instance; } initialize(apiKey) { if (!this.client) { this.client = new import_sdk.LinearClient({ apiKey }); } } getClient() { if (!this.client) { throw new Error("Linear client not initialized. Call initialize() first."); } return this.client; } }; var getLinearClient = () => { return LinearClientManager.getInstance().getClient(); }; // src/tools/shared/tool-definition.ts var defineTool = (tool) => ({ name: tool.name, description: tool.description, inputSchema: tool.inputSchema, handler: tool.handler }); // src/tools/issue-statuses/shared.ts var import_types = require("@modelcontextprotocol/sdk/types.js"); var import_zod = require("zod"); var IssueStatusListSchema = { teamId: import_zod.z.string().describe("The team UUID") }; var IssueStatusQuerySchema = { query: import_zod.z.string().describe("The UUID or name of the issue status to retrieve"), teamId: import_zod.z.string().describe("The team UUID") }; async function validateTeamOrThrow(teamId) { const linearClient = getLinearClient(); const team = await linearClient.team(teamId); if (!team) { let availableTeamsMessage = ""; try { const allTeams = await linearClient.teams(); if (allTeams.nodes.length > 0) { const teamList = allTeams.nodes.map((t) => ({ id: t.id, name: t.name })); availableTeamsMessage = ` Valid teams are: ${JSON.stringify(teamList, null, 2)}`; } else { availableTeamsMessage = " No teams available to list."; } } catch (_listError) { availableTeamsMessage = " (Could not fetch available teams for context.)"; } throw new import_types.McpError( import_types.ErrorCode.InvalidParams, `Team with ID '${teamId}' not found.${availableTeamsMessage}` ); } return team; } function throwInternalError(message, error) { const err = error; throw new import_types.McpError(import_types.ErrorCode.InternalError, `${message}: ${err.message || "Unknown error"}`); } // src/tools/issue-statuses/get_issue_status.ts var getIssueStatusTool = defineTool({ name: "get_issue_status", description: "Retrieve details of a specific issue status in Linear by name or ID", inputSchema: IssueStatusQuerySchema, handler: async ({ query, teamId }) => { try { const team = await validateTeamOrThrow(teamId); const states = await team.states(); let state = states.nodes.find((s) => s.id === query); if (!state) { state = states.nodes.find((s) => s.name.toLowerCase() === query.toLowerCase()); } if (state) { return { content: [ { type: "text", text: JSON.stringify({ id: state.id, name: state.name, color: state.color, type: state.type, description: state.description, position: state.position }) } ] }; } const validStatuses = states.nodes.map((s) => ({ id: s.id, name: s.name, type: s.type })); throw new Error( `Issue status with query "${query}" not found in team '${team.name}' (${teamId}). Valid statuses for this team are: ${JSON.stringify(validStatuses, null, 2)}` ); } catch (error) { throwInternalError("Failed to get issue status", error); } } }); // src/tools/issue-statuses/list_issue_statuses.ts var listIssueStatusesTool = defineTool({ name: "list_issue_statuses", description: "List available issues statuses in a Linear team", inputSchema: IssueStatusListSchema, handler: async ({ teamId }) => { try { const team = await validateTeamOrThrow(teamId); const states = await team.states(); return { content: [ { type: "text", text: JSON.stringify( states.nodes.map((state) => ({ id: state.id, name: state.name, color: state.color, type: state.type, description: state.description, position: state.position })) ) } ] }; } catch (error) { throwInternalError("Failed to list issue statuses", error); } } }); // src/tools/issue-statuses/index.ts var issueStatusTools = [listIssueStatusesTool, getIssueStatusTool]; // src/tools/issues/shared.ts var import_sdk2 = require("@linear/sdk"); var import_types2 = require("@modelcontextprotocol/sdk/types.js"); var import_zod2 = require("zod"); var IdSchema = { id: import_zod2.z.string().describe("The issue ID") }; var PaginationSchema = { limit: import_zod2.z.number().default(50).describe("The number of items to return"), before: import_zod2.z.string().optional().describe("A UUID to end at"), after: import_zod2.z.string().optional().describe("A UUID to start from"), orderBy: import_zod2.z.enum(["createdAt", "updatedAt"]).default("updatedAt") }; var IssueFilterSchema = { query: import_zod2.z.string().optional().describe("An optional search query"), teamId: import_zod2.z.string().optional().describe("The team UUID"), stateId: import_zod2.z.string().optional().describe("The state UUID"), assigneeId: import_zod2.z.string().optional().describe("The assignee UUID"), projectMilestoneId: import_zod2.z.string().uuid("Invalid project milestone ID").optional().describe("The project milestone ID to filter by"), includeArchived: import_zod2.z.boolean().default(true).describe("Whether to include archived issues"), limit: import_zod2.z.number().default(50).describe("The number of issues to return"), projectId: import_zod2.z.string().optional().describe("The project ID to filter by") }; var IssueCreateSchema = { title: import_zod2.z.string().describe("The issue title"), description: import_zod2.z.string().optional().describe("The issue description as Markdown"), teamId: import_zod2.z.string().describe("The team UUID"), priority: import_zod2.z.number().optional().describe("The issue priority. 0 = No priority, 1 = Urgent, 2 = High, 3 = Normal, 4 = Low."), projectId: import_zod2.z.string().optional().describe("The project ID to add the issue to"), stateId: import_zod2.z.string().optional().describe("The issue state ID"), assigneeId: import_zod2.z.string().optional().describe("The assignee ID"), labelIds: import_zod2.z.array(import_zod2.z.string()).optional().describe("Array of label IDs to set on the issue"), dueDate: import_zod2.z.string().optional().describe("The due date for the issue in ISO format"), projectMilestoneId: import_zod2.z.string().uuid("Invalid project milestone ID").optional().describe("The project milestone ID to associate the issue with") }; var IssueUpdateSchema = { id: import_zod2.z.string().describe("The issue ID"), title: import_zod2.z.string().optional().describe("The issue title"), description: import_zod2.z.string().optional().describe("The issue description as Markdown"), priority: import_zod2.z.number().optional().describe("The issue priority. 0 = No priority, 1 = Urgent, 2 = High, 3 = Normal, 4 = Low."), projectId: import_zod2.z.string().optional().describe("The project ID to add the issue to"), stateId: import_zod2.z.string().optional().describe("The issue state ID"), assigneeId: import_zod2.z.string().optional().describe("The assignee ID"), labelIds: import_zod2.z.array(import_zod2.z.string()).optional().describe("Array of label IDs to set on the issue"), dueDate: import_zod2.z.string().optional().describe("The due date for the issue in ISO format"), projectMilestoneId: import_zod2.z.string().uuid("Invalid project milestone ID").nullable().optional().describe("The project milestone ID to associate the issue with (null to remove)") }; var CommentCreateSchema = { issueId: import_zod2.z.string().describe("The issue ID"), body: import_zod2.z.string().describe("The content of the comment as Markdown") }; async function getAvailableTeamsJson(linearClient) { try { const teams = await linearClient.teams(); if (!teams.nodes || teams.nodes.length === 0) return "[]"; return JSON.stringify( teams.nodes.map((team) => ({ id: team.id, name: team.name, key: team.key })), null, 2 ); } catch (e) { return `"(Could not fetch available teams as JSON: ${e.message})"`; } } async function getAvailableProjectsJson(linearClient) { try { const projects = await linearClient.projects(); if (!projects.nodes || projects.nodes.length === 0) return "[]"; return JSON.stringify( projects.nodes.map((project) => ({ id: project.id, name: project.name })), null, 2 ); } catch (e) { return `"(Could not fetch available projects as JSON: ${e.message})"`; } } async function getAvailableStatesJson(linearClient, teamId) { if (!teamId) return `"(Cannot fetch states without a valid teamId.)"`; try { const team = await linearClient.team(teamId); if (!team) return `"(Could not fetch states: Team with ID '${teamId}' not found. Valid teams are: ${await getAvailableTeamsJson(linearClient)})"`; const states = await team.states(); if (!states.nodes || states.nodes.length === 0) return "[]"; return JSON.stringify( states.nodes.map((state) => ({ id: state.id, name: state.name, type: state.type })), null, 2 ); } catch (e) { return `"(Could not fetch states for team ${teamId} as JSON: ${e.message})"`; } } async function getAvailableAssigneesJson(linearClient) { try { const users = await linearClient.users({ filter: { active: { eq: true } } }); if (!users.nodes || users.nodes.length === 0) return "[]"; return JSON.stringify( users.nodes.map((user) => ({ id: user.id, name: user.displayName, email: user.email })), null, 2 ); } catch (e) { return `"(Could not fetch available assignees as JSON: ${e.message})"`; } } async function getAvailableLabelsJson(linearClient, teamId) { if (!teamId) return `"(Cannot fetch labels without a valid teamId.)"`; try { const team = await linearClient.team(teamId); if (!team) return `"(Could not fetch labels: Team with ID '${teamId}' not found. Valid teams are: ${await getAvailableTeamsJson(linearClient)})"`; const labels = await team.labels(); if (!labels.nodes || labels.nodes.length === 0) return "[]"; return JSON.stringify( labels.nodes.map((label) => ({ id: label.id, name: label.name, color: label.color })), null, 2 ); } catch (e) { return `"(Could not fetch labels for team ${teamId} as JSON: ${e.message})"`; } } async function getAvailableProjectMilestonesJson(linearClient, projectId) { try { if (projectId && projectId !== "any" && projectId.trim() !== "") { const project = await linearClient.project(projectId); if (!project) return `"(Could not fetch milestones: Project with ID '${projectId}' not found. Valid projects are: ${await getAvailableProjectsJson(linearClient)})"`; const milestones = await project.projectMilestones(); if (!milestones.nodes || milestones.nodes.length === 0) return "[]"; return JSON.stringify( milestones.nodes.map((m) => ({ id: m.id, name: m.name })), null, 2 ); } return `"(Project milestones are specific to a project. Please provide a valid projectId. Available projects: ${await getAvailableProjectsJson(linearClient)})"`; } catch (e) { const error = e; if (projectId && projectId !== "any" && error.message.toLowerCase().includes("not found")) { return `"(Could not fetch milestones for project '${projectId}': ${error.message}. Valid projects are: ${await getAvailableProjectsJson(linearClient)})"`; } return `"(Could not fetch project milestones for project ${projectId} as JSON: ${error.message})"`; } } async function mapIssueToDetails(issue, includeAttachments = false) { const [state, assignee, team, project, projectMilestone, labelsResult, attachmentsResult] = await Promise.all([ issue.state, issue.assignee, issue.team, issue.project, issue.projectMilestone, issue.labels(), includeAttachments ? issue.attachments() : Promise.resolve(null) ]); return { id: issue.id, identifier: issue.identifier, title: issue.title, description: issue.description, priority: issue.priority, state: state ? { id: state.id, name: state.name, color: state.color, type: state.type } : null, assignee: assignee ? { id: assignee.id, name: assignee.name, email: assignee.email } : null, team: team ? { id: team.id, name: team.name, key: team.key } : null, project: project ? { id: project.id, name: project.name } : null, projectMilestone: projectMilestone ? { id: projectMilestone.id, name: projectMilestone.name } : null, labels: labelsResult.nodes.map((l) => ({ id: l.id, name: l.name, color: l.color })), attachments: includeAttachments && attachmentsResult ? attachmentsResult.nodes.map((att) => ({ id: att.id, title: att.title, url: att.url, source: att.source, metadata: att.metadata, groupBySource: att.groupBySource, createdAt: att.createdAt, updatedAt: att.updatedAt })) : void 0, createdAt: issue.createdAt, updatedAt: issue.updatedAt, url: issue.url }; } async function handleLinearError(error, entityType, entityId, contextMessage, getAvailableJsonFn) { if (error instanceof import_types2.McpError) throw error; const err = error; const availableJson = typeof getAvailableJsonFn === "function" ? await getAvailableJsonFn() : getAvailableJsonFn; let specificMessage = `Invalid ${entityType}Id: '${entityId}'. ${contextMessage}.`; if (err.extensions?.userPresentableMessage) { specificMessage = `${specificMessage} Details: ${err.extensions.userPresentableMessage}`; } else if (err.message) { if (err.message.toLowerCase().includes("not found") || err.message.toLowerCase().includes("no entity found") || err.message.toLowerCase().includes("api error") || err.message.toLowerCase().includes("invalid uuid")) { specificMessage = `${specificMessage} ${entityType} not found or ID is invalid.`; } else { specificMessage = `${specificMessage} Error during validation: ${err.message}`; } } else { specificMessage = `${specificMessage} An unknown error occurred during ${entityType} validation.`; } throw new import_types2.McpError( import_types2.ErrorCode.InvalidParams, `${specificMessage} Valid ${entityType}s are: ${availableJson}` ); } async function validateTeam(linearClient, teamId, operationContext = "") { try { const team = await linearClient.team(teamId); if (!team) { const msg = operationContext ? `${operationContext}: ` : ""; throw new import_types2.McpError( import_types2.ErrorCode.InvalidParams, `${msg}Team with ID '${teamId}' not found. Valid teams are: ${await getAvailableTeamsJson(linearClient)}` ); } return team; } catch (e) { return handleLinearError( e, "team", teamId, operationContext, () => getAvailableTeamsJson(linearClient) ); } } async function validateProject(linearClient, projectId, operationContext = "") { try { const project = await linearClient.project(projectId); if (!project) { const msg = operationContext ? `${operationContext}: ` : ""; throw new import_types2.McpError( import_types2.ErrorCode.InvalidParams, `${msg}Project with ID '${projectId}' not found. Valid projects are: ${await getAvailableProjectsJson(linearClient)}` ); } return project; } catch (e) { return handleLinearError( e, "project", projectId, operationContext, () => getAvailableProjectsJson(linearClient) ); } } async function validateState(linearClient, teamId, stateId, operationContext = "") { const team = await validateTeam(linearClient, teamId, `for state validation ${operationContext}`); try { const states = await team.states({ filter: { id: { eq: stateId } } }); if (!states.nodes || states.nodes.length === 0) { const msg = operationContext ? `${operationContext}: ` : ""; throw new import_types2.McpError( import_types2.ErrorCode.InvalidParams, `${msg}State with ID '${stateId}' not found for team '${team.name}'. Valid states are: ${await getAvailableStatesJson(linearClient, teamId)}` ); } return states.nodes[0]; } catch (e) { return handleLinearError( e, "state", stateId, `for team '${team.name}' ${operationContext}`, () => getAvailableStatesJson(linearClient, teamId) ); } } async function validateAssignee(linearClient, assigneeId, operationContext = "") { try { const assignee = await linearClient.user(assigneeId); if (!assignee || !assignee.active) { const msg = operationContext ? `${operationContext}: ` : ""; let userStatus = "User not found."; if (assignee && !assignee.active) userStatus = `User '${assignee.displayName}' is not active.`; throw new import_types2.McpError( import_types2.ErrorCode.InvalidParams, `${msg}Assignee with ID '${assigneeId}' is invalid. ${userStatus} Valid assignees are: ${await getAvailableAssigneesJson(linearClient)}` ); } return assignee; } catch (e) { return handleLinearError( e, "assignee", assigneeId, operationContext, () => getAvailableAssigneesJson(linearClient) ); } } async function validateLabels(linearClient, teamId, labelIds, operationContext = "") { const team = await validateTeam(linearClient, teamId, `for label validation ${operationContext}`); try { const labels = await team.labels({ filter: { id: { in: labelIds } } }); const foundLabelIds = labels.nodes.map((l) => l.id); const notFoundLabelIds = labelIds.filter((id) => !foundLabelIds.includes(id)); if (notFoundLabelIds.length > 0) { const msg = operationContext ? `${operationContext}: ` : ""; throw new import_types2.McpError( import_types2.ErrorCode.InvalidParams, `${msg}Label(s) with ID(s) '${notFoundLabelIds.join(", ")}' not found for team '${team.name}'. Valid labels are: ${await getAvailableLabelsJson(linearClient, teamId)}` ); } } catch (e) { return handleLinearError( e, "label(s)", labelIds.join(", "), `for team '${team.name}' ${operationContext}`, () => getAvailableLabelsJson(linearClient, teamId) ); } } async function validateProjectMilestone(linearClient, projectMilestoneId, forProjectId, operationContext = "") { try { const milestone = await linearClient.projectMilestone(projectMilestoneId); if (!milestone) { const msg = operationContext ? `${operationContext}: ` : ""; throw new import_types2.McpError( import_types2.ErrorCode.InvalidParams, `${msg}Project milestone with ID '${projectMilestoneId}' not found. ${await getAvailableProjectMilestonesJson(linearClient, forProjectId ?? void 0)}` ); } if (forProjectId && milestone.projectId !== forProjectId) { const targetProject = await linearClient.project(forProjectId); const actualProject = await milestone.project; throw new import_types2.McpError( import_types2.ErrorCode.InvalidParams, `Project milestone '${milestone.name}' (${projectMilestoneId}) does not belong to project '${targetProject?.name ?? forProjectId}'. It belongs to '${actualProject?.name ?? milestone.projectId}'.` ); } return milestone; } catch (e) { return handleLinearError( e, "project milestone", projectMilestoneId, operationContext, () => getAvailableProjectMilestonesJson(linearClient, forProjectId ?? void 0) ); } } async function validateIssueExists(linearClient, issueId, operationContext = "") { try { const issue = await linearClient.issue(issueId); if (!issue) { let recentIssuesMessage = ""; try { const recentIssues = await linearClient.issues({ first: 5, orderBy: import_sdk2.LinearDocument.PaginationOrderBy.UpdatedAt }); if (recentIssues.nodes.length > 0) { recentIssuesMessage = ` Recent issues: ${JSON.stringify( recentIssues.nodes.map((iss) => ({ id: iss.id, title: iss.title, identifier: iss.identifier })), null, 2 )}`; } } catch { } throw new import_types2.McpError( import_types2.ErrorCode.InvalidParams, `${operationContext}: Issue with ID '${issueId}' not found.${recentIssuesMessage}` ); } return issue; } catch (e) { if (e instanceof import_types2.McpError) throw e; throw new import_types2.McpError( import_types2.ErrorCode.InternalError, `Error fetching issue '${issueId}' for ${operationContext}: ${e.message}` ); } } // src/tools/issues/create_comment.ts var createCommentTool = defineTool({ name: "create_comment", description: "Create a comment on a Linear issue by ID", inputSchema: CommentCreateSchema, handler: async ({ issueId, body }) => { const linearClient = getLinearClient(); await validateIssueExists(linearClient, issueId, "creating comment"); const commentPayload = await linearClient.createComment({ issueId, body }); const newComment = await commentPayload.comment; if (!newComment) throw new Error( `Failed to create comment or retrieve details. Sync ID: ${commentPayload.lastSyncId}` ); return { content: [ { type: "text", text: JSON.stringify({ id: newComment.id, body: newComment.body, createdAt: newComment.createdAt, updatedAt: newComment.updatedAt, userId: newComment.userId }) } ] }; } }); // src/tools/issues/create_issue.ts var createIssueTool = defineTool({ name: "create_issue", description: "Create a new Linear issue", inputSchema: IssueCreateSchema, handler: async ({ title, description, teamId, priority, projectId, stateId, assigneeId, labelIds, dueDate, projectMilestoneId }) => { const linearClient = getLinearClient(); await validateTeam(linearClient, teamId, "creating issue"); if (projectId) await validateProject(linearClient, projectId, "creating issue"); if (stateId) await validateState(linearClient, teamId, stateId, "creating issue"); if (assigneeId) await validateAssignee(linearClient, assigneeId, "creating issue"); if (labelIds && labelIds.length > 0) await validateLabels(linearClient, teamId, labelIds, "creating issue"); if (projectMilestoneId) await validateProjectMilestone(linearClient, projectMilestoneId, projectId, "creating issue"); const payload = { title, description, teamId, priority, projectId, stateId, assigneeId, labelIds, dueDate, projectMilestoneId }; const issueCreate = await linearClient.createIssue(payload); const newIssue = await issueCreate.issue; if (!newIssue) throw new Error( `Failed to create issue or retrieve details. Sync ID: ${issueCreate.lastSyncId}` ); const detailedNewIssue = await mapIssueToDetails(newIssue, false); return { content: [{ type: "text", text: JSON.stringify(detailedNewIssue) }] }; } }); // src/tools/issues/get_issue.ts var getIssueTool = defineTool({ name: "get_issue", description: "Retrieve a Linear issue details by ID, including attachments", inputSchema: IdSchema, handler: async ({ id }) => { const linearClient = getLinearClient(); const issue = await validateIssueExists(linearClient, id, "getting issue details"); const detailedIssue = await mapIssueToDetails(issue, true); return { content: [{ type: "text", text: JSON.stringify(detailedIssue) }] }; } }); // src/tools/issues/get_issue_git_branch_name.ts var getIssueGitBranchNameTool = defineTool({ name: "get_issue_git_branch_name", description: "Retrieve the branch name for a Linear issue by ID", inputSchema: IdSchema, handler: async ({ id }) => { const linearClient = getLinearClient(); const issue = await validateIssueExists(linearClient, id, "getting git branch name"); const branchName = await issue.branchName; return { content: [ { type: "text", text: JSON.stringify({ id: issue.id, identifier: issue.identifier, title: issue.title, branchName }) } ] }; } }); // src/tools/issues/list_comments.ts var import_zod3 = require("zod"); var listCommentsTool = defineTool({ name: "list_comments", description: "Retrieve comments for a Linear issue by ID", inputSchema: { issueId: import_zod3.z.string().describe("The ID of the issue to fetch comments for") }, handler: async ({ issueId }) => { const linearClient = getLinearClient(); const issue = await validateIssueExists(linearClient, issueId, "listing comments"); const comments = await issue.comments(); const commentDetails = comments.nodes.map((comment) => ({ id: comment.id, body: comment.body, createdAt: comment.createdAt, updatedAt: comment.updatedAt, userId: comment.userId })); return { content: [{ type: "text", text: JSON.stringify(commentDetails) }] }; } }); // src/tools/issues/list_issues.ts async function validateListIssuesInput(linearClient, { teamId, stateId, assigneeId, projectMilestoneId }) { if (teamId) await validateTeam(linearClient, teamId, "listing issues"); if (stateId) { if (!teamId) throw new Error("Cannot validate stateId: 'teamId' is required when 'stateId' is provided."); await validateState(linearClient, teamId, stateId, "listing issues"); } if (assigneeId) await validateAssignee(linearClient, assigneeId, "listing issues"); if (projectMilestoneId) await validateProjectMilestone(linearClient, projectMilestoneId, null, "listing issues"); } function buildIssueFilters({ query, teamId, stateId, assigneeId, projectMilestoneId, projectId, includeArchived = true, limit = 50 }) { const filters = { includeArchived, first: limit }; if (query) { filters.filter = { ...filters.filter || {}, or: [ { title: { containsIgnoreCase: query } }, { description: { containsIgnoreCase: query } } ] }; } if (teamId) filters.teamId = teamId; if (stateId) { filters.filter = { ...filters.filter || {}, state: { id: { eq: stateId } } }; } if (assigneeId) { filters.filter = { ...filters.filter || {}, assignee: { id: { eq: assigneeId } } }; } if (projectMilestoneId) { filters.filter = { ...filters.filter || {}, projectMilestone: { id: { eq: projectMilestoneId } } }; } if (projectId) { filters.projectId = projectId; filters.filter = { ...filters.filter || {}, project: { id: { eq: projectId } } }; } return filters; } var listIssuesTool = defineTool({ name: "list_issues", description: "List issues in the user's Linear workspace", inputSchema: IssueFilterSchema, handler: async ({ query, teamId, stateId, assigneeId, projectMilestoneId, projectId, includeArchived = true, limit = 50 }) => { const linearClient = getLinearClient(); await validateListIssuesInput(linearClient, { teamId, stateId, assigneeId, projectMilestoneId }); const filters = buildIssueFilters({ query, teamId, stateId, assigneeId, projectMilestoneId, projectId, includeArchived, limit }); const issuesConnection = await linearClient.issues( filters ); const issues = await Promise.all( issuesConnection.nodes.map((issueNode) => mapIssueToDetails(issueNode, false)) ); return { content: [{ type: "text", text: JSON.stringify(issues) }] }; } }); // src/tools/issues/update_issue.ts var import_types3 = require("@modelcontextprotocol/sdk/types.js"); var updateIssueTool = defineTool({ name: "update_issue", description: "Update an existing Linear issue", inputSchema: IssueUpdateSchema, handler: async ({ id, title, description, priority, projectId, stateId, assigneeId, labelIds, dueDate, projectMilestoneId }) => { const linearClient = getLinearClient(); const issueToUpdate = await validateIssueExists(linearClient, id, "updating issue"); const currentTeamId = (await issueToUpdate.team)?.id; const currentProjectId = (await issueToUpdate.project)?.id; await validateInputParameters({ linearClient, id, currentTeamId, currentProjectId, projectId, stateId, assigneeId, labelIds, projectMilestoneId }); const updatePayload = buildUpdatePayload({ title, description, priority, projectId, stateId, assigneeId, labelIds, dueDate, projectMilestoneId }); try { const issueUpdate = await linearClient.updateIssue(id, updatePayload); const updatedIssue = await issueUpdate.issue; if (!updatedIssue) { throw new import_types3.McpError( import_types3.ErrorCode.InternalError, `Failed to update issue or retrieve details. Sync ID: ${issueUpdate.lastSyncId}` ); } const detailedUpdatedIssue = await mapIssueToDetails(updatedIssue, false); return { content: [{ type: "text", text: JSON.stringify(detailedUpdatedIssue) }] }; } catch (error) { if (error instanceof import_types3.McpError) throw error; throw new import_types3.McpError( import_types3.ErrorCode.InternalError, `Error updating issue: ${error.message}` ); } } }); async function validateInputParameters({ linearClient, id, currentTeamId, currentProjectId, projectId, stateId, assigneeId, labelIds, projectMilestoneId }) { if (projectId) { await validateProject(linearClient, projectId, "updating issue"); } if (stateId) { if (!currentTeamId) { throw new import_types3.McpError( import_types3.ErrorCode.InvalidParams, `Issue '${id}' has no team, cannot validate stateId.` ); } await validateState(linearClient, currentTeamId, stateId, "updating issue"); } if (assigneeId) { await validateAssignee(linearClient, assigneeId, "updating issue"); } if (labelIds && labelIds.length > 0) { if (!currentTeamId) { throw new import_types3.McpError( import_types3.ErrorCode.InvalidParams, `Issue '${id}' has no team, cannot validate labelIds.` ); } await validateLabels(linearClient, currentTeamId, labelIds, "updating issue"); } if (projectMilestoneId !== void 0) { if (projectMilestoneId) { const targetProjectId = projectId ?? currentProjectId; await validateProjectMilestone( linearClient, projectMilestoneId, targetProjectId, "updating issue" ); } } } function buildUpdatePayload({ title, description, priority, projectId, stateId, assigneeId, labelIds, dueDate, projectMilestoneId }) { return { ...title !== void 0 && { title }, ...description !== void 0 && { description }, ...priority !== void 0 && { priority }, ...projectId !== void 0 && { projectId }, ...stateId !== void 0 && { stateId }, ...assigneeId !== void 0 && { assigneeId }, ...labelIds !== void 0 && { labelIds }, ...dueDate !== void 0 && { dueDate }, ...projectMilestoneId !== void 0 && { projectMilestoneId } }; } // src/tools/issues/index.ts var issueTools = [ listIssuesTool, createCommentTool, createIssueTool, getIssueGitBranchNameTool, getIssueTool, listCommentsTool, updateIssueTool ]; // src/tools/labels/list_issue_labels.ts var import_types4 = require("@modelcontextprotocol/sdk/types.js"); // src/tools/labels/shared.ts var import_zod4 = require("zod"); var LabelListSchema = { teamId: import_zod4.z.string().describe("The team UUID") }; async function getAvailableTeamsMessage(linearClient) { try { const allTeams = await linearClient.teams(); if (allTeams.nodes.length > 0) { const teamList = allTeams.nodes.map((t) => ({ id: t.id, name: t.name })); return ` Valid teams are: ${JSON.stringify(teamList, null, 2)}`; } return " No teams available to list."; } catch { return " (Could not fetch available teams for context.)"; } } function formatLabelNodes(labels) { return labels.map((label) => ({ id: label.id, name: label.name, color: label.color, description: label.description, createdAt: label.createdAt instanceof Date ? label.createdAt.toISOString() : typeof label.createdAt === "string" ? label.createdAt : void 0, updatedAt: label.updatedAt instanceof Date ? label.updatedAt.toISOString() : typeof label.updatedAt === "string" ? label.updatedAt : void 0 })); } // src/tools/labels/list_issue_labels.ts var listIssueLabelsTool = defineTool({ name: "list_issue_labels", description: "List available issue labels in a Linear team", inputSchema: LabelListSchema, handler: async ({ teamId }) => { const linearClient = getLinearClient(); try { const team = await linearClient.team(teamId); if (!team) { const availableTeamsMessage = await getAvailableTeamsMessage(linearClient); throw new import_types4.McpError( import_types4.ErrorCode.InvalidParams, `Team with ID '${teamId}' not found when trying to list labels.${availableTeamsMessage}` ); } const labels = await team.labels(); return { content: [ { type: "text", text: JSON.stringify(formatLabelNodes(labels.nodes)) } ] }; } catch (error) { const err = error; throw new import_types4.McpError( import_types4.ErrorCode.InternalError, `Failed to list issue labels: ${err.message || "Unknown error"}` ); } } }); // src/tools/labels/index.ts var labelTools = [listIssueLabelsTool]; // src/tools/my-issues/list_my_issues.ts var import_types5 = require("@modelcontextprotocol/sdk/types.js"); var listMyIssuesTool = defineTool({ name: "list_my_issues", description: "List issues assigned to the current user", inputSchema: PaginationSchema, handler: async ({ limit, before, after }) => { const linearClient = getLinearClient(); try { const viewer = await linearClient.viewer; const issues = await linearClient.issues({ filter: { assignee: { id: { eq: viewer.id } } }, first: limit, before, after }); const myIssues = await Promise.all( issues.nodes.map(async (issue) => { const state = issue.state ? await issue.state : null; const team = issue.team ? await issue.team : null; const cycle = issue.cycle ? await issue.cycle : null; return { id: issue.id, identifier: issue.identifier, title: issue.title, description: issue.description, priority: issue.priority, state: state?.name, team: team?.name, cycleName: cycle?.name, createdAt: issue.createdAt, updatedAt: issue.updatedAt, url: issue.url }; }) ); return { content: [ { type: "text", text: JSON.stringify(myIssues) } ] }; } catch (error) { const err = error; throw new import_types5.McpError( import_types5.ErrorCode.InternalError, `Failed to list my issues: ${err.message || "Unknown error"}` ); } } }); // src/tools/my-issues.ts var myIssuesTools = [listMyIssuesTool]; // src/tools/project-milestones/create_project_milestone.ts var import_types6 = require("@modelcontextprotocol/sdk/types.js"); // src/tools/project-milestones/shared.ts var import_zod5 = require("zod"); var ListProjectMilestonesInputSchema = import_zod5.z.object({ projectId: import_zod5.z.string().uuid("Invalid project ID") }); var CreateProjectMilestoneInputSchema = import_zod5.z.object({ projectId: import_zod5.z.string().uuid("Invalid project ID"), name: import_zod5.z.string().min(1, "Milestone name cannot be empty"), description: import_zod5.z.string().optional(), targetDate: import_zod5.z.string().datetime({ message: "Invalid ISO date string for targetDate" }).optional() }); var UpdateProjectMilestoneInputSchema = import_zod5.z.object({ milestoneId: import_zod5.z.string().uuid("Invalid milestone ID"), name: import_zod5.z.string().min(1, "Milestone name cannot be empty").optional(), description: import_zod5.z.string().optional(), targetDate: import_zod5.z.string().datetime({ message: "Invalid ISO date string for targetDate" }).optional() }); var DeleteProjectMilestoneInputSchema = import_zod5.z.object({ milestoneId: import_zod5.z.string().uuid("Invalid milestone ID") }); async function getAvailableProjectsJsonForError(linearClient) { try { const projects = await linearClient.projects(); if (!projects.nodes || projects.nodes.length === 0) { return "[]"; } const projectList = projects.nodes.map((p) => ({ id: p.id, name: p.name })); return JSON.stringify(projectList, null, 2); } catch (e) { const error = e; return `(Could not fetch available projects for context: ${error.message})`; } } // src/tools/project-milestones/create_project_milestone.ts var createProjectMilestoneTool = defineTool({ name: "create_project_milestone", description: "Creates a new milestone within a project.", inputSchema: CreateProjectMilestoneInputSchema.shape, handler: async ({ projectId, name, description, targetDate }) => { const linear = getLinearClient(); try { const project = await linear.project(projectId); if (!project) { const availableProjectsJson = await getAvailableProjectsJsonForError(linear); throw new import_types6.McpError( import_types6.ErrorCode.InvalidParams, `Project with ID "${projectId}" not found. Cannot create milestone. Valid projects are: ${availableProjectsJson}` ); } const payload = { projectId, name }; if (description) payload.description = description; if (targetDate) payload.targetDate = new Date(targetDate); const milestoneCreatePayload = await linear.createProjectMilestone(payload); const createdMilestone = await milestoneCreatePayload.projectMilestone; if (!milestoneCreatePayload.success || !createdMilestone) { throw new import_types6.McpError( import_types6.ErrorCode.InternalError, `Failed to create project milestone in Linear. Success: ${milestoneCreatePayload.success}, Last Sync ID: ${milestoneCreatePayload.lastSyncId}` ); } return { content: [ { type: "text", text: JSON.stringify({ id: createdMilestone.id, name: createdMilestone.name, description: createdMilestone.description, targetDate: createdMilestone.targetDate, sortOrder: createdMilestone.sortOrder, projectId: createdMilestone.projectId }) } ] }; } catch (error) { if (error instanceof import_types6.McpError) throw error; const err = error; if (err.message.toLowerCase().includes("not found") && err.message.toLowerCase().includes(projectId.toLowerCase())) { const availableProjectsJson = await getAvailableProjectsJsonForError(linear); throw new import_types6.McpError( import_types6.ErrorCode.InvalidParams, `Project with ID "${projectId}" not found when creating milestone. Valid projects are: ${availableProjectsJson}` ); } throw new import_types6.McpError( import_types6.ErrorCode.InternalError, `Failed to create project milestone: ${err.message || "Unknown error"}` ); } } }); // src/tools/project-milestones/delete_project_milestone.ts var import_types7 = require("@modelcontextprotocol/sdk/types.js"); var deleteProjectMilestoneTool = defineTool({ name: "delete_project_milestone", description: "Deletes a milestone.", inputSchema: DeleteProjectMilestoneInputSchema.shape, handler: async ({ milestoneId }) => { const linear = getLinearClient(); try { const deletePayload = await linear.deleteProjectMilestone(milestoneId); if (!deletePayload.success) { try { await linear.projectMilestone(milestoneId); throw new import_types7.McpError( import_types7.ErrorCode.InternalError, `Failed to delete project milestone "${milestoneId}" in Linear. Success: ${deletePayload.success}, Last Sync ID: ${deletePayload.lastSyncId}` ); } catch (_fetchError) { throw new import_types7.McpError( import_types7.ErrorCode.InvalidParams, `Project milestone with ID "${milestoneId}" not found. Please verify the milestone ID.` ); } } return { content: [ { type: "text", text: JSON.stringify({ success: true, message: `Project milestone "${milestoneId}" deleted successfully.` }) } ] }; } catch (error) { if (error instanceof import_types7.McpError) throw error; const err = error; throw new import_types7.McpError( import_types7.ErrorCode.InternalError, `Failed to delete project milestone: ${err.message || "Unknown error"}` ); } } }); // src/tools/project-milestones/list_project_milestones.ts var import_types8 = require("@modelcontextprotocol/sdk/types.js"); var listProjectMilestonesTool = defineTool({ name: "list_project_milestones", description: "Lists all milestones for a given project.", inputSchema: ListProjectMilestonesInputSchema.shape, handler: async ({ projectId }) => { const linear = getLinearClient(); try { const project = await linear.project(projectId); if (!project) { const availableProjectsJson = await getAvailableProjectsJsonForError(linear); throw new import_types8.McpError( import_types8.ErrorCode.InvalidParams, `Project with ID "${projectId}" not found. Valid projects are: ${availableProjectsJson}` ); } const milestonesResult = await project.projectMilestones(); let resultNodes = []; if (milestonesResult && Array.isArray(milestonesResult.nodes)) { resultNodes = milestonesResult.nodes; } else if (milestonesResult && typeof milestonesResult === "object" && milestonesResult !== null && "nodes" in milestonesResult && Array.isArray(milestonesResult.nodes)) { resultNodes = milestonesResult.nodes; } else if (Array.isArray(milestonesResult)) { resultNodes = milestonesResult; } else if (project.name) { } else { throw new import_types8.McpError( import_types8.ErrorCode.InternalError, "Could not retrieve milestones. Project data was inconsistent after initial fetch." ); } return { content: [ { type: "text", text: JSON.stringify( resultNodes.map((m) => ({ id: m.id, name: m.name, description: m.description, targetDate: m.targetDate, sortOrder: m.sortOrder, projectId: m.projectId })) ) } ] }; } catch (error) { if (error instanceof import_types8.McpError) throw error; const err = error; if (err.message.toLowerCase().includes("not found") && err.message.toLowerCase().includes(projectId.toLowerCase())) { const availableProjectsJson = await getAvailableProjectsJsonForError(linear); throw new import_types8.McpError( import_types8.ErrorCode.InvalidParams, `Project with ID "${projectId}" not found when listing milestones. Valid projects are: ${availableProjectsJson}` ); } throw new import_types8.McpError( import_types8.ErrorCode.InternalError, `Failed to list project milestones: ${err.message || "Unknown error"}` ); } } }); // src/tools/project-milestones/update_project_milestone.ts var import_types9 = require("@modelcontextprotocol/sdk/types.js"); var updateProjectMilestoneTool = defineTool({ name: "update_project_milestone", description: "Updates an existing milestone.", inputSchema: UpdateProjectMilestoneInputSchema.shape, handler: async ({ milestoneId, name, description, targetDate }) => { const linear = getLinearClient(); try { const payload = {}; if (name) payload.name = name; if (description) payload.description = description; if (targetDate) payload.targetDate = new Date(targetDate); if (Object.keys(payload).length === 0) { throw new import_types9.McpError( import_types9.ErrorCode.InvalidParams, "No update data provided for project milestone." ); } const milestoneUpdatePayload = await linear.updateProjectMilestone(milestoneId, payload); const updatedMilestone = await milestoneUpdatePayload.projectMilestone; if (!milestoneUpdatePayload.success || !updatedMilestone) { try { await linear.projectMilestone(milestoneId); throw new import_types9.McpError( import_types9.ErrorCode.InternalError, `Failed to update project milestone "${milestoneId}" in Linear. Success: ${milestoneUpdatePayload.success}, Last Sync ID: ${milestoneUpdatePayload.lastSyncId}` ); } catch (_fetchError) { throw new import_types9.McpError( import_types9.ErrorCode.InvalidParams, `Project milestone with ID "${milestoneId}" not found or update failed. Please verify the milestone ID.` ); } } return { content: [ { type: "text", text: JSON.stringify({ id: updatedMilestone.id, name: updatedMilestone.name, description: updatedMilestone.description, targetDate: updatedMilestone.targetDate, sortOrder: updatedMilestone.sortOrder, projectId: updatedMilestone.projectId }) } ] }; } catch (error) { if (error instanceof import_types9.McpError) throw error; const err = error; throw new import_types9.McpError( import_types9.ErrorCode.InternalError, `Failed to update project milestone: ${err.message || "Unknown error"}` ); } } }); // src/tools/project-milestones/index.ts var projectMilestoneTools = [ createProjectMilestoneTool, deleteProjectMilestoneTool, listProjectMilestonesTool, updateProjectMilestoneTool ]; // src/tools/projects/create_project.ts var import_types11 = require("@modelcontextprotocol/sdk/types.js"); // src/tools/projects/shared.ts var import_types10 = require("@modelcontextprotocol/sdk/types.js"); var import_zod6 = require("zod"); var ProjectFilterSchema = { limit: import_zod6.z.number().default(50).describe("The number of items to return"), before: import_zod6.z.string().optional().describe("A UUID to end at"), after: import_zod6.z.string().optional().describe("A UUID to start from"), orderBy: im