UNPKG

@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
"use strict"; 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 };