UNPKG

mcp-server-linear

Version:

An MCP server for interacting with Linear's API, providing tools for managing issues, projects, and teams

246 lines 9.8 kB
import { BaseHandler } from "../../../core/handlers/base.handler.js"; /** * Handler for issue-related operations. * Manages creating, updating, searching, and deleting issues. */ export class IssueHandler extends BaseHandler { constructor(auth, graphqlClient) { super(auth, graphqlClient); } /** * Creates a single issue. */ async handleCreateIssue(args) { try { const client = this.verifyAuth(); this.validateRequiredParams(args, ["title", "description", "teamId"]); const result = (await client.createIssue(args)); if (!result.issueCreate.success || !result.issueCreate.issue) { throw new Error("Failed to create issue"); } const issue = result.issueCreate.issue; const parentInfo = issue.parent ? `Parent: ${issue.parent.identifier} (${issue.parent.title})\n` : ""; const childrenInfo = issue.children?.nodes?.length ? `Children:\n${issue.children.nodes .map((child) => `- ${child.identifier}: ${child.title}`) .join("\n")}\n` : ""; return this.createJsonResponse({ issueCreate: { success: true, issue: { identifier: issue.identifier, title: issue.title, url: issue.url, project: issue.project, parent: issue.parent, children: issue.children, }, }, }); } catch (error) { this.handleError(error, "create issue"); } } /** * Creates multiple issues in bulk. */ async handleCreateIssues(args) { try { const client = this.verifyAuth(); this.validateRequiredParams(args, ["issues"]); if (!Array.isArray(args.issues)) { throw new Error("Issues parameter must be an array"); } const result = (await client.createIssues(args.issues)); if (!result.issueBatchCreate.success) { throw new Error("Failed to create issues"); } const createdIssues = result.issueBatchCreate.issues; return this.createResponse(`Successfully created ${createdIssues.length} issues:\n` + createdIssues .map((issue) => `- ${issue.identifier}: ${issue.title}\n URL: ${issue.url}`) .join("\n")); } catch (error) { this.handleError(error, "create issues"); } } /** * Updates multiple issues in bulk. */ async handleBulkUpdateIssues(args) { try { const client = this.verifyAuth(); this.validateRequiredParams(args, ["issueIds", "update"]); if (!Array.isArray(args.issueIds)) { throw new Error("IssueIds parameter must be an array"); } const result = (await client.updateIssues(args.issueIds, args.update)); if (!result.issueUpdate.success) { throw new Error("Failed to update issues"); } // Since the response only contains a single issue, we count the number of IDs that were updated const updatedCount = args.issueIds.length; return this.createResponse(`Successfully updated ${updatedCount} issues`); } catch (error) { this.handleError(error, "update issues"); } } /** * Searches for issues with filtering and pagination. */ async handleSearchIssues(args) { try { const client = this.verifyAuth(); const filter = {}; // Handle identifier-based searches first if (args.filter?.identifier) { filter.identifier = { in: [args.filter.identifier] }; } // If there's a query but no identifier filter, use it for searching else if (args.query) { // Pass the raw query to use Linear's native search capabilities filter.search = args.query; } if (args.filter?.project?.id?.eq) { filter.project = { id: { eq: args.filter.project.id.eq } }; } if (args.teamIds) { filter.team = { id: { in: args.teamIds } }; } if (args.assigneeIds) { filter.assignee = { id: { in: args.assigneeIds } }; } if (args.states) { filter.state = { name: { in: args.states } }; } if (typeof args.priority === "number") { filter.priority = { eq: args.priority }; } const result = (await client.searchIssues(filter, args.first || 50, args.after, args.orderBy || "updatedAt")); return this.createJsonResponse(result); } catch (error) { this.handleError(error, "search issues"); } } /** * Search for issues by their identifiers (e.g., ["MIC-78", "MIC-79"]) */ async handleSearchIssuesByIdentifier(args) { try { const client = this.verifyAuth(); this.validateRequiredParams(args, ["identifiers"]); if (!Array.isArray(args.identifiers)) { throw new Error("Identifiers parameter must be an array"); } const result = (await client.searchIssues({ identifier: { in: args.identifiers } }, 100, undefined, "updatedAt")); return this.createJsonResponse(result); } catch (error) { this.handleError(error, "search issues by identifier"); } } /** * Get a single issue by identifier, including all comments */ async handleGetIssue(args) { try { const client = this.verifyAuth(); this.validateRequiredParams(args, ["identifier"]); // Use the same query as search by identifier but with a single identifier const result = (await client.searchIssues({ identifier: { in: [args.identifier] } }, 1, undefined, "updatedAt")); if (!result.issues.nodes || result.issues.nodes.length === 0) { throw new Error(`Issue ${args.identifier} not found`); } return this.createJsonResponse({ issue: result.issues.nodes[0], }); } catch (error) { this.handleError(error, "get issue"); } } /** * Deletes a single issue. */ async handleDeleteIssue(args) { try { const client = this.verifyAuth(); this.validateRequiredParams(args, ["id"]); const result = (await client.deleteIssue(args.id)); if (!result.issueDelete.success) { throw new Error("Failed to delete issue"); } return this.createResponse(`Successfully deleted issue ${args.id}`); } catch (error) { this.handleError(error, "delete issue"); } } /** * Edits a single issue. */ async handleEditIssue(args) { try { const client = this.verifyAuth(); this.validateRequiredParams(args, ["issueId"]); // Construct the input object for the GraphQL mutation // Only include fields that are actually provided in the args const updateInput = {}; const optionalFields = [ "title", "description", "stateId", "priority", "assigneeId", "labelIds", "projectId", "projectMilestoneId", "estimate", "dueDate", "parentId", "sortOrder", ]; optionalFields.forEach((field) => { // Check for undefined or null, allowing empty strings and 0 if (args[field] !== undefined && args[field] !== null) { updateInput[field] = args[field]; } }); // Check if any update fields were provided besides issueId if (Object.keys(updateInput).length === 0) { throw new Error("No fields provided to update for issue " + args.issueId); } // Call the GraphQL client method (to be implemented in Step 5) // Assuming it returns an object like { issueUpdate: { success: boolean, issue: Issue } } const result = await client.updateIssue(args.issueId, updateInput); if (!result?.issueUpdate?.success || !result?.issueUpdate?.issue) { throw new Error(`Failed to update issue ${args.issueId}. API response: ${JSON.stringify(result)}`); } const updatedIssue = result.issueUpdate.issue; // Return a success response with basic issue details return this.createJsonResponse({ issueUpdate: { success: true, issue: { id: updatedIssue.id, identifier: updatedIssue.identifier, title: updatedIssue.title, url: updatedIssue.url, updatedAt: updatedIssue.updatedAt, // Include updatedAt for confirmation }, }, }); } catch (error) { this.handleError(error, "edit issue"); } } } //# sourceMappingURL=issue.handler.js.map