@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
123 lines (122 loc) • 5.89 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
const error_util_js_1 = require("../utils/error.util.js");
const logger_util_js_1 = require("../utils/logger.util.js");
const transport_util_js_1 = require("../utils/transport.util.js");
const vendor_atlassian_search_types_js_1 = require("./vendor.atlassian.search.types.js");
const zod_1 = require("zod");
/**
* Base API path for Confluence REST API v1 (using v1 instead of v2 to bypass the generic-content-type bug)
* @see https://developer.atlassian.com/cloud/confluence/rest/v1/api-group-search/
* @constant {string}
*/
const API_PATH = '/wiki/rest/api';
/**
* Search Confluence content using CQL
*
* @param {SearchParams} params - Parameters for the search query
* @returns {Promise<SearchResponseType>} Promise containing the search results
* @throws {Error} If Atlassian credentials are missing or API request fails
*/
async function search(params) {
const serviceLogger = logger_util_js_1.Logger.forContext('services/vendor.atlassian.search.service.ts', 'search');
serviceLogger.debug('Searching Confluence with params:', params);
const credentials = (0, transport_util_js_1.getAtlassianCredentials)();
if (!credentials) {
throw (0, error_util_js_1.createAuthMissingError)('Atlassian credentials are required for this operation');
}
// Build request parameters
const queryParams = {};
// Required CQL query
queryParams.cql = params.cql;
// Pagination
// v1 API uses start/limit instead of cursor
if (params.limit) {
queryParams.limit = params.limit.toString();
}
// The v1 API parameters are slightly different, but we can map most of them
if (params.includeTotalSize !== undefined) {
// v1 API always includes total size, no need for this parameter
}
if (params.includeArchivedSpaces !== undefined) {
// No direct equivalent in v1 API
}
if (params.excerpt) {
queryParams.excerpt = params.excerpt;
}
// Manually build query string to avoid URLSearchParams handling
const queryString = Object.entries(queryParams)
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
.join('&');
try {
// Construct the full URL for the API call using the v1 search endpoint
const baseUrl = `https://${credentials.siteName}.atlassian.net`;
const url = `${baseUrl}${API_PATH}/search${queryString ? `?${queryString}` : ''}`;
// For debugging
serviceLogger.debug(`Making direct fetch to v1 API endpoint: ${url}`);
// Construct Auth header
const authHeader = `Basic ${Buffer.from(`${credentials.userEmail}:${credentials.apiToken}`).toString('base64')}`;
// Make direct fetch call
const response = await fetch(url, {
method: 'GET',
headers: {
Authorization: authHeader,
Accept: 'application/json',
'Content-Type': 'application/json',
},
});
// Log the response status for debugging
serviceLogger.debug(`API response status: ${response.status}`);
// Check for error response
if (!response.ok) {
const errorText = await response.text();
serviceLogger.error(`API error response: ${errorText}`);
throw (0, error_util_js_1.createApiError)(`API request failed with status ${response.status}`, response.status, errorText);
}
// Parse the JSON response
const v1Data = (await response.json());
serviceLogger.debug(`Successfully retrieved ${v1Data.results?.length || 0} search results from v1 API`);
// The v1 API has a slightly different format, transform it to match the expected type
const transformedData = {
results: (v1Data.results || []).map((result) => {
return {
content: result.content || {},
space: result.space || {},
title: result.title || '',
excerpt: result.excerpt || '',
url: result.url || '',
resultGlobalContainer: result.resultGlobalContainer || {},
breadcrumbs: result.breadcrumbs || [],
entityType: result.entityType || '',
iconCssClass: result.iconCssClass || '',
lastModified: result.lastModified || '',
friendlyLastModified: result.friendlyLastModified || '',
score: result.score || 0,
};
}),
_links: v1Data._links || {},
total: v1Data.totalSize,
};
// Validate the transformed data using our schema
try {
const validatedData = vendor_atlassian_search_types_js_1.SearchResponseSchema.parse(transformedData);
serviceLogger.debug(`Successfully validated search results for ${validatedData.results.length} items`);
return validatedData;
}
catch (validationError) {
if (validationError instanceof zod_1.z.ZodError) {
serviceLogger.error('API response validation failed:', validationError.format());
// Log the data structure for debugging
serviceLogger.debug('Transformed data structure:', JSON.stringify(transformedData, null, 2).substring(0, 1000) + '...');
throw (0, error_util_js_1.createApiError)(`API response validation failed: ${validationError.message}`, 500, validationError);
}
// Re-throw other errors
throw validationError;
}
}
catch (error) {
serviceLogger.error('Error searching content:', error);
throw error; // Rethrow to be handled by the error handler util
}
}
exports.default = { search };