Skip to main content
Glama

Atlassian Bitbucket MCP Server

by aashari
formatter.util.ts13.6 kB
/** * Standardized formatting utilities for consistent output across all CLI and Tool interfaces. * These functions should be used by all formatters to ensure consistent formatting. */ import { Logger } from './logger.util.js'; // Ensure logger is imported import { ResponsePagination } from '../types/common.types.js'; // const formatterLogger = Logger.forContext('utils/formatter.util.ts'); // Define logger instance - Removed as unused /** * Format a date in a standardized way: YYYY-MM-DD HH:MM:SS UTC * @param dateString - ISO date string or Date object * @returns Formatted date string */ export function formatDate(dateString?: string | Date): string { if (!dateString) { return 'Not available'; } try { const date = typeof dateString === 'string' ? new Date(dateString) : dateString; // Format: YYYY-MM-DD HH:MM:SS UTC return date .toISOString() .replace('T', ' ') .replace(/\.\d+Z$/, ' UTC'); } catch { return 'Invalid date'; } } /** * Format a URL as a markdown link * @param url - URL to format * @param title - Link title * @returns Formatted markdown link */ export function formatUrl(url?: string, title?: string): string { if (!url) { return 'Not available'; } const linkTitle = title || url; return `[${linkTitle}](${url})`; } /** * Format pagination information in a standardized way for CLI output. * Includes separator, item counts, availability message, next page instructions, and timestamp. * @param pagination - The ResponsePagination object containing pagination details. * @returns Formatted pagination footer string for CLI. */ export function formatPagination(pagination: ResponsePagination): string { const methodLogger = Logger.forContext( 'utils/formatter.util.ts', 'formatPagination', ); const parts: string[] = [formatSeparator()]; // Start with separator const { count = 0, hasMore, nextCursor, total, page } = pagination; // Showing count and potentially total if (total !== undefined && total >= 0) { parts.push(`*Showing ${count} of ${total} total items.*`); } else if (count >= 0) { parts.push(`*Showing ${count} item${count !== 1 ? 's' : ''}.*`); } // More results availability if (hasMore) { parts.push('More results are available.'); } // Include the actual cursor value for programmatic use if (hasMore && nextCursor) { parts.push(`*Next cursor: \`${nextCursor}\`*`); // Assuming nextCursor holds the next page number for Bitbucket parts.push(`*Use --page ${nextCursor} to view more.*`); } else if (hasMore && page !== undefined) { // Fallback if nextCursor wasn't parsed but page exists const nextPage = page + 1; parts.push(`*Next cursor: \`${nextPage}\`*`); parts.push(`*Use --page ${nextPage} to view more.*`); } // Add standard timestamp parts.push(`*Information retrieved at: ${formatDate(new Date())}*`); const result = parts.join('\n').trim(); // Join with newline methodLogger.debug(`Formatted pagination footer: ${result}`); return result; } /** * Format a heading with consistent style * @param text - Heading text * @param level - Heading level (1-6) * @returns Formatted heading */ export function formatHeading(text: string, level: number = 1): string { const validLevel = Math.min(Math.max(level, 1), 6); const prefix = '#'.repeat(validLevel); return `${prefix} ${text}`; } /** * Format a list of key-value pairs as a bullet list * @param items - Object with key-value pairs * @param keyFormatter - Optional function to format keys * @returns Formatted bullet list */ export function formatBulletList( items: Record<string, unknown>, keyFormatter?: (key: string) => string, ): string { const lines: string[] = []; for (const [key, value] of Object.entries(items)) { if (value === undefined || value === null) { continue; } const formattedKey = keyFormatter ? keyFormatter(key) : key; const formattedValue = formatValue(value); lines.push(`- **${formattedKey}**: ${formattedValue}`); } return lines.join('\n'); } /** * Format a value based on its type * @param value - Value to format * @returns Formatted value */ function formatValue(value: unknown): string { if (value === undefined || value === null) { return 'Not available'; } if (value instanceof Date) { return formatDate(value); } // Handle URL objects with url and title properties if (typeof value === 'object' && value !== null && 'url' in value) { const urlObj = value as { url: string; title?: string }; if (typeof urlObj.url === 'string') { return formatUrl(urlObj.url, urlObj.title); } } if (typeof value === 'string') { // Check if it's a URL if (value.startsWith('http://') || value.startsWith('https://')) { return formatUrl(value); } // Check if it might be a date if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.test(value)) { return formatDate(value); } return value; } if (typeof value === 'boolean') { return value ? 'Yes' : 'No'; } return String(value); } /** * Format a separator line * @returns Separator line */ export function formatSeparator(): string { return '---'; } /** * Format a numbered list of items * @param items - Array of items to format * @param formatter - Function to format each item * @returns Formatted numbered list */ export function formatNumberedList<T>( items: T[], formatter: (item: T, index: number) => string, ): string { if (items.length === 0) { return 'No items.'; } return items.map((item, index) => formatter(item, index)).join('\n\n'); } /** * Format a raw diff output for display * * Parses and formats a raw unified diff string into a Markdown * formatted display with proper code block syntax highlighting. * * @param {string} rawDiff - The raw diff content from the API * @param {number} maxFiles - Maximum number of files to display in detail (optional, default: 5) * @param {number} maxLinesPerFile - Maximum number of lines to display per file (optional, default: 100) * @returns {string} Markdown formatted diff content */ export function formatDiff( rawDiff: string, maxFiles: number = 5, maxLinesPerFile: number = 100, ): string { if (!rawDiff || rawDiff.trim() === '') { return '*No changes found in this pull request.*'; } const lines = rawDiff.split('\n'); const formattedLines: string[] = []; let currentFile = ''; let fileCount = 0; let inFile = false; let truncated = false; let lineCount = 0; for (const line of lines) { // New file is marked by a line starting with "diff --git" if (line.startsWith('diff --git')) { if (inFile) { // Close previous file code block formattedLines.push('```'); formattedLines.push(''); } // Only process up to maxFiles fileCount++; if (fileCount > maxFiles) { truncated = true; break; } // Extract filename const filePath = line.match(/diff --git a\/(.*) b\/(.*)/); currentFile = filePath ? filePath[1] : 'unknown file'; formattedLines.push(`### ${currentFile}`); formattedLines.push(''); formattedLines.push('```diff'); inFile = true; lineCount = 0; } else if (inFile) { lineCount++; // Truncate files that are too long if (lineCount > maxLinesPerFile) { formattedLines.push( '// ... more lines omitted for brevity ...', ); formattedLines.push('```'); formattedLines.push(''); inFile = false; continue; } // Format diff lines with appropriate highlighting if (line.startsWith('+')) { formattedLines.push(line); } else if (line.startsWith('-')) { formattedLines.push(line); } else if (line.startsWith('@@')) { // Change section header formattedLines.push(line); } else { // Context line formattedLines.push(line); } } } // Close the last code block if necessary if (inFile) { formattedLines.push('```'); } // Add truncation notice if we limited the output if (truncated) { formattedLines.push(''); formattedLines.push( `*Output truncated. Only showing the first ${maxFiles} files.*`, ); } return formattedLines.join('\n'); } /** * Optimizes markdown content to address Bitbucket Cloud's rendering quirks * * IMPORTANT: This function does NOT convert between formats (unlike Jira's ADF conversion). * Bitbucket Cloud API natively accepts and returns markdown format. This function specifically * addresses documented rendering issues in Bitbucket's markdown renderer by applying targeted * formatting adjustments for better display in the Bitbucket UI. * * Known Bitbucket rendering issues this function fixes: * - List spacing and indentation (prevents items from concatenating on a single line) * - Code block formatting (addresses BCLOUD-20503 and similar bugs) * - Nested list indentation (ensures proper hierarchy display) * - Inline code formatting (adds proper spacing around backticks) * - Diff syntax preservation (maintains +/- at line starts) * - Excessive line break normalization * - Heading spacing consistency * * Use this function for both: * - Content received FROM the Bitbucket API (to properly display in CLI/tools) * - Content being sent TO the Bitbucket API (to ensure proper rendering in Bitbucket UI) * * @param {string} markdown - The original markdown content * @returns {string} Optimized markdown with workarounds for Bitbucket rendering issues */ export function optimizeBitbucketMarkdown(markdown: string): string { const methodLogger = Logger.forContext( 'utils/formatter.util.ts', 'optimizeBitbucketMarkdown', ); if (!markdown || markdown.trim() === '') { return markdown; } methodLogger.debug('Optimizing markdown for Bitbucket rendering'); // First, let's extract code blocks to protect them from other transformations const codeBlocks: string[] = []; let optimized = markdown.replace( /```(\w*)\n([\s\S]*?)```/g, (_match, language, code) => { // Store the code block and replace with a placeholder const placeholder = `__CODE_BLOCK_${codeBlocks.length}__`; codeBlocks.push(`\n\n\`\`\`${language}\n${code}\n\`\`\`\n\n`); return placeholder; }, ); // Fix numbered lists with proper spacing // Match numbered lists (1. Item) and ensure proper spacing between items optimized = optimized.replace( /^(\d+\.)\s+(.*?)$/gm, (_match, number, content) => { // Keep the list item and ensure it ends with double line breaks if it doesn't already return `${number} ${content.trim()}\n\n`; }, ); // Fix bullet lists with proper spacing optimized = optimized.replace( /^(\s*)[-*]\s+(.*?)$/gm, (_match, indent, content) => { // Ensure proper indentation and spacing for bullet lists return `${indent}- ${content.trim()}\n\n`; }, ); // Ensure nested lists have proper indentation // Matches lines that are part of nested lists and ensures proper indentation // REMOVED: This step added excessive leading spaces causing Bitbucket to treat lists as code blocks // optimized = optimized.replace( // /^(\s+)[-*]\s+(.*?)$/gm, // (_match, indent, content) => { // // For nested items, ensure proper indentation (4 spaces per level) // const indentLevel = Math.ceil(indent.length / 2); // const properIndent = ' '.repeat(indentLevel); // return `${properIndent}- ${content.trim()}\n\n`; // }, // ); // Fix inline code formatting - ensure it has spaces around it for rendering optimized = optimized.replace(/`([^`]+)`/g, (_match, code) => { // Ensure inline code is properly formatted with spaces before and after // but avoid adding spaces within diff lines (+ or - prefixed) const trimmedCode = code.trim(); const firstChar = trimmedCode.charAt(0); // Don't add spaces if it's part of a diff line if (firstChar === '+' || firstChar === '-') { return `\`${trimmedCode}\``; } return ` \`${trimmedCode}\` `; }); // Ensure diff lines are properly preserved // This helps with preserving + and - prefixes in diff code blocks optimized = optimized.replace( /^([+-])(.*?)$/gm, (_match, prefix, content) => { return `${prefix}${content}`; }, ); // Remove excessive line breaks (more than 2 consecutive) optimized = optimized.replace(/\n{3,}/g, '\n\n'); // Restore code blocks codeBlocks.forEach((codeBlock, index) => { optimized = optimized.replace(`__CODE_BLOCK_${index}__`, codeBlock); }); // Fix double formatting issues (heading + bold) which Bitbucket renders incorrectly // Remove bold formatting from headings as headings are already emphasized optimized = optimized.replace( /^(#{1,6})\s+\*\*(.*?)\*\*\s*$/gm, (_match, hashes, content) => { return `\n${hashes} ${content.trim()}\n\n`; }, ); // Fix bold text within headings (alternative pattern) optimized = optimized.replace( /^(#{1,6})\s+(.*?)\*\*(.*?)\*\*(.*?)$/gm, (_match, hashes, before, boldText, after) => { // Combine text without bold formatting since heading already provides emphasis const cleanContent = (before + boldText + after).trim(); return `\n${hashes} ${cleanContent}\n\n`; }, ); // Ensure headings have proper spacing (for headings without bold issues) optimized = optimized.replace( /^(#{1,6})\s+(.*?)$/gm, (_match, hashes, content) => { // Skip if already processed by bold removal above if (content.includes('**')) { return _match; // Leave as-is, will be handled by bold removal patterns } return `\n${hashes} ${content.trim()}\n\n`; }, ); // Ensure the content ends with a single line break optimized = optimized.trim() + '\n'; methodLogger.debug('Markdown optimization complete'); return optimized; }

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/aashari/mcp-server-atlassian-bitbucket'

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