list_teams
Retrieve a detailed list of Linear teams, including members and projects, to gain a comprehensive overview of your workspace structure. Filter and limit results for precise insights.
Instructions
List Linear teams with details about their members, projects, and issues. Use this to get a high-level view of all teams in your Linear workspace.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| debug | No | Debug mode to show extra diagnostics | |
| includeMembers | No | Include sparse member listing for each team | |
| includeProjects | No | Include sparse project listing for each team | |
| limit | No | Maximum number of teams to return | |
| nameFilter | No | Filter teams by name (partial match) |
Implementation Reference
- src/tools/list-teams.js:328-525 (handler)The primary handler function that implements the core logic for the 'list_teams' tool. It processes input parameters, interacts with the Linear API via effects, fetches and formats team data, and returns a structured text response or detailed error.const handler = async ( ctx, { nameFilter, includeMembers, includeProjects, limit, debug } ) => { const logger = ctx.effects.logger; try { // Log details about parameters logger.debug('List teams called with parameters:', { nameFilter, includeMembers, includeProjects, 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 teams using the Linear SDK client logger.debug('Executing Linear API to list teams'); const results = await listTeams( linearClient, { nameFilter }, { includeMembers, includeProjects, limit }, logger ); // Log the results count logger.info(`Found ${results.results.length} teams matching criteria`); // Format the output let responseText = ''; if (results.results.length === 0) { responseText = 'No teams found matching your criteria.'; } else { responseText = 'Teams found:\n\n'; // Format dates for display const formatDisplayDate = timestamp => { if (!timestamp) return 'Not available'; try { const date = new Date(timestamp); return date.toLocaleString(); } catch (e) { return 'Invalid date'; } }; results.results.forEach((team, index) => { responseText += `${index + 1}. **${team.name}** (${team.key}) [ID: ${ team.id }]\n`; if (team.description) { responseText += ` Description: ${team.description}\n`; } // Add metrics const memberCount = team.memberCount || 0; const projectCount = team.projectCount || 0; const issueCount = team.issueCount || 0; const completedIssueCount = team.completedIssueCount || 0; responseText += ` Members: ${memberCount} | Projects: ${projectCount} | Issues: ${completedIssueCount}/${issueCount} completed\n`; // Add created/updated dates if (team.createdAt) { responseText += ` Created: ${formatDisplayDate(team.createdAt)}\n`; } if (team.url) { responseText += ` URL: ${team.url}\n`; } // Add members if included if (team.members && team.members.length > 0) { responseText += ` Members: `; const memberNames = team.members .slice(0, 5) .map(m => m.displayName || m.name) .join(', '); responseText += memberNames; if (team.members.length > 5) { responseText += `, +${team.members.length - 5} more`; } responseText += '\n'; } // Add projects if included if (team.projects && team.projects.length > 0) { responseText += ` Projects: `; const projectNames = team.projects .slice(0, 5) .map(p => p.name) .join(', '); responseText += projectNames; if (team.projects.length > 5) { responseText += `, +${team.projects.length - 5} more`; } responseText += '\n'; } responseText += '\n'; }); } logger.debug('Returning formatted list results'); return { content: [{ type: 'text', text: responseText }], }; } catch (error) { logger.error(`Error listing teams: ${error.message}`); logger.error(error.stack); // Create a user-friendly error message with troubleshooting guidance let errorMessage = `Error listing teams: ${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 += `\nParameters: - nameFilter: ${nameFilter || '<not specified>'} - includeMembers: ${includeMembers} - includeProjects: ${includeProjects} - 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, }; } };
- src/tools/list-teams.js:22-45 (schema)Zod input schema defining parameters for the list_teams tool, including optional filters and options for members, projects, limit, and debug mode.const ListTeamsInputSchema = z.object({ nameFilter: z .string() .optional() .describe('Filter teams by name (partial match)'), includeMembers: z .boolean() .default(true) .describe('Include sparse member listing for each team'), includeProjects: z .boolean() .default(true) .describe('Include sparse project listing for each team'), limit: z .number() .min(1) .max(100) .default(25) .describe('Maximum number of teams to return'), debug: z .boolean() .default(false) .describe('Debug mode to show extra diagnostics'), });
- src/tools/list-teams.js:530-536 (registration)Tool factory registration using create_tool, specifying the name 'list_teams', description, input schema, and handler function.export const ListTeams = create_tool({ name: 'list_teams', description: 'List Linear teams with details about their members, projects, and issues. Use this to get a high-level view of all teams in your Linear workspace.', inputSchema: ListTeamsInputSchema, handler, });
- src/tools/list-teams.js:113-312 (helper)Core helper function that performs the actual Linear API interactions to list and enrich team data with members, projects, and metrics.async function listTeams( client, filters = {}, { includeMembers = true, includeProjects = true, limit = 25 } = {}, logger ) { try { logger?.debug('Building Linear SDK parameters', { filters, includeMembers, includeProjects, limit, }); // Get all teams // @ts-ignore - The Linear SDK types may not be fully accurate const teamsResponse = await client.teams(); logger?.debug(`Found ${teamsResponse.nodes.length} teams`); // Filter teams by name if specified let filteredTeams = teamsResponse.nodes; if (filters.nameFilter) { const nameFilterLower = filters.nameFilter.toLowerCase(); logger?.debug(`Filtering teams by name: ${filters.nameFilter}`); filteredTeams = filteredTeams.filter(team => { const name = team.name.toLowerCase(); const key = team.key.toLowerCase(); const description = team.description?.toLowerCase() || ''; // Check for direct inclusion if ( name.includes(nameFilterLower) || key.includes(nameFilterLower) || description.includes(nameFilterLower) ) { return true; } // No match found return false; }); logger?.debug( `After name filtering, found ${filteredTeams.length} teams` ); } // Apply limit to teams filteredTeams = filteredTeams.slice(0, limit); // Process teams to extract detailed information const processedTeams = await Promise.all( filteredTeams.map(async team => { logger?.debug(`Processing team ${team.name} (${team.id})`); // Build base team object const teamData = { id: team.id, name: team.name, key: team.key, description: team.description, createdAt: formatDate(team.createdAt), updatedAt: formatDate(team.updatedAt), color: team.color, private: team.private, // @ts-ignore - SDK may have different property name cycleEnable: team.cyclesEnabled || team.cycleEnable, timezone: team.timezone, // @ts-ignore - SDK structure may differ from types markedAsDuplicate: team.markedAsDuplicate, // @ts-ignore - SDK structure may differ from types issuesPerCycle: team.issuesPerCycle, // @ts-ignore - SDK structure may differ from types url: team.url, }; // Add members if requested if (includeMembers) { try { // @ts-ignore - The Linear SDK types may not be fully accurate const membersResponse = await team.members(); if (membersResponse?.nodes) { teamData.members = membersResponse.nodes.map(member => ({ id: member.id, name: member.name, displayName: member.displayName || member.name, active: member.active !== false, })); teamData.memberCount = teamData.members.length; } logger?.debug( `Added ${teamData.memberCount || 0} members for team ${team.name}` ); } catch (membersError) { logger?.warn( `Error fetching members for team ${team.name}: ${membersError.message}` ); } } // Add projects if requested if (includeProjects) { try { // @ts-ignore - The Linear SDK types may not be fully accurate const projectsResponse = await team.projects(); if (projectsResponse?.nodes) { teamData.projects = await Promise.all( projectsResponse.nodes.map(async project => { // Get state information let stateName = undefined; try { if (project.state) { const state = await project.state; if (state) { // @ts-ignore - SDK structure may differ from types stateName = state.name; } } } catch (stateError) { logger?.warn( `Error fetching project state: ${stateError.message}` ); } return { id: project.id, name: project.name, state: stateName, // @ts-ignore - SDK has different property name completed: project.completedAt ? true : false, }; }) ); teamData.projectCount = teamData.projects.length; } logger?.debug( `Added ${teamData.projectCount || 0} projects for team ${ team.name }` ); } catch (projectsError) { logger?.warn( `Error fetching projects for team ${team.name}: ${projectsError.message}` ); } } // Add issue counts try { // @ts-ignore - The Linear SDK types may not be fully accurate const issuesResponse = await team.issues(); if (issuesResponse) { teamData.issueCount = issuesResponse.nodes.length; teamData.activeIssueCount = issuesResponse.nodes.filter( issue => !issue.completedAt ).length; teamData.completedIssueCount = issuesResponse.nodes.filter( issue => issue.completedAt ).length; } logger?.debug( `Added issue counts for team ${team.name}: ${ teamData.issueCount || 0 } total` ); } catch (issuesError) { logger?.warn( `Error fetching issues for team ${team.name}: ${issuesError.message}` ); } return teamData; }) ); logger?.debug(`Successfully processed ${processedTeams.length} teams`); return TeamSearchResultsSchema.parse({ results: processedTeams }); } catch (error) { // Enhanced error logging logger?.error(`Error listing Linear teams: ${error.message}`, { filters, includeMembers, includeProjects, limit, 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; } }
- src/index.js:109-149 (registration)Top-level MCP server registration: instantiates ListTeams at line 115 and registers it via server.tool in the loop (lines 121-149).const all_tools = [ new tools.ListIssues(toolContext), new tools.GetIssue(toolContext), new tools.ListMembers(toolContext), new tools.ListProjects(toolContext), new tools.GetProject(toolContext), new tools.ListTeams(toolContext), new tools.AddComment(toolContext), new tools.CreateIssue(toolContext), ]; // Register tools with the MCP server for (const tool of all_tools) { server.tool( tool.name, tool.description, tool.inputSchema.shape ?? {}, async args => { try { // Call our tool const result = await tool.call(args); // Return format expected by MCP SDK return { content: result.content, error: result.isError ? { message: result.content[0]?.text || 'An error occurred', } : undefined, }; } catch (error) { logger.error(`Error executing tool ${tool.name}: ${error.message}`); return { content: [{ type: 'text', text: `Error: ${error.message}` }], error: { message: error.message }, }; } } ); }