Skip to main content
Glama
iceener

Linear Streamable MCP Server

by iceener
mappers.ts6.74 kB
import type { ListCommentsOutput, ListIssuesOutput, ListProjectsOutput, ListTeamsOutput, ListUsersOutput, } from '../schemas/outputs.js'; import { logger } from './logger.js'; // Generic helpers / guards export function isRecord(value: unknown): value is Record<string, unknown> { return typeof value === 'object' && value !== null; } export function hasFunction< Fn extends (...args: unknown[]) => unknown = (...args: unknown[]) => unknown, >(value: unknown, key: string): value is Record<string, Fn> { return ( isRecord(value) && typeof (value as Record<string, unknown>)[key] === 'function' ); } export function hasProperty<T = unknown>( value: unknown, key: string, ): value is Record<string, T> { return isRecord(value) && key in value; } export async function mapIssueNodeToListItem( node: unknown, ): Promise<ListIssuesOutput['items'][number]> { // Labels const labels = await (async () => { try { if (hasFunction(node, 'labels')) { const conn = await ( node as { labels: () => Promise<{ nodes: Array<{ id: string; name: string }>; }>; } ).labels(); return conn.nodes.map((l) => ({ id: l.id, name: l.name })); } } catch (error) { logger.debug('mappers', { message: 'Failed to fetch labels', error: (error as Error).message, }); } return [] as Array<{ id: string; name: string }>; })(); // Related names (best-effort with logging) let stateName: string | undefined; let stateIdFromRelation: string | undefined; let projectName: string | undefined; let assigneeName: string | undefined; try { const s = (node as { state?: Promise<{ id?: string; name?: string }> }).state; if (s && typeof (s as unknown) === 'object') { const resolved = await s; stateName = resolved?.name ?? undefined; stateIdFromRelation = resolved?.id ?? undefined; } } catch (error) { logger.debug('mappers', { message: 'Failed to fetch state relation', error: (error as Error).message, }); } try { const p = (node as { project?: Promise<{ name?: string }> }).project; if (p && typeof (p as unknown) === 'object') { const resolved = await p; projectName = resolved?.name ?? undefined; } } catch (error) { logger.debug('mappers', { message: 'Failed to fetch project relation', error: (error as Error).message, }); } try { const a = (node as { assignee?: Promise<{ name?: string }> }).assignee; if (a && typeof (a as unknown) === 'object') { const resolved = await a; assigneeName = resolved?.name ?? undefined; } } catch (error) { logger.debug('mappers', { message: 'Failed to fetch assignee relation', error: (error as Error).message, }); } // Issue class has these as required properties per SDK types const issue = node as { id: string; identifier: string; title: string; priority: number; url: string; description?: string | null; estimate?: number | null; dueDate?: string | null; archivedAt?: Date | string | null; createdAt: Date | string; updatedAt: Date | string; }; const id = issue.id; const identifier = issue.identifier; const title = issue.title; const description = issue.description ?? undefined; const priority = issue.priority; const estimate = issue.estimate ?? undefined; // Use stateId from relation, fall back to empty string (required by output schema) const stateId = stateIdFromRelation ?? ''; const projectId = (node as { projectId?: string | null })?.projectId ?? undefined; const assigneeId = (node as { assigneeId?: string | null })?.assigneeId ?? undefined; const createdAt = String(issue.createdAt ?? ''); const updatedAt = String(issue.updatedAt ?? ''); const archivedAt = issue.archivedAt ? String(issue.archivedAt) : undefined; const dueDate = issue.dueDate ?? undefined; const url = issue.url; return { id, identifier, title, description, priority, estimate, stateId, stateName, projectId, projectName, assigneeId, assigneeName, createdAt, updatedAt, archivedAt, dueDate, url, labels, }; } export function mapProjectNodeToListItem( node: unknown, ): ListProjectsOutput['items'][number] { // Project class per SDK types const project = node as { id: string; name: string; state: string; description: string; priority: number; targetDate?: string | null; }; // Try to get leadId if available const leadId = (node as { leadId?: string | null })?.leadId ?? undefined; return { id: project.id, name: project.name, state: project.state, teamId: undefined, // Projects can have multiple teams via teams() - caller should resolve if needed leadId, targetDate: project.targetDate ?? undefined, description: project.description ?? undefined, }; } export function mapTeamNodeToListItem(node: unknown): ListTeamsOutput['items'][number] { return { id: (node as { id?: string })?.id ?? '', key: (node as { key?: string | null })?.key ?? undefined, name: (node as { name?: string })?.name ?? '', }; } export function mapUserNodeToListItem(node: unknown): ListUsersOutput['items'][number] { return { id: (node as { id?: string })?.id ?? '', name: (node as { name?: string | null })?.name ?? undefined, email: (node as { email?: string | null })?.email ?? undefined, displayName: (node as { displayName?: string | null })?.displayName ?? undefined, avatarUrl: (node as { avatarUrl?: string | null })?.avatarUrl ?? undefined, }; } export async function mapCommentNodeToListItem( node: unknown, ): Promise<ListCommentsOutput['items'][number]> { let user: { id: string; name?: string } | undefined; try { const u = await (node as { user?: Promise<unknown> }).user; if (u && isRecord(u)) { user = { id: (u as { id?: string })?.id ?? '', name: (u as { name?: string | null })?.name ?? undefined, }; } } catch (error) { logger.debug('mappers', { message: 'Failed to fetch comment user relation', error: (error as Error).message, }); } const comment = node as { id: string; body?: string | null; url?: string | null; createdAt: Date | string; updatedAt?: Date | string | null; }; return { id: comment.id, body: comment.body ?? undefined, url: comment.url ?? undefined, createdAt: String(comment.createdAt), updatedAt: comment.updatedAt ? String(comment.updatedAt) : undefined, user, }; }

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