001/*
002 * Copyright 2022-2026 Revetware LLC.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package com.soklet;
018
019import org.jspecify.annotations.NonNull;
020
021import javax.annotation.concurrent.ThreadSafe;
022import java.net.URI;
023
024import static java.util.Objects.requireNonNull;
025
026/**
027 * Thread-safe broadcast publisher for application-owned MCP subscription-change
028 * events.
029 * <p>
030 * Every event must be made available to every current listener; a
031 * competing-consumer queue is not a valid implementation. Implementations may
032 * perform process-local delivery or coordinate through a distributed service.
033 * The application owns the publisher. Soklet closes only registrations
034 * returned by {@link #subscribe(McpSubscriptionEventListener)} and never closes
035 * the publisher itself.
036 * <p>
037 * This SPI distributes application change events only. Publication requests
038 * local reevaluation for the configured notification family; it is not by
039 * itself a wire notification or an authorization grant. Soklet owns all MCP
040 * stream-delivery mechanics: per-stream queues, duplicate-event coalescing,
041 * backpressure, accepted-filter matching, caller-visible catalog comparison,
042 * and MCP wire serialization. Publisher events carry no endpoint, client,
043 * principal, or authorization-partition target. Publisher implementations must
044 * not attempt to perform authorization or stream-delivery functions.
045 *
046 * @author <a href="https://www.revetkn.com">Mark Allen</a>
047 */
048@ThreadSafe
049public interface McpSubscriptionEventPublisher {
050        /**
051         * Creates an independent in-memory publisher with default behavior.
052         * <p>
053         * The publisher reaches only listeners registered with that instance in the
054         * current process and invokes them synchronously on the publishing thread.
055         * Closing a registration does not wait for a delivery already selected or
056         * in flight. If a listener throws a runtime exception, all other current
057         * listeners are still attempted before the first exception is rethrown with
058         * later exceptions suppressed.
059         *
060         * @return in-memory event publisher
061         */
062        @NonNull
063        static McpSubscriptionEventPublisher fromInMemoryDefaults() {
064                return new DefaultMcpSubscriptionEventPublisher();
065        }
066
067        /**
068         * Registers a listener for broadcast events.
069         *
070         * @param listener thread-safe listener
071         * @return an idempotently closable listener registration
072         */
073        @NonNull
074        McpSubscriptionEventRegistration subscribe(
075                        @NonNull McpSubscriptionEventListener listener);
076
077        /**
078         * Broadcasts a coarse subscription-change event.
079         *
080         * @param subscriptionEvent subscription-change event
081         */
082        void publish(@NonNull McpSubscriptionEvent subscriptionEvent);
083
084        /** Broadcasts a resource-list-changed event. */
085        default void publishResourcesListChanged() {
086                publish(McpSubscriptionEvent.resourcesListChanged());
087        }
088
089        /**
090         * Broadcasts a resource-updated event.
091         *
092         * @param resourceUri absolute normalized changed-resource URI in ASCII wire
093         *                    form
094         * @throws NullPointerException if {@code resourceUri} is null
095         * @throws IllegalArgumentException if the URI is relative, not normalized,
096         *                                  or not in ASCII wire form
097         */
098        default void publishResourceUpdated(@NonNull URI resourceUri) {
099                publish(McpSubscriptionEvent.resourceUpdated(
100                                requireNonNull(resourceUri)));
101        }
102
103        /** Broadcasts a tool-list-changed event. */
104        default void publishToolsListChanged() {
105                publish(McpSubscriptionEvent.toolsListChanged());
106        }
107
108        /** Broadcasts a prompt-list-changed event. */
109        default void publishPromptsListChanged() {
110                publish(McpSubscriptionEvent.promptsListChanged());
111        }
112}