Skip to main content
Glama

Atlassian Bitbucket MCP Server

by aashari
pagination.util.ts7.26 kB
import { Logger } from './logger.util.js'; import { DATA_LIMITS } from './constants.util.js'; import { ResponsePagination } from '../types/common.types.js'; /** * Represents the possible pagination types. */ export enum PaginationType { CURSOR = 'cursor', // Confluence, Bitbucket (some endpoints) OFFSET = 'offset', // Jira PAGE = 'page', // Bitbucket (most endpoints) } /** * Interface representing the common structure of paginated data from APIs. * This union type covers properties used by offset, cursor, and page-based pagination. */ interface PaginationData { // Shared results?: unknown[]; values?: unknown[]; count?: number; size?: number; // Total count in Bitbucket page responses hasMore?: boolean; _links?: { next?: string }; // Confluence cursor // Offset-based (Jira) startAt?: number; maxResults?: number; total?: number; nextPage?: string; // Alternative next indicator for offset // Page-based (Bitbucket) page?: number; pagelen?: number; next?: string; // Bitbucket page URL } /** * Extract pagination information from API response * @param data The API response containing pagination information * @param paginationType The type of pagination mechanism used * @returns Object with nextCursor, hasMore, and count properties */ export function extractPaginationInfo<T extends Partial<PaginationData>>( data: T, paginationType: PaginationType, ): ResponsePagination | undefined { if (!data) { return undefined; } let pagination: ResponsePagination | undefined; const methodLogger = Logger.forContext( 'utils/pagination.util.ts', 'extractPaginationInfo', ); switch (paginationType) { case PaginationType.PAGE: { // Bitbucket page-based pagination (page, pagelen, size, next) if (data.page !== undefined && data.pagelen !== undefined) { const hasMore = !!data.next; let nextCursorValue: string | undefined = undefined; if (hasMore) { try { // First attempt to parse the full URL if it looks like one if ( typeof data.next === 'string' && data.next.includes('://') ) { const nextUrl = new URL(data.next); nextCursorValue = nextUrl.searchParams.get('page') || undefined; methodLogger.debug( `Successfully extracted page from URL: ${nextCursorValue}`, ); } else if (data.next === 'available') { // Handle the 'available' placeholder used in some transformedResponses nextCursorValue = String(Number(data.page) + 1); methodLogger.debug( `Using calculated next page from 'available': ${nextCursorValue}`, ); } else if (typeof data.next === 'string') { // Try to use data.next directly if it's not a URL but still a string nextCursorValue = data.next; methodLogger.debug( `Using next value directly: ${nextCursorValue}`, ); } } catch (e) { // If URL parsing fails, calculate the next page based on current page nextCursorValue = String(Number(data.page) + 1); methodLogger.debug( `Calculated next page after URL parsing error: ${nextCursorValue}`, ); methodLogger.warn( `Failed to parse next URL: ${data.next}`, e, ); } } pagination = { hasMore, count: data.values?.length ?? 0, page: data.page, size: data.pagelen, total: data.size, nextCursor: nextCursorValue, // Store next page number as cursor }; } break; } case PaginationType.OFFSET: { // Jira offset-based pagination const countOffset = data.values?.length; if ( data.startAt !== undefined && data.maxResults !== undefined && data.total !== undefined && data.startAt + data.maxResults < data.total ) { pagination = { hasMore: true, count: countOffset, total: data.total, nextCursor: String(data.startAt + data.maxResults), }; } else if (data.nextPage) { pagination = { hasMore: true, count: countOffset, nextCursor: data.nextPage, }; } break; } case PaginationType.CURSOR: { // Confluence cursor-based pagination const countCursor = data.results?.length; if (data._links && data._links.next) { const nextUrl = data._links.next; const cursorMatch = nextUrl.match(/cursor=([^&]+)/); if (cursorMatch && cursorMatch[1]) { pagination = { hasMore: true, count: countCursor, nextCursor: decodeURIComponent(cursorMatch[1]), }; } } break; } default: methodLogger.warn(`Unknown pagination type: ${paginationType}`); } // Ensure a default pagination object if none was created but data exists if (!pagination && (data.results || data.values)) { pagination = { hasMore: false, count: data.results?.length ?? data.values?.length ?? 0, }; } return pagination; } /** * Validates and enforces page size limits to prevent excessive data exposure (CWE-770) * @param requestedPageSize The requested page size from the client * @param contextInfo Optional context for logging (e.g., endpoint name) * @returns The validated page size (clamped to maximum allowed) */ export function validatePageSize( requestedPageSize?: number, contextInfo?: string, ): number { const methodLogger = Logger.forContext( 'utils/pagination.util.ts', 'validatePageSize', ); // Use default if not specified if (!requestedPageSize || requestedPageSize <= 0) { const defaultSize = DATA_LIMITS.DEFAULT_PAGE_SIZE; methodLogger.debug( `Using default page size: ${defaultSize}${contextInfo ? ` for ${contextInfo}` : ''}`, ); return defaultSize; } // Enforce maximum page size limit if (requestedPageSize > DATA_LIMITS.MAX_PAGE_SIZE) { const clampedSize = DATA_LIMITS.MAX_PAGE_SIZE; methodLogger.warn( `Page size ${requestedPageSize} exceeds maximum limit. Clamped to ${clampedSize}${contextInfo ? ` for ${contextInfo}` : ''}`, ); return clampedSize; } methodLogger.debug( `Using requested page size: ${requestedPageSize}${contextInfo ? ` for ${contextInfo}` : ''}`, ); return requestedPageSize; } /** * Validates pagination data to ensure it doesn't exceed configured limits * @param paginationData The pagination data to validate * @param contextInfo Optional context for logging * @returns True if data is within limits, false otherwise */ export function validatePaginationLimits( paginationData: { count?: number; size?: number; pagelen?: number }, contextInfo?: string, ): boolean { const methodLogger = Logger.forContext( 'utils/pagination.util.ts', 'validatePaginationLimits', ); // Check if the response contains more items than our maximum allowed const itemCount = paginationData.count ?? 0; const pageSize = paginationData.size ?? paginationData.pagelen ?? 0; if (itemCount > DATA_LIMITS.MAX_PAGE_SIZE) { methodLogger.warn( `Response contains ${itemCount} items, exceeding maximum of ${DATA_LIMITS.MAX_PAGE_SIZE}${contextInfo ? ` for ${contextInfo}` : ''}`, ); return false; } if (pageSize > DATA_LIMITS.MAX_PAGE_SIZE) { methodLogger.warn( `Response page size ${pageSize} exceeds maximum of ${DATA_LIMITS.MAX_PAGE_SIZE}${contextInfo ? ` for ${contextInfo}` : ''}`, ); return false; } return true; }

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-bitbucket'

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