001/*
002 * Copyright 2022-2026 Revetware LLC.
003 * Licensed under the Apache License, Version 2.0 (the "License");
004 * you may not use this file except in compliance with the License.
005 * You may obtain a copy at https://www.apache.org/licenses/LICENSE-2.0
006 * Unless required by applicable law or agreed to in writing, software
007 * distributed under the License is distributed on an "AS IS" BASIS,
008 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
009 * See the License for the specific language governing permissions and
010 * limitations under the License.
011 */
012package com.soklet;
013
014import org.jspecify.annotations.NonNull;
015
016import javax.annotation.concurrent.ThreadSafe;
017import java.util.List;
018import java.util.Optional;
019
020import static java.util.Objects.requireNonNull;
021
022/**
023 * Immutable, framework-supplied Skills pagination input.
024 *
025 * <p>The initial-selection optional is the authoritative phase marker: present,
026 * even for an empty list, means first-page selection was performed; absent means
027 * the application must restore a continuation snapshot. A present cursor,
028 * including an empty string, always identifies continuation. Soklet preserves
029 * opaque cursor text without authenticating or retaining application snapshots.
030 *
031 * @author <a href="https://www.revetkn.com">Mark Allen</a>
032 */
033@ThreadSafe
034public final class McpSkillListContext {
035        @NonNull
036        private final Optional<@NonNull String> cursor;
037        @NonNull
038        private final Optional<@NonNull List<@NonNull McpSkillRegistration>> initialSkillRegistrations;
039
040        private McpSkillListContext(@NonNull Optional<@NonNull String> cursor,
041                        @NonNull Optional<@NonNull List<@NonNull McpSkillRegistration>> initialSkillRegistrations) {
042                this.cursor = requireNonNull(cursor);
043                this.initialSkillRegistrations = requireNonNull(initialSkillRegistrations).map(List::copyOf);
044                if (cursor.isPresent() == initialSkillRegistrations.isPresent())
045                        throw new IllegalArgumentException("Invalid MCP Skills list context phase.");
046        }
047
048        @NonNull
049        static McpSkillListContext from(@NonNull Optional<@NonNull String> cursor,
050                        @NonNull Optional<@NonNull List<@NonNull McpSkillRegistration>> initialSkillRegistrations) {
051                return new McpSkillListContext(cursor, initialSkillRegistrations);
052        }
053
054        /**
055         * Returns exact opaque client cursor text, including present-empty values.
056         * The application owns integrity, authorization and content binding, expiry,
057         * retained snapshot availability, and cross-node portability.
058         *
059         * @return client cursor, or empty when omitted on the first page
060         */
061        @NonNull
062        public Optional<@NonNull String> getCursor() { return this.cursor; }
063
064        /**
065         * Returns the first-page access/discovery-filtered and variant-selected
066         * registrations in canonical endpoint order. The first page must be an ordered
067         * subsequence of these exact instances. Present-empty still denotes the first
068         * page; absent requires restoration of an application-owned continuation.
069         *
070         * @return immutable initial selection, or empty for a continuation
071         */
072        @NonNull
073        public Optional<@NonNull List<@NonNull McpSkillRegistration>> getInitialSkillRegistrations() {
074                return this.initialSkillRegistrations;
075        }
076
077        /** @return diagnostic rendering without cursor or registration data */
078        @Override
079        @NonNull
080        public String toString() { return "McpSkillListContext[redacted]"; }
081}