Skip to main content
Glama
transport.util.ts10.2 kB
/* eslint-disable no-dupe-else-if */ import { Logger } from './logger.util.js'; import { config } from './config.util.js'; import { createAuthInvalidError, createApiError, createUnexpectedError, createNotFoundError, McpError, } from './error.util.js'; import { saveRawResponse } from './response.util.js'; /** * Interface for Atlassian API credentials */ export interface AtlassianCredentials { siteName: string; userEmail: string; apiToken: string; } /** * Interface for HTTP request options */ export interface RequestOptions { method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'; headers?: Record<string, string>; body?: unknown; } /** * Transport response wrapper that includes the data and the path to the raw response file */ export interface TransportResponse<T> { data: T; rawResponsePath: string | null; } // Create a logger for the utility const utilLogger = Logger.forContext('utils/transport.util.ts'); /** * Get Atlassian credentials from environment variables * @returns AtlassianCredentials object or null if credentials are missing */ export function getAtlassianCredentials(): AtlassianCredentials | null { const siteName = config.get('ATLASSIAN_SITE_NAME'); const userEmail = config.get('ATLASSIAN_USER_EMAIL'); const apiToken = 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 Transport response with data and raw response path */ export async function fetchAtlassian<T>( credentials: AtlassianCredentials, path: string, options: RequestOptions = {}, ): Promise<TransportResponse<T>> { const fetchLogger = 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: RequestInit = { 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: number; 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: Record<string, unknown>) => 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 createAuthInvalidError( `Authentication failed. Confluence API: ${errorMessage}`, originalError, ); } else if (response.status === 403) { throw createApiError( `Access denied. Confluence API: ${errorMessage}`, 403, originalError, ); } else if (response.status === 404) { throw createNotFoundError( `Resource not found. Confluence API: ${errorMessage}`, originalError, ); } else if (response.status === 429) { throw createApiError( `Rate limit exceeded. Confluence API: ${errorMessage}`, 429, originalError, ); } else if (response.status >= 500) { throw createApiError( `Confluence service error. Detail: ${errorMessage}`, response.status, originalError, ); } else { // For other API errors throw createApiError( `Confluence API request failed. Detail: ${errorMessage}`, response.status, originalError, ); } } // Handle 204 No Content responses (common for DELETE operations) if (response.status === 204) { fetchLogger.debug('Received 204 No Content response'); return { data: {} as T, rawResponsePath: null }; } // Handle empty responses (some endpoints return 200/201 with no body) const responseText = await response.text(); if (!responseText || responseText.trim() === '') { fetchLogger.debug('Received empty response body'); return { data: {} as T, rawResponsePath: null }; } // For JSON responses, parse the text we already read try { const responseJson = JSON.parse(responseText); fetchLogger.debug(`Response body:`, responseJson); // Save raw response to file and capture the path const rawResponsePath = saveRawResponse( url, requestOptions.method || 'GET', options.body, responseJson, response.status, parseFloat(requestDuration), ); return { data: responseJson as T, rawResponsePath }; } catch { fetchLogger.debug( `Could not parse response as JSON, returning raw content`, ); return { data: responseText as unknown as T, rawResponsePath: null, }; } } 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 McpError) { throw error; } // Handle network errors (typically TypeErrors from fetch) if (error instanceof TypeError) { fetchLogger.debug('Network error details:', error); throw 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 createApiError( `Invalid response format from Confluence API: ${error.message}`, 500, error, ); } // Generic error handler for any other types of errors throw createUnexpectedError( `Unexpected error while calling Confluence API: ${error instanceof Error ? error.message : String(error)}`, error, ); } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/aashari/mcp-server-atlassian-confluence'

If you have feedback or need assistance with the MCP directory API, please join our Discord server