Skip to main content
Glama

mcp-jira-stdio

formatters.ts10.6 kB
import { McpToolResponse } from '../types/common.js'; import { maybeRedactAccountId, maybeRedactEmail } from './redaction.js'; import { JiraProject, JiraIssue, JiraSearchResult, JiraUser, JiraIssueType, JiraPriority, JiraStatus, JiraComment, JiraProjectDetails, } from '../types/jira.js'; export function formatProjectsResponse(projects: JiraProject[]): McpToolResponse { const projectsList = projects .map( (project) => `• **${project.key}** - ${project.name}\n ${project.description || 'No description'}\n Type: ${project.projectTypeKey} | Private: ${project.isPrivate ? 'Yes' : 'No'}` ) .join('\n\n'); return { content: [ { type: 'text', text: `Found ${projects.length} visible project(s):\n\n${projectsList}`, }, ], }; } function adfToPlainText(adf: any): string { // Very lightweight ADF to text conversion (best-effort) try { const walk = (node: any): string => { if (!node) return ''; switch (node.type) { case 'text': return node.text || ''; case 'paragraph': case 'heading': case 'blockquote': return (node.content || []).map(walk).join(''); case 'codeBlock': return '\n' + (node.content || []).map(walk).join('') + '\n'; case 'bulletList': case 'orderedList': return (node.content || []).map(walk).join('\n'); case 'listItem': return '• ' + (node.content || []).map(walk).join(''); case 'hardBreak': return '\n'; default: if (Array.isArray(node.content)) return node.content.map(walk).join(''); return ''; } }; if (adf && Array.isArray(adf.content)) { return adf.content.map(walk).join('\n').trim(); } } catch { // ignore and fallback } return ''; } export function formatIssueResponse(issue: JiraIssue): McpToolResponse { const { key, fields } = issue; const summary = fields?.summary || 'No summary'; const assigneeText = fields?.assignee ? `${fields.assignee.displayName} (${maybeRedactAccountId(fields.assignee.accountId)})` : 'Unassigned'; const reporterText = fields?.reporter ? `${fields.reporter.displayName} (${maybeRedactAccountId(fields.reporter.accountId)})` : 'Unknown'; const labelsArray = Array.isArray(fields?.labels) ? fields.labels : []; const labelsText = labelsArray.length > 0 ? labelsArray.join(', ') : 'None'; const componentsArray = Array.isArray(fields?.components) ? fields.components : []; const componentsText = componentsArray.length > 0 ? componentsArray.map((c) => c.name).join(', ') : 'None'; let description = fields?.description as any; if (description && typeof description === 'object') { const parsed = adfToPlainText(description); description = parsed || '[rich text description omitted]'; } if (!description || typeof description !== 'string') { description = 'No description provided'; } const createdText = fields?.created ? new Date(fields.created).toISOString() : 'N/A'; const updatedText = fields?.updated ? new Date(fields.updated).toISOString() : 'N/A'; return { content: [ { type: 'text', text: `**${key}: ${summary}**\n\n**Status:** ${fields?.status?.name || 'Unknown'}\n**Priority:** ${fields?.priority?.name || 'None'}\n**Assignee:** ${assigneeText}\n**Reporter:** ${reporterText}\n**Project:** ${fields?.project?.name || 'Unknown'} (${fields?.project?.key || 'N/A'})\n**Issue Type:** ${fields?.issuetype?.name || 'Unknown'}\n**Labels:** ${labelsText}\n**Components:** ${componentsText}\n**Created:** ${createdText}\n**Updated:** ${updatedText}\n\n**Description:**\n${description}`, }, ], }; } export function formatSearchResultsResponse(result: JiraSearchResult): McpToolResponse { if (result.issues.length === 0) { return { content: [ { type: 'text', text: 'No issues found matching your search criteria.', }, ], }; } const issuesList = result.issues .map((issue) => { const assignee = issue.fields.assignee?.displayName || 'Unassigned'; return `• **${issue.key}** - ${issue.fields.summary}\n Status: ${issue.fields.status.name} | Assignee: ${assignee} | Priority: ${issue.fields.priority?.name || 'None'}`; }) .join('\n\n'); const rangeStart = result.startAt + 1; const rangeEnd = Math.min(result.startAt + result.maxResults, result.total); const showRangeInline = result.total > result.maxResults || result.startAt > 0; // Build pagination info let paginationInfo = ''; if (showRangeInline) { paginationInfo = `\n\n*Showing ${rangeStart}-${rangeEnd} of ${result.total} results*`; } // Add nextPageToken info if present and not last page if (result.nextPageToken && !result.isLast) { paginationInfo += `\n*Next page token: \`${result.nextPageToken}\`*`; } else if (result.isLast) { paginationInfo += '\n*This is the last page of results*'; } return { content: [ { type: 'text', text: `Found ${result.total} issue(s)${showRangeInline ? ` (showing ${rangeStart}-${rangeEnd})` : ''}:\n\n${issuesList}${paginationInfo}`, }, ], }; } export function formatErrorResponse(error: any): McpToolResponse { let errorMessage = 'An unexpected error occurred.'; if (typeof error === 'string') { errorMessage = error; } else if (error instanceof Error) { errorMessage = error.message; } else if (error?.response?.data) { const jiraError = error.response.data; if (jiraError.errorMessages && jiraError.errorMessages.length > 0) { errorMessage = jiraError.errorMessages.join(', '); } else if (jiraError.errors) { errorMessage = Object.entries(jiraError.errors) .map(([field, message]) => `${field}: ${message}`) .join(', '); } } return { content: [ { type: 'text', text: `❌ Error: ${errorMessage}`, }, ], }; } export function formatSuccessResponse(message: string): McpToolResponse { return { content: [ { type: 'text', text: `✅ ${message}`, }, ], }; } export function formatUsersResponse(users: JiraUser[]): McpToolResponse { if (users.length === 0) { return { content: [ { type: 'text', text: 'No users found matching your search criteria.', }, ], }; } const usersList = users .map( (user) => `• **${user.displayName}** (${maybeRedactAccountId(user.accountId)})\n Email: ${maybeRedactEmail(user.emailAddress)} | Active: ${user.active ? 'Yes' : 'No'} | Type: ${user.accountType}` ) .join('\n\n'); return { content: [ { type: 'text', text: `Found ${users.length} user(s):\n\n${usersList}`, }, ], }; } export function formatIssueTypesResponse(issueTypes: JiraIssueType[]): McpToolResponse { if (issueTypes.length === 0) { return { content: [ { type: 'text', text: 'No issue types found.', }, ], }; } const typesList = issueTypes .map( (type) => `• **${type.name}** (ID: ${type.id})\n ${type.description}\n Subtask: ${type.subtask ? 'Yes' : 'No'}` ) .join('\n\n'); return { content: [ { type: 'text', text: `Found ${issueTypes.length} issue type(s):\n\n${typesList}`, }, ], }; } export function formatPrioritiesResponse(priorities: JiraPriority[]): McpToolResponse { if (priorities.length === 0) { return { content: [ { type: 'text', text: 'No priorities found.', }, ], }; } const prioritiesList = priorities .map((priority) => `• **${priority.name}** (ID: ${priority.id})\n ${priority.description}`) .join('\n\n'); return { content: [ { type: 'text', text: `Found ${priorities.length} priority level(s):\n\n${prioritiesList}`, }, ], }; } export function formatStatusesResponse(statuses: JiraStatus[]): McpToolResponse { if (statuses.length === 0) { return { content: [ { type: 'text', text: 'No statuses found.', }, ], }; } const statusesList = statuses .map( (status) => `• **${status.name}** (ID: ${status.id})\n ${status.description}\n Category: ${status.statusCategory.name} (${status.statusCategory.key})` ) .join('\n\n'); return { content: [ { type: 'text', text: `Found ${statuses.length} status(es):\n\n${statusesList}`, }, ], }; } export function formatCommentResponse(comment: JiraComment): McpToolResponse { const visibilityText = comment.visibility ? `\n**Visibility:** ${comment.visibility.type} - ${comment.visibility.value}` : ''; return { content: [ { type: 'text', text: `**Comment added successfully** **Author:** ${comment.author.displayName} **Created:** ${new Date(comment.created).toISOString()}${visibilityText} **Content:** ${comment.body}`, }, ], }; } export function formatProjectDetailsResponse(project: JiraProjectDetails): McpToolResponse { const leadText = project.lead ? `${project.lead.displayName} (${project.lead.accountId})` : 'No lead assigned'; const componentsText = project.components.length > 0 ? project.components.map((c) => c.name).join(', ') : 'None'; const versionsText = project.versions.length > 0 ? project.versions.map((v) => v.name).join(', ') : 'None'; const issueTypesText = project.issueTypes.length > 0 ? project.issueTypes.map((t) => `${t.name}${t.subtask ? ' (Subtask)' : ''}`).join(', ') : 'None'; const rolesText = Object.keys(project.roles).length > 0 ? Object.keys(project.roles).join(', ') : 'None'; const insightText = project.insight ? `\n\n**Project Insights:** Total Issues: ${project.insight.totalIssueCount} Last Updated: ${new Date(project.insight.lastIssueUpdateTime).toISOString()}` : ''; return { content: [ { type: 'text', text: `**${project.key}: ${project.name}** **Description:** ${project.description || 'No description'} **Type:** ${project.projectTypeKey} **Private:** ${project.isPrivate ? 'Yes' : 'No'} **Lead:** ${leadText} **Components:** ${componentsText} **Versions:** ${versionsText} **Issue Types:** ${issueTypesText} **Roles:** ${rolesText}${insightText}`, }, ], }; }

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

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