Skip to main content
Glama
scoutos

Linear MCP Server

by scoutos

list_members

Retrieve Linear team member details, including usernames, display names, and emails, with optional name-based filtering. Supports efficient member lookup for team management.

Instructions

List Linear team members with optional filtering by name. This tool is useful for finding member details including usernames, display names, and emails.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
debugNo
limitNo
nameFilterNo
teamIdNo

Implementation Reference

  • Registration of the 'list_members' tool using the create_tool factory, specifying name, description, input schema, and handler.
    export const ListMembers = create_tool({
      name: 'list_members',
      description:
        'List Linear team members with optional filtering by name. This tool is useful for finding member details including usernames, display names, and emails.',
      inputSchema: ListMembersInputSchema,
      handler,
    });
  • Main handler function for the list_members tool. It creates a Linear client, calls the listMembers helper, formats the results into a readable list, and handles errors with debug support.
    const handler = async (ctx, { teamId, nameFilter, limit, debug }) => {
      const logger = ctx.effects.logger;
    
      try {
        // Log details about config and parameters
        logger.debug('List members called with parameters:', {
          teamId,
          nameFilter,
          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 members using the Linear SDK client with filters
        logger.debug('Executing Linear API list with filters');
        const results = await listMembers(
          linearClient,
          {
            teamId,
            nameFilter,
          },
          {
            limit,
          },
          logger
        );
    
        // Log the results count
        logger.info(`Found ${results.results.length} members matching criteria`);
    
        // Format the output
        let responseText = '';
    
        if (results.results.length === 0) {
          responseText = 'No members found matching your criteria.';
        } else {
          responseText = 'Members found:\n\n';
    
          results.results.forEach((member, index) => {
            // 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';
              }
            };
    
            // Determine member type
            let memberType = 'Regular user';
            if (member.organizationMembership) {
              if (member.organizationMembership.owner) memberType = 'Owner';
              else if (member.organizationMembership.guest) memberType = 'Guest';
              else if (member.organizationMembership.member) memberType = 'Member';
            }
            if (member.admin) memberType += ' (Admin)';
            if (member.isMe) memberType += ' (You)';
    
            responseText += `${index + 1}. ${member.displayName}\n`;
            responseText += `   ID: ${member.id}\n`;
            responseText += `   Username: ${member.name}\n`;
    
            if (member.email) {
              responseText += `   Email: ${member.email}\n`;
            }
    
            responseText += `   Status: ${member.active ? 'Active' : 'Inactive'}\n`;
            responseText += `   Role: ${
              member.role !== 'unknown' ? member.role : memberType
            }\n`;
    
            // Add timestamps
            responseText += `   Created: ${formatDisplayDate(member.createdAt)}\n`;
            if (member.updatedAt) {
              responseText += `   Updated: ${formatDisplayDate(
                member.updatedAt
              )}\n`;
            }
            if (member.lastSeen) {
              responseText += `   Last seen: ${formatDisplayDate(
                member.lastSeen
              )}\n`;
            }
    
            responseText += '\n';
          });
        }
    
        logger.debug('Returning formatted list results');
        return {
          content: [{ type: 'text', text: responseText }],
        };
      } catch (error) {
        logger.error(`Error listing members: ${error.message}`);
        logger.error(error.stack);
    
        // Create a user-friendly error message with troubleshooting guidance
        let errorMessage = `Error listing members: ${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:
    - teamId: ${teamId || '<not specified>'}
    - nameFilter: ${nameFilter || '<not specified>'}
    - 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,
        };
      }
    };
  • Zod input schema for the list_members tool, defining optional teamId, nameFilter, limit (default 25), and debug flag.
    const ListMembersInputSchema = z.object({
      teamId: z.string().optional(),
      nameFilter: z.string().optional(),
      limit: z.number().min(1).max(100).default(25),
      debug: z.boolean().default(false), // Debug mode to show extra diagnostics
    });
  • Helper function implementing the core logic: queries Linear SDK users API, applies name and team filters, enriches user data with membership details and timestamps, validates with Zod schema.
    async function listMembers(client, filters = {}, { limit = 25 } = {}, logger) {
      try {
        logger?.debug('Building Linear SDK filter parameters', {
          filters,
          limit,
        });
    
        // Build query parameters similar to how issues() is used in list-tickets.js
        const queryParams = {
          first: limit, // Limit number of results
        };
    
        // Note: We're using the users() method from the Linear SDK
        // This follows the same pattern as issues() in list-tickets.js
        logger?.debug(
          'Querying users with params:',
          JSON.stringify(queryParams, null, 2)
        );
    
        // @ts-ignore - The Linear SDK types may not be fully accurate
        const usersResponse = await client.users(queryParams);
        logger?.debug(`Found ${usersResponse.nodes.length} users`);
    
        // Process users - filter and format them
        let filteredUsers = usersResponse.nodes;
    
        // Apply name filter if provided
        if (filters.nameFilter) {
          const nameFilterLower = filters.nameFilter.toLowerCase();
          logger?.debug(`Filtering by name: ${filters.nameFilter}`);
    
          filteredUsers = filteredUsers.filter(user => {
            const name = user.name.toLowerCase();
            const displayName = user.displayName?.toLowerCase() || '';
    
            return (
              name.includes(nameFilterLower) ||
              displayName.includes(nameFilterLower)
            );
          });
    
          logger?.debug(
            `After name filtering, found ${filteredUsers.length} users`
          );
        }
    
        // Apply team filter if provided
        // Note: This requires looking up team membership which we'll implement if the API supports it
        if (filters.teamId) {
          logger?.debug(`Filtering by team ID: ${filters.teamId}`);
    
          // This would require team membership lookup
          // For now, we'll just log that it's not implemented
          logger?.warn('Team filtering not implemented yet');
        }
    
        // Limit the results
        filteredUsers = filteredUsers.slice(0, limit);
        logger?.debug(`After limiting, returning ${filteredUsers.length} users`);
    
        // Convert to our schema format
        const members = await Promise.all(
          filteredUsers.map(async user => {
            // Get organization membership details if available
            let organizationMembership = undefined;
            try {
              // @ts-ignore - The Linear SDK types may not include this property
              if (user.organizationMembership) {
                // If it's a promise, await it
                // @ts-ignore - Handle potential promise
                const membership =
                  // @ts-ignore - Check for promise
                  typeof user.organizationMembership.then === 'function'
                    ? // @ts-ignore - Await the promise
                      await user.organizationMembership
                    : // @ts-ignore - Or use directly
                      user.organizationMembership;
    
                if (membership) {
                  organizationMembership = {
                    id: membership.id,
                    owner: membership.owner || false,
                    member: membership.member || false,
                    guest: membership.guest || false,
                  };
                  logger?.debug(`Found membership details for user: ${user.name}`);
                }
              }
            } catch (membershipError) {
              logger?.warn(
                `Error fetching membership data: ${membershipError.message}`
              );
            }
    
            // Format timestamps to be consistent
            const formatDate = timestamp => {
              if (!timestamp) return undefined;
              return new Date(timestamp).toISOString();
            };
    
            return {
              id: user.id,
              name: user.name,
              email: user.email,
              displayName: user.displayName || user.name,
              active: user.active === false ? false : true, // Default to active if not explicitly false
              // Add timestamps
              createdAt: formatDate(user.createdAt),
              updatedAt: formatDate(user.updatedAt),
              lastSeen: formatDate(user.lastSeen),
              // Add role information
              // @ts-ignore - The Linear SDK types may not include these properties
              admin: user.admin || false,
              // @ts-ignore - The Linear SDK types may not include these properties
              isMe: user.isMe || false,
              // @ts-ignore - The Linear SDK types may not include these properties
              role: user.role || 'unknown',
              // Add organization membership details
              organizationMembership,
            };
          })
        );
    
        logger?.debug(`Successfully processed ${members.length} users`);
    
        return MemberSearchResultsSchema.parse({ results: members });
      } catch (error) {
        // Enhanced error logging
        logger?.error(`Error listing Linear members: ${error.message}`, {
          filters,
          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;
      }
    }
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are provided, so the description carries the full burden of behavioral disclosure. It mentions the tool lists members with filtering and is useful for finding details, but doesn't cover critical behaviors like pagination (implied by 'limit' parameter), authentication requirements, rate limits, error handling, or whether it's read-only (though implied by 'list'). For a tool with 4 parameters and no annotation coverage, this is insufficient.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is concise and front-loaded, with two sentences that directly state the purpose and utility. There's no wasted text, and it efficiently communicates core information. However, it could be slightly more structured by explicitly separating purpose from usage, but this is minor.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness2/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the complexity (4 parameters, no annotations, no output schema), the description is incomplete. It lacks details on parameter meanings, behavioral traits (e.g., pagination, auth), and output format. While it states the purpose clearly, it doesn't provide enough context for an agent to reliably use the tool without additional inference or errors.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0%, so the description must compensate for undocumented parameters. It only mentions 'optional filtering by name,' which corresponds to the 'nameFilter' parameter, but ignores 'teamId,' 'limit,' and 'debug.' This adds minimal value beyond the schema, failing to fully address the coverage gap. With 0% coverage, a baseline of 3 is not met due to incomplete parameter explanation.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool's purpose: 'List Linear team members with optional filtering by name.' It specifies the verb ('List'), resource ('Linear team members'), and scope ('optional filtering by name'), which is specific and actionable. However, it doesn't explicitly differentiate from sibling tools like 'list_teams' or 'list_issues', which would be needed for a score of 5.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides minimal guidance: 'This tool is useful for finding member details including usernames, display names, and emails.' It implies usage for retrieving member information but lacks explicit when-to-use scenarios, prerequisites, or alternatives (e.g., when to use 'list_teams' instead). No exclusions or comparisons to sibling tools are mentioned, leaving gaps in usage context.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Related Tools

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