Skip to main content
Glama
atlassian.search.controller.ts5.66 kB
import { Logger } from '../utils/logger.util.js'; import { handleControllerError } from '../utils/error-handler.util.js'; import { ControllerResponse } from '../types/common.types.js'; import atlassianSearchService from '../services/vendor.atlassian.search.service.js'; import { formatSearchResults } from './atlassian.search.formatter.js'; import { extractPaginationInfo, PaginationType, } from '../utils/pagination.util.js'; import { DEFAULT_PAGE_SIZE, applyDefaults } from '../utils/defaults.util.js'; import { SearchParams } from '../services/vendor.atlassian.search.types.js'; import { SearchToolArgsType } from '../tools/atlassian.search.types.js'; import { buildErrorContext } from '../utils/error-handler.util.js'; import { ensureMcpError } from '../utils/error.util.js'; import { formatHeading, formatPagination } from '../utils/formatter.util.js'; const controllerLogger = 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: string): string { // 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: SearchToolArgsType): string { const cqlParts: string[] = []; 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: SearchToolArgsType = {}, ): Promise<ControllerResponse> { const methodLogger = Logger.forContext( 'controllers/atlassian.search.controller.ts', 'search', ); methodLogger.debug('Searching Confluence with options:', options); try { const defaults: Partial<SearchToolArgsType> = { limit: DEFAULT_PAGE_SIZE, }; const mergedOptions = applyDefaults<SearchToolArgsType>( 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: SearchParams = { cql: finalCql, limit: mergedOptions.limit, cursor: mergedOptions.cursor, excerpt: 'highlight', includeArchivedSpaces: false, }; const searchData = await atlassianSearchService.search(params); methodLogger.debug( `Retrieved ${searchData.results.length} search results. Has more: ${searchData._links?.next ? 'yes' : 'no'}`, ); const pagination = extractPaginationInfo( searchData, PaginationType.CURSOR, 'Search', ); // Format the search results const formattedResults = 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 += `${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 = formatPagination(pagination); finalContent += '\n\n' + paginationString; } return { content: finalContent, }; } catch (error) { const mcpError = 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 handleControllerError( mcpError, // Pass the potentially modified error buildErrorContext( 'Search', 'performing', 'controllers/atlassian.search.controller.ts@search', {}, { cql: options.cql || '', query: options.query || '', spaceKey: options.spaceKey, limit: options.limit, cursor: options.cursor, }, ), ); } } export default { search };

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