Skip to main content
Glama

ClinicalTrials.gov MCP Server

index.ts•5.8 kB
/** * @fileoverview Pagination utilities for MCP list operations. * Implements cursor-based pagination per MCP specification 2025-06-18. * * MCP Pagination Model: * - Opaque cursor-based approach (not numbered pages) * - Cursor is an opaque string token representing a position in the result set * - Page size is determined by server (clients MUST NOT assume fixed page size) * - Invalid cursors should result in error code -32602 (Invalid params) * * @see {@link https://modelcontextprotocol.io/specification/2025-06-18/utils/pagination | MCP Pagination Spec} * @module src/utils/pagination */ import { JsonRpcErrorCode, McpError } from '@/types-global/errors.js'; import type { RequestContext } from '@/utils/index.js'; import { logger } from '@/utils/index.js'; /** * Generic pagination state that can be encoded into a cursor. * Implementations can extend this with additional fields as needed. */ export interface PaginationState { /** Current page offset or starting position */ offset: number; /** Maximum number of items per page */ limit: number; /** Optional additional state (implementation-specific) */ [key: string]: unknown; } /** * Result of a paginated operation. */ export interface PaginatedResult<T> { /** Array of items for the current page */ items: T[]; /** Opaque cursor for the next page, undefined if no more results */ nextCursor?: string; /** Total count if available (optional, some backends may not support this efficiently) */ totalCount?: number; } /** * Encodes pagination state into an opaque cursor string. * Uses base64-encoded JSON for transparency during development. * Can be optimized to use more compact/opaque formats in production if needed. * * @param state - The pagination state to encode * @returns Base64-encoded cursor string * @throws {McpError} If encoding fails */ export function encodeCursor(state: PaginationState): string { try { const jsonString = JSON.stringify(state); const base64 = Buffer.from(jsonString, 'utf-8').toString('base64url'); return base64; } catch (error) { throw new McpError( JsonRpcErrorCode.InternalError, 'Failed to encode pagination cursor', { error: error instanceof Error ? error.message : String(error) }, ); } } /** * Decodes an opaque cursor string back into pagination state. * Validates the cursor format and throws McpError for invalid cursors per spec. * * @param cursor - The opaque cursor string to decode * @param context - Request context for logging * @returns Decoded pagination state * @throws {McpError} If cursor is invalid (code -32602) */ export function decodeCursor( cursor: string, context: RequestContext, ): PaginationState { try { const jsonString = Buffer.from(cursor, 'base64url').toString('utf-8'); const state = JSON.parse(jsonString) as PaginationState; // Validate required fields if ( typeof state.offset !== 'number' || typeof state.limit !== 'number' || state.offset < 0 || state.limit <= 0 ) { throw new Error('Invalid pagination state structure'); } return state; } catch (error) { logger.warning('Failed to decode pagination cursor', { ...context, cursor, error: error instanceof Error ? error.message : String(error), }); throw new McpError( JsonRpcErrorCode.InvalidParams, 'Invalid pagination cursor. The cursor may be expired, corrupted, or from a different request.', { cursor }, ); } } /** * Extracts the cursor parameter from MCP request metadata. * Handles both params.cursor and _meta.cursor locations per spec. * * @param params - Request params object * @returns Cursor string if present, undefined otherwise */ export function extractCursor(params?: { cursor?: string; _meta?: { cursor?: string }; }): string | undefined { return params?.cursor ?? params?._meta?.cursor; } /** * Helper to paginate an in-memory array. * Useful for simple list operations that don't require database pagination. * * @param items - Full array of items to paginate * @param cursorStr - Optional cursor from client request * @param defaultPageSize - Default page size if cursor doesn't specify * @param maxPageSize - Maximum allowed page size * @param context - Request context for logging * @returns Paginated result with nextCursor if more items exist */ export function paginateArray<T>( items: T[], cursorStr: string | undefined, defaultPageSize: number, maxPageSize: number, context: RequestContext, ): PaginatedResult<T> { let offset = 0; let limit = defaultPageSize; // Decode cursor if provided if (cursorStr) { const state = decodeCursor(cursorStr, context); offset = state.offset; limit = Math.min(state.limit, maxPageSize); // Enforce max page size } // Validate bounds if (offset >= items.length) { return { items: [], totalCount: items.length, }; } // Extract page const pageItems = items.slice(offset, offset + limit); const hasMore = offset + limit < items.length; // Build result, conditionally adding nextCursor only if it exists const result: PaginatedResult<T> = { items: pageItems, totalCount: items.length, }; // Only add nextCursor if more results exist if (hasMore) { result.nextCursor = encodeCursor({ offset: offset + limit, limit }); } return result; } /** * Default pagination configuration values. * These can be overridden via environment variables in config module. */ export const DEFAULT_PAGINATION_CONFIG = { /** Default number of items per page */ DEFAULT_PAGE_SIZE: 50, /** Maximum allowed items per page */ MAX_PAGE_SIZE: 1000, /** Minimum allowed items per page */ MIN_PAGE_SIZE: 1, } as const;

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/cyanheads/clinicaltrialsgov-mcp-server'

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