@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
244 lines (243 loc) • 13.1 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const logger_util_js_1 = require("../utils/logger.util.js");
const error_handler_util_js_1 = require("../utils/error-handler.util.js");
const error_util_js_1 = require("../utils/error.util.js");
const atlassian_pages_formatter_js_1 = require("./atlassian.pages.formatter.js");
const vendor_atlassian_pages_service_js_1 = __importDefault(require("../services/vendor.atlassian.pages.service.js"));
const vendor_atlassian_spaces_service_js_1 = __importDefault(require("../services/vendor.atlassian.spaces.service.js"));
const atlassian_comments_controller_js_1 = require("./atlassian.comments.controller.js");
const defaults_util_js_1 = require("../utils/defaults.util.js");
const pagination_util_js_1 = require("../utils/pagination.util.js");
const adf_util_js_1 = require("../utils/adf.util.js");
const formatter_util_js_1 = require("../utils/formatter.util.js");
/**
* Controller for managing Confluence pages.
* Provides functionality for listing pages and retrieving page details.
*/
// Create a contextualized logger for this file
const controllerLogger = logger_util_js_1.Logger.forContext('controllers/atlassian.pages.controller.ts');
// Log controller initialization
controllerLogger.debug('Confluence pages controller initialized');
// Simple in-memory cache for space key to ID mapping
const spaceKeyCache = {};
const CACHE_TTL = 3600000; // 1 hour in milliseconds
/**
* List pages from Confluence with filtering options
* @param options - Options for filtering pages
* @param options.spaceIds - Filter by space ID(s)
* @param options.spaceKeys - Filter by space key(s) - user-friendly alternative to spaceId
* @param options.query - Filter by text in title, content or labels
* @param options.status - Filter by page status
* @param options.sort - Sort order for results
* @param options.limit - Maximum number of pages to return
* @param options.cursor - Pagination cursor for subsequent requests
* @returns Promise with formatted pages list content including pagination information
* @throws Error if page listing fails
*/
async function list(options = {}) {
const methodLogger = logger_util_js_1.Logger.forContext('controllers/atlassian.pages.controller.ts', 'list');
methodLogger.debug('Listing Confluence pages with options:', options);
methodLogger.info(`Fetching Confluence pages (limit: ${options.limit || defaults_util_js_1.DEFAULT_PAGE_SIZE})`);
try {
const defaults = {
limit: defaults_util_js_1.DEFAULT_PAGE_SIZE,
sort: '-modified-date',
status: ['current'],
};
const mergedOptions = (0, defaults_util_js_1.applyDefaults)(options, defaults);
let resolvedSpaceIds = mergedOptions.spaceIds || []; // Use renamed option
// Handle space key resolution if provided
if (mergedOptions.spaceKeys && mergedOptions.spaceKeys.length > 0) {
// Use renamed option
methodLogger.debug(`Resolving ${mergedOptions.spaceKeys.length} space keys to IDs`);
const currentTime = Date.now();
const keysToResolve = [];
const currentResolvedIds = []; // IDs resolved in this specific call
// Check cache first
mergedOptions.spaceKeys.forEach((key) => {
// Use renamed option
const cached = spaceKeyCache[key];
if (cached && currentTime - cached.timestamp < CACHE_TTL) {
methodLogger.debug(`Using cached ID for space key "${key}": ${cached.id}`);
currentResolvedIds.push(cached.id);
}
else {
keysToResolve.push(key);
}
});
if (keysToResolve.length > 10) {
methodLogger.warn(`Resolving ${keysToResolve.length} space keys - this may impact performance`);
}
if (keysToResolve.length > 0) {
try {
const spacesResponse = await vendor_atlassian_spaces_service_js_1.default.list({
keys: keysToResolve,
limit: 100,
});
if (spacesResponse.results &&
spacesResponse.results.length > 0) {
// Explicitly type 'space' parameter
spacesResponse.results.forEach((space) => {
spaceKeyCache[space.key] = {
id: space.id,
timestamp: currentTime,
};
currentResolvedIds.push(space.id);
});
// Explicitly type 'space' parameter
const resolvedKeys = spacesResponse.results.map((space) => space.key);
const failedKeys = keysToResolve.filter((key) => !resolvedKeys.includes(key));
if (failedKeys.length > 0) {
methodLogger.warn(`Could not resolve space keys: ${failedKeys.join(', ')}`);
}
}
}
catch (resolveError) {
// Type error explicitly
const error = (0, error_util_js_1.ensureMcpError)(resolveError); // Ensure it's an McpError
methodLogger.error('Failed to resolve space keys', error.message);
if (resolvedSpaceIds.length === 0 &&
currentResolvedIds.length === 0) {
// Throw only if no direct IDs and no keys resolved successfully
throw (0, error_util_js_1.createApiError)(`Failed to resolve any provided space keys: ${error.message}`, 400, error);
}
// Otherwise log warning and continue
methodLogger.warn('Proceeding with directly provided IDs and any cached/resolved keys, despite resolution error.');
}
}
// Combine resolved IDs (from this call) with any directly provided IDs
resolvedSpaceIds = [
...new Set([...resolvedSpaceIds, ...currentResolvedIds]),
];
}
// Final check: If keys/IDs were provided but none resolved, return empty.
if ((mergedOptions.spaceKeys || mergedOptions.spaceIds) && // Use renamed options
resolvedSpaceIds.length === 0) {
methodLogger.warn('No valid space IDs found to query. Returning empty.');
return {
content: 'No pages found. Specified space keys/IDs are invalid or inaccessible.',
};
}
// Map controller options to service parameters
const params = {
// Keep conditional spread for optional params
...(mergedOptions.title && { title: mergedOptions.title }),
...(mergedOptions.status && { status: mergedOptions.status }),
...(mergedOptions.sort && { sort: mergedOptions.sort }),
...(mergedOptions.limit !== undefined && {
limit: mergedOptions.limit,
}),
...(mergedOptions.cursor && { cursor: mergedOptions.cursor }),
...(mergedOptions.parentId && { parentId: mergedOptions.parentId }),
};
// Explicitly add spaceId if resolvedSpaceIds has items
if (resolvedSpaceIds && resolvedSpaceIds.length > 0) {
params.spaceId = resolvedSpaceIds;
}
methodLogger.debug('Using service params (initial):', params);
// Add extra detailed log right before the service call
methodLogger.debug(`Final check before service call - params.spaceId: ${JSON.stringify(params.spaceId)}`);
methodLogger.debug('Final check before service call - full params object:', params);
const pagesData = await vendor_atlassian_pages_service_js_1.default.list(params);
methodLogger.debug(`Retrieved ${pagesData.results.length} pages. Has more: ${pagesData._links?.next ? 'yes' : 'no'}`);
const pagination = (0, pagination_util_js_1.extractPaginationInfo)(pagesData, pagination_util_js_1.PaginationType.CURSOR, 'Page');
// Pass the results array and baseUrl to the formatter
const baseUrl = pagesData._links?.base || '';
const formattedPages = (0, atlassian_pages_formatter_js_1.formatPagesList)(pagesData.results, baseUrl);
// Create the complete content string by appending the pagination information
let finalContent = formattedPages;
// Only add pagination information if it exists and contains relevant information
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) {
throw (0, error_handler_util_js_1.handleControllerError)(error, {
entityType: 'Pages',
operation: 'listing',
source: 'controllers/atlassian.pages.controller.ts@list',
});
}
}
/**
* Get details of a specific Confluence page
* @param args - Object containing the ID of the page to retrieve
* @param args.pageId - The ID of the page
* @returns Promise with formatted page details content
* @throws Error if page retrieval fails
*/
async function get(args) {
const { pageId } = args;
const methodLogger = logger_util_js_1.Logger.forContext('controllers/atlassian.pages.controller.ts', 'get');
methodLogger.debug(`Getting Confluence page with ID: ${pageId}...`);
try {
// Map controller options to service parameters
const params = {
bodyFormat: defaults_util_js_1.PAGE_DEFAULTS.BODY_FORMAT,
includeLabels: defaults_util_js_1.PAGE_DEFAULTS.INCLUDE_LABELS,
includeOperations: defaults_util_js_1.PAGE_DEFAULTS.INCLUDE_PROPERTIES, // Changed to correct parameter
includeWebresources: defaults_util_js_1.PAGE_DEFAULTS.INCLUDE_WEBRESOURCES,
includeCollaborators: defaults_util_js_1.PAGE_DEFAULTS.INCLUDE_COLLABORATORS,
includeVersion: defaults_util_js_1.PAGE_DEFAULTS.INCLUDE_VERSION,
};
methodLogger.debug('Using service params:', params);
// Get page data from the API
const pageData = await vendor_atlassian_pages_service_js_1.default.get(pageId, params);
// Log only key information instead of the entire response
methodLogger.debug(`Retrieved page: ${pageData.title} (${pageData.id})`);
// Convert ADF to Markdown before formatting
let markdownBody = '*Content format not supported or unavailable*';
if (pageData.body?.atlas_doc_format?.value) {
try {
markdownBody = (0, adf_util_js_1.adfToMarkdown)(pageData.body.atlas_doc_format.value);
methodLogger.debug('Successfully converted ADF to Markdown for page body');
}
catch (conversionError) {
methodLogger.error('ADF to Markdown conversion failed for page body', conversionError);
// Keep the default error message for markdownBody
}
}
else {
methodLogger.warn('No ADF content available for page', { pageId });
}
// Fetch recent comments for this page
let commentsSummary = null;
try {
methodLogger.debug(`Fetching recent comments for page ID: ${pageId}`);
commentsSummary =
await atlassian_comments_controller_js_1.atlassianCommentsController.listPageComments({
pageId,
limit: 3, // Get just a few recent comments
bodyFormat: 'atlas_doc_format', // Get the comments in ADF format
});
methodLogger.debug(`Retrieved comments summary for page.`);
}
catch (error) {
methodLogger.warn(`Failed to fetch comments: ${error instanceof Error ? error.message : String(error)}`);
// Continue even if we couldn't get the comments
}
// Format the page data for display, passing the converted markdown body and comments
const formattedPage = (0, atlassian_pages_formatter_js_1.formatPageDetails)(pageData, markdownBody, commentsSummary);
return {
content: formattedPage,
};
}
catch (error) {
throw (0, error_handler_util_js_1.handleControllerError)(error, {
entityType: 'Page',
entityId: pageId,
operation: 'retrieving',
source: 'controllers/atlassian.pages.controller.ts@get',
});
}
}
exports.default = { list, get };