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

204 lines (203 loc) 10.4 kB
"use strict"; /* eslint-disable no-dupe-else-if */ Object.defineProperty(exports, "__esModule", { value: true }); exports.getAtlassianCredentials = getAtlassianCredentials; exports.fetchAtlassian = fetchAtlassian; const logger_util_js_1 = require("./logger.util.js"); const config_util_js_1 = require("./config.util.js"); const error_util_js_1 = require("./error.util.js"); // Create a logger for the utility const utilLogger = logger_util_js_1.Logger.forContext('utils/transport.util.ts'); /** * Get Atlassian credentials from environment variables * @returns AtlassianCredentials object or null if credentials are missing */ function getAtlassianCredentials() { const siteName = config_util_js_1.config.get('ATLASSIAN_SITE_NAME'); const userEmail = config_util_js_1.config.get('ATLASSIAN_USER_EMAIL'); const apiToken = config_util_js_1.config.get('ATLASSIAN_API_TOKEN'); if (!siteName || !userEmail || !apiToken) { utilLogger.warn('Missing Atlassian credentials. Please set ATLASSIAN_SITE_NAME, ATLASSIAN_USER_EMAIL, and ATLASSIAN_API_TOKEN environment variables.'); return null; } return { siteName, userEmail, apiToken, }; } /** * Fetch data from Atlassian API * @param credentials Atlassian API credentials * @param path API endpoint path (without base URL) * @param options Request options * @returns Response data */ async function fetchAtlassian(credentials, path, options = {}) { const fetchLogger = logger_util_js_1.Logger.forContext('utils/transport.util.ts', 'fetchAtlassian'); const { siteName, userEmail, apiToken } = credentials; // Ensure path starts with a slash const normalizedPath = path.startsWith('/') ? path : `/${path}`; // Construct the full URL const baseUrl = `https://${siteName}.atlassian.net`; const url = `${baseUrl}${normalizedPath}`; // Set up authentication and headers const headers = { Authorization: `Basic ${Buffer.from(`${userEmail}:${apiToken}`).toString('base64')}`, 'Content-Type': 'application/json', Accept: 'application/json', ...options.headers, }; // Prepare request options const requestOptions = { method: options.method || 'GET', headers, body: options.body ? JSON.stringify(options.body) : undefined, }; fetchLogger.debug(`Calling Atlassian API: ${url}`); // Track API call performance const startTime = performance.now(); let endTime; try { const response = await fetch(url, requestOptions); endTime = performance.now(); const requestDuration = (endTime - startTime).toFixed(2); // Log successful API call duration at info level for significant operations if (parseFloat(requestDuration) > 1000) { // If request took more than 1 second, log at warn level fetchLogger.warn(`API call to ${path} took ${requestDuration}ms`); } else if (options.method && options.method !== 'GET') { // For non-GET operations, log at info level fetchLogger.info(`${options.method} operation to ${path} completed in ${requestDuration}ms`); } else { // For regular GET operations, log at debug level fetchLogger.debug(`API call completed in ${requestDuration}ms`); } // Log the raw response status and headers fetchLogger.debug(`Raw response received: ${response.status} ${response.statusText}`, { url, status: response.status, statusText: response.statusText, headers: Object.fromEntries(response.headers.entries()), }); if (!response.ok) { const errorText = await response.text(); fetchLogger.error(`API error: ${response.status} ${response.statusText}`, errorText); // Try to parse the error response let errorMessage = `${response.status} ${response.statusText}`; let parsedError = null; try { if (errorText && (errorText.startsWith('{') || errorText.startsWith('['))) { parsedError = JSON.parse(errorText); // Log the full parsed error for debugging fetchLogger.debug('Parsed Confluence error', parsedError); // Confluence v2 API - { "title": "xxx", "status": xxx, "detail": "xxx" } format if (parsedError.title) { errorMessage = parsedError.title; // Include detail if available if (parsedError.detail && !errorMessage.includes(parsedError.detail)) { errorMessage += `: ${parsedError.detail}`; } } // Older API format or error format: { "message": "xxx" } else if (parsedError.message) { errorMessage = parsedError.message; // Optionally include reason if available if (parsedError.reason && !errorMessage.includes(parsedError.reason)) { errorMessage += `: ${parsedError.reason}`; } } // Format: {"errors":[{"message":"Invalid query","extensions":{...}}]} else if (parsedError.errors && Array.isArray(parsedError.errors) && parsedError.errors.length > 0) { if (parsedError.errors[0].message || parsedError.errors[0].title) { // Join multiple error messages if available, limiting to first 3 const errorMsgs = parsedError.errors .slice(0, 3) .map((e) => e.message ? String(e.message) : e.title ? String(e.title) : 'Unknown error'); errorMessage = errorMsgs.join('; '); // Add count indicator if there are more than 3 errors if (parsedError.errors.length > 3) { errorMessage += `; and ${parsedError.errors.length - 3} more errors`; } } } // Older API might return errorMessages array (like Jira) else if (parsedError.errorMessages && Array.isArray(parsedError.errorMessages) && parsedError.errorMessages.length > 0) { errorMessage = parsedError.errorMessages.join('; '); } // Try to look for status code description else if (parsedError.statusCode && parsedError.message) { errorMessage = `${parsedError.statusCode}: ${parsedError.message}`; } } } catch (parseError) { fetchLogger.debug(`Error parsing error response:`, parseError); // Fall back to the default error message } // Use the parsed error object or raw text as originalError for context const originalError = parsedError || errorText; // Classify HTTP errors based on status code if (response.status === 401) { throw (0, error_util_js_1.createAuthInvalidError)(`Authentication failed. Confluence API: ${errorMessage}`, originalError); } else if (response.status === 403) { throw (0, error_util_js_1.createApiError)(`Access denied. Confluence API: ${errorMessage}`, 403, originalError); } else if (response.status === 404) { throw (0, error_util_js_1.createNotFoundError)(`Resource not found. Confluence API: ${errorMessage}`, originalError); } else if (response.status === 429) { throw (0, error_util_js_1.createApiError)(`Rate limit exceeded. Confluence API: ${errorMessage}`, 429, originalError); } else if (response.status >= 500) { throw (0, error_util_js_1.createApiError)(`Confluence service error. Detail: ${errorMessage}`, response.status, originalError); } else { // For other API errors throw (0, error_util_js_1.createApiError)(`Confluence API request failed. Detail: ${errorMessage}`, response.status, originalError); } } // Clone the response to log its content without consuming it const clonedResponse = response.clone(); const responseJson = await clonedResponse.json(); fetchLogger.debug(`Response body:`, responseJson); return response.json(); } catch (error) { endTime = performance.now(); const failedRequestDuration = (endTime - startTime).toFixed(2); fetchLogger.error(`Request failed after ${failedRequestDuration}ms`, error); // If it's already an McpError, just rethrow it if (error instanceof error_util_js_1.McpError) { throw error; } // Handle network errors (typically TypeErrors from fetch) if (error instanceof TypeError) { fetchLogger.debug('Network error details:', error); throw (0, error_util_js_1.createApiError)(`Network error connecting to Confluence API: ${error.message}`, 500, // Will be classified as NETWORK_ERROR by detectErrorType error); } // Handle JSON parsing errors if (error instanceof SyntaxError) { fetchLogger.debug('JSON parsing error:', error); throw (0, error_util_js_1.createApiError)(`Invalid response format from Confluence API: ${error.message}`, 500, error); } // Generic error handler for any other types of errors throw (0, error_util_js_1.createUnexpectedError)(`Unexpected error while calling Confluence API: ${error instanceof Error ? error.message : String(error)}`, error); } }