@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
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 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 };