Skip to main content
Glama
create-issue.js10.7 kB
/** * Linear issue creation tool */ import { z } from 'zod'; import { create_tool } from './utils/mod.js'; import { IssueSchema } from '../effects/linear/types/types.js'; /** * CreateIssueContext type definition * Using JSDoc for now, but this could be converted to TypeScript or Zod schema in the future * * @typedef {Object} CreateIssueContext * @property {import('../utils/config/mod.js').Config} config * @property {Object} effects * @property {import('../effects/linear/index.js').LinearEffect} effects.linear * @property {import('../effects/logging/mod.js').LoggingEffect} effects.logger */ /** * Input schema for CreateIssue tool */ const CreateIssueInputSchema = z.object({ title: z.string().describe('The title of the issue to create'), teamId: z .string() .min(1, { message: 'Team ID is required' }) .describe('The ID of the Linear team where the issue will be created'), description: z .string() .optional() .describe('The detailed description of the issue'), priority: z .number() .min(0) .max(4) .optional() .describe('The priority of the issue (0-4)'), assigneeId: z .string() .optional() .describe('The ID of the user to assign the issue to'), stateId: z .string() .optional() .describe('The ID of the state to set for the issue'), projectId: z .string() .optional() .describe('The ID of the project to associate with the issue'), debug: z .boolean() .default(false) .describe('Debug mode to show extra diagnostics'), }); /** * Creates a new issue in Linear * * @param {import('@linear/sdk').LinearClient} client - Linear client from SDK * @param {string} title - The title of the issue * @param {string} teamId - The ID of the team where the issue will be created * @param {Object} options - Additional issue options * @param {string} [options.description] - The detailed description of the issue * @param {number} [options.priority] - The priority of the issue (0-4) * @param {string} [options.assigneeId] - The ID of the user to assign the issue to * @param {string} [options.stateId] - The ID of the state to set for the issue * @param {string} [options.projectId] - The ID of the project to associate with the issue * @param {import('../effects/logging/mod.js').LoggingEffect} [logger] - Optional logger * @returns {Promise<import('../effects/linear/types/types.js').Issue>} The created issue */ async function createIssue(client, title, teamId, options = {}, logger) { try { logger?.debug(`Creating new Linear issue with title: ${title}`); // Prepare the issue creation input for the Linear SDK const issueInput = { title, teamId, description: options.description, priority: options.priority, assigneeId: options.assigneeId, stateId: options.stateId, projectId: options.projectId, }; // Filter out undefined values Object.keys(issueInput).forEach(key => { if (issueInput[key] === undefined) { delete issueInput[key]; } }); logger?.debug('Issue creation payload:', issueInput); // Create the issue using the Linear SDK const issueResult = await client.createIssue(issueInput); if (!issueResult) { throw new Error('Failed to create issue, received null response'); } // Linear API returns a promise for the created issue const issueData = await issueResult.issue; if (!issueData) { throw new Error('Failed to retrieve issue data from response'); } logger?.debug(`Successfully created issue: ${issueData.id}`); // Load related data asynchronously let assigneeData = null; let projectData = null; let stateData = null; if (issueData.assignee) { assigneeData = await issueData.assignee; } if (issueData.project) { projectData = await issueData.project; } if (issueData.state) { stateData = await issueData.state; } // Process and validate the issue data using our schema const processedIssue = IssueSchema.parse({ id: issueData.id, title: issueData.title, description: issueData.description || undefined, // Convert null to undefined status: stateData?.name, priority: issueData.priority, assignee: assigneeData ? { id: assigneeData.id, name: assigneeData.name, email: assigneeData.email, } : undefined, project: projectData ? { id: projectData.id, name: projectData.name, } : undefined, createdAt: issueData.createdAt instanceof Date ? issueData.createdAt.toISOString() : issueData.createdAt, updatedAt: issueData.updatedAt instanceof Date ? issueData.updatedAt.toISOString() : issueData.updatedAt, }); return processedIssue; } catch (error) { // Enhanced error logging logger?.error(`Error creating Linear issue: ${error.message}`, { title, teamId, options, stack: error.stack, }); // Check if it's a Zod validation error (formatted differently) if (error.name === 'ZodError') { logger?.error( 'Zod validation error details:', JSON.stringify(error.errors, null, 2) ); } // Rethrow the error for the tool to handle throw error; } } /** * Handler for CreateIssue tool * @type {import('./types/mod.js').ToolHandler<CreateIssueContext, typeof CreateIssueInputSchema>} */ const handler = async ( ctx, { title, teamId, description, priority, assigneeId, stateId, projectId, debug, } ) => { const logger = ctx.effects.logger; try { // Log details about config and parameters logger.debug('Create issue called with parameters:', { title, teamId, description: description ? `${description.substring(0, 20)}...` : undefined, priority, assigneeId, stateId, projectId, debug, }); // Debug log for API key (masked) const apiKey = ctx.config.linearApiKey || ''; const maskedKey = apiKey ? apiKey.substring(0, 4) + '...' + apiKey.substring(apiKey.length - 4) : '<not set>'; logger.debug(`Using Linear API key: ${maskedKey}`); if (!ctx.config.linearApiKey) { throw new Error('LINEAR_API_KEY is not configured'); } // Create a Linear client using our effect logger.debug('Creating Linear client'); const linearClient = ctx.effects.linear.createClient( ctx.config.linearApiKey ); // Create the issue using the Linear SDK client logger.debug('Executing Linear API to create issue'); const options = { description, priority, assigneeId, stateId, projectId, }; const result = await createIssue( linearClient, title, teamId, options, logger ); // Log that we created the issue logger.info(`Created issue with ID: ${result.id}`); // Format the output const formatDate = timestamp => { if (!timestamp) return 'Just now'; const date = new Date(timestamp); return date.toLocaleString(); }; let responseText = ''; responseText += `✅ Issue created successfully\n\n`; // Add issue details responseText += `**Issue ID:** ${result.id}\n`; responseText += `**Title:** ${result.title}\n`; if (result.status) { responseText += `**Status:** ${result.status}\n`; } if (result.priority !== undefined) { const priorityLabels = ['No priority', 'Urgent', 'High', 'Medium', 'Low']; responseText += `**Priority:** ${priorityLabels[result.priority]}\n`; } if (result.assignee) { responseText += `**Assigned to:** ${result.assignee.name}\n`; } if (result.project) { responseText += `**Project:** ${result.project.name}\n`; } responseText += `**Created at:** ${formatDate(result.createdAt)}\n\n`; if (result.description) { responseText += `**Description:**\n${result.description}\n`; } logger.debug('Returning formatted issue result'); return { content: [{ type: 'text', text: responseText }], }; } catch (error) { logger.error(`Error creating issue: ${error.message}`); logger.error(error.stack); // Create a user-friendly error message with troubleshooting guidance let errorMessage = `Error creating issue: ${error.message}`; // Add detailed diagnostic information if in debug mode if (debug) { errorMessage += '\n\n=== DETAILED DEBUG INFORMATION ==='; // Add parameters that were used errorMessage += `\nParameters: - title: ${title} - teamId: ${teamId} - description: ${description ? 'provided' : 'not provided'} - priority: ${priority !== undefined ? priority : 'not provided'} - assigneeId: ${assigneeId || 'not provided'} - stateId: ${stateId || 'not provided'} - projectId: ${projectId || 'not provided'}`; // Check if API key is configured const apiKey = ctx.config.linearApiKey || ''; const keyStatus = apiKey ? `API key is configured (${apiKey.substring( 0, 4 )}...${apiKey.substring(apiKey.length - 4)})` : 'API key is NOT configured - set LINEAR_API_KEY'; errorMessage += `\n\nLinear API Status: ${keyStatus}`; // Add error details if (error.name) { errorMessage += `\nError type: ${error.name}`; } if (error.code) { errorMessage += `\nError code: ${error.code}`; } if (error.stack) { errorMessage += `\n\nStack trace: ${error.stack .split('\n') .slice(0, 3) .join('\n')}`; } // Add Linear API info for manual testing errorMessage += `\n\nLinear API: Using official Linear SDK (@linear/sdk) For manual testing, try using the SDK directly or the Linear API Explorer in the Linear UI.`; } // Add a note that debug mode can be enabled for more details if (!debug) { errorMessage += `\n\nFor more detailed diagnostics, retry with debug:true in the input.`; } return { content: [ { type: 'text', text: errorMessage, }, ], isError: true, }; } }; /** * CreateIssue tool factory */ export const CreateIssue = create_tool({ name: 'create_issue', description: 'Create a new issue in Linear. This tool is useful for adding new tasks, bugs, or feature requests to your Linear workspace.', inputSchema: CreateIssueInputSchema, handler, }); // Export for testing export { createIssue };

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/scoutos/mcp-linear'

If you have feedback or need assistance with the MCP directory API, please join our Discord server