Skip to main content
Glama
list-issues.js14.6 kB
/** * Linear issue listing tool */ import { z } from 'zod'; import { create_tool } from './utils/mod.js'; import { IssueSchema, SearchResultsSchema, } from '../effects/linear/types/types.js'; /** * ListIssuesContext type definition * Using JSDoc for now, but this could be converted to TypeScript or Zod schema in the future * * @typedef {Object} ListIssuesContext * @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 ListIssues tool */ const ListIssuesInputSchema = z.object({ assignedToMe: z.boolean().default(false), assignee: z.string().optional(), status: z.string().optional(), project: z.string().optional(), sortBy: z.enum(['createdAt', 'updatedAt']).default('createdAt'), sortDirection: z.enum(['ASC', 'DESC']).default('DESC'), limit: z.number().min(1).max(100).default(25), debug: z.boolean().default(false), // Debug mode to show extra diagnostics }); /** * Lists issues in Linear using the SDK with optional filtering * * @param {import('@linear/sdk').LinearClient} client - Linear client from SDK * @param {Object} filters - Filter criteria * @param {boolean} [filters.assignedToMe=false] - Only show issues assigned to me * @param {string} [filters.assignee] - Filter by assignee identifier * @param {string} [filters.status] - Filter by status name * @param {string} [filters.project] - Filter by project name * @param {Object} options - Search options * @param {number} [options.limit=25] - Maximum number of results to return * @param {'priority' | 'createdAt' | 'updatedAt'} [options.sortBy] - Field to sort by * @param {'ASC' | 'DESC'} [options.sortDirection='ASC'] - Direction to sort * @param {import('../effects/logging/mod.js').LoggingEffect} [logger] - Optional logger * @returns {Promise<import('../effects/linear/types/types.js').SearchResults>} Search results */ async function listIssues( client, filters = {}, { limit = 25, sortBy, sortDirection = 'ASC' } = {}, logger ) { try { logger?.debug('Building Linear SDK filter parameters', { filters, limit, sortBy: sortBy || 'none', sortDirection, }); // Build filters properly using Linear SDK filter syntax const filter = {}; // Add assignee filter if (filters.assignedToMe) { // For assignedToMe we'll use the viewer API directly, // but we'll still build the filter for normal query as fallback filter.assignee = { id: { eq: 'me' } }; } else if (filters.assignee) { filter.assignee = { name: { eq: filters.assignee } }; } // Add status filter if (filters.status) { // Properly filter by state name filter.state = { name: { eq: filters.status } }; logger?.debug(`Filtering by state name: ${filters.status}`); } // Add project filter if (filters.project) { // Filter by project name filter.project = { name: { eq: filters.project } }; logger?.debug(`Filtering by project name: ${filters.project}`); } logger?.debug('Built filter object:', JSON.stringify(filter, null, 2)); // Use the sort direction as provided let direction = sortDirection; logger?.debug(`Using sort direction: ${direction} for field: ${sortBy}`); // Build search parameters const searchParams = { first: limit, }; // Add sort parameters if specified if (sortBy) { searchParams.orderBy = sortBy; searchParams.orderDirection = direction; } // Build the full GraphQL query parameters with proper filter const queryParams = { filter, // Use the properly constructed filter object ...searchParams, }; // Log the complete query parameters for debugging logger?.debug( 'Full Linear API query parameters:', JSON.stringify(queryParams, null, 2) ); let result; try { // First try using the viewer API which may be more reliable for getting assigned issues if (filters.assignedToMe) { logger?.debug( 'Using viewer.assignedIssues API for retrieving assigned issues' ); const me = await client.viewer; result = await me.assignedIssues(searchParams); logger?.debug('Successfully retrieved issues using viewer API'); } else { // Otherwise use the direct GraphQL API from Linear SDK logger?.debug('Using issues API for general issue search'); // @ts-ignore - The Linear SDK types are not accurate for the GraphQL API result = await client.issues(queryParams); logger?.debug('Successfully retrieved issues using issues API'); } } catch (error) { logger?.error(`Linear API error:`, error); throw error; } // Log the raw response structure (without full content) logger?.debug(`Linear API response structure:`, { hasNodes: !!result.nodes, nodeCount: result.nodes?.length || 0, firstNodeKeys: result.nodes?.[0] ? Object.keys(result.nodes[0]) : [], }); // Log search results at debug level logger?.debug(`Linear list returned ${result.nodes.length} results`); // Process issues from Linear SDK format to our domain model // The Linear SDK returns promises for many fields, so we need to await them const processedIssues = []; for (const issue of result.nodes) { try { logger?.debug(`Processing issue ${issue.id}`); // Get state/status information (it's a promise in the Linear SDK) let statusName = 'Unknown'; try { if (issue.state) { const state = await issue.state; if (state && state.name) { statusName = state.name; logger?.debug(`Found state: ${statusName}`); } } } catch (stateError) { logger?.warn(`Error fetching state data: ${stateError.message}`); } // Get assignee if present (it's a promise in the Linear SDK) let assigneeData = undefined; try { if (issue.assignee) { const assignee = await issue.assignee; if (assignee) { assigneeData = { id: assignee.id, name: assignee.name, email: assignee.email, }; logger?.debug(`Found assignee: ${assignee.name}`); } } } catch (assigneeError) { logger?.warn( `Error fetching assignee data: ${assigneeError.message}` ); } // Get project if present (it's a promise in the Linear SDK) let projectData = undefined; try { if (issue.project) { const project = await issue.project; if (project) { projectData = { id: project.id, name: project.name, }; logger?.debug(`Found project: ${project.name}`); } } } catch (projectError) { logger?.warn(`Error fetching project data: ${projectError.message}`); } const processedIssue = IssueSchema.parse({ id: issue.id, identifier: issue.identifier || undefined, // Add the issue identifier (e.g. TEAM-123) url: issue.url || undefined, // Add the issue URL for linking title: issue.title, description: issue.description || undefined, priority: issue.priority, assignee: assigneeData, project: projectData, status: statusName, createdAt: issue.createdAt, updatedAt: issue.updatedAt, }); processedIssues.push(processedIssue); } catch (processError) { logger?.error( `Error processing issue ${issue.id}: ${processError.message}` ); // Continue processing other issues even if one fails } } logger?.debug( `Successfully processed ${processedIssues.length}/${result.nodes.length} issues` ); const issues = processedIssues; // Return the processed results return SearchResultsSchema.parse({ results: issues }); } catch (error) { // Enhanced error logging logger?.error(`Error listing Linear issues: ${error.message}`, { filters, limit, sortBy, sortDirection, 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 ListIssues tool * @type {import('./types/mod.js').ToolHandler<ListIssuesContext, typeof ListIssuesInputSchema>} */ const handler = async ( ctx, { assignedToMe, assignee, status, project, sortBy, sortDirection, limit, debug, } ) => { const logger = ctx.effects.logger; try { // Log details about config and parameters logger.debug('List issues called with parameters:', { assignedToMe, assignee, status, project, sortBy, sortDirection, limit, 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 ); // List issues using the Linear SDK client with filters logger.debug('Executing Linear API list with filters'); const results = await listIssues( linearClient, { assignedToMe, assignee, status, project, }, { limit, sortBy, sortDirection, }, logger ); // Log the results count logger.info(`Found ${results.results.length} issues matching criteria`); // Format the output let responseText = ''; logger.info(`Result: ${JSON.stringify(results, null, 2)}`); if (results.results.length === 0) { responseText = 'No issues found matching your criteria.'; } else { responseText = 'Issues found:\n\n'; results.results.forEach((issue, index) => { const priorityMap = { 0: 'No priority', 1: 'Urgent', 2: 'High', 3: 'Medium', 4: 'Low', }; const priority = issue.priority ?? 0; // Format timestamps to be more readable const formatDate = timestamp => { if (!timestamp) return 'Unknown'; const date = new Date(timestamp); return date.toLocaleString(); }; responseText += `${index + 1}. ${issue.title || 'Untitled'}\n`; responseText += ` ID: ${issue.id}\n`; if (issue.identifier) { responseText += ` Identifier: ${issue.identifier}\n`; } if (issue.url) { responseText += ` URL: ${issue.url}\n`; } responseText += ` Status: ${issue.status || 'Unknown'}\n`; responseText += ` Priority: ${priorityMap[priority] || 'Unknown'}\n`; if (issue.project) { responseText += ` Project: ${issue.project.name}\n`; } if (issue.assignee) { responseText += ` Assignee: ${issue.assignee.name}\n`; } responseText += ` Created: ${formatDate(issue.createdAt)}\n`; responseText += ` Updated: ${formatDate(issue.updatedAt)}\n`; responseText += '\n'; }); } logger.debug('Returning formatted list results'); return { content: [{ type: 'text', text: responseText }], }; } catch (error) { logger.error(`Error listing issues: ${error.message}`); logger.error(error.stack); // Create a user-friendly error message with troubleshooting guidance let errorMessage = `Error listing issues: ${error.message}`; // Add detailed diagnostic information if in debug mode if (debug) { errorMessage += '\n\n=== DETAILED DEBUG INFORMATION ==='; // Add filter parameters that were used errorMessage += `\nFilter parameters: - assignedToMe: ${assignedToMe} - assignee: ${assignee || '<not specified>'} - status: ${status || '<not specified>'} - project: ${project || '<not specified>'} - sortBy: ${sortBy} - sortDirection: ${sortDirection} - limit: ${limit}`; // 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, }; } }; /** * ListIssues tool factory */ export const ListIssues = create_tool({ name: 'list_issues', description: 'List Linear issues (also called tickets) with filtering by assignee, status, and project. Use this to browse and find issues in your Linear workspace.', inputSchema: ListIssuesInputSchema, handler, }); // Export for testing export { listIssues };

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