001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.juneau.rest.server.mcp; 018 019import static org.apache.juneau.commons.utils.Shorts.*; 020import static org.apache.juneau.commons.utils.StringUtils.*; 021import java.util.*; 022 023import org.apache.juneau.commons.inject.*; 024 025/** 026 * Pagination strategy for MCP {@code list} dispatchers (tools / prompts / resources / resource templates). 027 * 028 * <p> 029 * Built-in strategies: 030 * <ul> 031 * <li>{@link #SINGLE_PAGE} - Returns everything in one page (default; ignores any incoming cursor). 032 * <li>{@link #fixedSize(int)} - Slices a list into pages of a fixed size using opaque integer cursors. 033 * </ul> 034 */ 035@FunctionalInterface 036public interface McpCursor { 037 038 /** 039 * Slices a descriptor list into a page returned from {@code tools/list}, {@code prompts/list}, 040 * {@code resources/list}, or {@code resources/templates/list}. 041 * 042 * @param all The full list of descriptors. Never {@code null}. 043 * @param cursor Opaque cursor from the request, or {@code null} for the first page. 044 * @param ctx Per-request bean store (provided for cursor strategies that need access to the request). 045 * @param <T> Element type. 046 * @return A non-{@code null} page. 047 */ 048 <T> McpPage<T> page(List<T> all, String cursor, BeanStore ctx); 049 050 /** 051 * Returns everything in a single page; emits no {@code nextCursor}. 052 */ 053 McpCursor SINGLE_PAGE = new McpCursor() { 054 @Override 055 public <T> McpPage<T> page(List<T> all, String cursor, BeanStore ctx) { 056 return new McpPage<>(all == null ? List.of() : all, null); 057 } 058 }; 059 060 /** 061 * Returns a fixed-page-size cursor. 062 * 063 * <p> 064 * Cursor representation is an opaque integer offset (parsed leniently; non-numeric values restart at offset 0). 065 * 066 * @param pageSize Maximum items per page. Must be {@code > 0}. 067 * @return A new cursor strategy. 068 */ 069 static McpCursor fixedSize(int pageSize) { 070 if (pageSize <= 0) 071 throw iaex("pageSize must be positive: %s", pageSize); 072 return new McpCursor() { 073 @Override 074 public <T> McpPage<T> page(List<T> all, String cursor, BeanStore ctx) { 075 var src = all == null ? List.<T>of() : all; 076 var offset = parseOffset(cursor); 077 if (offset >= src.size()) 078 return new McpPage<>(List.of(), null); 079 var end = Math.min(offset + pageSize, src.size()); 080 var slice = src.subList(offset, end); 081 var next = end < src.size() ? Integer.toString(end) : null; 082 return new McpPage<>(slice, next); 083 } 084 }; 085 } 086 087 /** 088 * Parses a cursor as a non-negative integer offset; returns {@code 0} for {@code null} or non-numeric inputs. 089 * 090 * @param cursor Cursor value (may be {@code null}). 091 * @return Parsed offset. 092 */ 093 static int parseOffset(String cursor) { 094 if (isEmpty(cursor)) 095 return 0; 096 try { 097 var v = Integer.parseInt(cursor); 098 return Math.max(v, 0); 099 } catch (NumberFormatException e) { 100 return 0; 101 } 102 } 103 104 /** 105 * Extracts the opaque pagination cursor from a JSON-RPC {@code params} value. 106 * 107 * @param params The raw params value from the request. Can be <jk>null</jk>. 108 * @return The {@code cursor} parameter, or <jk>null</jk> if absent. 109 * @throws org.apache.juneau.bean.jsonrpc.McpException If {@code params} is present but is not a 110 * JSON object. 111 */ 112 static String cursorOf(Object params) { 113 return McpParamUtils.strParam(McpParamUtils.asMap(params), "cursor"); 114 } 115}