Skip to main content
Glama
iceener

Linear Streamable MCP Server

by iceener
get-issues.ts6.21 kB
/** * Get Issues tool - fetch multiple issues by ID in batch. */ import { z } from 'zod'; import { toolsMetadata } from '../../../config/metadata.js'; import { config } from '../../../config/env.js'; import { GetIssueOutputSchema, GetIssuesOutputSchema } from '../../../schemas/outputs.js'; import { getLinearClient } from '../../../services/linear/client.js'; import { summarizeBatch } from '../../../utils/messages.js'; import { makeConcurrencyGate } from '../../../utils/limits.js'; import { logger } from '../../../utils/logger.js'; import { defineTool, type ToolContext, type ToolResult } from '../types.js'; const InputSchema = z.object({ ids: z .array(z.string()) .min(1) .max(50) .describe('Issue IDs to fetch. Accepts UUIDs or short identifiers like ENG-123.'), }); export const getIssuesTool = defineTool({ name: toolsMetadata.get_issues.name, title: toolsMetadata.get_issues.title, description: toolsMetadata.get_issues.description, inputSchema: InputSchema, annotations: { readOnlyHint: true, destructiveHint: false, }, handler: async (args, context: ToolContext): Promise<ToolResult> => { const client = await getLinearClient(context); const gate = makeConcurrencyGate(3); const ids = args.ids; const results: Array<{ requestedId: string; success: boolean; issue?: ReturnType<typeof GetIssueOutputSchema.parse>; error?: { code: string; message: string; suggestions?: string[] }; }> = []; for (let i = 0; i < ids.length; i++) { const id = ids[i] as string; try { const issue = await gate(() => client.issue(id)); const labels = (await issue.labels()).nodes.map((l) => ({ id: l.id, name: l.name, })); // Await lazy-loaded relations const assigneeData = await issue.assignee; const stateData = await issue.state; const projectData = await issue.project; const issueUrl = (issue as unknown as { url?: string })?.url; const structured = GetIssueOutputSchema.parse({ id: issue.id, title: issue.title, description: issue.description ?? undefined, identifier: issue.identifier ?? undefined, url: issueUrl, assignee: assigneeData ? { id: assigneeData.id, name: assigneeData.name ?? undefined, } : undefined, state: stateData ? { id: stateData.id, name: stateData.name ?? '', type: (stateData as unknown as { type?: string })?.type, } : undefined, project: projectData ? { id: projectData.id, name: projectData.name ?? undefined, } : undefined, labels, branchName: issue.branchName ?? undefined, attachments: (await issue.attachments()).nodes, }); results.push({ requestedId: id, success: true, issue: structured, }); } catch (error) { await logger.error('get_issues', { message: 'Failed to fetch issue', id, error: (error as Error).message, }); results.push({ requestedId: id, success: false, error: { code: 'NOT_FOUND', message: (error as Error).message, suggestions: [ 'Verify the issue ID or identifier is correct.', 'Use list_issues to find valid issue IDs.', 'Check if the issue was archived (use includeArchived: true).', ], }, }); } } const succeeded = results.filter((r) => r.success).length; const failed = results.filter((r) => !r.success).length; const summary = { succeeded, failed, }; // Build meta with next steps const meta = { nextSteps: [ 'Use update_issues to modify state, assignee, or labels.', 'Use add_comments to add context or updates.', ], relatedTools: ['update_issues', 'add_comments', 'list_issues'], }; const structuredBatch = GetIssuesOutputSchema.parse({ results, summary, meta }); const okIds = results .filter((r) => r.success) .map((r) => r.issue?.identifier ?? r.issue?.id ?? r.requestedId); // Build summary without next steps (tips go at the end) const summaryLine = summarizeBatch({ action: 'Fetched issues', ok: succeeded, total: ids.length, okIdentifiers: okIds as string[], failures: results .filter((r) => !r.success) .map((r, idx) => ({ index: idx, id: r.requestedId, error: r.error?.message ?? '' })), }); const previewLines = results .filter((r) => r.success && r.issue) .map((r) => { const it = r.issue as unknown as { identifier?: string; id: string; url?: string; state?: { name?: string }; assignee?: { name?: string }; title: string; }; const stateNm = it.state?.name as string | undefined; const assNm = it.assignee?.name as string | undefined; const prefix = it.url ? `[${it.identifier ?? it.id}](${it.url})` : it.identifier ?? it.id; return `${prefix} '${it.title}'${ stateNm ? ` — state ${stateNm}` : '' }${assNm ? `, assignee ${assNm}` : ''}`; }); // Compose: summary → preview → tip const textParts = [summaryLine]; if (previewLines.length > 0) { textParts.push(`Preview:\n${previewLines.map((l) => `- ${l}`).join('\n')}`); } textParts.push('Tip: Use update_issues to modify, or list_issues to discover more.'); const fullMessage = textParts.join('\n\n'); const parts: Array<{ type: 'text'; text: string }> = [ { type: 'text', text: fullMessage }, ]; if (config.LINEAR_MCP_INCLUDE_JSON_IN_CONTENT) { parts.push({ type: 'text', text: JSON.stringify(structuredBatch) }); } return { content: parts, structuredContent: structuredBatch }; }, });

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/iceener/linear-streamable-mcp-server'

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