Skip to main content
Glama
linear-project.ts11 kB
/** * Linear Project Resources Module * * Defines resource templates and handlers for Linear project resources. * * @module resources/linear-project */ import { McpResourceTemplate } from '../types.js'; import { registerResource, ResourceHandler, ResourceArgs, ResourceResponse } from '../registry.js'; import { linearClient } from '../linear.js'; /** * Linear Project resource template */ const linearProjectResourceTemplate: McpResourceTemplate = { uriTemplate: 'linear-project:///{projectId}', name: 'linear-project', description: 'Linear project details', mimeType: 'application/json', }; /** * Linear Project Issues resource template */ const linearProjectIssuesResourceTemplate: McpResourceTemplate = { uriTemplate: 'linear-project:///{projectId}/issues', name: 'linear-project-issues', description: 'Linear project issues', mimeType: 'application/json', }; /** * Linear Project Milestones resource template */ const linearProjectMilestonesResourceTemplate: McpResourceTemplate = { uriTemplate: 'linear-project:///{projectId}/milestones', name: 'linear-project-milestones', description: 'Linear project milestones', mimeType: 'application/json', }; // Define types for the responses export interface LinearProjectDetailResponse { id: string; name: string; description: string; color: string; icon?: string; progress: number; status?: { id: string; name: string; color: string; type: string; }; lead?: { id: string; name: string; displayName: string; }; teams: Array<{ id: string; name: string; key: string; }>; members: Array<{ id: string; name: string; displayName: string; }>; startDate?: string; targetDate?: string; createdAt: Date; updatedAt: Date; } export interface LinearProjectIssuesResponse { project: { id: string; name: string; description: string; }; issues: Array<{ id: string; title: string; identifier: string; description?: string; status: string; priority: number; assignee?: { id: string; name: string; displayName: string; }; url: string; createdAt: Date; updatedAt: Date; }>; } export interface LinearProjectMilestonesResponse { project: { id: string; name: string; description: string; }; milestones: Array<{ id: string; name: string; description?: string; targetDate?: string; status: string; sortOrder: number; issues: { total: number; completed: number; }; createdAt: Date; updatedAt: Date; }>; stats: { total: number; byStatus: Record<string, number>; upcoming: number; overdue: number; completed: number; }; } /** * Handler for Linear project resources */ export const linearProjectResourceHandler: ResourceHandler = async ( args: ResourceArgs ): Promise<ResourceResponse<LinearProjectDetailResponse | null>> => { try { // Extract the projectId from the URI const projectId = args.projectId as string; if (!projectId) { return { isError: true, errorMessage: 'Invalid project ID', data: null, }; } // Fetch the project from Linear const project = await linearClient.project(projectId); if (!project) { return { isError: true, errorMessage: `Project with ID ${projectId} not found`, data: null, }; } // Fetch related entities const status = await project.status; const lead = await project.lead; const teamsConnection = await project.teams(); const teams = teamsConnection?.nodes || []; const membersConnection = await project.members(); const members = membersConnection?.nodes || []; // Format the response return { data: { id: project.id, name: project.name, description: project.description, color: project.color, icon: project.icon, progress: project.progress, status: status ? { id: await status.id, name: await status.name, color: await status.color, type: await status.type, } : undefined, lead: lead ? { id: await lead.id, name: await lead.name, displayName: await lead.displayName, } : undefined, teams: await Promise.all( teams.map(async team => ({ id: team.id, name: team.name, key: team.key, })) ), members: await Promise.all( members.map(async member => ({ id: member.id, name: member.name, displayName: member.displayName, })) ), startDate: project.startDate, targetDate: project.targetDate, createdAt: project.createdAt, updatedAt: project.updatedAt, } as LinearProjectDetailResponse, isError: false, }; } catch (error) { return { isError: true, errorMessage: typeof error === 'string' ? error : String(error), data: null, }; } }; /** * Handler for Linear project issues resources */ export const linearProjectIssuesResourceHandler: ResourceHandler = async ( args: ResourceArgs ): Promise<ResourceResponse<LinearProjectIssuesResponse | null>> => { try { // Extract the projectId from the URI const projectId = args.projectId as string; if (!projectId) { return { isError: true, errorMessage: 'Invalid project ID', data: null, }; } // Fetch the project from Linear const project = await linearClient.project(projectId); if (!project) { return { isError: true, errorMessage: `Project with ID ${projectId} not found`, data: null, }; } // Fetch the project's issues const issuesConnection = await project.issues({ first: 50 }); const issues = issuesConnection?.nodes || []; // Format the response return { data: { project: { id: project.id, name: project.name, description: project.description, }, issues: await Promise.all( issues.map(async issue => { const state = await issue.state; const assignee = await issue.assignee; return { id: issue.id, title: issue.title, identifier: issue.identifier, description: issue.description, status: state ? await state.name : 'Unknown', priority: issue.priority, assignee: assignee ? { id: await assignee.id, name: await assignee.name, displayName: await assignee.displayName, } : undefined, url: issue.url, createdAt: issue.createdAt, updatedAt: issue.updatedAt, }; }) ), } as LinearProjectIssuesResponse, isError: false, }; } catch (error) { return { isError: true, errorMessage: typeof error === 'string' ? error : String(error), data: null, }; } }; /** * Handler for Linear project milestones resources */ export const linearProjectMilestonesResourceHandler: ResourceHandler = async ( args: ResourceArgs ): Promise<ResourceResponse<LinearProjectMilestonesResponse | null>> => { try { // Extract the projectId from the URI const projectId = args.projectId as string; if (!projectId) { return { isError: true, errorMessage: 'Invalid project ID', data: null, }; } // Fetch the project from Linear const project = await linearClient.project(projectId); if (!project) { return { isError: true, errorMessage: `Project with ID ${projectId} not found`, data: null, }; } // In a real implementation, we would fetch the milestones from Linear // For now, we'll simulate with mock data const mockMilestones = [ { id: 'milestone1', name: 'Alpha Release', description: 'Initial feature-complete release for internal testing', targetDate: '2023-06-15', status: 'completed', sortOrder: 1, issues: { total: 5, completed: 5, }, createdAt: new Date('2023-01-15T12:00:00Z'), updatedAt: new Date('2023-06-18T09:30:00Z'), }, { id: 'milestone2', name: 'Beta Release', description: 'Public beta testing phase', targetDate: '2023-08-01', status: 'inProgress', sortOrder: 2, issues: { total: 8, completed: 5, }, createdAt: new Date('2023-01-15T12:10:00Z'), updatedAt: new Date('2023-07-10T11:45:00Z'), }, { id: 'milestone3', name: 'Final Release', description: 'Production launch', targetDate: '2023-09-30', status: 'planned', sortOrder: 3, issues: { total: 10, completed: 0, }, createdAt: new Date('2023-01-15T12:20:00Z'), updatedAt: new Date('2023-01-15T12:20:00Z'), }, ]; // Calculate statistics const stats = { total: mockMilestones.length, byStatus: {} as Record<string, number>, upcoming: 0, overdue: 0, completed: 0, }; const now = new Date(); mockMilestones.forEach(milestone => { // Count by status stats.byStatus[milestone.status] = (stats.byStatus[milestone.status] || 0) + 1; // Count completed if (milestone.status === 'completed') { stats.completed++; } // Check if milestone is upcoming or overdue if (milestone.targetDate) { const targetDate = new Date(milestone.targetDate); if (targetDate > now && milestone.status !== 'completed') { stats.upcoming++; } else if (targetDate < now && milestone.status !== 'completed') { stats.overdue++; } } }); // Format the response return { data: { project: { id: project.id, name: project.name, description: project.description, }, milestones: mockMilestones, stats, } as LinearProjectMilestonesResponse, isError: false, }; } catch (error) { return { isError: true, errorMessage: typeof error === 'string' ? error : String(error), data: null, }; } }; // Register the resources registerResource(linearProjectResourceTemplate, linearProjectResourceHandler); registerResource(linearProjectIssuesResourceTemplate, linearProjectIssuesResourceHandler); registerResource(linearProjectMilestonesResourceTemplate, linearProjectMilestonesResourceHandler);

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/magarcia/mcp-server-linearapp'

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