Skip to main content
Glama
mcp-server.ts26.3 kB
#!/usr/bin/env node import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListToolsRequestSchema, Tool, } from "@modelcontextprotocol/sdk/types.js"; import { LinearService } from "./services/linear-service.js"; import { SSEManager } from "./services/sse-manager.js"; import dotenv from "dotenv"; import * as path from "path"; // Load .env from the project root (where the dist folder is), not from cwd // __dirname in CommonJS will be the dist directory dotenv.config({ path: path.join(__dirname, '..', '.env') }); const server = new Server( { name: "linear-mcp", version: "1.0.0", }, { capabilities: { tools: {}, }, } ); // Wrap initialization in try-catch to catch startup errors let sseManager: SSEManager; let linearService: LinearService; try { sseManager = new SSEManager(15000); // Default heartbeat interval linearService = new LinearService(process.env.LINEAR_API_KEY!, sseManager); } catch (error) { console.error("Failed to initialize services:", error); process.exit(1); } const tools: Tool[] = [ { name: "mcp__linear__list_issues", description: "List issues in the user's Linear workspace", inputSchema: { type: "object", properties: { teamId: { type: "string", description: "The team key (e.g., 'OPS', 'SOFT')" }, projectId: { type: "string", description: "The project identifier" }, cycleId: { type: "string", description: "The cycle identifier" }, assigneeId: { type: "string", description: "The assignee username or email" }, stateId: { type: "string", description: "The state name (e.g., 'Todo', 'In Progress', 'Done')" }, limit: { type: "number", default: 50, description: "The number of issues to return" }, query: { type: "string", description: "An optional search query" }, createdAt: { type: "string", description: "Return only issues created on or after this ISO-8601 date-time" }, updatedAt: { type: "string", description: "Return only issues updated on or after this ISO-8601 date-time" }, includeArchived: { type: "boolean", default: true, description: "Whether to include archived issues" }, parentId: { type: "string", description: "The parent issue identifier (e.g., 'OPS-123')" } }, }, }, { name: "mcp__linear__create_issue", description: "Create a new Linear issue", inputSchema: { type: "object", properties: { title: { type: "string", description: "The issue title" }, teamId: { type: "string", description: "The team key (e.g., 'OPS', 'SOFT')" }, description: { type: "string", description: "The issue description as Markdown" }, assigneeId: { type: "string", description: "The assignee username or email" }, stateId: { type: "string", description: "The state name (e.g., 'Todo', 'In Progress', 'Done')" }, priority: { type: "number", description: "The issue priority. 0 = No priority, 1 = Urgent, 2 = High, 3 = Normal, 4 = Low." }, labelIds: { type: "array", items: { type: "string" }, description: "Array of label names (e.g., ['Bug', 'Feature', 'Blocked'])" }, projectId: { type: "string", description: "The project identifier" }, cycleId: { type: "string", description: "The cycle identifier" }, parentId: { type: "string", description: "The parent issue identifier (e.g., 'OPS-123')" }, dueDate: { type: "string", description: "The due date for the issue in ISO format" }, links: { type: "array", items: { type: "object", properties: { url: { type: "string", format: "uri" }, title: { type: "string", minLength: 1 } }, required: ["url", "title"] }, description: "Array of link objects to attach to the issue" } }, required: ["title", "teamId"], }, }, { name: "mcp__linear__create_subissue", description: "Create a sub-issue under a parent issue", inputSchema: { type: "object", properties: { parentId: { type: "string", description: "The parent issue identifier (e.g., 'OPS-123')" }, title: { type: "string", description: "The sub-issue title" }, description: { type: "string", description: "The sub-issue description (optional)" }, stateId: { type: "string", description: "The state name (optional, e.g., 'Todo')" }, labelIds: { type: "array", items: { type: "string" }, description: "Label names (optional)" } }, required: ["parentId", "title"] }, }, { name: "mcp__linear__update_issue", description: "Update an existing Linear issue", inputSchema: { type: "object", properties: { id: { type: "string", description: "The issue identifier (e.g., 'OPS-123')" }, title: { type: "string", description: "The issue title" }, description: { type: "string", description: "The issue description as Markdown" }, assigneeId: { type: "string", description: "The assignee username or email" }, stateId: { type: "string", description: "The state name (e.g., 'Todo', 'In Progress', 'Done')" }, priority: { type: "number", description: "The issue priority. 0 = No priority, 1 = Urgent, 2 = High, 3 = Normal, 4 = Low." }, labelIds: { type: "array", items: { type: "string" }, description: "Array of label names (e.g., ['Bug', 'Feature', 'Blocked'])" }, projectId: { type: "string", description: "The project identifier" }, cycleId: { type: "string", description: "The cycle identifier" }, parentId: { type: "string", description: "The parent issue identifier (e.g., 'OPS-123')" }, dueDate: { type: "string", description: "The due date for the issue in ISO format" }, estimate: { type: "number", description: "The numerical issue estimate value" }, links: { type: "array", items: { type: "object", properties: { url: { type: "string", format: "uri" }, title: { type: "string", minLength: 1 } }, required: ["url", "title"] }, description: "Array of link objects to attach to the issue" } }, required: ["id"], }, }, { name: "mcp__linear__get_issue", description: "Get issue details formatted as markdown with full description, comments, and metadata", inputSchema: { type: "object", properties: { id: { type: "string", description: "The issue identifier (e.g., 'OPS-123')" } }, required: ["id"], }, }, { name: "mcp__linear__list_teams", description: "List teams in the user's Linear workspace", inputSchema: { type: "object", properties: { query: { type: "string", description: "An optional search query" }, includeArchived: { type: "boolean", default: false, description: "Whether to include archived teams" }, limit: { type: "number", default: 50, description: "The number of items to return" }, orderBy: { type: "string", enum: ["createdAt", "updatedAt"], default: "updatedAt" }, createdAt: { type: "string", description: "Return only teams created on or after this ISO-8601 date-time" }, updatedAt: { type: "string", description: "Return only teams updated on or after this ISO-8601 date-time" }, after: { type: "string", description: "A cursor to start from (for pagination)" }, before: { type: "string", description: "A cursor to end at (for pagination)" } }, }, }, { name: "mcp__linear__list_states", description: "List all workflow states for a team", inputSchema: { type: "object", properties: { teamId: { type: "string", description: "The team key (e.g., 'OPS', 'SOFT')" } }, required: ["teamId"], }, }, { name: "mcp__linear__list_labels", description: "List all labels for a team", inputSchema: { type: "object", properties: { teamId: { type: "string", description: "The team key (e.g., 'OPS', 'SOFT')" } }, required: ["teamId"], }, }, { name: "mcp__linear__search_issues", description: "Search for issues using full text search with filters", inputSchema: { type: "object", properties: { query: { type: "string", description: "The search query for full text search across issue titles and descriptions" }, teamId: { type: "string", description: "Filter by team key (e.g., 'OPS', 'SOFT')" }, stateId: { type: "string", description: "Filter by state name (e.g., 'Todo', 'In Progress', 'Done')" }, labelIds: { type: "array", items: { type: "string" }, description: "Filter by label names (e.g., ['Bug', 'Feature'])" }, createdAfter: { type: "string", description: "Filter issues created on or after this ISO-8601 date-time" }, updatedAfter: { type: "string", description: "Filter issues updated on or after this ISO-8601 date-time" }, includeArchived: { type: "boolean", default: false, description: "Whether to include archived issues in search results" }, limit: { type: "number", default: 50, description: "The maximum number of results to return" } }, required: ["query"], }, }, { name: "mcp__linear__file_history", description: "Get Linear issues that modified a specific file in the git repository", inputSchema: { type: "object", properties: { file_path: { type: "string", description: "Path to the file (relative or absolute)" }, limit: { type: "number", default: 20, description: "Maximum number of issues to return" } }, required: ["file_path"], }, }, { name: "mcp__linear__add_comment", description: "Add a comment to a Linear issue", inputSchema: { type: "object", properties: { id: { type: "string", description: "The issue identifier (e.g., 'MCP-1')" }, comment: { type: "string", description: "The comment text (supports markdown)" } }, required: ["id", "comment"], }, }, { name: "mcp__linear__delete_issue", description: "Archive/delete a Linear issue", inputSchema: { type: "object", properties: { id: { type: "string", description: "The issue identifier (e.g., 'MCP-1')" } }, required: ["id"], }, }, { name: "mcp__linear__list_comments", description: "List comments on a Linear issue", inputSchema: { type: "object", properties: { issueId: { type: "string", description: "The issue identifier (e.g., 'MCP-1')" } }, required: ["issueId"], }, }, { name: "mcp__linear__list_projects", description: "List projects in the user's Linear workspace", inputSchema: { type: "object", properties: { limit: { type: "number", default: 50, description: "The number of projects to return" } }, }, }, { name: "mcp__linear__get_project", description: "Get details for a specific project", inputSchema: { type: "object", properties: { id: { type: "string", description: "The project identifier or name" } }, required: ["id"], }, }, { name: "mcp__linear__create_project", description: "Create a new project", inputSchema: { type: "object", properties: { name: { type: "string", description: "The project name" }, description: { type: "string", description: "The project description" }, teamIds: { type: "array", items: { type: "string" }, description: "Array of team keys (e.g., ['OPS', 'SOFT'])" }, startDate: { type: "string", description: "The project start date in ISO format" }, targetDate: { type: "string", description: "The project target date in ISO format" } }, required: ["name", "teamIds"], }, }, { name: "mcp__linear__update_project", description: "Update an existing project", inputSchema: { type: "object", properties: { id: { type: "string", description: "The project identifier" }, name: { type: "string", description: "The project name" }, description: { type: "string", description: "The project description" }, startDate: { type: "string", description: "The project start date in ISO format" }, targetDate: { type: "string", description: "The project target date in ISO format" } }, required: ["id"], }, }, { name: "mcp__linear__get_team", description: "Get details for a specific team", inputSchema: { type: "object", properties: { id: { type: "string", description: "The team key (e.g., 'OPS') or ID" } }, required: ["id"], }, }, { name: "mcp__linear__list_users", description: "List users in the Linear workspace", inputSchema: { type: "object", properties: { limit: { type: "number", default: 50, description: "The number of users to return" } }, }, }, { name: "mcp__linear__get_user", description: "Get details for a specific user", inputSchema: { type: "object", properties: { id: { type: "string", description: "The user email or ID" } }, required: ["id"], }, }, { name: "mcp__linear__get_current_user", description: "Get details for the currently authenticated user", inputSchema: { type: "object", properties: {}, }, }, { name: "mcp__linear__get_capabilities", description: "Get the capabilities and features of the Linear MCP server", inputSchema: { type: "object", properties: {}, }, }, ]; server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools }; }); server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { switch (name) { case "mcp__linear__list_issues": const result = await linearService.listIssues(args || {}); return { content: [ { type: "text", text: result.table || "No issues found", }, ], }; case "mcp__linear__get_issue": const issueData = await linearService.getIssueMarkdown(args || {}); return { content: [ { type: "text", text: issueData.markdown, }, ], }; case "mcp__linear__create_issue": const newIssue = await linearService.createIssue(args || {}); return { content: [ { type: "text", text: `✅ Created issue **${newIssue.identifier}**: ${newIssue.title}`, }, ], }; case "mcp__linear__create_subissue": { // First, get the parent issue to retrieve its teamId const parentIssueData = await linearService.getIssueMarkdown({ id: args?.parentId }); // Extract team info from the markdown using regex const teamMatch = parentIssueData.markdown.match(/\*\*Team:\*\* .+ \((\w+)\)/); if (!teamMatch || !teamMatch[1]) { throw new Error(`Could not determine team for parent issue ${String(args?.parentId)}`); } const teamId = teamMatch[1]; // Extract the parent issue UUID from the markdown const idMatch = parentIssueData.markdown.match(/\*\*ID:\*\* `([a-f0-9-]+)`/); if (!idMatch || !idMatch[1]) { throw new Error(`Could not extract UUID for parent issue ${String(args?.parentId)}`); } const parentUuid = idMatch[1]; // Create the sub-issue with the parent's teamId and resolved UUID const subissueArgs = { title: args?.title, description: args?.description, teamId: teamId, parentId: parentUuid, stateId: args?.stateId, labelIds: args?.labelIds }; const newSubissue = await linearService.createIssue(subissueArgs); return { content: [ { type: "text", text: `✅ Created sub-issue **${newSubissue.identifier}** under **${String(args?.parentId)}**: ${newSubissue.title}`, }, ], }; } case "mcp__linear__update_issue": const updatedIssue = await linearService.updateIssue(args || {}); return { content: [ { type: "text", text: `✅ Updated issue **${updatedIssue.identifier}**: ${updatedIssue.title}`, }, ], }; case "mcp__linear__list_teams": const teams = await linearService.listTeams(args || {}); const teamsTable = teams.nodes.map((team: any) => `| ${team.key} | ${team.name} | ${team.description || '-'} |`).join('\n'); return { content: [ { type: "text", text: `| Key | Name | Description |\n|---|---|---|\n${teamsTable}`, }, ], }; case "mcp__linear__list_states": const statesResult = await linearService.listStates(args || {}); const statesByType = statesResult.states.reduce((acc: any, state: any) => { if (!acc[state.type]) acc[state.type] = []; acc[state.type].push(state.name); return acc; }, {}); const statesList = Object.entries(statesByType) .map(([type, states]: [string, any]) => `${type}:\n${states.map((s: string) => ` - ${s}`).join('\n')}`) .join('\n\n'); return { content: [ { type: "text", text: statesList, }, ], }; case "mcp__linear__list_labels": const labelsResult = await linearService.listLabels(args || {}); const labelsList = labelsResult.labels.map((label: any) => `- ${label.name}`).join('\n'); return { content: [ { type: "text", text: labelsList, }, ], }; case "mcp__linear__search_issues": const searchResult = await linearService.searchIssues(args || {}); return { content: [ { type: "text", text: searchResult.table || "No issues found", }, ], }; case "mcp__linear__file_history": const fileHistoryResult = await linearService.getFileHistory(args || {}); return { content: [ { type: "text", text: fileHistoryResult.formatted || fileHistoryResult.message || "No issues found", }, ], }; case "mcp__linear__add_comment": const commentArgs = { issueId: args?.id, body: args?.comment }; const commentResult = await linearService.createComment(commentArgs); let responseText: string; if (commentResult.chunked) { responseText = `✅ Added comment to issue (split into ${commentResult.totalParts} parts due to length)`; } else { responseText = `✅ Added comment to issue`; } return { content: [ { type: "text", text: responseText, }, ], }; case "mcp__linear__delete_issue": const deleteResult = await linearService.deleteIssue(args || {}); return { content: [ { type: "text", text: deleteResult.success ? "✅ Issue archived successfully" : "❌ Failed to archive issue", }, ], }; case "mcp__linear__list_comments": const comments = await linearService.listComments(args || {}); if (comments.nodes.length === 0) { return { content: [ { type: "text", text: "*No comments found*", }, ], }; } const commentsText = comments.nodes.map((comment: any) => `**#${comment.position} - ${comment.user?.name || 'Unknown'}** (${new Date(comment.createdAt).toLocaleString()})\n${comment.body}` ).join('\n\n---\n\n'); return { content: [ { type: "text", text: commentsText, }, ], }; case "mcp__linear__list_projects": const projects = await linearService.listProjects(args || {}); if (projects.nodes.length === 0) { return { content: [ { type: "text", text: "*No projects found*", }, ], }; } const projectsTable = projects.nodes.map((project: any) => { const teams = project.teams.map((t: any) => t.key).join(', '); return `| ${project.name} | ${teams} | ${project.description || '-'} |`; }).join('\n'); return { content: [ { type: "text", text: `| Name | Teams | Description |\n|---|---|---|\n${projectsTable}`, }, ], }; case "mcp__linear__get_project": const project = await linearService.getProject(args || {}); const projectTeams = project.teams.map((t: any) => `${t.name} (${t.key})`).join(', '); const projectIssues = project.issues.length > 0 ? project.issues.map((i: any) => `- ${i.identifier}: ${i.title}`).join('\n') : '*No issues*'; return { content: [ { type: "text", text: `# ${project.name}\n\n**Teams:** ${projectTeams}\n**Description:** ${project.description || 'No description'}\n**Start Date:** ${project.startDate || 'Not set'}\n**Target Date:** ${project.targetDate || 'Not set'}\n\n## Issues\n${projectIssues}`, }, ], }; case "mcp__linear__create_project": const newProject = await linearService.createProject(args || {}); return { content: [ { type: "text", text: `✅ Created project **${newProject.name}**\n${newProject.url}`, }, ], }; case "mcp__linear__update_project": const updatedProject = await linearService.updateProject(args || {}); return { content: [ { type: "text", text: `✅ Updated project **${updatedProject.name}**`, }, ], }; case "mcp__linear__get_team": const team = await linearService.getTeam(args || {}); return { content: [ { type: "text", text: `# ${team.name} (${team.key})\n\n${team.description || 'No description'}\n\n**Created:** ${new Date(team.createdAt).toLocaleDateString()}`, }, ], }; case "mcp__linear__list_users": const users = await linearService.listUsers(args || {}); const usersTable = users.nodes.map((user: any) => `| ${user.name} | ${user.email} | ${user.active ? 'Active' : 'Inactive'} |` ).join('\n'); return { content: [ { type: "text", text: `| Name | Email | Status |\n|---|---|---|\n${usersTable}`, }, ], }; case "mcp__linear__get_user": const user = await linearService.getUser(args || {}); return { content: [ { type: "text", text: `# ${user.name}\n\n**Email:** ${user.email}\n**Status:** ${user.active ? 'Active' : 'Inactive'}\n**Member Since:** ${new Date(user.createdAt).toLocaleDateString()}`, }, ], }; case "mcp__linear__get_current_user": const currentUser = await linearService.getCurrentUser(args || {}); return { content: [ { type: "text", text: `# ${currentUser.name} (You)\n\n**Email:** ${currentUser.email}\n**Status:** ${currentUser.active ? 'Active' : 'Inactive'}\n**Member Since:** ${new Date(currentUser.createdAt).toLocaleDateString()}`, }, ], }; case "mcp__linear__get_capabilities": const capabilities = await linearService.getCapabilities(args || {}); const methodsList = capabilities.methods.join('\n- '); const featuresText = JSON.stringify(capabilities.features, null, 2); return { content: [ { type: "text", text: `# Linear MCP Server Capabilities\n\n**Version:** ${capabilities.version}\n\n## Available Methods\n- ${methodsList}\n\n## Features\n\`\`\`json\n${featuresText}\n\`\`\``, }, ], }; default: throw new Error(`Unknown tool: ${name}`); } } catch (error) { return { content: [ { type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}`, }, ], isError: true, }; } }); async function runServer() { try { const transport = new StdioServerTransport(); await server.connect(transport); console.error("Linear MCP server running on stdio"); } catch (error) { console.error("Failed to start MCP server:", error); process.exit(1); } } // Add uncaught exception handler process.on('uncaughtException', (error) => { console.error('Uncaught exception:', error); process.exit(1); }); process.on('unhandledRejection', (reason, promise) => { console.error('Unhandled rejection at:', promise, 'reason:', reason); process.exit(1); }); runServer().catch((error) => { console.error("Server startup error:", error); process.exit(1); });

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/bleugreen/linear-mcp'

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