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.Immutable;
022import javax.annotation.concurrent.NotThreadSafe;
023import java.util.ArrayList;
024import java.util.LinkedHashMap;
025import java.util.LinkedHashSet;
026import java.util.List;
027import java.util.Map;
028import java.util.Objects;
029import java.util.Set;
030
031import static java.util.Objects.requireNonNull;
032
033/**
034 * Public programmatic MCP schema DSL.
035 *
036 * @author <a href="https://www.revetkn.com">Mark Allen</a>
037 */
038@Immutable
039public final class McpSchema {
040        @NonNull
041        private final McpObject value;
042
043        private McpSchema(@NonNull McpObject value) {
044                requireNonNull(value);
045                this.value = value;
046        }
047
048        /**
049         * Creates a builder for a root-level object schema.
050         *
051         * @return a new object-schema builder
052         */
053        @NonNull
054        public static ObjectBuilder object() {
055                return new ObjectBuilder();
056        }
057
058        /**
059         * Converts this schema into its MCP JSON-schema object representation.
060         *
061         * @return the schema value
062         */
063        @NonNull
064        public McpObject toValue() {
065                return this.value;
066        }
067
068        @Override
069        public boolean equals(Object other) {
070                if (this == other)
071                        return true;
072
073                if (!(other instanceof McpSchema mcpSchema))
074                        return false;
075
076                return this.value.equals(mcpSchema.value);
077        }
078
079        @Override
080        public int hashCode() {
081                return Objects.hash(this.value);
082        }
083
084        @Override
085        public String toString() {
086                return "McpSchema{value=%s}".formatted(this.value);
087        }
088
089        /**
090         * Builder for root-level object schemas.
091         */
092        @NotThreadSafe
093        public static final class ObjectBuilder {
094                @NonNull
095                private final Map<String, McpValue> properties;
096                @NonNull
097                private final Set<String> required;
098
099                private ObjectBuilder() {
100                        this.properties = new LinkedHashMap<>();
101                        this.required = new LinkedHashSet<>();
102                }
103
104                /**
105                 * Adds a required scalar property.
106                 *
107                 * @param name the property name
108                 * @param type the property type
109                 * @return this builder
110                 */
111                @NonNull
112                public ObjectBuilder required(@NonNull String name,
113                                                                                                                                        @NonNull McpType type) {
114                        requireNonNull(name);
115                        requireNonNull(type);
116                        this.properties.put(name, type.toSchemaValue());
117                        this.required.add(name);
118                        return this;
119                }
120
121                /**
122                 * Adds an optional scalar property.
123                 *
124                 * @param name the property name
125                 * @param type the property type
126                 * @return this builder
127                 */
128                @NonNull
129                public ObjectBuilder optional(@NonNull String name,
130                                                                                                                                        @NonNull McpType type) {
131                        requireNonNull(name);
132                        requireNonNull(type);
133                        this.properties.put(name, type.toSchemaValue());
134                        this.required.remove(name);
135                        return this;
136                }
137
138                /**
139                 * Adds a required enum-backed string property.
140                 *
141                 * @param name the property name
142                 * @param values the allowed enum values
143                 * @return this builder
144                 */
145                @NonNull
146                public ObjectBuilder requiredEnum(@NonNull String name,
147                                                                                                                                                        @NonNull String... values) {
148                        requireNonNull(name);
149                        requireNonNull(values);
150                        this.properties.put(name, enumSchema(values));
151                        this.required.add(name);
152                        return this;
153                }
154
155                /**
156                 * Adds an optional enum-backed string property.
157                 *
158                 * @param name the property name
159                 * @param values the allowed enum values
160                 * @return this builder
161                 */
162                @NonNull
163                public ObjectBuilder optionalEnum(@NonNull String name,
164                                                                                                                                                        @NonNull String... values) {
165                        requireNonNull(name);
166                        requireNonNull(values);
167                        this.properties.put(name, enumSchema(values));
168                        this.required.remove(name);
169                        return this;
170                }
171
172                /**
173                 * Builds the immutable schema value.
174                 *
175                 * @return the built schema
176                 */
177                @NonNull
178                public McpSchema build() {
179                        Map<String, McpValue> value = new LinkedHashMap<>();
180                        value.put("type", new McpString("object"));
181                        value.put("properties", new McpObject(this.properties));
182                        value.put("additionalProperties", new McpBoolean(false));
183
184                        if (!this.required.isEmpty()) {
185                                List<McpValue> requiredValues = new ArrayList<>();
186
187                                for (String requiredName : this.required)
188                                        requiredValues.add(new McpString(requiredName));
189
190                                value.put("required", new McpArray(requiredValues));
191                        }
192
193                        return new McpSchema(new McpObject(value));
194                }
195
196                @NonNull
197                private static McpObject enumSchema(@NonNull String... values) {
198                        requireNonNull(values);
199
200                        List<McpValue> enumValues = new ArrayList<>(values.length);
201
202                        for (String value : values) {
203                                requireNonNull(value);
204                                enumValues.add(new McpString(value));
205                        }
206
207                        return new McpObject(Map.of(
208                                        "type", new McpString("string"),
209                                        "enum", new McpArray(enumValues)
210                        ));
211                }
212        }
213}