@aashari/mcp-server-atlassian-bitbucket
Version:
Node.js/TypeScript MCP server for Atlassian Bitbucket. Enables AI systems (LLMs) to interact with workspaces, repositories, and pull requests via tools (list, get, comment, search). Connects AI directly to version control workflows through the standard MC
170 lines (169 loc) • 8.01 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.PaginationType = void 0;
exports.extractPaginationInfo = extractPaginationInfo;
exports.validatePageSize = validatePageSize;
exports.validatePaginationLimits = validatePaginationLimits;
const logger_util_js_1 = require("./logger.util.js");
const constants_util_js_1 = require("./constants.util.js");
/**
* Represents the possible pagination types.
*/
var PaginationType;
(function (PaginationType) {
PaginationType["CURSOR"] = "cursor";
PaginationType["OFFSET"] = "offset";
PaginationType["PAGE"] = "page";
})(PaginationType || (exports.PaginationType = PaginationType = {}));
/**
* 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
*/
function extractPaginationInfo(data, paginationType) {
if (!data) {
return undefined;
}
let pagination;
const methodLogger = logger_util_js_1.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 = 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)
*/
function validatePageSize(requestedPageSize, contextInfo) {
const methodLogger = logger_util_js_1.Logger.forContext('utils/pagination.util.ts', 'validatePageSize');
// Use default if not specified
if (!requestedPageSize || requestedPageSize <= 0) {
const defaultSize = constants_util_js_1.DATA_LIMITS.DEFAULT_PAGE_SIZE;
methodLogger.debug(`Using default page size: ${defaultSize}${contextInfo ? ` for ${contextInfo}` : ''}`);
return defaultSize;
}
// Enforce maximum page size limit
if (requestedPageSize > constants_util_js_1.DATA_LIMITS.MAX_PAGE_SIZE) {
const clampedSize = constants_util_js_1.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
*/
function validatePaginationLimits(paginationData, contextInfo) {
const methodLogger = logger_util_js_1.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 > constants_util_js_1.DATA_LIMITS.MAX_PAGE_SIZE) {
methodLogger.warn(`Response contains ${itemCount} items, exceeding maximum of ${constants_util_js_1.DATA_LIMITS.MAX_PAGE_SIZE}${contextInfo ? ` for ${contextInfo}` : ''}`);
return false;
}
if (pageSize > constants_util_js_1.DATA_LIMITS.MAX_PAGE_SIZE) {
methodLogger.warn(`Response page size ${pageSize} exceeds maximum of ${constants_util_js_1.DATA_LIMITS.MAX_PAGE_SIZE}${contextInfo ? ` for ${contextInfo}` : ''}`);
return false;
}
return true;
}