Skip to content

Commit d4ada72

Browse files
committed
feat: Add getTextContent method to retrieve message text content
Signed-off-by: liugddx <[email protected]>
1 parent 647fc61 commit d4ada72

File tree

9 files changed

+976
-3
lines changed

9 files changed

+976
-3
lines changed
Lines changed: 298 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,298 @@
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

Comments
 (0)