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

134 lines (133 loc) 6.26 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 vendor_atlassian_search_service_js_1 = __importDefault(require("../services/vendor.atlassian.search.service.js")); const atlassian_search_formatter_js_1 = require("./atlassian.search.formatter.js"); const pagination_util_js_1 = require("../utils/pagination.util.js"); const defaults_util_js_1 = require("../utils/defaults.util.js"); const error_handler_util_js_2 = require("../utils/error-handler.util.js"); const error_util_js_1 = require("../utils/error.util.js"); const formatter_util_js_1 = require("../utils/formatter.util.js"); const controllerLogger = logger_util_js_1.Logger.forContext('controllers/atlassian.search.controller.ts'); controllerLogger.debug('Search controller initialized'); /** * Escapes special characters in a string for safe use within CQL quotes. * Uses JSON.stringify to handle escaping and removes the outer quotes. * @param value The string to escape. * @returns Escaped string, suitable for placing inside CQL double quotes. */ function escapeCqlValue(value) { // JSON.stringify correctly escapes quotes, backslashes, etc. const jsonString = JSON.stringify(value); // Remove the leading and trailing double quotes added by stringify return jsonString.slice(1, -1); } /** * Builds a CQL query string from provided options. * @param options SearchOptions containing filters. * @returns The constructed CQL string. */ function buildCqlQuery(options) { const cqlParts = []; if (options.title) { cqlParts.push(`title ~ "${escapeCqlValue(options.title)}"`); } if (options.spaceKey) { cqlParts.push(`space = "${escapeCqlValue(options.spaceKey)}"`); } if (options.labels && options.labels.length > 0) { const escapedLabels = options.labels.map(escapeCqlValue); escapedLabels.forEach((label) => cqlParts.push(`label = "${label}"`)); } if (options.contentType) { cqlParts.push(`type = ${options.contentType}`); } if (options.query) { cqlParts.push(`text ~ "${escapeCqlValue(options.query)}"`); } const generatedCql = cqlParts.join(' AND '); if (options.cql && options.cql.trim()) { if (generatedCql) { return `(${generatedCql}) AND (${options.cql})`; } else { return options.cql; } } else { return generatedCql || ''; } } /** * Search Confluence content using CQL * @param options - Search options including CQL query and pagination * @returns Promise with formatted search results and pagination info * @throws Error if search operation fails */ async function search(options = {}) { const methodLogger = logger_util_js_1.Logger.forContext('controllers/atlassian.search.controller.ts', 'search'); methodLogger.debug('Searching Confluence with options:', options); try { const defaults = { limit: defaults_util_js_1.DEFAULT_PAGE_SIZE, }; const mergedOptions = (0, defaults_util_js_1.applyDefaults)(options, defaults); const finalCql = buildCqlQuery(mergedOptions); if (!finalCql || finalCql.trim() === '') { methodLogger.warn('No CQL criteria provided for search. Returning empty.'); return { content: 'Please provide search criteria (CQL, title, space, etc.).', }; } methodLogger.debug(`Executing generated CQL: ${finalCql}`); const params = { cql: finalCql, limit: mergedOptions.limit, cursor: mergedOptions.cursor, excerpt: 'highlight', includeArchivedSpaces: false, }; const searchData = await vendor_atlassian_search_service_js_1.default.search(params); methodLogger.debug(`Retrieved ${searchData.results.length} search results. Has more: ${searchData._links?.next ? 'yes' : 'no'}`); const pagination = (0, pagination_util_js_1.extractPaginationInfo)(searchData, pagination_util_js_1.PaginationType.CURSOR, 'Search'); // Format the search results const formattedResults = (0, atlassian_search_formatter_js_1.formatSearchResults)(searchData.results); // Prepare the complete content string with CQL and pagination information let finalContent = ''; // Add the executed CQL query if available if (finalCql && finalCql.trim()) { finalContent += `${(0, formatter_util_js_1.formatHeading)('Executed CQL Query', 3)}\n\`${finalCql}\`\n\n`; } // Add the formatted search results finalContent += formattedResults; // 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) { const mcpError = (0, error_util_js_1.ensureMcpError)(error); // Check if it's a 400 error, potentially from bad CQL if (mcpError.statusCode === 400) { mcpError.message = `Search failed (Status 400 - Bad Request): ${mcpError.message}. This may be due to invalid CQL syntax. Please check your CQL query, ensure terms in text searches are quoted (e.g., text ~ "your terms"), and refer to the Confluence CQL documentation.`; } throw (0, error_handler_util_js_1.handleControllerError)(mcpError, // Pass the potentially modified error (0, error_handler_util_js_2.buildErrorContext)('Search', 'performing', 'controllers/atlassian.search.controller.ts@search', {}, { cql: options.cql || '', query: options.query || '', spaceKey: options.spaceKey, limit: options.limit, cursor: options.cursor, })); } } exports.default = { search };