Skip to main content
Glama
server.ts23.4 kB
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; import { fetch } from "undici"; import { loadEnvFile } from "./load-env.js"; // Load environment variables from .env file silently loadEnvFile('/Users/imranbeg/mcp-get-server/.env'); // Jira configuration from environment variables const JIRA_URL = process.env.JIRA_URL; const JIRA_EMAIL = process.env.JIRA_EMAIL; const JIRA_API_TOKEN = process.env.JIRA_API_TOKEN; if (!JIRA_URL || !JIRA_EMAIL || !JIRA_API_TOKEN) { console.error("Missing required Jira environment variables:"); console.error("- JIRA_URL: Your Jira instance URL (e.g., https://yourcompany.atlassian.net)"); console.error("- JIRA_EMAIL: Your Jira email address"); console.error("- JIRA_API_TOKEN: Your Jira API token"); process.exit(1); } const mcp = new McpServer( { name: "mcp-jira-server", version: "1.0.0", }, { capabilities: { tools: {}, }, } ); // Helper function to create Jira API headers function getJiraHeaders(): Record<string, string> { const auth = Buffer.from(`${JIRA_EMAIL}:${JIRA_API_TOKEN}`).toString('base64'); return { 'Authorization': `Basic ${auth}`, 'Accept': 'application/json', 'Content-Type': 'application/json', }; } // Register Jira get issue tool mcp.registerTool( "get_jira_issue", { title: "Get Jira Issue", description: "Get detailed information about a Jira issue by its key (e.g., PROJ-123)", inputSchema: { issueKey: z.string().describe("The Jira issue key (e.g., PROJ-123, TASK-456)"), }, }, async (args: { issueKey: string }) => { try { const url = `${JIRA_URL}/rest/api/3/issue/${args.issueKey}`; const response = await fetch(url, { method: "GET", headers: getJiraHeaders(), }); if (!response.ok) { const errorText = await response.text(); return { content: [ { type: "text", text: `Failed to fetch Jira issue ${args.issueKey}: ${response.status} ${response.statusText}\n${errorText}`, }, ], isError: true, }; } const issueData = await response.json() as any; // Extract key information from the issue const issue = issueData; const fields = issue.fields; const summary = fields.summary || 'No summary'; const description = fields.description || 'No description'; const status = fields.status?.name || 'Unknown status'; const assignee = fields.assignee?.displayName || 'Unassigned'; const reporter = fields.reporter?.displayName || 'Unknown reporter'; const priority = fields.priority?.name || 'No priority'; const issueType = fields.issuetype?.name || 'Unknown type'; const created = fields.created ? new Date(fields.created).toLocaleDateString() : 'Unknown'; const updated = fields.updated ? new Date(fields.updated).toLocaleDateString() : 'Unknown'; // Format description (remove HTML tags if present) const cleanDescription = typeof description === 'string' ? description.replace(/<[^>]*>/g, '').trim() : JSON.stringify(description); return { content: [ { type: "text", text: `**${issue.key}: ${summary}** **Status:** ${status} **Type:** ${issueType} **Priority:** ${priority} **Assignee:** ${assignee} **Reporter:** ${reporter} **Created:** ${created} **Updated:** ${updated} **Description:** ${cleanDescription} **Full Issue Data:** Available in structured content below.`, }, ], structuredContent: { issueKey: issue.key, summary, description: cleanDescription, status, issueType, priority, assignee, reporter, created, updated, url: `${JIRA_URL}/browse/${issue.key}`, fullData: issueData, }, }; } catch (error) { return { content: [ { type: "text", text: `Error fetching Jira issue ${args.issueKey}: ${error instanceof Error ? error.message : String(error)}`, }, ], isError: true, }; } } ); // List Jira projects mcp.registerTool( "list_jira_projects", { title: "List Jira Projects", description: "List accessible Jira projects with pagination and optional query.", inputSchema: { query: z.string().optional().describe("Optional search query for project key/name"), startAt: z.number().int().min(0).optional().describe("Pagination start index (default 0)"), maxResults: z.number().int().min(1).max(100).optional().describe("Page size (1-100, default 50)"), }, }, async (args: { query?: string; startAt?: number; maxResults?: number }) => { try { const params = new URLSearchParams(); if (args.query) params.set("query", args.query); if (typeof args.startAt === "number") params.set("startAt", String(args.startAt)); if (typeof args.maxResults === "number") params.set("maxResults", String(args.maxResults)); const url = `${JIRA_URL}/rest/api/3/project/search${params.toString() ? `?${params.toString()}` : ""}`; const response = await fetch(url, { method: "GET", headers: getJiraHeaders() }); if (!response.ok) { const errorText = await response.text(); return { content: [{ type: "text", text: `Failed to list projects: ${response.status} ${response.statusText}\n${errorText}` }], isError: true }; } const data = await response.json() as any; const projects = (data.values || []).map((p: any) => ({ id: p.id, key: p.key, name: p.name, lead: p.lead?.displayName, projectType: p.projectTypeKey })); return { content: [{ type: "text", text: `Found ${data.total ?? projects.length} projects (showing ${projects.length}).` }], structuredContent: { total: data.total ?? projects.length, startAt: data.startAt ?? 0, maxResults: data.maxResults ?? projects.length, projects }, }; } catch (error) { return { content: [{ type: "text", text: `Error listing projects: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); // Get Jira project details mcp.registerTool( "get_jira_project", { title: "Get Jira Project Details", description: "Get full metadata for a Jira project by key or ID.", inputSchema: { projectIdOrKey: z.string().describe("Project key or ID (e.g., PROJ or 10001)"), }, }, async (args: { projectIdOrKey: string }) => { try { const url = `${JIRA_URL}/rest/api/3/project/${encodeURIComponent(args.projectIdOrKey)}`; const response = await fetch(url, { method: "GET", headers: getJiraHeaders() }); if (!response.ok) { const errorText = await response.text(); return { content: [{ type: "text", text: `Failed to get project ${args.projectIdOrKey}: ${response.status} ${response.statusText}\n${errorText}` }], isError: true }; } const project = await response.json() as any; return { content: [{ type: "text", text: `Project ${project.key}: ${project.name}` }], structuredContent: { id: project.id, key: project.key, name: project.name, url: `${JIRA_URL}/jira/software/c/projects/${project.key}`, lead: project.lead, components: project.components, issueTypes: project.issueTypes, raw: project }, }; } catch (error) { return { content: [{ type: "text", text: `Error getting project ${args.projectIdOrKey}: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); // Get project issue types and statuses mcp.registerTool( "get_project_statuses", { title: "Get Project Issue Types and Statuses", description: "Get available statuses for each issue type in a project.", inputSchema: { projectIdOrKey: z.string().describe("Project key or ID (e.g., PROJ or 10001)"), }, }, async (args: { projectIdOrKey: string }) => { try { const url = `${JIRA_URL}/rest/api/3/project/${encodeURIComponent(args.projectIdOrKey)}/statuses`; const response = await fetch(url, { method: "GET", headers: getJiraHeaders() }); if (!response.ok) { const errorText = await response.text(); return { content: [{ type: "text", text: `Failed to get statuses for ${args.projectIdOrKey}: ${response.status} ${response.statusText}\n${errorText}` }], isError: true }; } const arr = await response.json() as any[]; const summary = arr.map((t: any) => ({ issueType: t.name, statuses: (t.statuses || []).map((s: any) => s.name) })); return { content: [{ type: "text", text: `Found ${summary.length} issue types with statuses.` }], structuredContent: { projectIdOrKey: args.projectIdOrKey, types: summary, raw: arr } }; } catch (error) { return { content: [{ type: "text", text: `Error getting project statuses for ${args.projectIdOrKey}: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); // Search Jira issues using JQL mcp.registerTool( "search_jira_issues", { title: "Search Jira Issues (JQL)", description: "Search issues using JQL with pagination and field selection.", inputSchema: { jql: z.string().describe("JQL query (e.g., project=PROJ AND status=\"In Progress\")"), startAt: z.number().int().min(0).optional().describe("Pagination start index (default 0)"), maxResults: z.number().int().min(1).max(100).optional().describe("Page size (1-100, default 50)"), fields: z.string().optional().describe("Comma-separated fields to return (default: key,summary,status,assignee,priority,issuetype,updated)") }, }, async (args: { jql: string; startAt?: number; maxResults?: number; fields?: string }) => { try { const body: any = { jql: args.jql, startAt: args.startAt ?? 0, maxResults: args.maxResults ?? 50, fields: (args.fields ? args.fields.split(",").map(s => s.trim()) : ["key","summary","status","assignee","priority","issuetype","updated"]).filter(Boolean) }; const url = `${JIRA_URL}/rest/api/3/search`; const response = await fetch(url, { method: "POST", headers: getJiraHeaders(), body: JSON.stringify(body) }); if (!response.ok) { const errorText = await response.text(); return { content: [{ type: "text", text: `Failed to search issues: ${response.status} ${response.statusText}\n${errorText}` }], isError: true }; } const data = await response.json() as any; const items = (data.issues || []).map((it: any) => ({ key: it.key, summary: it.fields?.summary, status: it.fields?.status?.name, assignee: it.fields?.assignee?.displayName, priority: it.fields?.priority?.name, type: it.fields?.issuetype?.name, updated: it.fields?.updated, url: `${JIRA_URL}/browse/${it.key}` })); return { content: [{ type: "text", text: `Found ${data.total ?? items.length} issues (showing ${items.length}).` }], structuredContent: { total: data.total ?? items.length, startAt: data.startAt ?? 0, maxResults: data.maxResults ?? items.length, issues: items, raw: data } }; } catch (error) { return { content: [{ type: "text", text: `Error searching issues: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); // List issues in a project (JQL helper) mcp.registerTool( "list_project_issues", { title: "List Issues in Project", description: "List issues for a project with optional JQL tail filters.", inputSchema: { projectKey: z.string().describe("Project key (e.g., PROJ)"), jqlTail: z.string().optional().describe("Optional extra JQL, e.g., AND status=\"In Progress\""), startAt: z.number().int().min(0).optional().describe("Pagination start index (default 0)"), maxResults: z.number().int().min(1).max(100).optional().describe("Page size (1-100, default 50)"), }, }, async (args: { projectKey: string; jqlTail?: string; startAt?: number; maxResults?: number }) => { const jql = `project=${args.projectKey}${args.jqlTail ? " " + args.jqlTail : ""}`; return await (async () => { try { const body: any = { jql, startAt: args.startAt ?? 0, maxResults: args.maxResults ?? 50, fields: ["key","summary","status","assignee","priority","issuetype","updated"] }; const response = await fetch(`${JIRA_URL}/rest/api/3/search`, { method: "POST", headers: getJiraHeaders(), body: JSON.stringify(body) }); if (!response.ok) { const errorText = await response.text(); return { content: [{ type: "text", text: `Failed to list project issues: ${response.status} ${response.statusText}\n${errorText}` }], isError: true }; } const data = await response.json() as any; const items = (data.issues || []).map((it: any) => ({ key: it.key, summary: it.fields?.summary, status: it.fields?.status?.name, assignee: it.fields?.assignee?.displayName, priority: it.fields?.priority?.name, type: it.fields?.issuetype?.name, updated: it.fields?.updated, url: `${JIRA_URL}/browse/${it.key}` })); return { content: [{ type: "text", text: `Found ${data.total ?? items.length} issues in ${args.projectKey} (showing ${items.length}).` }], structuredContent: { total: data.total ?? items.length, startAt: data.startAt ?? 0, maxResults: data.maxResults ?? items.length, issues: items, projectKey: args.projectKey, raw: data } }; } catch (error) { return { content: [{ type: "text", text: `Error listing issues for ${args.projectKey}: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } })(); } ); // Get issue comments mcp.registerTool( "get_jira_issue_comments", { title: "Get Jira Issue Comments", description: "Retrieve comments for a Jira issue with pagination.", inputSchema: { issueIdOrKey: z.string().describe("Issue key or ID (e.g., PROJ-123)"), startAt: z.number().int().min(0).optional().describe("Pagination start index (default 0)"), maxResults: z.number().int().min(1).max(100).optional().describe("Page size (1-100, default 50)"), }, }, async (args: { issueIdOrKey: string; startAt?: number; maxResults?: number }) => { try { const params = new URLSearchParams(); if (typeof args.startAt === "number") params.set("startAt", String(args.startAt)); if (typeof args.maxResults === "number") params.set("maxResults", String(args.maxResults)); const url = `${JIRA_URL}/rest/api/3/issue/${encodeURIComponent(args.issueIdOrKey)}/comment${params.toString() ? `?${params.toString()}` : ""}`; const response = await fetch(url, { method: "GET", headers: getJiraHeaders() }); if (!response.ok) { const errorText = await response.text(); return { content: [{ type: "text", text: `Failed to get comments for ${args.issueIdOrKey}: ${response.status} ${response.statusText}\n${errorText}` }], isError: true }; } const data = await response.json() as any; const comments = (data.comments || []).map((c: any) => ({ id: c.id, author: c.author?.displayName, created: c.created, updated: c.updated, body: typeof c.body === 'string' ? c.body : JSON.stringify(c.body) })); return { content: [{ type: "text", text: `Found ${data.total ?? comments.length} comments (showing ${comments.length}).` }], structuredContent: { issueIdOrKey: args.issueIdOrKey, total: data.total ?? comments.length, startAt: data.startAt ?? 0, maxResults: data.maxResults ?? comments.length, comments, raw: data } }; } catch (error) { return { content: [{ type: "text", text: `Error getting comments for ${args.issueIdOrKey}: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); // List boards mcp.registerTool( "list_boards", { title: "List Boards", description: "List Jira boards with optional type and project filter.", inputSchema: { type: z.enum(["scrum","kanban"]).optional().describe("Board type filter"), projectKeyOrId: z.string().optional().describe("Filter boards by project key or ID"), startAt: z.number().int().min(0).optional().describe("Pagination start index (default 0)"), maxResults: z.number().int().min(1).max(100).optional().describe("Page size (1-100, default 50)"), }, }, async (args: { type?: "scrum"|"kanban"; projectKeyOrId?: string; startAt?: number; maxResults?: number }) => { try { const params = new URLSearchParams(); if (args.type) params.set("type", args.type); if (args.projectKeyOrId) params.set("projectKeyOrId", args.projectKeyOrId); if (typeof args.startAt === "number") params.set("startAt", String(args.startAt)); if (typeof args.maxResults === "number") params.set("maxResults", String(args.maxResults)); const url = `${JIRA_URL}/rest/agile/1.0/board${params.toString() ? `?${params.toString()}` : ""}`; const response = await fetch(url, { method: "GET", headers: getJiraHeaders() }); if (!response.ok) { const errorText = await response.text(); return { content: [{ type: "text", text: `Failed to list boards: ${response.status} ${response.statusText}\n${errorText}` }], isError: true }; } const data = await response.json() as any; const boards = (data.values || []).map((b: any) => ({ id: b.id, name: b.name, type: b.type, location: b.location })); return { content: [{ type: "text", text: `Found ${data.total ?? boards.length} boards (showing ${boards.length}).` }], structuredContent: { total: data.total ?? boards.length, startAt: data.startAt ?? 0, maxResults: data.maxResults ?? boards.length, boards, raw: data } }; } catch (error) { return { content: [{ type: "text", text: `Error listing boards: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); // List sprints on a board mcp.registerTool( "list_board_sprints", { title: "List Board Sprints", description: "List sprints for a given board with optional state filter.", inputSchema: { boardId: z.number().int().describe("Board ID"), state: z.enum(["active","future","closed"]).optional().describe("Sprint state filter"), startAt: z.number().int().min(0).optional().describe("Pagination start index (default 0)"), maxResults: z.number().int().min(1).max(100).optional().describe("Page size (1-100, default 50)"), }, }, async (args: { boardId: number; state?: "active"|"future"|"closed"; startAt?: number; maxResults?: number }) => { try { const params = new URLSearchParams(); if (args.state) params.set("state", args.state); if (typeof args.startAt === "number") params.set("startAt", String(args.startAt)); if (typeof args.maxResults === "number") params.set("maxResults", String(args.maxResults)); const url = `${JIRA_URL}/rest/agile/1.0/board/${args.boardId}/sprint${params.toString() ? `?${params.toString()}` : ""}`; const response = await fetch(url, { method: "GET", headers: getJiraHeaders() }); if (!response.ok) { const errorText = await response.text(); return { content: [{ type: "text", text: `Failed to list sprints for board ${args.boardId}: ${response.status} ${response.statusText}\n${errorText}` }], isError: true }; } const data = await response.json() as any; const sprints = (data.values || []).map((s: any) => ({ id: s.id, name: s.name, state: s.state, startDate: s.startDate, endDate: s.endDate, completeDate: s.completeDate })); return { content: [{ type: "text", text: `Found ${data.total ?? sprints.length} sprints (showing ${sprints.length}).` }], structuredContent: { total: data.total ?? sprints.length, startAt: data.startAt ?? 0, maxResults: data.maxResults ?? sprints.length, boardId: args.boardId, sprints, raw: data } }; } catch (error) { return { content: [{ type: "text", text: `Error listing sprints for board ${args.boardId}: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); // List issues in a sprint mcp.registerTool( "list_sprint_issues", { title: "List Sprint Issues", description: "List issues in a given sprint with pagination.", inputSchema: { sprintId: z.number().int().describe("Sprint ID"), startAt: z.number().int().min(0).optional().describe("Pagination start index (default 0)"), maxResults: z.number().int().min(1).max(100).optional().describe("Page size (1-100, default 50)"), jql: z.string().optional().describe("Optional additional JQL to filter sprint issues"), }, }, async (args: { sprintId: number; startAt?: number; maxResults?: number; jql?: string }) => { try { const params = new URLSearchParams(); if (typeof args.startAt === "number") params.set("startAt", String(args.startAt)); if (typeof args.maxResults === "number") params.set("maxResults", String(args.maxResults)); if (args.jql) params.set("jql", args.jql); const url = `${JIRA_URL}/rest/agile/1.0/sprint/${args.sprintId}/issue${params.toString() ? `?${params.toString()}` : ""}`; const response = await fetch(url, { method: "GET", headers: getJiraHeaders() }); if (!response.ok) { const errorText = await response.text(); return { content: [{ type: "text", text: `Failed to list sprint issues: ${response.status} ${response.statusText}\n${errorText}` }], isError: true }; } const data = await response.json() as any; const items = (data.issues || []).map((it: any) => ({ key: it.key, summary: it.fields?.summary, status: it.fields?.status?.name, assignee: it.fields?.assignee?.displayName, priority: it.fields?.priority?.name, type: it.fields?.issuetype?.name, updated: it.fields?.updated, url: `${JIRA_URL}/browse/${it.key}` })); return { content: [{ type: "text", text: `Found ${data.total ?? items.length} issues in sprint ${args.sprintId} (showing ${items.length}).` }], structuredContent: { sprintId: args.sprintId, total: data.total ?? items.length, startAt: data.startAt ?? 0, maxResults: data.maxResults ?? items.length, issues: items, raw: data } }; } catch (error) { return { content: [{ type: "text", text: `Error listing sprint issues: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); // TODO: Future Jira tools to implement // - search_jira_issues: Search for issues with JQL // - create_jira_issue: Create new issues // - update_jira_issue: Update existing issues // - add_jira_comment: Add comments to issues // - get_jira_projects: List available projects // - get_jira_issue_types: Get issue types for a project // - get_jira_priorities: Get available priorities // - get_jira_statuses: Get available statuses // - assign_jira_issue: Assign issues to users // - transition_jira_issue: Move issues through workflow states const transport = new StdioServerTransport(); mcp.connect(transport).catch((err: unknown) => { // eslint-disable-next-line no-console console.error("Failed to start MCP server:", err); process.exit(1); });

Implementation Reference

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/imrnbeg/jira-mcp'

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