| 
 | 1 | +/*  | 
 | 2 | + * Copyright 2025-2025 the original author or authors.  | 
 | 3 | + *  | 
 | 4 | + * Licensed under the Apache License, Version 2.0 (the "License");  | 
 | 5 | + * you may not use this file except in compliance with the License.  | 
 | 6 | + * You may obtain a copy of the License at  | 
 | 7 | + *  | 
 | 8 | + *      https://www.apache.org/licenses/LICENSE-2.0  | 
 | 9 | + *  | 
 | 10 | + * Unless required by applicable law or agreed to in writing, software  | 
 | 11 | + * distributed under the License is distributed on an "AS IS" BASIS,  | 
 | 12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  | 
 | 13 | + * See the License for the specific language governing permissions and  | 
 | 14 | + * limitations under the License.  | 
 | 15 | + */  | 
 | 16 | + | 
 | 17 | +package org.springframework.ai.mcp;  | 
 | 18 | + | 
 | 19 | +import java.util.List;  | 
 | 20 | +import java.util.Map;  | 
 | 21 | +import java.util.Set;  | 
 | 22 | +import java.util.function.Predicate;  | 
 | 23 | + | 
 | 24 | +import io.modelcontextprotocol.spec.McpSchema;  | 
 | 25 | +import org.slf4j.Logger;  | 
 | 26 | +import org.slf4j.LoggerFactory;  | 
 | 27 | + | 
 | 28 | +import org.springframework.util.StringUtils;  | 
 | 29 | + | 
 | 30 | +/**  | 
 | 31 | + * A metadata-based implementation of {@link McpToolFilter} that filters MCP tools based  | 
 | 32 | + * on their metadata properties. This filter supports filtering by type, category,  | 
 | 33 | + * priority, tags, and custom metadata fields.  | 
 | 34 | + *  | 
 | 35 | + * <p>  | 
 | 36 | + * Example usage:  | 
 | 37 | + * </p>  | 
 | 38 | + *  | 
 | 39 | + * <pre class="code">  | 
 | 40 | + * // Filter tools by type  | 
 | 41 | + * var filter = MetadataBasedToolFilter.builder().types(Set.of("RealTimeAnalysis")).build();  | 
 | 42 | + *  | 
 | 43 | + * // Filter tools by category and minimum priority  | 
 | 44 | + * var filter = MetadataBasedToolFilter.builder()  | 
 | 45 | + *     .categories(Set.of("market", "analytics"))  | 
 | 46 | + *     .minPriority(7)  | 
 | 47 | + *     .build();  | 
 | 48 | + * </pre>  | 
 | 49 | + *  | 
 | 50 | + */  | 
 | 51 | +public class MetadataBasedToolFilter implements McpToolFilter {  | 
 | 52 | + | 
 | 53 | +	private static final Logger logger = LoggerFactory.getLogger(MetadataBasedToolFilter.class);  | 
 | 54 | + | 
 | 55 | +	private final Set<String> types;  | 
 | 56 | + | 
 | 57 | +	private final Set<String> categories;  | 
 | 58 | + | 
 | 59 | +	private final Set<String> tags;  | 
 | 60 | + | 
 | 61 | +	private final Integer minPriority;  | 
 | 62 | + | 
 | 63 | +	private final Integer maxPriority;  | 
 | 64 | + | 
 | 65 | +	private final Map<String, Predicate<Object>> customFilters;  | 
 | 66 | + | 
 | 67 | +	private MetadataBasedToolFilter(Builder builder) {  | 
 | 68 | +		this.types = builder.types;  | 
 | 69 | +		this.categories = builder.categories;  | 
 | 70 | +		this.tags = builder.tags;  | 
 | 71 | +		this.minPriority = builder.minPriority;  | 
 | 72 | +		this.maxPriority = builder.maxPriority;  | 
 | 73 | +		this.customFilters = builder.customFilters;  | 
 | 74 | +	}  | 
 | 75 | + | 
 | 76 | +	@Override  | 
 | 77 | +	public boolean test(McpConnectionInfo connectionInfo, McpSchema.Tool tool) {  | 
 | 78 | +		// Extract metadata from tool  | 
 | 79 | +		// Note: MCP Java SDK's Tool might have a meta() method or we need to extract  | 
 | 80 | +		// metadata from description or other fields  | 
 | 81 | +		Map<String, Object> metadata = extractMetadataFromTool(tool);  | 
 | 82 | + | 
 | 83 | +		if (metadata.isEmpty()) {  | 
 | 84 | +			// If no metadata is present, check if we're filtering anything  | 
 | 85 | +			// If filters are set, exclude tools without metadata  | 
 | 86 | +			return types.isEmpty() && categories.isEmpty() && tags.isEmpty() && minPriority == null  | 
 | 87 | +					&& maxPriority == null && customFilters.isEmpty();  | 
 | 88 | +		}  | 
 | 89 | + | 
 | 90 | +		// Filter by type  | 
 | 91 | +		if (!types.isEmpty()) {  | 
 | 92 | +			Object typeValue = metadata.get("type");  | 
 | 93 | +			if (typeValue == null || !types.contains(typeValue.toString())) {  | 
 | 94 | +				return false;  | 
 | 95 | +			}  | 
 | 96 | +		}  | 
 | 97 | + | 
 | 98 | +		// Filter by category  | 
 | 99 | +		if (!categories.isEmpty()) {  | 
 | 100 | +			Object categoryValue = metadata.get("category");  | 
 | 101 | +			if (categoryValue == null || !categories.contains(categoryValue.toString())) {  | 
 | 102 | +				return false;  | 
 | 103 | +			}  | 
 | 104 | +		}  | 
 | 105 | + | 
 | 106 | +		// Filter by tags  | 
 | 107 | +		if (!tags.isEmpty()) {  | 
 | 108 | +			Object tagsValue = metadata.get("tags");  | 
 | 109 | +			if (tagsValue == null) {  | 
 | 110 | +				return false;  | 
 | 111 | +			}  | 
 | 112 | +			if (tagsValue instanceof List) {  | 
 | 113 | +				List<?> toolTags = (List<?>) tagsValue;  | 
 | 114 | +				boolean hasMatchingTag = toolTags.stream().anyMatch(tag -> tags.contains(tag.toString()));  | 
 | 115 | +				if (!hasMatchingTag) {  | 
 | 116 | +					return false;  | 
 | 117 | +				}  | 
 | 118 | +			}  | 
 | 119 | +			else if (!tags.contains(tagsValue.toString())) {  | 
 | 120 | +				return false;  | 
 | 121 | +			}  | 
 | 122 | +		}  | 
 | 123 | + | 
 | 124 | +		// Filter by priority  | 
 | 125 | +		if (minPriority != null || maxPriority != null) {  | 
 | 126 | +			Object priorityValue = metadata.get("priority");  | 
 | 127 | +			if (priorityValue != null) {  | 
 | 128 | +				try {  | 
 | 129 | +					int priority = priorityValue instanceof Number ? ((Number) priorityValue).intValue()  | 
 | 130 | +							: Integer.parseInt(priorityValue.toString());  | 
 | 131 | + | 
 | 132 | +					if (minPriority != null && priority < minPriority) {  | 
 | 133 | +						return false;  | 
 | 134 | +					}  | 
 | 135 | +					if (maxPriority != null && priority > maxPriority) {  | 
 | 136 | +						return false;  | 
 | 137 | +					}  | 
 | 138 | +				}  | 
 | 139 | +				catch (NumberFormatException e) {  | 
 | 140 | +					logger.warn("Invalid priority value in metadata: {}", priorityValue);  | 
 | 141 | +					return false;  | 
 | 142 | +				}  | 
 | 143 | +			}  | 
 | 144 | +			else {  | 
 | 145 | +				// No priority metadata, but we require it for filtering  | 
 | 146 | +				return false;  | 
 | 147 | +			}  | 
 | 148 | +		}  | 
 | 149 | + | 
 | 150 | +		// Apply custom filters  | 
 | 151 | +		for (Map.Entry<String, Predicate<Object>> entry : customFilters.entrySet()) {  | 
 | 152 | +			Object value = metadata.get(entry.getKey());  | 
 | 153 | +			if (!entry.getValue().test(value)) {  | 
 | 154 | +				return false;  | 
 | 155 | +			}  | 
 | 156 | +		}  | 
 | 157 | + | 
 | 158 | +		return true;  | 
 | 159 | +	}  | 
 | 160 | + | 
 | 161 | +	/**  | 
 | 162 | +	 * Extracts metadata from an MCP tool. This implementation attempts to extract  | 
 | 163 | +	 * metadata from the tool's description if it follows a specific format: [key1=value1,  | 
 | 164 | +	 * key2=value2] Description text  | 
 | 165 | +	 *  | 
 | 166 | +	 * Subclasses can override this method to implement different metadata extraction  | 
 | 167 | +	 * strategies.  | 
 | 168 | +	 * @param tool the MCP tool  | 
 | 169 | +	 * @return a map of metadata key-value pairs  | 
 | 170 | +	 */  | 
 | 171 | +	protected Map<String, Object> extractMetadataFromTool(McpSchema.Tool tool) {  | 
 | 172 | +		// Try to extract from description if it has metadata prefix  | 
 | 173 | +		String description = tool.description();  | 
 | 174 | +		if (StringUtils.hasText(description) && description.startsWith("[")) {  | 
 | 175 | +			int endIdx = description.indexOf("]");  | 
 | 176 | +			if (endIdx > 0) {  | 
 | 177 | +				String metadataStr = description.substring(1, endIdx);  | 
 | 178 | +				return parseMetadataString(metadataStr);  | 
 | 179 | +			}  | 
 | 180 | +		}  | 
 | 181 | + | 
 | 182 | +		// If MCP Tool has a meta() method, try to use it  | 
 | 183 | +		// This would require reflection or waiting for the MCP SDK to expose it  | 
 | 184 | +		// For now, return empty map  | 
 | 185 | +		return Map.of();  | 
 | 186 | +	}  | 
 | 187 | + | 
 | 188 | +	private Map<String, Object> parseMetadataString(String metadataStr) {  | 
 | 189 | +		Map<String, Object> metadata = new java.util.HashMap<>();  | 
 | 190 | +		String[] entries = metadataStr.split(",");  | 
 | 191 | +		for (String entry : entries) {  | 
 | 192 | +			String[] parts = entry.split("=", 2);  | 
 | 193 | +			if (parts.length == 2) {  | 
 | 194 | +				String key = parts[0].trim();  | 
 | 195 | +				String value = parts[1].trim();  | 
 | 196 | +				if (StringUtils.hasText(key) && StringUtils.hasText(value)) {  | 
 | 197 | +					metadata.put(key, value);  | 
 | 198 | +				}  | 
 | 199 | +			}  | 
 | 200 | +		}  | 
 | 201 | +		return metadata;  | 
 | 202 | +	}  | 
 | 203 | + | 
 | 204 | +	public static Builder builder() {  | 
 | 205 | +		return new Builder();  | 
 | 206 | +	}  | 
 | 207 | + | 
 | 208 | +	public static final class Builder {  | 
 | 209 | + | 
 | 210 | +		private Set<String> types = Set.of();  | 
 | 211 | + | 
 | 212 | +		private Set<String> categories = Set.of();  | 
 | 213 | + | 
 | 214 | +		private Set<String> tags = Set.of();  | 
 | 215 | + | 
 | 216 | +		private Integer minPriority;  | 
 | 217 | + | 
 | 218 | +		private Integer maxPriority;  | 
 | 219 | + | 
 | 220 | +		private Map<String, Predicate<Object>> customFilters = Map.of();  | 
 | 221 | + | 
 | 222 | +		private Builder() {  | 
 | 223 | +		}  | 
 | 224 | + | 
 | 225 | +		/**  | 
 | 226 | +		 * Set the allowed tool types.  | 
 | 227 | +		 * @param types the types to filter by  | 
 | 228 | +		 * @return this builder  | 
 | 229 | +		 */  | 
 | 230 | +		public Builder types(Set<String> types) {  | 
 | 231 | +			this.types = types != null ? Set.copyOf(types) : Set.of();  | 
 | 232 | +			return this;  | 
 | 233 | +		}  | 
 | 234 | + | 
 | 235 | +		/**  | 
 | 236 | +		 * Set the allowed tool categories.  | 
 | 237 | +		 * @param categories the categories to filter by  | 
 | 238 | +		 * @return this builder  | 
 | 239 | +		 */  | 
 | 240 | +		public Builder categories(Set<String> categories) {  | 
 | 241 | +			this.categories = categories != null ? Set.copyOf(categories) : Set.of();  | 
 | 242 | +			return this;  | 
 | 243 | +		}  | 
 | 244 | + | 
 | 245 | +		/**  | 
 | 246 | +		 * Set the required tags (tool must have at least one of these tags).  | 
 | 247 | +		 * @param tags the tags to filter by  | 
 | 248 | +		 * @return this builder  | 
 | 249 | +		 */  | 
 | 250 | +		public Builder tags(Set<String> tags) {  | 
 | 251 | +			this.tags = tags != null ? Set.copyOf(tags) : Set.of();  | 
 | 252 | +			return this;  | 
 | 253 | +		}  | 
 | 254 | + | 
 | 255 | +		/**  | 
 | 256 | +		 * Set the minimum priority threshold.  | 
 | 257 | +		 * @param minPriority the minimum priority  | 
 | 258 | +		 * @return this builder  | 
 | 259 | +		 */  | 
 | 260 | +		public Builder minPriority(Integer minPriority) {  | 
 | 261 | +			this.minPriority = minPriority;  | 
 | 262 | +			return this;  | 
 | 263 | +		}  | 
 | 264 | + | 
 | 265 | +		/**  | 
 | 266 | +		 * Set the maximum priority threshold.  | 
 | 267 | +		 * @param maxPriority the maximum priority  | 
 | 268 | +		 * @return this builder  | 
 | 269 | +		 */  | 
 | 270 | +		public Builder maxPriority(Integer maxPriority) {  | 
 | 271 | +			this.maxPriority = maxPriority;  | 
 | 272 | +			return this;  | 
 | 273 | +		}  | 
 | 274 | + | 
 | 275 | +		/**  | 
 | 276 | +		 * Add a custom filter for a specific metadata field.  | 
 | 277 | +		 * @param key the metadata key  | 
 | 278 | +		 * @param predicate the predicate to test the value  | 
 | 279 | +		 * @return this builder  | 
 | 280 | +		 */  | 
 | 281 | +		public Builder addCustomFilter(String key, Predicate<Object> predicate) {  | 
 | 282 | +			if (this.customFilters.isEmpty()) {  | 
 | 283 | +				this.customFilters = new java.util.HashMap<>();  | 
 | 284 | +			}  | 
 | 285 | +			else if (!(this.customFilters instanceof java.util.HashMap)) {  | 
 | 286 | +				this.customFilters = new java.util.HashMap<>(this.customFilters);  | 
 | 287 | +			}  | 
 | 288 | +			this.customFilters.put(key, predicate);  | 
 | 289 | +			return this;  | 
 | 290 | +		}  | 
 | 291 | + | 
 | 292 | +		public MetadataBasedToolFilter build() {  | 
 | 293 | +			return new MetadataBasedToolFilter(this);  | 
 | 294 | +		}  | 
 | 295 | + | 
 | 296 | +	}  | 
 | 297 | + | 
 | 298 | +}  | 
0 commit comments