@aashari/mcp-server-atlassian-confluence
Version:
Node.js/TypeScript MCP server for Atlassian Confluence. Provides tools enabling AI systems (LLMs) to list/get spaces & pages (content formatted as Markdown) and search via CQL. Connects AI seamlessly to Confluence knowledge bases using the standard MCP in
248 lines (247 loc) • 11.8 kB
JavaScript
;
/**
* Controller for Confluence comments
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.atlassianCommentsController = void 0;
const logger_util_js_1 = require("../utils/logger.util.js");
const error_handler_util_js_1 = require("../utils/error-handler.util.js");
const vendor_atlassian_comments_service_js_1 = require("../services/vendor.atlassian.comments.service.js");
const pagination_util_js_1 = require("../utils/pagination.util.js");
const atlassian_comments_formatter_js_1 = require("./atlassian.comments.formatter.js");
const atlassian_inline_comments_formatter_js_1 = require("./atlassian.inline-comments.formatter.js");
const defaults_util_js_1 = require("../utils/defaults.util.js");
const adf_util_js_1 = require("../utils/adf.util.js");
const formatter_util_js_1 = require("../utils/formatter.util.js");
// Create logger for this controller
const logger = logger_util_js_1.Logger.forContext('controllers/atlassian.comments.controller.ts');
/**
* List comments for a specific Confluence page
*
* @param options - Options for listing comments
* @returns Controller response with formatted comments and pagination info
*/
async function listPageComments(options) {
const methodLogger = logger.forMethod('listPageComments');
try {
// Apply defaults and prepare service parameters
const { pageId, limit = defaults_util_js_1.DEFAULT_PAGE_SIZE, start = 0, bodyFormat = 'atlas_doc_format', // Explicitly define default
} = options;
methodLogger.debug('Listing page comments', {
pageId,
limit,
start,
bodyFormat,
});
// Call the service to get comments data
const commentsData = await vendor_atlassian_comments_service_js_1.atlassianCommentsService.listPageComments({
pageId,
limit,
start,
bodyFormat,
});
// Extract pagination information
const pagination = (0, pagination_util_js_1.extractPaginationInfo)(commentsData, pagination_util_js_1.PaginationType.OFFSET, 'Comment');
// Convert ADF content to Markdown and extract highlighted text for inline comments
const convertedComments = commentsData.results.map((comment) => {
let markdownBody = '*Content format not supported or unavailable*';
// Convert comment body from ADF to Markdown
if (comment.body?.atlas_doc_format?.value) {
try {
markdownBody = (0, adf_util_js_1.adfToMarkdown)(comment.body.atlas_doc_format.value);
methodLogger.debug(`Successfully converted ADF to Markdown for comment ${comment.id}`);
}
catch (conversionError) {
methodLogger.error(`ADF conversion failed for comment ${comment.id}`, conversionError);
// Keep default error message
}
}
else {
methodLogger.warn(`No ADF content available for comment ${comment.id}`);
}
// Extract the highlighted text for inline comments
let highlightedText = undefined;
if (comment.extensions?.location === 'inline' &&
comment.extensions.inlineProperties) {
// Safely access inlineProperties fields with type checking
const props = comment.extensions
.inlineProperties;
// Try different properties that might contain the highlighted text
// Some Confluence versions use different property names
highlightedText =
props.originalSelection || props.textContext;
// If not found in standard properties, check for custom properties
if (!highlightedText && 'selectionText' in props) {
highlightedText = String(props.selectionText || '');
}
if (highlightedText) {
methodLogger.debug(`Found highlighted text for comment ${comment.id}: ${highlightedText.substring(0, 50)}${highlightedText.length > 50 ? '...' : ''}`);
}
else {
methodLogger.warn(`No highlighted text found for inline comment ${comment.id}`);
}
}
// Return comment with added context
return {
...comment,
convertedMarkdownBody: markdownBody,
highlightedText,
};
});
// Format the comments for display
const baseUrl = commentsData._links?.base || '';
const formattedContent = (0, atlassian_comments_formatter_js_1.formatCommentsList)(convertedComments, pageId, baseUrl);
// Create the final content with pagination information included
let finalContent = formattedContent;
// Add pagination information if available
if (pagination &&
(pagination.hasMore || pagination.count !== undefined)) {
const paginationString = (0, formatter_util_js_1.formatPagination)(pagination);
finalContent += '\n\n' + paginationString;
}
return {
content: finalContent,
};
}
catch (error) {
// Handle errors
throw (0, error_handler_util_js_1.handleControllerError)(error, {
entityType: 'Comment',
operation: 'list',
source: 'controllers/atlassian.comments.controller.ts@listPageComments',
additionalInfo: { pageId: options.pageId },
});
}
}
/**
* List inline comments only for a specific Confluence page
*
* @param options - Options for listing inline comments
* @returns Controller response with formatted inline comments and pagination info
*/
async function listInlineComments(options) {
const methodLogger = logger.forMethod('listInlineComments');
try {
// Apply defaults and prepare service parameters
const { pageId, includeResolved = false, sortBy = 'position', limit = defaults_util_js_1.DEFAULT_PAGE_SIZE, start = 0, bodyFormat = 'atlas_doc_format', } = options;
methodLogger.debug('Listing inline comments for page', {
pageId,
includeResolved,
sortBy,
limit,
start,
bodyFormat,
});
// Get all comments first with a higher limit to ensure we capture inline comments
// since we'll filter them locally
const allCommentsData = await vendor_atlassian_comments_service_js_1.atlassianCommentsService.listPageComments({
pageId,
limit: 250, // Get more comments to filter inline ones
start: 0, // Always start from beginning for inline filtering
bodyFormat,
});
methodLogger.debug('Retrieved all comments for filtering', {
totalComments: allCommentsData.results.length,
pageId,
});
// Filter for inline comments only
const inlineCommentsRaw = allCommentsData.results.filter((comment) => {
const isInline = comment.extensions?.location === 'inline';
// Apply resolved filter if needed
if (!includeResolved && comment.status !== 'current') {
return false;
}
return isInline;
});
methodLogger.debug('Filtered inline comments', {
inlineCount: inlineCommentsRaw.length,
totalCount: allCommentsData.results.length,
includeResolved,
});
// Convert ADF content to Markdown and extract highlighted text for inline comments
const convertedComments = inlineCommentsRaw.map((comment) => {
let markdownBody = '*Content format not supported or unavailable*';
// Convert comment body from ADF to Markdown
if (comment.body?.atlas_doc_format?.value) {
try {
markdownBody = (0, adf_util_js_1.adfToMarkdown)(comment.body.atlas_doc_format.value);
methodLogger.debug(`Successfully converted ADF to Markdown for inline comment ${comment.id}`);
}
catch (conversionError) {
methodLogger.error(`ADF conversion failed for inline comment ${comment.id}`, conversionError);
// Keep default error message
}
}
else {
methodLogger.warn(`No ADF content available for inline comment ${comment.id}`);
}
// Extract the highlighted text for inline comments
let highlightedText = undefined;
if (comment.extensions?.inlineProperties) {
// Safely access inlineProperties fields with type checking
const props = comment.extensions
.inlineProperties;
// Try different properties that might contain the highlighted text
highlightedText =
props.originalSelection || props.textContext;
// If not found in standard properties, check for custom properties
if (!highlightedText && 'selectionText' in props) {
highlightedText = String(props.selectionText || '');
}
if (highlightedText) {
methodLogger.debug(`Found highlighted text for inline comment ${comment.id}: ${highlightedText.substring(0, 50)}${highlightedText.length > 50 ? '...' : ''}`);
}
else {
methodLogger.warn(`No highlighted text found for inline comment ${comment.id}`);
}
}
// Return comment with added context
return {
...comment,
convertedMarkdownBody: markdownBody,
highlightedText,
};
});
// Sort inline comments by requested order
if (sortBy === 'position') {
convertedComments.sort((a, b) => {
// Sort by marker position or container ID if available
const aPos = a.extensions?.inlineProperties?.markerRef || a.id;
const bPos = b.extensions?.inlineProperties?.markerRef || b.id;
return String(aPos).localeCompare(String(bPos));
});
}
else if (sortBy === 'created') {
// Sort by ID as a proxy for creation order (newer IDs = later created)
convertedComments.sort((a, b) => a.id.localeCompare(b.id));
}
// Apply pagination after filtering and sorting
const paginatedComments = convertedComments.slice(start, start + limit);
methodLogger.debug('Applied pagination to inline comments', {
totalInline: convertedComments.length,
start,
limit,
returned: paginatedComments.length,
});
// Format the inline comments for display
const baseUrl = allCommentsData._links?.base || '';
const formattedContent = (0, atlassian_inline_comments_formatter_js_1.formatInlineCommentsList)(paginatedComments, pageId, baseUrl, convertedComments.length, start, limit);
return {
content: formattedContent,
};
}
catch (error) {
// Handle errors
throw (0, error_handler_util_js_1.handleControllerError)(error, {
entityType: 'InlineComment',
operation: 'list',
source: 'controllers/atlassian.comments.controller.ts@listInlineComments',
additionalInfo: { pageId: options.pageId },
});
}
}
// Export controller functions
exports.atlassianCommentsController = {
listPageComments,
listInlineComments,
};