blob: c0861c9447da8da6779076c812e035e3f39c5a62 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.solr.mcp.server;
import static org.junit.jupiter.api.Assertions.*;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.Arrays;
import java.util.List;
import org.apache.solr.mcp.server.indexing.IndexingService;
import org.apache.solr.mcp.server.metadata.CollectionService;
import org.apache.solr.mcp.server.metadata.SchemaService;
import org.apache.solr.mcp.server.search.SearchService;
import org.junit.jupiter.api.Test;
import org.springaicommunity.mcp.annotation.McpTool;
import org.springaicommunity.mcp.annotation.McpToolParam;
/**
* Tests for MCP tool registration and annotation validation. Ensures all
* services expose their methods correctly as MCP tools with proper annotations
* and descriptions.
*/
class McpToolRegistrationTest {
@Test
void testSearchServiceHasToolAnnotation() throws NoSuchMethodException {
// Get the search method from SearchService
Method searchMethod = SearchService.class.getMethod("search", String.class, String.class, List.class,
List.class, List.class, Integer.class, Integer.class);
// Verify it has the @McpTool annotation
assertTrue(searchMethod.isAnnotationPresent(McpTool.class),
"SearchService.search method should have @McpTool annotation");
// Verify the annotation properties
McpTool toolAnnotation = searchMethod.getAnnotation(McpTool.class);
assertEquals("search", toolAnnotation.name(), "McpTool name should be 'search' (kebab-case)");
assertNotNull(toolAnnotation.description(), "McpTool description should not be null");
assertFalse(toolAnnotation.description().isBlank(), "McpTool description should not be blank");
}
@Test
void testSearchServiceToolParametersHaveAnnotations() throws NoSuchMethodException {
// Get the search method
Method searchMethod = SearchService.class.getMethod("search", String.class, String.class, List.class,
List.class, List.class, Integer.class, Integer.class);
// Verify all parameters have @McpToolParam annotations
Parameter[] parameters = searchMethod.getParameters();
assertTrue(parameters.length > 0, "Search method should have parameters");
for (Parameter param : parameters) {
assertTrue(param.isAnnotationPresent(McpToolParam.class),
"Parameter " + param.getName() + " should have @McpToolParam annotation");
McpToolParam paramAnnotation = param.getAnnotation(McpToolParam.class);
assertNotNull(paramAnnotation.description(), "Parameter " + param.getName() + " should have description");
assertFalse(paramAnnotation.description().isBlank(),
"Parameter " + param.getName() + " description should not be blank");
}
}
@Test
void testIndexingServiceHasToolAnnotations() {
// Get all methods from IndexingService
Method[] methods = IndexingService.class.getDeclaredMethods();
// Find methods with @McpTool annotation
List<Method> mcpToolMethods = Arrays.stream(methods).filter(m -> m.isAnnotationPresent(McpTool.class)).toList();
// Verify at least one method has the annotation
assertFalse(mcpToolMethods.isEmpty(),
"IndexingService should have at least one method with @McpTool annotation");
// Verify each tool has proper annotations
for (Method method : mcpToolMethods) {
McpTool toolAnnotation = method.getAnnotation(McpTool.class);
assertNotNull(toolAnnotation.name(), "Tool name should not be null");
assertFalse(toolAnnotation.name().isBlank(), "Tool name should not be blank");
assertNotNull(toolAnnotation.description(), "Tool description should not be null");
assertFalse(toolAnnotation.description().isBlank(), "Tool description should not be blank");
}
}
@Test
void testCollectionServiceHasToolAnnotations() {
// Get all methods from CollectionService
Method[] methods = CollectionService.class.getDeclaredMethods();
// Find methods with @McpTool annotation
List<Method> mcpToolMethods = Arrays.stream(methods).filter(m -> m.isAnnotationPresent(McpTool.class)).toList();
// Verify at least one method has the annotation
assertFalse(mcpToolMethods.isEmpty(),
"CollectionService should have at least one method with @McpTool annotation");
// Verify each tool has proper annotations
for (Method method : mcpToolMethods) {
McpTool toolAnnotation = method.getAnnotation(McpTool.class);
assertNotNull(toolAnnotation.description(), "Tool description should not be null");
assertFalse(toolAnnotation.description().isBlank(), "Tool description should not be blank");
}
}
@Test
void testSchemaServiceHasToolAnnotations() {
// Get all methods from SchemaService
Method[] methods = SchemaService.class.getDeclaredMethods();
// Find methods with @McpTool annotation
List<Method> mcpToolMethods = Arrays.stream(methods).filter(m -> m.isAnnotationPresent(McpTool.class)).toList();
// Verify at least one method has the annotation
assertFalse(mcpToolMethods.isEmpty(), "SchemaService should have at least one method with @McpTool annotation");
// Verify each tool has proper annotations
for (Method method : mcpToolMethods) {
McpTool toolAnnotation = method.getAnnotation(McpTool.class);
assertNotNull(toolAnnotation.description(), "Tool description should not be null");
assertFalse(toolAnnotation.description().isBlank(), "Tool description should not be blank");
}
}
@Test
void testAllMcpToolsHaveUniqueNames() {
// Collect all MCP tool names from all services
List<String> toolNames = new java.util.ArrayList<>();
// SearchService
addToolNames(SearchService.class, toolNames);
// IndexingService
addToolNames(IndexingService.class, toolNames);
// CollectionService
addToolNames(CollectionService.class, toolNames);
// SchemaService
addToolNames(SchemaService.class, toolNames);
// Verify all tool names are unique
long uniqueCount = toolNames.stream().distinct().count();
assertEquals(toolNames.size(), uniqueCount,
"All MCP tool names should be unique across all services. Found tools: " + toolNames);
}
@Test
void testMcpToolParametersFollowConventions() throws NoSuchMethodException {
// Get the search method
Method searchMethod = SearchService.class.getMethod("search", String.class, String.class, List.class,
List.class, List.class, Integer.class, Integer.class);
Parameter[] parameters = searchMethod.getParameters();
// Verify first parameter (collection) is required
McpToolParam firstParam = parameters[0].getAnnotation(McpToolParam.class);
assertTrue(firstParam.required() || !firstParam.required(),
"First parameter annotation should specify required status");
// Verify optional parameters have required=false
for (int i = 1; i < parameters.length; i++) {
McpToolParam param = parameters[i].getAnnotation(McpToolParam.class);
// Optional parameters should be marked as such in description or required flag
assertNotNull(param.description(), "Parameter should have description indicating if it's optional");
}
}
// Helper method to extract tool names from a service class
private void addToolNames(Class<?> serviceClass, List<String> toolNames) {
Method[] methods = serviceClass.getDeclaredMethods();
Arrays.stream(methods).filter(m -> m.isAnnotationPresent(McpTool.class)).forEach(m -> {
McpTool annotation = m.getAnnotation(McpTool.class);
// Use name if provided, otherwise use method name
String toolName = annotation.name().isBlank() ? m.getName() : annotation.name();
toolNames.add(toolName);
});
}
}