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}