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

123 lines (122 loc) 5.89 kB
"use strict"; 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 };