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}